Good writeup — the (len - 2) % 12 rule alone will save people an afternoon. Same rabbit hole, companion .tres side: we generate TileSet resources programmatically and load-test them in real 4.7, and these three cost us time the same way:
-
.tres string fields are parsed with escape sequences. A terrain/resource name containing a quote or backslash must be written as " or \, or the file refuses to load with “Parse Error: Unterminated string”. Anything user-supplied that ends up in a name field needs escaping before you write the text.
-
If you build the TileSet via script instead of writing text: TileData physics must be written AFTER the atlas source is attached with add_source(). Physics layers belong to the TileSet, so before add_source() the TileData sees zero physics layers and collision writes error out (or silently do nothing, depending on the call). Ordering is the whole fix.
-
Terrain peering bits, if you generate autotiles: in “Match Corners and Sides” mode a corner bit (say NE) only participates when both adjacent side bits (N and E) agree. Setting corners without their sides looks valid in the file and simply never matches while painting.
And I can confirm the tile-center physics origin from the end of your post — hit the exact same half-tile offset before figuring it out.






