On this page

Game data: sources and measurements

The research behind the game-data skill, the data runner (ci/godot/lib/Data.ps1) and the culture-parse architecture rule, gathered on 2026-10-21. It draws on Godot's documentation and the 4.7.2 class reference, studio devlogs and talks. Tests ran on Linux x86_64 (4 cores), .NET SDK 8.0.131 (runtime 8.0.31) and Godot 4.7.2 mono and standard, with C# [GlobalClass] Resources saved, edited by hand and reloaded, and a project exported with --export-release Linux. Marks: tested means run here, doc means stated in documentation or a published talk, and unconfirmed means not verified.

Sources

Source What it gave
Godot: resources "Upon exporting a game, Godot serializes resource files as binary files (*.res)"
Godot: exporting projects The include filter "allows non-resource files such as .txt, .json and .csv to be exported"
Godot: troubleshooting physics "Since resources are shared by default..."
GodotSharp.xml (4.7.2), Resource.Duplicate A sub-resource "will only be duplicated if it's local"
Orbitect: spreadsheets and JSON, Balancing with spreadsheets (search summaries) Sheet exported to JSON by script; derived columns stay in the sheet
CastleDB (search summary) A "structured static database": a model per sheet, references between sheets, one line-based JSON file that merges
Scott Bilas, A Data-Driven Game Object System (GDC 2002, Dungeon Siege) Objects assembled from data, ">7300 unique object types"
Hades data tables (Nexus, sgg-mod-format) Data as Lua tables that mods patch by merging
Factorio FFF #55, FFF #47 A checksum of the state every tick; desyncs are "typically just a value of one variable"
Asset Library: Edit Resources as Table 2, CSV Data Importer Editor tools for tables (not tested)
A Sony online consultant, "Crash Course in Online Features for Programmers" (GDC; notes) Balance config from a server: no code in it, the game playable with any local version, no rebalancing mid-mission, its version in saves

Custom Resources (tested)

What Result
Rename an [Export] property (Damage to BaseDamage) and load an old .tres No error or warning, also with --verbose; the C# default (1, was 12). Re-saving drops Damage for good. A removed property behaves the same
Rename the class and file, moving the .cs.uid too, .tres with a uid= Loads as the new class; stale path and script_class are tolerated
The same with a .tres without a uid= Parse Error: [ext_resource] referenced non-existent resource; GD.Load returns null
ResourceSaver.Save from a running game Writes the ext_resource without a uid
Values equal to the default, an empty array Not written to the file
[Export(PropertyHint.Range, "0,100")] int Hp set to 999 by hand Loaded as 999
An enum property set to 7 (no such member); Speed = "fast" on a float Loaded as 7, and as 0, with no error
A missing ExtResource id The whole load fails
Two scene instances whose [Export] Weapon is axe.tres; a.Weapon.BaseDamage += 5 One object: b and a later GD.Load see the change
ResourceLocalToScene = true A copy per PackedScene.Instantiate(); two GD.Load calls still share
Duplicate(true) An external .tres sub-resource stays shared; fields that are not exported are dropped (7 became 0, a List<int> came back empty)
Duplicate(false) Shares the Array<string>: adding to the copy changed the original
DuplicateDeep(Resource.DeepDuplicateMode.All) (4.5+) Copies the external sub-resource too
Cost, 1000 runs each Duplicate(true) of a 1000-string array and a 20-deep chain: 1.47 ms; Duplicate() of 4 fields: about 45 µs; new: about 5 µs
Edit axe.tres on disk, then GD.Load The cached, stale object
ResourceLoader.Load(path, null, CacheMode.Replace) Reloads into the same object; every holder sees the new value. CacheMode.Ignore returns a new object

So: definitions are read-only at run time and per-instance state lives in a plain C# object built from them; hints guard only the inspector; renames need every .tres edited in the same commit and a test that compares each file's keys with the class's stored properties (GetPropertyList() entries with PropertyUsageFlags.Storage).

Data files in an export (tested, default import settings)

File In the project In the exported game
Any .csv Imported as a translation, even a data table (items.csv produced items.name.translation) The file is absent: FileAccess.FileExists false
.csv with importer="keep" (Import As > Keep File) Read with FileAccess.GetCsvLine(), quoted commas handled Exported and readable
.json A JSON resource, also readable with FileAccess Exported and readable, no include filter needed
.txt, .yaml Readable Absent unless the preset's include filter names them
.tres Text Binary behind w.tres.remap; DirAccess lists only the .remap; ResourceLoader.ListDirectory("res://data") lists w.tres and loading by path works

A loader that scans res://data with DirAccess for *.tres works in the editor and finds nothing in the export.

JSON numbers and culture (tested)

Spreadsheets and published practice (doc)

The data runner

data.json in a game names each table's file (JSON or CSV), its id column and its fields; the runner is selected whenever the file exists and needs no Godot. Field rules: type (string, int, number, bool), min, max, enum, pattern (both case-sensitive: Rare is not rare), ref (another table whose ids the value must name), list (a JSON array, or ; between values in a CSV cell), optional and acyclic (on a ref to the field's own table: every loop among the rows is reported as a -> b -> a is a cycle: none of them can come first, at most five, long ones shortened; prerequisite chains, quest order, recipes). Two more checks look across rows. "totals" on a table adds a field up per group and fails a group short of a number or of another table's field: { "sum": "xp", "by": "zone", "atLeast": "zones.xpToNext" } reports zone 'steppes': xp adds up to 900, short of xpToNext 1000 (90%) (Mists of Pandaria shipped zones about 10% short of the next level); a target row without the field needs nothing. "names" at the top level (table, field, files as globs where **/ may match no folder, allow) fails on capitalised words of four letters or more one or two letters from a canonical name, with the file and line (a comic and the game spelled one character Belrysa and Belysra; narrative). A required list field fails when it is empty, so a loot table cannot lose its last drop. A mistyped id, enum value, column or table is reported with the nearest name when one is close (one edit up to six letters, two up to nine; a swap of two letters is one edit). Columns not listed fail unless the table says "extra": "allow". A JSON table is an array of objects or an object keyed by id. A CSV table without importer="keep" in its committed .import fails, since it would not ship. Numbers in CSV cells are parsed with the invariant culture. One JUnit case per table; tests/Test-Data.ps1 checks it against tests/data-fixture.

Dialogue tables ("kind": "responses")

A JSON table of "kind": "responses" holds the rules of Example.Core's ResponseSystem (Valve's dialogue rule database, valve.md item 48), one row per rule; ResponseTable.Load in examples/csharp/src/Example.Core/Narrative/ResponseTable.cs builds the rules from the same file, so what the runner passed is what the game loads:

{ "tables": { "barks": { "file": "data/barks.json", "kind": "responses", "facts": ["lives", "combo"], "voiced": false } } }
Column Rule Required
id Rule.Name, unique yes
who the speaker; absent means anyone no
concept what happened (LifeLost, Idle) yes
criteria text in the grammar below: lives>0;!cheer_cooldown no
lines text, or { "text", "voice" } with the voice file (res:// or relative to the project); one is picked at random yes, at least one
then { "who", "concept" }: the follow-up, looked up when the line ends; no who asks everyone in earshot no
remember [{ "fact", "value", "world", "for" }]: value a number, true, false or a name (1 if absent), world for everyone, for seconds above 0 no
once the rule removes itself after it plays no

The columns come predefined (a table's own fields add more, with "extra": "allow" as usual; ResponseTable.Load takes their names as extraColumns). Besides the field rules the runner fails: a criteria syntax error, with the column ('coach_hurt' criteria column 8: '<' needs a number, not 'lots'); a then naming a concept no rule of any responses table has (with the near name), or sent to a speaker with no rule for it; a voice file that does not exist; with "voiced": true, a line without one; two rules with the same who, concept and criteria in any order (they tie, and one is drawn at random each time: one rule with all the lines says the same); and criteria that no facts can satisfy together (health<10;health>20, weapon=axe;weapon=bow, !seen;seen=1). A fact read in criteria that no rule remembers and the table's facts (those the game supplies: health, the event's facts) does not list is a warning, fact-never-written, with the near name (gag_cooldow for gag_cooldown). Translations sit on each line, { "text": "Next wall.", "locales": { "fr": "Mur suivant." } } or { "fr": { "text", "voice" } }; with "locales": ["fr", "de"] on the table the runner fails a line without a locale's text ('intercom_bye' line 1 has no 'de' text), a locale the table does not list (with the near name), a locale's voice file that does not exist and, with "voiced": true, a locale without one. ResponseTable.Load(json, symbols, locale: "fr") reads a locale's lines and voices; the C# example's barks.json is in English and French. tests/data-fixture has a broken table for each check, examples/csharp/game/data/barks.json a green one that ResponseTableTests plays.

"type": "criteria" checks the same grammar in any table (quest conditions, unlock rules). The grammar, one for both parsers (CriteriaSyntax in ResponseRules.cs, Read-ResponseCriteria in ci/godot/lib/Data.ps1):

criteria  = blank | criterion { ";" criterion }
criterion = blank ( "!" name | name blank op blank value ) blank
op        = "<=" | ">=" | "<" | ">" | "="
value     = number | "true" | "false" | name      after <, <=, >, >=: a number only
number    = [ "-" ] digit { digit } [ "." digit { digit } ]      ASCII digits; no exponent, NaN or Infinity
name      = ( letter | "_" ) { letter | digit | "_" | "." | "-" }
blank     = { " " | "\t" }

!key passes when the fact is missing or 0; true is 1 and false 0; a name is a symbol, equal only to itself; x<n is the interval up to the double just below n. Errors name the 1-based column in the whole text. Both parsers run the 37 cases of examples/csharp/tests/Example.Core.Tests/criteria-examples.json (ResponseTableTests in C#, tests/Test-Data.ps1 in PowerShell) and must give the same criteria and the same message for every error.

Soft-lock levels ("kind": "softlock")

Example.Core's SoftLockCheck (examples/csharp/src/Example.Core/Level/SoftLock.cs, game-playtest section 6) searches every order of play for a state from which the goal can no longer be reached. A JSON table of "kind": "softlock" writes levels down as data, so the data runner runs the same search (Invoke-SoftLockSearch in ci/godot/lib/Data.ps1) for any game, GDScript included, and a C# game loads the file with SoftLockTable.Load (examples/csharp/src/Example.Core/Level/SoftLockTable.cs):

{ "id": "vault", "areas": ["Hall", "Closet", "Exit"], "start": "Hall", "goals": ["Exit"],
  "items": ["key", { "name": "box", "max": 1, "canBeLost": true, "start": 0 }],
  "pickups": [{ "area": "Hall", "item": "key", "count": 1 }],
  "supplies": [{ "area": "Hall", "item": "key", "upTo": 1, "name": "key rack" }],
  "passages": [{ "from": "Hall", "to": "Closet", "name": "closet door", "oneWay": false, "needs": "box", "needsCount": 1,
                 "uses": "key", "useCount": 1 }],
  "everyItemCanBeLost": false, "maxStates": 100000 }

Only areas and goals are required; start defaults to the first area, a count to 1, an item's max to 1. Rules are added as pickups, then supplies, then passages, each in file order, and both searches try actions in that order, so a failure is the same text in both: 'vault' Soft-lock after 2 step(s): pick up key in Hall; use key on closet door, go Hall -> Closet. Stuck in Closet, holding nothing: 2 of 5 reachable states cannot reach the goal. The runner also fails an area or item a rule names that the level does not list (with the near name), a count outside 1 to the item's max, an unknown key on an item or rule, and a level past maxStates. The ten levels of examples/csharp/tests/Example.Core.Tests/softlock-examples.json run in both (SoftLockTests, tests/Test-Data.ps1) and must give the same text, state counts included.

Currencies need a sink ("economy")

A table of money flows per hour, Example.Core's EconomySim rows (a currency, a rate, negative for a sink), names its two fields: "economy": { "currency": "currency", "rate": "perHour" }. The runner fails a currency with a source and no sink (currency 'gems' has no sink: daily_gems, quest_gems pay it in and nothing takes it out, so it only piles up), as EconomySim.WithoutSink does in code; when the currency field is a ref, every currency of that table needs a sink even with no flow at all. Simulating the balance over 10, 100 and 1,000 hours stays EconomySim's (progression).

Placed objects in levels

ci/godot/templates/placement_check.gd, copied into a game's tests, checks every node of the groups ai_point, interactable, zipline, landmark and route in each level against limits read from gameplay code (game-data section 6). tests/Test-Placement.ps1 proves each rule on tests/placement-fixture. Navigation answered under --headless after 9 to 11 physics frames for the first level of a GUT run; the checks took about 10 ms per 1,000 nodes. Source and the talk's numbers: Assassin's Creed Origins: world design data. The landmark and route groups check wayfinding (a landmark that moves or is seen from one stretch only, a route with no landmark ahead on the way back): Stop Getting Lost: Make Cognitive Maps.

Not confirmed