The failure shape to watch for
Nearly every trap on this page has the same signature, and it is worth naming because once you recognise it you will start catching the others yourself:Your own read-back confirms a change that did not take.Not an error. Not a crash. You set a value, you read it back to check, the check passes, and the thing you asked for did not happen:
ProjectSettings.set_setting()returns fine and reads back fine, and the value is gone next run because nothing calledsave().- A feature-tag override is live and driving the engine, while
get_setting()reports the base value and tells you it did not apply. PathFollow2D.progressset before the node is in the tree reads back exactly what you set, and the node never moves.PackedScene.pack()andResourceSaver.save()both return success and write a scene with your nodes missing.FontFile.load_dynamic_font()returns error code 0 having loaded zero faces.
Where user:// actually goes
A save file written to user://save.json lands here:
Note
Godot, not Summer. This is the same surprise as the export-template directory
described on the headless page — our product name is not in the path.
“I wrote a save file and it is not there” almost always ends here.<name> segment comes from config/name in project.godot, so it changes when the
project is renamed. Do not construct the path. Ask the engine:
config/name is AuthoringProbe.
Project settings and feature-tag overrides
project.godot supports per-platform overrides with a feature-tag suffix — difficulty.macos
overrides difficulty when running on macOS. They work. They are also invisible to the most
obvious way of checking them.
Given a project.godot containing:
- Read effective values with
get_setting_with_override(). Use plainget_setting()only when you specifically want the unoverridden base value. - Always define a base key alongside any override. An override on its own leaves the base
key null —
hard.macos=1exists,harddoes not. Anything readinghardgetsnull, includingget_setting_with_override()on a platform that is not macOS.
Localisation: three formats, three rules
Three file formats reach the same place by different routes, and only one of them is comfortable to write by hand..po — hand-writable, loads with no import pass
.po — hand-writable, loads with no import pass
This is the format an agent should use. Write it, load it, done. No import pass, no
sidecar, no Loading it directly returns a real The locale is read from the
.godot/ directory.Translation:Language: header, so a .po file carries its own locale and you
do not declare it anywhere else..csv — needs an import pass, and never loads as itself
.csv — needs an import pass, and never loads as itself
A translation It requires an import pass, and the importer writes sibling files, one per locale:Load the sibling, never the
.csv has a keys column and one column per locale:.csv. See the warning below — this is the silent one..translation — binary, produced by the importer
.translation — binary, produced by the importer
Compiled output, not source. You cannot hand-write one. Write a
file reports it as binary, and the loaded resource is an
OptimizedTranslation rather than a plain Translation:.po, or write a .csv and import it.internationalization/locale/translations project setting.
Both .po and .translation files can be listed:
tr() resolves against whichever is current:
.csv route is the only one here that needs the import pass. Running it is not free and
not always safe — see importing assets for the
--import hazard, which matters if an editor is open. Writing a .po avoids the question
entirely, which is the main reason to prefer it.
Saving scenes from a script
ResourceSaver.save(PackedScene) is the reliable way to produce a .tscn. It has one
requirement that is easy to miss and fails quietly.
Tilemaps: where hand-writing a .tscn stops
An agent can hand-write most of a .tscn. It cannot hand-write a tilemap.
TileMapLayer cells do not serialise as readable text. The entire layer — every cell, its
source, its atlas coordinate and its alternative — is one packed byte array on the node called
tile_map_data. This is what it looks like in a real saved scene:
set_cell() and let the engine write the bytes. This is the whole job:
get_used_cells() on the reloaded scene returns
[(-3, 7), (11, -2)] — coordinates, sources, atlas positions and alternatives all intact.
The tile_map_data byte format
The tile_map_data byte format
Documented so you can read a blob — to diff two scenes, or to check what an editor-produced
layer actually contains. It is not an invitation to write one by hand.A Run against a 26-byte layer holding two cells with distinct sources, distinct non-zero
alternatives and negative coordinates in both axes:Which is exactly the layer that produced it.
<H uint16 format version, measured as 0, followed by 12 bytes per cell:All little-endian. Cell order is insertion order, not sorted. The version field at the head
is the thing to check before trusting any of this on a future build — if it is not
0, stop.A decoder, verified against engine-produced bytes:Ordering and serialisation rules
Two more rules that share the shape of everything else on this page: the operation appears to succeed, or fails opaquely, rather than telling you what is wrong.A Path2D must be in the tree before you position a PathFollow2D
Add the Path2D to the tree, let a frame pass, then set the position:
add_child and the frame, that last line prints (0.0, 0.0).
A Resource defined as an inner class cannot be serialised
Give the resource its own script file with a class_name:
For AI agents
Authoring project data directly works well. These are the parts that will burn you, and as on the headless page, every one of them fails by appearing to succeed:user://is underGodot, notSummer. CallOS.get_user_data_dir()rather than building the path. The<name>segment isconfig/nameand changes with the project.get_setting()hides feature-tag overrides. Useget_setting_with_override()for the effective value. The engine applies the override internally, so a naive check disagrees with reality and tempts you to “fix” a setting that was already right.- Always write a base key next to any override.
foo.macos=1alone leavesfoonull. - Write
.po, not.csv..poloads directly with no import pass.load()on a translation.csvreturns null before and after import — and after import it returns null with no error at all. Load the generatedres://<name>.<locale>.translationsibling instead. - Set the locale explicitly. The default is the OS locale, measured
en_DK, not"en". - Set
owneron every child beforepack(). OtherwiseResourceSaver.savereturns0and writes an empty scene. - Never text-edit
tile_map_data. Callset_cell()and save aPackedScene. The blob is fixed-width binary with no strings to match on, and a misaligned edit yields plausible-looking garbage rather than an error. - Put the
Path2Din the tree before settingprogress. Setting it early is silently ignored while the property still reads back the value you assigned. - Never declare a
Resourceas an inner class. It saves with error0and reloads with its script and all exported properties gone. Give it a file and aclass_name.

