Skip to content

Remote Config & Game Data

Change your game’s behavior and balance data remotely — no app update required. Remote Config handles feature flags and key-value settings. Game Data provides full balance tables (items, enemies, levels) with local caching.

QuestData.fetch_remote_config()

Fetches the latest config values from the server. The SDK automatically includes the player’s player_id so segment-specific overrides are applied.

Config values are cached locally (to disk) and available offline after the first successful fetch.

QuestData.get_config(key: String, default_value: Variant) -> Variant
ParameterTypeDescription
keyStringConfig key name
default_valueVariantReturned if key doesn’t exist or config hasn’t been fetched yet

Returns: The config value, or default_value if not found.

QuestData.get_all_configs() -> Dictionary

Returns: All config key-value pairs as a Dictionary.

func _ready():
# Fetch latest config on game start
QuestData.fetch_remote_config()
func get_difficulty_multiplier() -> float:
return QuestData.get_config("difficulty_multiplier", 1.0)
func is_holiday_event_active() -> bool:
return QuestData.get_config("holiday_event", false)
func get_max_lives() -> int:
return QuestData.get_config("max_lives", 3)

Configs can have different values per player segment. For example, “VIP” players could get max_lives = 5 while everyone else gets 3. Set up overrides in Configuration > Remote Config in the dashboard.

Tag players using set_user_tag() (see Players & Segments) and the SDK fetches the correct config values automatically.

Remote Config integrates with the Experiments system. When a player is assigned to an experiment variant, the config values for that variant are merged into the response. No SDK changes needed — just call get_config() as usual.


Game Data provides full balance tables — structured data like item stats, enemy configurations, or level definitions. Think of it as a spreadsheet your game can read at runtime.

QuestData.get_game_data(table_name: String, callback: Callable = Callable(), force_refresh: bool = false)
ParameterTypeDefaultDescription
table_nameStringrequiredName of the data table
callbackCallableCallable()Called with Array of row Dictionaries
force_refreshboolfalseSkip cache and fetch from server

The callback receives an Array of Dictionaries, where each Dictionary is a row from the table.

QuestData.get_game_data_cached(table_name: String) -> Array

Returns cached data synchronously. Returns an empty Array if the table hasn’t been fetched yet.

QuestData.preload_game_data(table_names: Array, callback: Callable = Callable())

Fetches multiple tables in parallel. The callback fires when all tables are loaded.

QuestData.clear_game_data_cache()

Clears all cached game data, forcing a re-fetch on next access.

# Preload all balance tables on game start
func _ready():
QuestData.preload_game_data(["weapons", "enemies", "levels"], func():
print("All game data loaded!")
start_game()
)
# Read weapon stats
func get_weapon(weapon_id: String) -> Dictionary:
var weapons = QuestData.get_game_data_cached("weapons")
for weapon in weapons:
if weapon.get("id") == weapon_id:
return weapon
return {}
# Fetch with callback (first time or force refresh)
func refresh_enemies():
QuestData.get_game_data("enemies", func(rows: Array):
for enemy in rows:
print(enemy["name"], " - HP: ", enemy["hp"])
, true) # force_refresh = true

Live Balancing takes Game Data one step further: instead of reading rows manually, you bind a Resource directly to a table row. The SDK updates your resource’s @export properties in-place whenever the table changes — at boot, on demand, or pushed live from the dashboard via WebSocket.

QuestData.bind_balancing(resource: Resource, table: String, row_id: String = "", id_column: String = "id") -> void
ParameterTypeDefaultDescription
resourceResourcerequiredThe Resource instance whose @export properties will be overridden
tableStringrequiredName of the game data table
row_idString""Row to match against. If empty, reads the resource’s id property automatically
id_columnString"id"Column name used to find the matching row

Registers the resource for live overrides and immediately applies current cached values (or triggers a fetch if not yet cached). Safe to call in _ready().

var hut := preload("res://buildings/hut.tres") as Building
QuestData.bind_balancing(hut, "buildings") # reads hut.id automatically
QuestData.unbind_balancing(resource: Resource) -> int

Removes all bindings for resource. Returns the number of bindings removed. Call in _exit_tree() if the resource is scene-local (global resources don’t need this).

QuestData.refresh_balancing(table_name: String = "") -> void

Force-fetches table_name and reapplies overrides to all bindings. When table_name is omitted, refreshes every bound table. Useful after a manual config change in development.

QuestData.get_realtime() -> QDRealtime

Returns the live WebSocket channel. Primarily used for pivot-table consumers that need to react to table changes manually (scalar bindings via bind_balancing are updated automatically).

QuestData.get_realtime().gamedata_updated.connect(func(table, _ver, _act, _env):
if table == "building_costs":
_rebuild_cost_cache()
)
signal QuestData.balancing_applied(resource: Resource, table: String)

Emitted after apply_overrides mutated at least one property on resource. Fires once per resource per update pass — only when something actually changed, never on no-op fetches.

Use this to trigger UI refreshes without polling or table-level routing:

QuestData.balancing_applied.connect(func(res: Resource, _table: String) -> void:
if res == my_building_data:
_refresh_tooltip()
)

  1. fetch_remote_config() sends a GET request with the player’s ID
  2. The server returns configs merged with any segment overrides and experiment variants
  3. Values are cached in memory and persisted to disk (user://quest_config.save)
  4. get_config() reads from the in-memory cache (instant, no network)
  1. get_game_data() checks the local cache first
  2. On cache miss (or force_refresh), fetches from GET /v1/gamedata/:table_name
  3. Response includes rows, columns, and a version number
  4. Data is cached in memory and persisted to disk (user://quest_gamedata.save)
  5. Multiple calls to the same table while a fetch is pending are batched (callback queued)
  • Configuration > Remote Config — Create and edit config keys, set segment overrides
  • Live Ops > Balancing — Edit game data tables with a spreadsheet UI, CSV import, version history
  • Live Ops > Experiments — Create A/B tests that override config values
  1. Always provide a default valueget_config("key", default) ensures your game works even if the config hasn’t been fetched yet
  2. Fetch config early — Call fetch_remote_config() in _ready() or on the loading screen
  3. Preload game data — Use preload_game_data() on the loading screen to avoid mid-game fetches
  4. Use get_game_data_cached() in hot paths — It’s synchronous and free; get_game_data() with a callback is for initial loading