Skip to content

Wait for SDK Ready

QuestData runs as a Godot autoload and finishes its sync init in _ready(). If your own autoloads call into the SDK from their _ready(), Godot’s autoload order decides who runs first — and you may race against the SDK. This page shows the one pattern that eliminates that race.

# In your own autoload or any Node._ready()
func _ready() -> void:
if not QuestData.is_ready():
await QuestData.sdk_ready
# From here on it's safe: all sub-modules initialized, player-ID known
QuestData.track("game_start", { "version": "1.0.0" })

…or the one-liner via the idempotent helper:

func _ready() -> void:
await QuestData.await_ready()
QuestData.track("game_start", { "version": "1.0.0" })

await_ready() returns immediately when the SDK is already up, otherwise it blocks until the sdk_ready signal fires. Either form is fine — pick what reads better in your codebase.

QuestData’s _ready() does a non-trivial amount of work:

  1. Config load — parsing quest_data/api_key, api_url, feature flags from ProjectSettings
  2. Player-ID resolve — disk read of the persistent user://quest_data_player_id file
  3. HTTP / Timer / Logger setup — child nodes added to the scene tree
  4. Game-data cache rehydrate — loading the last known balancing tables from disk so the game has values even when offline
  5. session_start event — first event of the session

If your autoload sits before QuestData in the autoload order (or both run in the same frame and yours happens to win), API calls like track(), bind_balancing(), get_game_data() will hit a half-initialized SDK. In debug builds this crashes hard via assert(false, …) so you catch the bug on the first run. The fix is always the same: wait for sdk_ready.

Code paths that always run after frame 0 are already safe — the SDK has finished initializing by then:

  • _process(delta), _physics_process(delta)
  • _input(event), _unhandled_input(event)
  • Signal handlers connected at runtime (button clicks, timer timeouts, networking callbacks)
  • Anything called from gameplay scripts that load after the main scene boots

The pattern is only needed at the boundary: autoload _ready(), the first frame of the main scene, or any code that may race the SDK’s init.

When a bug report says “events aren’t tracked”, ask the reporter to log the SDK’s init section:

print(JSON.stringify(QuestData.dump_state().sdk_ready, " "))

Sample output:

{
"is_ready": true,
"sdk_ready_emitted": true,
"init_completed_at": 1731504732.451,
"seconds_since_init": 47.2,
"sub_modules_initialized": {
"config": true,
"player": true,
"session": true,
"http": true,
"event_queue": true,
"game_data": true,
"balancing": true
}
}

If is_ready is false or any sub_modules_initialized.* flag is false, the SDK never finished its boot — likely an autoload-order issue or a fatal config error. Open the Godot Output + Debugger → Errors panels for the underlying cause.

If you register Resource-based balancing overrides via bind_balancing() / bind_balancing_pivot(), the SDK silently fails on typos in table, id_column, row_id, pivot_key_col or pivot_value_col — your .tres defaults stay active and you wonder why nothing changes.

After sdk_ready and a 5-second grace window, dump_state().balancing.bindings_with_issues lists every problematic binding:

print(JSON.stringify(
QuestData.dump_state().balancing.bindings_with_issues, " "
))

The same diagnostic also fires as push_warning(...) at bind-time when the referenced table is already cached — you’ll see the warning in the Godot console the moment you call bind_balancing() with bad arguments. Categories: table_not_in_schema, column_not_found, row_not_found, never_applied.