Skip to main content

Save & Load

The StoryFlow plugin provides a complete save/load system that persists global variables, character state, and once-only option tracking across play sessions. Save data is stored as JSON files in the user data directory, with a simple slot-based API on the StoryFlowRuntime autoload.

What Gets Saved

When you call save_to_slot(), the following data is serialized into a JSON file and written to disk:

  • Global Variables (Dictionary) - All project-level variables with their current values, including arrays of any type (boolean, integer, float, string, image, character, audio). This captures the full state of your story's persistent data.
  • Runtime Characters (Dictionary) - Character definitions with any modified character variables. If your story changes a character's name, portrait, or custom variables during play, those modifications are preserved.
  • Once-Only Options (Dictionary) - Tracking data for which "once only" dialogue options have been used. Keys follow the format nodeId-optionId, so the system knows exactly which options to hide on subsequent visits.

What Does NOT Get Saved

Save files represent "between dialogue" checkpoints. The following data is intentionally excluded:

  • Local variables - These are per-script and reset every time a script is entered. They are transient by design and do not carry over between sessions.
  • Current dialogue position / execution state - The current node, call stack, and flow stack are not persisted. A save does not capture "where you are" in a conversation.
  • Mid-dialogue state - If a dialogue is currently being displayed to the player, that in-progress state is not saved. The manager actively guards against this (see Active Dialogue Guard below).

Checkpoint Model

Think of StoryFlow saves as story checkpoints, not mid-sentence bookmarks. They capture the cumulative result of all player choices (variable values, used options, character modifications) but not the current position in a dialogue tree. Your game is responsible for knowing which script to start when loading a save - typically by storing a "current scene" identifier in a global variable.

Save & Load Functions

All save/load functions live on StoryFlowRuntime, the autoload singleton that is registered when you enable the plugin. Access it from any script via StoryFlowRuntime or through a component's get_manager() method.

Saving Progress

save_to_slot() serializes the current global variables, runtime characters, and once-only options into a JSON string and writes it to a file in the save directory.

GDScript
# Save current StoryFlow state
var manager = story_flow_component.get_manager()

var success: bool = manager.save_to_slot("save_slot_1")
if success:
    print("StoryFlow state saved successfully.")

Parameters:

  • slot_name (String) - The name of the save slot. Can be any string. The file will be saved as user://storyflow_saves/{slot_name}.json.

Returns: bool - true if the save was written successfully, false on failure.

Loading Progress

load_from_slot() reads a save file from disk, deserializes the JSON data, and replaces the current global variables, runtime characters, and once-only options with the saved values.

GDScript
# Load StoryFlow state
var manager = story_flow_component.get_manager()

var success: bool = manager.load_from_slot("save_slot_1")
if success:
    print("StoryFlow state loaded successfully.")
    # Now start the appropriate script based on your game logic
    story_flow_component.start_dialogue_with_script("scripts/chapter2")
else:
    print("Failed to load save.")

Parameters: Same as save_to_slot().

Returns: bool - true if the load succeeded, false if the file does not exist, deserialization failed, or a dialogue is currently active.

Loading Refuses While Dialogue Is Active

load_from_slot() will return false and do nothing if any StoryFlow component currently has an active dialogue. This is a safety guard - replacing variables and character data mid-execution would corrupt the runtime state. Always ensure all dialogues are stopped before attempting to load. See Active Dialogue Guard for details.

Checking & Deleting Saves

Two utility functions let you check for existing saves and delete them:

GDScript
var manager = story_flow_component.get_manager()

# Check if a save exists
var exists: bool = manager.does_save_exist("save_slot_1")

# Delete a save
manager.delete_save("save_slot_1")

These are useful for building save slot UIs - gray out empty slots, show confirmation dialogs before deleting, and so on.

Listing Save Slots

list_save_slots() returns a PackedStringArray of all slot names that have save files. Use this to populate a save/load menu dynamically.

GDScript
var manager = story_flow_component.get_manager()

# List all existing save slots
var slots: PackedStringArray = manager.list_save_slots()
for slot_name in slots:
    print("Found save: ", slot_name)

# Example: populate a save slot UI
func _populate_save_menu() -> void:
    var slots = manager.list_save_slots()
    for slot_name in slots:
        var button = Button.new()
        button.text = slot_name
        button.pressed.connect(_on_load_slot.bind(slot_name))
        save_list_container.add_child(button)

Save Format

Save files are stored as JSON in the Godot user data directory:

Code
user://storyflow_saves/
├── save_slot_1.json
├── save_slot_2.json
└── autosave.json

Each JSON file contains the serialized global variables, runtime character data, and once-only option tracking. The JSON format makes saves human-readable and easy to inspect during development. You can find the actual directory path by checking Godot's documentation for user:// on your target platform.

Why JSON?

StoryFlow uses JSON serialization rather than Godot's built-in ResourceSaver. JSON handles nested and recursive data structures correctly, ensures array variables of any depth are preserved, and produces human-readable save files that are easy to inspect during development. It also avoids platform-specific binary format issues.

Reset Functions

The manager provides reset functions for returning state to project defaults:

  • reset_global_variables() - Resets all global variables to their default values as defined in the project. Does not affect characters or once-only options.
  • reset_runtime_characters() - Resets all character definitions to their project defaults, reverting any in-game modifications to character variables, names, or portraits.
  • reset_all_state() - Resets global variables, runtime characters, and clears the once-only options tracking. This is a full reset intended for a "New Game" flow.
GDScript
# New Game flow
var manager = story_flow_component.get_manager()

# Reset everything to project defaults
manager.reset_all_state()

# Optionally delete the old save
manager.delete_save("save_slot_1")

# Start the opening script
story_flow_component.start_dialogue_with_script("scripts/intro")

Active Dialogue Guard

The manager tracks whether any StoryFlow component currently has an active dialogue via register_dialogue_start() and register_dialogue_end() calls (called automatically by the component). The is_dialogue_active() function returns true if any component has an active dialogue.

Never Load During Active Dialogue

load_from_slot() checks is_dialogue_active() before proceeding. If any dialogue is active, the load is rejected and the function returns false. This prevents a class of bugs where:

  • Variable values change underneath a running script, causing incorrect branching
  • Character data is swapped mid-dialogue, producing mismatched names or portraits
  • Once-only option state is cleared while the player is viewing those options

How to handle this in practice:

GDScript
var manager = story_flow_component.get_manager()

# Check before loading
if manager.is_dialogue_active():
    # Stop all active dialogues first, or show a message to the player
    push_warning("Cannot load while dialogue is active.")
    return

var success: bool = manager.load_from_slot("save_slot_1")
if success:
    story_flow_component.start_dialogue_with_script("scripts/chapter2")

If your game has a pause menu where the player can load saves, ensure that opening the pause menu either stops all active dialogues or disables the load option while dialogue is running.

Integration with Game Saves

StoryFlow's save system uses its own JSON files in user://storyflow_saves/. This gives you flexibility in how you integrate it with your game's own save system.

Paired Slot Strategy

Use a consistent naming convention to pair your game save and StoryFlow save slots. This keeps the two systems synchronized without any file format conflicts.

GDScript
# Save game
func save_game(slot_name: String) -> void:
    var manager = story_flow_component.get_manager()

    # Save your game state
    var game_save = MyGameSave.new()
    game_save.player_position = player.position
    game_save.current_level = current_level
    ResourceSaver.save(game_save, "user://saves/%s.tres" % slot_name)

    # Save StoryFlow state with the same slot name
    manager.save_to_slot(slot_name)

# Load game
func load_game(slot_name: String) -> void:
    var manager = story_flow_component.get_manager()

    # Load StoryFlow state first (so variables are ready)
    manager.load_from_slot(slot_name)

    # Load your game state
    var game_save = load("user://saves/%s.tres" % slot_name) as MyGameSave
    player.position = game_save.player_position

    # Resume from the appropriate script
    var current_scene = story_flow_component.get_string_variable("current_scene")
    story_flow_component.start_dialogue_with_script(current_scene)

New Game Flow

When the player starts a new game, reset all StoryFlow state and optionally clear the save:

GDScript
func new_game() -> void:
    var manager = story_flow_component.get_manager()

    # 1. Reset everything to project defaults
    manager.reset_all_state()

    # 2. Optionally delete the old save
    manager.delete_save("save_slot_1")

    # 3. Initialize your game state
    _setup_new_game()

    # 4. Start the opening script
    story_flow_component.start_dialogue_with_script("scripts/intro")

Continue / Load Game Flow

When the player loads an existing save:

GDScript
func continue_game(slot_name: String) -> void:
    var manager = story_flow_component.get_manager()

    # 1. Check if save exists
    if not manager.does_save_exist(slot_name):
        _show_notification("No save found.")
        return

    # 2. Load StoryFlow state
    var success = manager.load_from_slot(slot_name)
    if not success:
        _show_notification("Failed to load save.")
        return

    # 3. Load your game state from slot
    _load_game_state(slot_name)

    # 4. Read "current_scene" global variable from StoryFlow
    var current_scene = story_flow_component.get_string_variable("current_scene")

    # 5. Start the appropriate script
    story_flow_component.start_dialogue_with_script(current_scene)

Tracking the Current Scene

Since StoryFlow saves do not include execution position, you need a way to know which script to start after loading. A common pattern is to create a global string variable called something like current_scene and update it each time the player enters a new scene. On load, read this variable and use it to start the correct script.

Need Help?

Join our Discord community to ask questions, share your projects, report bugs, and get support from the team and other users.

Join Discord