Quest/Dialogue Save System

Overview

Quest Forge uses two complementary singleton components to persist game state: Quest Save System and Dialogue Save System. In their default configuration these are tightly integrated — a single JSON file on disk captures quest progress, dialogue history, location discoveries, active waypoints, fog-of-war exploration data, and the player's world position all in one atomic write.

Quest Save System (QuestSaveSystem) is the primary owner of the save file. It serialises the full state of QuestManager, collects sub-data from its sibling systems, writes it all to disk as human-readable JSON via JSON.NET, and reads it back on load.

Dialogue Save System (DialogueSaveSystem) tracks which DialogueTrigger conversations have been completed and stores the DialogueManager's runtime variables (flags, strings, integers). It contributes its data to the quest save and restores from it — or, optionally, manages its own standalone file.

Both components call DontDestroyOnLoad and survive scene transitions.


Scene Setup

Place both components on a single persistent GameObject (e.g. your _Managers object). The recommended configuration is:

  • Quest Save System + Dialogue Save System on the same GameObject, with Integrate With Quest Saves enabled on the Dialogue Save System.

  • Both singletons auto-destroy duplicates in Awake.

  • Quest Save System must have access to QuestManager — it calls QuestManager.Instance in Start.


Quest Save System

Inspector Properties

Save Settings

Property
Type
Default
Description

Save File Name

string

"quest_save"

Base name of the save file, without the .json extension.

Auto Save Enabled

bool

true

Enables both the state-change auto-save and the periodic timer save.

Auto Save Interval

float

300 s

How often a periodic auto-save fires (in seconds). Set to 0 to disable periodic saves while keeping state-change saves active.

Save On Quit

bool

true

Calls SaveGame() in OnApplicationQuit.

Load On Start

bool

true

If a save file exists, calls LoadGame() in Start automatically.

Debug Mode

bool

false

Enables verbose logging via QuestLogger at Debug level.

Save Location

Property
Type
Default
Description

Use Persistent Data Path

bool

true

Writes to Application.persistentDataPath/Saves/{saveFileName}.json. Recommended for shipping.

Custom Save Path

string

""

Directory used when Use Persistent Data Path is off. The .json filename is still appended automatically.


Auto-Save Triggers

Quest Save System monitors for changes every Update frame and saves immediately when any of the following occur:

Trigger
What Changed

Quest state change

The count of active or completed quests differs from the last-saved counts.

Tracked quest change

The currently tracked quest ID changed (without a count change).

Periodic timer

autoSaveInterval seconds have elapsed since the last save.

Application quit

OnApplicationQuit fires and saveOnQuit is true.

Quest trigger activated

A DialogueTrigger or scene quest trigger marks itself activated.

Location discovered

A new location is marked discovered via MarkLocationDiscovered().

Dialogue completed

DialogueSaveSystem.MarkDialogueTriggerCompleted() calls QuestSaveSystem.SaveGame() when integration is enabled.

After any state-change or trigger save, the periodic timer resets so the next periodic save fires a full interval later.


What Gets Saved

Every call to SaveGame() captures a complete snapshot of the following:

Section
Contents

Metadata

Save file name, timestamp, save version ("1.0").

Player position

World-space position and Y-axis (yaw) rotation. Written from QuestManager.playerTransform.

Active quests

Full QuestInstanceSaveData for every quest currently in progress.

Completed quests

Full QuestInstanceSaveData for every finished quest.

Failed quests

Full QuestInstanceSaveData for every failed quest.

Abandoned quests

Full QuestInstanceSaveData for every abandoned quest.

Available quest IDs

String list of quest IDs that are currently available to the player.

Dialogue data

Completed trigger IDs, trigger metadata, and all DialogueManager runtime variables (flags, strings, integers).

Quest trigger data

IDs of scene quest triggers that have been activated and destroyed/deactivated.

Location discoveries

HashSet of discovered location IDs and per-location metadata.

World map waypoints

Name, world position, colour (RGBA hex), and icon index for every custom waypoint.

Fog-of-war breadcrumbs

List of world positions recorded as the player explored.


Load Sequence

When LoadGame() runs, the following steps execute in order:

  1. Deserialise — JSON file is read and converted to QuestSaveData.

  2. Version check — If saveVersion != "1.0", MigrateSaveData() is attempted. If migration fails, loading is aborted.

  3. Clear quests — All four quest dictionaries in QuestManager are emptied via reflection.

  4. Restore quests — Each QuestInstanceSaveData is converted back to a live QuestInstance and injected into the correct dictionary. UpdateObjectiveStatus() is called on each instance.

  5. Refresh available questsquestManager.ForceRefreshAvailableQuests().

  6. Restore dialogueDialogueSaveSystem.RestoreSaveData() repopulates completed triggers and restores dialogue variables to DialogueManager.

  7. Restore quest triggers — The activatedQuestTriggers HashSet and metadata dictionary are repopulated.

  8. Restore locationsLocationManager.LoadFromSaveData() syncs all discovered location IDs.

  9. Restore waypointsPOIManager.ClearAllWaypoints(), then each WaypointSaveEntry is re-created via POIManager.CreateWaypoint(). Colour is parsed from the hex string; icon sprite is resolved from WaypointPickerUI.GetIconByIndex().

  10. Restore fog of warWorldMapFogOfWar.LoadBreadcrumbs() if breadcrumbs are present.

  11. Restore player position (deferred one frame) — A coroutine waits for WaitForEndOfFrame to ensure physics and character controllers are initialised before the teleport.

  12. Notify listeners (deferred one frame) — A second coroutine publishes QuestProgressUpdated for all active quests and QuestCompleted for all completed quests on QuestEventBus, allowing POI Markers, UI, and other subscribers to re-evaluate their state.


Save File Format

Files are plain UTF-8 JSON, indented, with DateTimeFormat = "yyyy-MM-ddTHH:mm:ss". Vector3 values are serialised by a custom Vector3Converter. The files are human-readable and can be inspected or edited directly in any text editor.

Default path (Windows): C:\Users\{User}\AppData\LocalLow\{Company}\{Product}\Saves\quest_save.json


Public API

Core Save / Load

Method
Returns
Description

SaveGame(string slotName = null)

bool

Saves to the named slot, or the default saveFileName if null. Returns true on success.

LoadGame(string slotName = null)

bool

Loads from the named slot. Returns true on success.

QuickSave()

void

Saves to the hardcoded "quicksave" slot.

QuickLoad()

void

Loads from the "quicksave" slot.

CreateNamedSave(string saveName)

bool

Alias for SaveGame(saveName).

Save File Management

Method
Returns
Description

SaveFileExists(string slotName = null)

bool

Returns true if the named (or default) save file is present on disk.

DeleteSave(string slotName = null)

bool

Deletes the save file. Returns true if deletion succeeded.

GetSaveInfo(string slotName = null)

QuestSaveData

Deserialises the save file and returns the data object without applying it to the game state. Use for save-slot UI (timestamps, quest counts, etc.).

GetAvailableSaveSlots()

List<string>

Scans the saves directory and returns all slot names (file names without .json).

ExportSaveAsJson(string slotName = null)

string

Returns the raw JSON content of the save file as a string.

ImportSaveFromJson(string json, string slotName = null)

bool

Validates the JSON, writes it to disk, and returns true on success. Does not automatically load — call LoadGame() afterwards.

Quest Trigger API

Scene objects that represent one-time quest triggers call these methods to persist their activated state.

Method
Returns
Description

MarkQuestTriggerActivated(triggerId, questName, actionPerformed, position, wasDestroyed, wasDeactivated)

void

Records the trigger as used. Creates rich metadata on first call; increments timesActivated on subsequent calls. Triggers an auto-save.

IsQuestTriggerActivated(string triggerId)

bool

Returns true if the trigger has been recorded as used.

GetQuestTriggerMetadata(string triggerId)

QuestTriggerMetadata

Returns the stored metadata, or null if the trigger has never been activated.

GetActivatedQuestTriggerCount()

int

Total count of recorded activated triggers.

ResetQuestTrigger(string triggerId)

void

Removes the trigger from the activated set, allowing it to fire again.

ResetAllQuestTriggers()

void

Clears all activated trigger data.

Location Discovery API

Method
Returns
Description

GetLocationDiscoveryData()

LocationDiscoverySaveData

Returns the live discovery data object (creates empty if none exists).

MarkLocationDiscovered(LocationDiscoverySaveData data)

void

Replaces the cached discovery data with the updated version and triggers an auto-save.

IsLocationDiscovered(string locationId)

bool

Returns true if the location ID is in the discovered set.

GetDiscoveredLocationCount()

int

Total number of discovered locations.

ResetAllLocationDiscoveries()

void

Clears discovery data and calls LocationManager.ResetAllDiscoveries().


Dialogue Save System

Inspector Properties

Property
Type
Default
Description

Integrate With Quest Saves

bool

true

When enabled, dialogue state is bundled into the quest save file. Calling MarkDialogueTriggerCompleted() automatically triggers QuestSaveSystem.SaveGame(). Recommended — keeps a single unified save file.

Debug Mode

bool

false

Enables verbose logging at Debug level.


Integration Modes

Integrated (default)integrateWithQuestSaves = true:

  • Dialogue data lives inside QuestSaveData.dialogueData.

  • QuestSaveSystem calls DialogueSaveSystem.CreateSaveData() during every save and RestoreSaveData() during every load.

  • No separate file is ever written by DialogueSaveSystem.

  • Any call to MarkDialogueTriggerCompleted() triggers QuestSaveSystem.SaveGame() immediately.

StandaloneintegrateWithQuestSaves = false:

  • Use SaveToFile(path) and LoadFromFile(path) directly on DialogueSaveSystem.

  • Useful for projects that have a separate dialogue system with its own save pipeline.


Runtime State

Internal Collection
Type
Contents

completedDialogueTriggers

HashSet<string>

IDs of all triggers that have fired. O(1) lookup.

triggerMetadata

Dictionary<string, DialogueTriggerMetadata>

Rich metadata for each trigger: dialogue name, fire count, timestamps, world position.


Public API

Trigger State

Method
Returns
Description

MarkDialogueTriggerCompleted(triggerId, dialogueName, triggerPosition)

void

Adds the ID to the completed set and creates or updates metadata. If integration is enabled, triggers QuestSaveSystem.SaveGame() immediately.

IsDialogueTriggerCompleted(string triggerId)

bool

HashSet.Contains() — O(1) lookup.

ResetDialogueTrigger(string triggerId)

void

Removes the ID, allowing the trigger to fire again. Does not remove metadata.

ResetAllDialogueTriggers()

void

Clears both the completed set and all metadata.

GetTriggerMetadata(string triggerId)

DialogueTriggerMetadata

Returns the metadata object, or null if the trigger has never fired.

GetCompletedDialogueCount()

int

Count of unique completed trigger IDs.

GetCompletedDialogueTriggers()

List<string>

A copy of all completed trigger IDs. Safe to iterate without modification risk.

Save / Load (Standalone Mode)

Method
Returns
Description

CreateSaveData()

DialogueSaveData

Packages current runtime state — completed triggers, metadata, and all DialogueManager variables — into a serialisable object. Called by QuestSaveSystem internally during integrated saves.

RestoreSaveData(DialogueSaveData saveData)

void

Clears runtime state and restores from the provided data object. Also calls DialogueManager.RestoreVariables() to reinstall flags, strings, and integers. Called by QuestSaveSystem internally during integrated loads.

SaveToFile(string savePath)

bool

Serialises and writes a standalone dialogue save file. Returns true on success.

LoadFromFile(string savePath)

bool

Reads and deserialises a standalone dialogue save file. Returns true on success.


Data Structures Reference

QuestSaveData — Top-Level Container

The root object serialised to the .json file.

Field
Type
Description

saveVersion

string

Always "1.0". Triggers migration if mismatched on load.

saveName

string

The slot name used when saving.

saveTime

DateTime

ISO-formatted timestamp of the save.

playerName

string

Reserved for future use.

totalPlayTime

int

Reserved for future use (seconds).

playerPosition

Vector3

World position at save time.

playerYRotation

float

Y-axis (yaw) rotation in degrees.

hasPlayerPosition

bool

False for saves predating position tracking — skips teleport on load.

activeQuests

List<QuestInstanceSaveData>

All in-progress quests.

completedQuests

List<QuestInstanceSaveData>

All finished quests.

failedQuests

List<QuestInstanceSaveData>

All failed quests.

abandonedQuests

List<QuestInstanceSaveData>

All abandoned quests.

availableQuestIds

List<string>

Quest IDs currently available to accept.

dialogueData

DialogueSaveData

Dialogue trigger state and runtime variables.

questTriggerData

QuestTriggerSaveData

Scene trigger activation history.

locationDiscoveryData

LocationDiscoverySaveData

Discovered location IDs and metadata.

worldMapData

WorldMapSaveData

Custom waypoints and fog-of-war breadcrumbs.

Helper methods: GetTotalQuestCount(), GetFormattedSaveTime().


QuestInstanceSaveData — Per-Quest State

Field
Type
Description

questId

string

The Quest.QuestID used to look up the Quest ScriptableObject on load.

questName

string

Stored for readability in the JSON file; not used for lookup.

status

QuestStatus

Active / Completed / Failed / Abandoned.

startTime

DateTime

When the quest was accepted.

completeTime

DateTime?

When the quest was completed (nullable).

isTracked

bool

Whether the player had this quest tracked in the HUD.

timesCompleted

int

Supports repeatable quests.

progress

QuestProgressSaveData

Full copy of all objective counters and flags.


QuestProgressSaveData — Objective Counters

All seven progress dictionaries are deep-copied on save and restored on load via direct calls to QuestProgress setter methods.

Field
Key → Value

killCounts

EnemyType → count

itemCounts

ItemId → count

flags

FlagId → bool

locations

LocationId → Vector3

stringValues

Key → string

interactionCounts

ObjectId → count

uniqueInteractions

ObjectId → List<string>


DialogueSaveData — Dialogue State

Field
Type
Description

completedDialogueTriggers

List<string>

All trigger IDs that have been fired.

triggerMetadata

Dict<string, DialogueTriggerMetadata>

Per-trigger rich data.

saveTime

DateTime

Timestamp of the last save.

dialogueFlags

Dict<string, bool>

All boolean variables from DialogueManager.

dialogueStrings

Dict<string, string>

All string variables from DialogueManager.

dialogueIntegers

Dict<string, int>

All integer variables from DialogueManager.

DialogueTriggerMetadata

Field
Description

triggerId

The trigger's unique ID.

dialogueName

Name of the dialogue asset that was played.

timesTriggered

How many times this trigger has fired (for non-once triggers).

firstTriggerTime

DateTime of the first activation.

lastTriggerTime

DateTime of the most recent activation.

triggerPosition

World position of the trigger object.


QuestTriggerSaveData — Scene Trigger History

Field
Description

activatedTriggers

List of trigger IDs that have fired and should remain spent.

triggerMetadata

Per-trigger rich data (quest name, action, timestamps, position, wasDestroyed, wasDeactivated).


LocationDiscoverySaveData — Location History

Field
Description

discoveredLocationIds

HashSet<string> of all discovered location IDs.

locationMetadata

Per-location rich data: name, player position at discovery, timestamp, whether quest-related, and which quest triggered it.

totalLocationsDiscovered

Running count.

firstDiscoveryTime / lastDiscoveryTime

Optional aggregate timestamps.


WorldMapSaveData — Map State

Field
Description

mapWaypoints

List of WaypointSaveEntry — one per custom waypoint.

explorationBreadcrumbs

List of Vector3 world positions recorded for fog-of-war reveal.

lastZoom

Last map zoom level (only applied if saveMapViewState = true).

lastPanOffset

Last map pan offset (only applied if saveMapViewState = true).

WaypointSaveEntry

Field
Description

name

Display name of the waypoint.

position

World-space position.

createdTime

When the player placed it.

colorHex

RGBA hex string without # (e.g. "FFFF00FF"). Parsed with ColorUtility.TryParseHtmlString() on load. Empty = use yellow fallback.

iconIndex

Zero-based index into WaypointPickerUI.availableIcons. -1 = use POIManager default icon.


Other Tips

  • JSON.NET is required — Both save systems use Newtonsoft.Json. Ensure the JSON.NET for Unity package is present in the project. Missing it will cause JsonConvert type errors at compile time.

  • Quest definitions must exist on loadRestoreQuestInstance() looks up the Quest ScriptableObject by ID via QuestManager.GetQuestDefinition(). If you rename or delete a Quest asset between saves, that quest is silently skipped. Keep a log of removed quest IDs if players may have saves containing them.

  • Save version migration is stubbed — The current migration framework logs a warning and returns false for any version other than "1.0", aborting the load. Add version-specific migration blocks inside MigrateSaveData() before shipping any format-breaking update.

  • Player position is deferred — The teleport happens at end-of-frame, not during LoadGame(). If your game has any first-frame logic that reads the player position, it may see the pre-load position. Defer your own post-load logic with a short yield return null if needed.

  • Quest events are deferred — POI Markers and UI components that subscribe to QuestEventBus in Start() will not receive events published during LoadGame() because their Start() may not have run yet. The save system handles this by firing events at end-of-frame. Custom listeners should follow the same pattern.

  • Reflection for QuestManager accessRestoreQuestInstance() and ClearAllQuests() use System.Reflection to write into QuestManager's private dictionaries. This is necessary because QuestManager encapsulates those collections. If QuestManager's private field names are ever refactored, update the string literals in these methods accordingly.

  • GetSaveInfo() for UI — Use this method to preview a save slot in a save/load menu without actually loading the game state. It deserialises the full QuestSaveData object, which includes timestamps, quest counts, and player name, without touching QuestManager.

  • Fog-of-war breadcrumbs can grow large — Every exploration step is stored as a Vector3. For large open worlds, the breadcrumb list can grow to thousands of entries. Consider periodically pruning it via WorldMapFogOfWar settings.

Last updated