Documentation menu

Godot integration

Description: Export a Narratyr project into a Godot 4 project and drive dialogues and quests from GDScript.

Prerelease. The Godot integration ships with Narratyr rather than from the Asset Library — the exporter writes the addon into your project for you. Details here may change before release.

The Godot exporter writes two things into your game project: a reusable GDScript addon, and JSON, GDScript, translations, and assets generated from your Narratyr document. Your game talks to the addon through autoload singletons and creates flow players when it wants to run a dialogue.

Exporting covers the idea in general terms. This page is the Godot specifics: where to point the exporter, what each button writes, how to register the autoloads, and what your game has to implement before a dialogue will run.

Before you start

  • A Godot 4 project. The runtime is GDScript and works in both GDScript and C# Godot projects, but the API shown here is GDScript.
  • Your Narratyr project, saved. Saving is especially important if it contains assets: the exporter copies them from the saved project's asset directory.
  • A dialogue UI of your own. Narratyr walks the graph and tells you which line or choices are current; it deliberately does not impose a visual style or scene structure.

No native extension or compiler toolchain is required.

Setting the export path

Open the Export view in Narratyr and pick the Godot tab. Point Godot Project Path at the folder containing project.godot — the project root, not addons/, narratyr_data/, or the folder above the project.

MyGame/
  project.godot       <-- choose this folder
  addons/
  scenes/
  scripts/

For a project at /home/me/games/MyGame/project.godot, the export path is /home/me/games/MyGame.

The exporter writes relative to whichever folder you choose. If the path is one level off, you will get a new addons/ or narratyr_data/ folder in the wrong place rather than a working integration. The path is remembered per Narratyr project and per engine.

Source Locale is the language code for the text currently authored in Narratyr, such as en, en_GB, or ja. It becomes the locale column in Godot's translation CSV and the source-language value in the translator XLIFF.

Install Addon vs Export Data

The two buttons write different parts of the integration.

Install Addon Export Data
Writes Runtime and editor GDScript under addons/narratyr_data/ Project-specific files under narratyr_data/, copied assets, and translator files
Needs a compile No No
Run it when Once at setup, and after a Narratyr update changes the addon Whenever authored content or schema changes
Typical frequency Occasionally Constantly

Install Addon writes the graph walker, expression evaluator, variable and quest managers, registries, save helper, and editor tooling. It does not enable the plugin or add autoloads for you, because those are settings owned by your Godot project.

Export Data writes the current variables, enumerations, entities, graphs, quests, translation source, generated quest-id constants, and binary assets. Unlike the Unreal exporter, a schema change does not produce compiled types in Godot: export data again and the GDScript runtime reads the new JSON.

Rule of thumb. Install the addon when the integration itself changes. Export data when your game data changes.

First-time setup

Do these steps once for a new Godot project:

  1. In Narratyr, set the Godot project path and click Install Addon.
  2. Click Export Data.
  3. Open the Godot project. Under Project → Project Settings → Plugins, enable Narratyr.
  4. Under Project → Project Settings → Autoload, register the variable manager, graph registry, quest manager, and entity registry as described below.
  5. Under Project → Project Settings → Localization → Translations, add the imported source translation, normally res://narratyr_data/translations.en.translation for an en source locale.

Godot imports the exported CSV into one .translation resource per locale column. If the resource does not appear immediately, let the filesystem scan finish or reimport narratyr_data/translations.csv before adding it.

Registering the autoloads

Add these four scripts on the Autoload page:

Suggested name Path Purpose
VariableManager res://addons/narratyr_data/narratyr_variable_manager.gd Runtime variable values, expression functions, interpolation
GraphRegistry res://addons/narratyr_data/narratyr_graph_registry.gd Lazy graph loading and cross-graph resolution
QuestManager res://addons/narratyr_data/narratyr_quest_manager.gd Quest instances, objectives, and quest-owned flows
EntityRegistry res://addons/narratyr_data/narratyr_entity_registry.gd Entity lookup by stable Narratyr id or game id

Keep Enable turned on for each one.

Do not name an autoload NarratyrVariableManager, NarratyrGraphRegistry, NarratyrQuestManager, or NarratyrEntityRegistry. Those are the scripts' class_name values, and Godot does not allow an autoload global to use the same name. The suggested short names are used throughout this guide.

The quest manager finds the variable manager and graph registry by type, independent of autoload order or the names you chose. You can also wire custom instances explicitly:

QuestManager.initialize(VariableManager, GraphRegistry)

The entity registry is independent and is only required if your game uses exported entities, dialogue speakers, or entity-reference fields.

What lands in your project

MyGame/
  addons/
    narratyr_data/                 installed addon; generated by Install Addon
      plugin.cfg
      narratyr_flow_player.gd
      narratyr_variable_manager.gd
      narratyr_quest_manager.gd
      narratyr_entity_registry.gd
      narratyr_graph_registry.gd
      narratyr_save_state.gd
      ...
  narratyr_data/                   generated by Export Data
    variables.json
    enumerations.json
    graph_registry.json            graph id -> JSON path
    graph_index.json               names and ids for the Inspector dropdown
    graphs/                        one JSON file per graph
    quest_data.json
    narratyr_quest_ids.gd          generated QuestIds constants
    entity_registry.json
    entities/                      one JSON file per entity type
    translations.csv               Godot runtime translation source
    assets/                        mirrors Narratyr's asset folders
  NarratyrLocalization/            translator handoff; ignored by Godot
    .gdignore
    translator-source.csv
    translator-source.xlf

Files that have no corresponding data are omitted — for example, a project with no variables has no variables.json.

Treat addons/narratyr_data/ and narratyr_data/ as generated output. Install or export overwrites the files it owns, so put game code in your own scripts/ directory and do not modify generated files in place. Godot's .import, .uid, and .translation artifacts are engine-generated companions and are not written by Narratyr.

The exporter currently does not remove every old Godot file that is no longer produced. An orphaned graph JSON is harmless because graph_registry.json no longer points to it, but you can delete stale files from the generated folders if you want a clean tree.

The classes you will actually use

Class Kind What it is for
NarratyrVariableManager Node, normally an autoload Read and write variables, register game functions, interpolate @{Var} text
NarratyrQuestManager Node, normally an autoload Start, complete, fail, and query quests; bind quest-wide signals
NarratyrQuestInstance RefCounted, one per quest Live quest and objective state, localized quest text, owned flow players
NarratyrFlowPlayer Node Walk a graph and expose the current dialogue or choices
NarratyrGraphRegistry Node, normally an autoload Resolve a stable graph id to parsed graph data
NarratyrEntityRegistry Node, normally an autoload Resolve exported entity dictionaries
NarratyrNodeUtil Static helper Typed accessors for node dictionaries
NarratyrSaveState Static helper Capture and restore variables and quests in the correct order
QuestIds Generated constants class Readable quest references such as QuestIds.TheAmulet

Every graph node and entity is a Dictionary. This keeps the addon independent of your authored schema: adding an entity field changes JSON, not runtime source code.

NarratyrVariableManager

The variable manager loads variables.json and enumerations.json on startup. Use the typed accessors when you know the declaration's type:

VariableManager.set_variable_int("BoarsKilled", 5)
VariableManager.set_variable_bool("MetTheJarl", true)

var killed: int = VariableManager.get_variable_int("BoarsKilled")
var met_jarl: bool = VariableManager.get_variable_bool("MetTheJarl")

Other useful methods include:

  • get_variable(), get_variable_number(), and get_variable_string()
  • get_variable_array() and set_variable_array() for list and set variables; sets are de-duplicated at the write boundary
  • get_all_variable_names() and get_all_variables()
  • has_variable(), get_declared_type(), and is_collection()
  • reset_to_defaults() for starting a new game
  • interpolate_variables() for @{VarName} placeholders and text-selection directives

Every successful write emits variable_changed(var_name, new_value). That signal also wakes gates and event listeners, so write through the manager rather than changing its internal dictionary.

NarratyrQuestManager

The quest manager loads quest_data.json and creates one NarratyrQuestInstance per quest graph.

QuestManager.discover_quest(QuestIds.DownWithTheJarl)
QuestManager.start_quest(QuestIds.DownWithTheJarl)

var active_quests := QuestManager.get_active_quests()
var state := QuestManager.get_quest_state(QuestIds.DownWithTheJarl)

QuestIds is generated from quest names. Each constant's value is the stable graph id, so the runtime identity does not change when you rename a quest. A rename does change the constant's readable member name; update game code that uses that member after exporting.

Useful queries include get_quest_instance(), get_quest_definition(), get_quests_by_state(), get_quest_prerequisites(), and are_quest_start_requirements_satisfied().

Gameplay-driven objectives need a game-side update. Reaching an Objective node activates it, and an Update Objective node can change it from the graph. If the completion condition lives in gameplay — killing five boars, for example — notify the manager:

QuestManager.update_objective(
    QuestIds.DownWithTheJarl,
    "the-stable-objective-node-id",
    "completed"
)

The status can be "completed", "failed", "not_applicable", or "active" to reopen a terminal objective. The optional fourth and fifth arguments patch the objective's hidden and optional flags; pass null to leave a value unchanged.

Use complete_quest(), fail_quest(), or reset_quest() for explicit quest lifecycle changes. reset_all() silently returns every quest and objective to its authored initial state, which is useful when starting a new game.

NarratyrFlowPlayer

The flow player walks a graph. It auto-advances through routing, instruction, objective, and entry nodes; gates wait for their conditions; dialogue and choice nodes pause for the player. A blocking custom node also pauses, while a non-blocking custom node emits its signal and continues.

For a standalone dialogue, create a player, add it to the scene tree, initialize it, bind signals, and then call play():

var conversation: NarratyrFlowPlayer

func start_conversation(graph_id: String) -> void:
    var graph := GraphRegistry.resolve_graph(graph_id)
    if graph.is_empty():
        return

    conversation = NarratyrFlowPlayer.new()
    add_child(conversation)
    conversation.initialize(VariableManager, GraphRegistry)
    conversation.dialogue_node_entered.connect(_on_dialogue)
    conversation.flow_ended.connect(_on_conversation_ended)
    conversation.flow_error.connect(_on_flow_error)
    conversation.play(graph)

Adding the player as a child gives it a lifecycle and allows it to find the quest-manager autoload for quest-status expressions. Keep the reference while your UI needs to call continue_flow() or choose().

Calling play(graph) with no start node resolves the graph's entry node. Pass a stable node id as the second argument only when you deliberately want another starting point.

NarratyrEntityRegistry

Entity references in graphs and fields store the entity's stable Narratyr id. Resolve that id with find_entity_by_id():

var speaker_id := NarratyrNodeUtil.speaker_entity(dialogue_node)
var speaker := EntityRegistry.find_entity_by_id(speaker_id)
if not speaker.is_empty():
    speaker_label.text = speaker.get("display_name", "")
    var profile_path: String = speaker.get("profile_image", "")
    if not profile_path.is_empty():
        portrait.texture = load(profile_path)

An entity dictionary contains id, technical_name (the game-facing external id), display_name, profile_image, and a properties dictionary grouped by Property Set technical name.

Use find_entity(game_id) for a game-facing external id, or narratyr_id_to_game_id() / game_id_to_narratyr_id() to translate between the two. find_entities_by_type(type_name) returns every entity for one Entity Type.

NarratyrNodeUtil and graph metadata

Flow signals carry raw dictionaries. NarratyrNodeUtil provides stable accessors for the common fields:

func _on_dialogue(node: Dictionary) -> void:
    var id := NarratyrNodeUtil.id(node)
    var speaker_id := NarratyrNodeUtil.speaker_entity(node)
    var source_text := NarratyrNodeUtil.text(node)
    var fields := NarratyrNodeUtil.field_values(node)

For user-defined graph metadata, use the registry:

var metadata := GraphRegistry.resolve_graph_metadata(graph_id)
var difficulty = GraphRegistry.get_graph_metadata_field(graph_id, "Difficulty")

get_graph_metadata_field() looks up a field by its authored PascalCase name and returns null when it is absent.

Flow signals you need to handle

Nothing in Narratyr draws UI. A working integration binds the signals that tell the game what the flow reached, displays the result, and resumes the supplied flow player when the player acts.

Bind on the quest manager for quest content

For quest-driven content, connect once at startup:

Signal Payload Why you want it
quest_flow_player_ready (quest_id, flow_player) Fires after a flow exists but before it runs; attach that flow's own signals here
quest_dialogue_started (quest_id, flow_player, dialogue_node) A quest flow reached dialogue; open your dialogue UI and retain this specific player
quest_custom_node_started (quest_id, flow_player, custom_node) A quest flow entered a custom node; read its payload and resume it if blocking
quest_state_changed (quest_id, new_state) Update quest logs and notifications
objective_state_changed (quest_id, objective) Update objective UI; the dictionary includes state, hidden, and optional
quest_progress_changed (quest_id) Broad “something changed” notification, also used internally by gates
func _ready() -> void:
    QuestManager.quest_flow_player_ready.connect(_on_quest_flow_ready)
    QuestManager.quest_dialogue_started.connect(_on_quest_dialogue)
    QuestManager.objective_state_changed.connect(_on_objective_changed)

func _on_quest_flow_ready(quest_id: String, flow: NarratyrFlowPlayer) -> void:
    flow.flow_error.connect(func(message: String): _on_flow_error(quest_id, message))
    flow.event_listener_fired.connect(
        func(node: Dictionary): _on_listener_fired(quest_id, flow, node)
    )

func _on_quest_dialogue(
    quest_id: String,
    flow: NarratyrFlowPlayer,
    node: Dictionary
) -> void:
    dialogue_ui.present(quest_id, flow, node)

A quest can have a primary flow plus secondary flows spawned by async listeners or Fork nodes. Save restore also creates fresh flow-player nodes. That is why manager signals hand you the relevant player and why quest_flow_player_ready is the right binding point — do not assume a quest has one permanent player.

The flow player's own signals

Every visited node emits node_entered(node_id, node_type) and later node_exited(node_id, node_type). There is also a typed signal for each node kind, including:

  • dialogue_node_entered, choice_entered, and custom_node_entered
  • objective_entered, instruction_entered, and gate_entered
  • start_dialogue_node_entered, start_quest_fragment_entered, and start_quest_async_entered
  • complete_quest_entered, update_objective_entered, and end_conversation_entered

Lifecycle signals are:

Signal Meaning
graph_pushed(from_graph_id, to_graph_id) The flow dived into a dialogue or fragment
graph_popped(to_graph_id) The flow returned to a parent graph
event_listener_fired(node) A listener is about to interrupt the current presentation; suspend the old UI
flow_ended(reason) The run ended as Natural, Stopped, Error, or DeadEnd
flow_error(message) Missing data, an evaluation failure, or details of a dead end

Treat DeadEnd as a content error during development. It means the current node had outgoing edges but every route was blocked; flow_error includes the per-edge reasons.

Building the dialogue UI

At a dialogue or choice stop, ask the flow whether the author created a choice menu. is_at_player_choice_menu() distinguishes a real menu from ordinary conditional routing.

If it returns false, display one Continue affordance:

continue_button.pressed.connect(flow.continue_flow)

If it returns true, render get_available_paths(). Each path may contain:

  • choice_label, label_on_disabled, and slot_id
  • passable, hidden, and disabled
  • blocked_reason when no route can be taken

The index accepted by choose() is the index in the passable-path list. Count every passable path, including hidden paths, while building buttons:

var choose_index := 0
for path in flow.get_available_paths():
    var runtime_index := -1
    if path.get("passable", false):
        runtime_index = choose_index
        choose_index += 1

    if path.get("hidden", false):
        continue

    var button := Button.new()
    button.disabled = runtime_index < 0 or path.get("disabled", false)
    var label: String = path.get("choice_label", "")
    if button.disabled and not path.get("label_on_disabled", "").is_empty():
        label = path["label_on_disabled"]
    button.text = label

    if runtime_index >= 0:
        button.pressed.connect(flow.choose.bind(runtime_index))
    choice_container.add_child(button)

Hidden and disabled are authored presentation rules. A path can also be non-passable because its edge condition is false. Do not call choose() for any disabled or non-passable button.

Stopping on other node types

The default stop set is dialogue, choice, and custom; only custom nodes whose exported blocking flag is true actually halt. You can replace stop_on_types before playback:

func _on_quest_flow_ready(_quest_id: String, flow: NarratyrFlowPlayer) -> void:
    flow.stop_on_types = ["dialogue", "choice", "custom", "objective"]

A type you add has no automatic resume mechanism. Call continue_flow() after your game finishes handling it, or the flow remains parked. The runtime logs a development warning when this happens.

try_continue() advances only when a route is currently passable and otherwise stays at the current node. QuestManager.try_advance_quest(quest_id) forwards to the primary quest flow.

Gates that read Narratyr variables or quest status re-evaluate automatically. If a gate depends on state the addon cannot observe — inventory or world state behind a game function, for example — call flow.reevaluate_gate() or QuestManager.reevaluate_quest_gate(quest_id) after that state changes.

Implementing functions

Functions declared in Functions are calls into your game. Godot does not generate a base class or stubs for them; create an object with methods whose names and parameters exactly match the definitions in Narratyr:

# scripts/game_functions.gd
class_name GameFunctions
extends RefCounted

var inventory: Array[String] = []

func hasInventoryItem(item_id: String) -> bool:
    return item_id in inventory

func addInventoryItem(item_id: String, count: int) -> void:
    for _i in range(count):
        inventory.append(item_id)

Register one instance before any graph runs:

func _ready() -> void:
    VariableManager.function_host = GameFunctions.new()

The expression evaluator sends plain Godot values and calls the method by its authored name. Lists and sets arrive as Array; enum values arrive as their authored integers.

Do not implement Narratyr's built-ins (length, count, indexOf, randomInteger, randomNumber, debugLog, traversal counters, or quest-status functions). The addon handles those before it consults your function host.

Functions cannot appear in event listener conditions. Listeners re-evaluate on observable state changes, and the addon cannot know when opaque game state changed. Mirror that state into a Narratyr variable instead.

Save and restore

Narratyr does not choose a save-file format or slot. NarratyrSaveState gives you one dictionary containing variables, traversal history, quests, objectives, and every live quest flow:

func save_narratyr(path: String) -> void:
    var state := NarratyrSaveState.capture(VariableManager, QuestManager)
    var file := FileAccess.open(path, FileAccess.WRITE)
    file.store_string(JSON.stringify(state))

func load_narratyr(path: String) -> void:
    var file := FileAccess.open(path, FileAccess.READ)
    var state: Dictionary = JSON.parse_string(file.get_as_text())
    var result := NarratyrSaveState.restore(state, VariableManager, QuestManager)
    if result == NarratyrSaveState.RestoreResult.REFUSED_MAJOR_MISMATCH:
        # The save is unreadable by this build and NOTHING was restored.
        # Tell the player; do not carry on as though it loaded.
        pass

The helper restores variables before quests, so resumed edge and gate evaluation sees the loaded values.

Things to know:

  • Major version mismatches may be refused; minor ones are not. The snapshot carries a major ("version") and a minor ("minor_version"). Major version changes are generally reserved for modifications that would break compatibility. A major mismatch means a persisted key changed shape — an error is pushed, nothing is restored, and the save should be treated as unloadable. A minor mismatch only ever means keys were added, so the restore proceeds after a warning and anything the snapshot lacks keeps its default.
  • The snapshot is an interchange format, but restore is not. The captured dictionary is plain data, so you can re-encode it into any container or format you like, and "version" / "minor_version" travel with it. Going the other way, rebuild the dictionary and hand it to restore(): reconstructing state by calling individual setters is not supported. There is no "set quest state", the objective flags and activation order round-trip only through the snapshot, and secondary flow players can only be rebuilt by the quest instance that owns them.
  • restore() returns what it did. RESTORED, RESTORED_WITH_MINOR_MISMATCH, REFUSED_MAJOR_MISMATCH (nothing restored — the managers still hold whatever they had), or SUBSYSTEMS_MISSING. The refusal is also pushed to the log, but the return value is what lets you react in your UI. To decide before committing to a load, compare the snapshot's "version" against NarratyrSaveState.CURRENT_MAJOR_VERSION.
  • Editing a quest's objectives does not invalidate its saves. Objective state is restored by objective id, so adding, removing, or reordering objectives in Narratyr is safe. An objective in the save that the quest no longer has is skipped with a warning; one added since the save starts at its authored state.
  • Reordering an enumeration does not invalidate its saves either. An enum-typed variable persists its entry key, not the enumeration's integer, so a saved value follows its key. An entry the enumeration no longer has is skipped with a warning and the variable keeps its current value; for a list or set, one unresolvable entry leaves the whole collection alone rather than silently dropping members. Renaming or deleting an entry is the case that costs you a saved value — reordering and appending are free.
  • Restoring an active quest creates new flow-player nodes and emits quest_flow_player_ready before each one resumes. Manager-level bindings therefore rebuild themselves.
  • A standalone flow player is not part of the quest snapshot. Store its capture_state() dictionary yourself and call restore_state() after restoring Narratyr variables.
  • A new game normally calls VariableManager.reset_to_defaults() and QuestManager.reset_all(). The quest reset is intentionally silent, so refresh quest UI yourself.
  • JSON is convenient for the exported value types. If your game uses var_to_bytes(), a custom Resource, or another save format, the captured dictionary can live inside that format instead.

Localization, interpolation, and rich text

The runtime translation source is narratyr_data/translations.csv. Its stable keys cover dialogue text, choice labels, quest and objective text, enumeration display labels, translatable entity fields, and graph metadata.

After Godot imports the CSV and you add its generated .translation resource in Project Settings, use normal tr() lookups. Graph node dictionaries retain the source string, so use it as a fallback when a translation is missing:

func localized_field(item: Dictionary, field: String) -> String:
    var fallback: String = item.get(field, "")
    var key := "%s.%s" % [item.get("id", ""), field]
    var translated := tr(key)
    return fallback if translated == key else translated

For a quest, NarratyrQuestInstance already provides this behavior:

var quest := QuestManager.get_quest_instance(quest_id)
var localized_line := quest.localized_field(dialogue_node, "text")
var displayed_line := VariableManager.interpolate_variables(localized_line)

It also has get_quest_name(), get_quest_description(), get_objective_name(), get_objective_description(), and get_objective_group_name(). QuestManager.get_quest_definition() returns a localized snapshot suitable for building a quest log.

Localization and variable interpolation are separate operations. Translate first, then call interpolate_variables() so every locale can position its @{Var} placeholders naturally.

For a choice, the translation key is "%s.label" % path["slot_id"]. Use the raw choice_label as its fallback, following the same pattern as the helper above.

Rich text. Set the project's rich text format to Godot (BBCode) in Narratyr's project settings. Exporting a project configured for another engine's markup produces a warning. Present the resulting text in a RichTextLabel with BBCode enabled; fonts, colors, and theme resources remain under your game's control.

NarratyrLocalization/translator-source.csv and translator-source.xlf are richer handoff files for translators. They include speaker, graph, and role context. The folder contains a .gdignore, so Godot does not import these files as runtime resources.

Editor tooling

Enabling the Narratyr plugin adds a variable viewer and a graph-id Inspector control.

RPG Variables panel

The RPG Variables bottom panel shows every variable's name, type, and value. Outside a play session it reads authored defaults from variables.json. During a debug session it receives live values from the variable-manager autoload; click Refresh to request a new snapshot.

The current panel is a viewer rather than an editor. Change values through the running game, the Remote scene tree, or your own debug commands and watch the panel update.

If it stays on defaults while the game is running, confirm that the variable-manager autoload is enabled and that you launched the game from an editor debug session.

Graph-id dropdowns in the Inspector

The plugin can turn an exported String property into a graph-name dropdown while storing the stable graph id. Mark the property with an export placeholder:

@export_placeholder("narratyr_graph_id")
var any_graph_id: String

@export_placeholder("narratyr_graph_id:dialogue")
var conversation_id: String

@export_placeholder("narratyr_graph_id:quest")
var quest_id: String

The optional suffix filters the list to dialogue or quest graphs. The choices come from graph_index.json, so export data again after adding or renaming graphs. If a referenced graph disappears, the Inspector preserves its id and displays it as missing rather than silently clearing the property.

Iterating

The day-to-day loop is: change content in Narratyr, click Export Data, and return to Godot. Let Godot finish its filesystem scan before running if JSON, translations, or copied assets changed.

Re-run Install Addon after a Narratyr update that ships runtime changes. Because the addon is GDScript, Godot reparses it; there is no native build step. Re-enable the plugin if your project settings had it disabled, but a normal reinstall does not change the plugin's path.

Exports are derived artifacts. Teams can commit addons/narratyr_data/, narratyr_data/, and the relevant Godot import metadata, or regenerate them as part of a build step. Whichever policy you choose, keep the Narratyr source project as the authority.

From the command line

The narratyr CLI runs the same exporter headlessly:

narratyr export --engine godot --operation data \
  --project /path/to/MyStory.ntproj \
  --out /path/to/MyGodotProject \
  --source-locale en

--operation accepts install, data, or both. --json emits a machine-readable result for CI. The CLI reads the license installed by the desktop app, so the same entitlements apply.

Troubleshooting

“Exporting is disabled.” The free tier caps how many graph nodes a project can export. The message names the cap and your project's node count — activate a Pro licence or reduce the node count.

The addon or data does not appear in Godot. Confirm the export path is the directory that contains project.godot, then wait for Godot's filesystem scan. An addons/ folder beside, rather than inside, the project is a sign the path was one level off.

The Narratyr plugin will not enable. Run Install Addon again and inspect the first GDScript parse error in Godot's Output panel. Also confirm the project is Godot 4, not Godot 3.

An autoload reports a name conflict. Its autoload name matches the script's class_name. Use VariableManager, GraphRegistry, QuestManager, and EntityRegistry, not the longer Narratyr… names.

A manager says its JSON file is missing. Run Export Data, confirm narratyr_data/ is at the project root, and restart the running game so the autoload reloads its defaults.

Text displays a key such as abc123.text. Add the imported source .translation resource under Project Settings → Localization → Translations. For user-created lookup helpers, fall back to the raw node field when tr(key) == key.

A flow stops and nothing happens. Make sure the UI connected before play() or through quest_flow_player_ready. Also bind flow_error: a DeadEnd means the node had routes but none were passable; a custom node may be authored as blocking and need continue_flow().

A listener never fires. Listener conditions cannot call game functions and are re-evaluated on observable writes. Mirror the relevant state into a Narratyr variable and write it through VariableManager.

A gate never opens. If its condition depends on state outside the variable and quest managers, call reevaluate_gate() after that state changes.