Skip to content

Repository files navigation

Blobsmith Autotile Wirer

A Godot 4 editor plugin that turns a flat autotile sheet (PNG) into a fully wired TileSet resource: a terrain set, the correct terrain peering bits on every tile, and optional collision — in one click or one GDScript call.

License: MIT Godot 4 GDScript

Wiring a blob autotile in Godot by hand means clicking the peering bits for 47 tiles, one neighbor at a time, without getting a single mask wrong. This plugin does the whole pass from the tile's position in the sheet: it knows which of the 47 canonical blob masks each cell represents, and sets the matching terrain bits, terrain mode and collisions automatically.

The companion Blobsmith pixel-art tool painting a 47-blob sheet

Above: the separate Blobsmith pixel-art tool used to paint a 47-blob source sheet. This plugin is the next step — it takes such a sheet and wires it into a Godot 4 TileSet.

Free tileset pack — no install, no account, MIT

⬇ Download the starter pack (16 wired TileSets, 200 KB) · release notes · browse the files

Four terrains — grass, stone, sand, water — each at 16px and 32px, in both of Godot 4's terrain layouts, each shipped as a .png sheet plus an already-wired .tres. You do not need this plugin, or any plugin, to use them: drop both files of a pair into your project, add a TileMapLayer, set its TileSet, and paint in the Terrains tab.

The eight 32px TileSets of the starter pack, painted by Godot's terrain painter — 47-blob on top, 16-tile Match Sides below

Match Corners and Sides (47 tiles, 8×6) Match Sides (16 tiles, 8×2)
Grass grass_47blob_16px · grass_47blob_32px grass_16sides_16px · grass_16sides_32px
Stone stone_47blob_16px · stone_47blob_32px stone_16sides_16px · stone_16sides_32px
Sand sand_47blob_16px · sand_47blob_32px sand_16sides_16px · sand_16sides_32px
Water water_47blob_16px · water_47blob_32px water_16sides_16px · water_16sides_32px

Pick the 47-blob files for ground that meets other ground (they resolve diagonals); pick the 16-tile files for pipes, fences, wires, roads and cave walls, where a diagonal touch should not connect — and where you only have to draw a third as many tiles when you replace this art with your own. Each .tres declares its own terrain mode, so you do not have to set it.

Every tile carries its terrain peering bits and a collision polygon. Each of the sixteen was loaded headless in Godot 4.3, 4.4 and 4.7 stable, painted with set_cells_terrain_connect and read back — 216 checks, 216 passing on each of the three (verify_starter_pack.gd, which reads each file's tile count and terrain mode from the pack manifest rather than assuming one shape). It is procedural placeholder art, not a hand-drawn asset pack: use it to prototype, to learn how Godot terrains behave, and as a known-correct reference when your own sheet paints the wrong tile. Details and per-file checks: examples/starter-pack/README.md.

New: side-view platformer that runs on F5 — examples/platformer/: 47-tile blob ground that connects itself, three one-way platform tiles on their own physics layer, and a player.gd that walks, jumps up through platforms and drops down through them (hold down + jump). 16px and 32px TileSets plus a painted demo level. The real player.gd is driven through the real demo scene in Godot 4.3, 4.4 and 4.7 — 22/22 checks on each, with controls (one-way switched off: the jump hits the plank from below). ⬇ Download the platformer pack (65 KB) · browse the files · why platforms get their own layer: docs/why-you-cannot-drop-through-the-one-way-tile.md.

New: isometric, already a diamond — examples/isometric/: eight isometric 47-tile blob TileSets — grass, stone, sand, water, 16px (32×16 cell) and 32px (64×32 cell) — with tile_shape Isometric and tile_layout Diamond Right set, terrain peering bits renamed to the isometric sides and corners, and a diamond collision polygon instead of the bounding box. Checked in Godot 4.3, 4.4 and 4.7 — 128/128 on each, including painting the same cells on the square starter pack and demanding the same tile (the check that catches a peering rename rotated by one step). ⬇ Download the isometric pack (189 KB) · release notes · browse the files · why a hand-made isometric TileSet paints a rectangle: docs/why-my-isometric-tilemap-is-not-a-diamond.md.

New: roads, fences, pipes and streams — line tiles that connect — examples/lines/: eight line-connector TileSets — road, fence, pipe, stream, 16px and 32px — each a Match Sides terrain with a tile for all 16 side combinations (post, dead end, straight, corner, T, cross), transparent outside the line so it sits on its own layer over your ground; fences and pipes collide as post + one bar per connected side. The trap it documents: Connect joins two parallel fences into a ladder, even painted one call per fence — Path keeps them apart. Checked in Godot 4.3, 4.4 and 4.7 — 120/120 on each. ⬇ Download the roads/fences/pipes pack (74 KB) · release notes · browse the files

a road network, a fenced pen, a stream and a pipe run; and two parallel fences painted with Connect (a ladder) and with Path

New: hexagon terrains, pointy-top and flat-top — examples/hex/: sixteen hexagon TileSets — grass, stone, sand, water, pointy-top and flat-top, 16px and 32px — each a Match Sides terrain with a tile for all 64 side combinations, the right tile_offset_axis set, and a hexagon collision polygon. Hex peering-bit names change with the offset axis (bottom_right_side is a different neighbour pointy-top and flat-top), so each file is wired for its own axis. Checked in Godot 4.3, 4.4 and 4.7 — 256/256 on each, including every side of every cell of a painted island against get_neighbor_cell. ⬇ Download the hex pack (399 KB) · release notes · browse the files

a pointy-top grass island and a flat-top water island, painted by Godot

New: top-down dungeon walls that draw their own faces — examples/dungeon/: four 3/4-view dungeon TileSets — stone brick walls on flagstone and rough rock walls on dirt, 16px and 32px — where the walls are a 47-tile blob terrain and every wall tile with an open south side draws its front face. Lay the floor, paint walls over it with Connect, and the faces land on exactly the walls with floor below them; walls carry a full-cell collision square, the floor none. Checked in Godot 4.3, 4.4 and 4.7 — 64/64 on each, including 364 painted wall cells whose bits must match their neighbours and a face check read from the pixels of the tile the engine picked. ⬇ Download the dungeon pack (170 KB) · release notes · browse the files

the same dungeon room in the stone and the cave style, painted by Godot

New: Match Corners — the third terrain mode, 16 tiles instead of 47 — examples/corners/: eight Match Corners TileSets — grass over soil, sand over water, snow over rock, lava over basalt, 16px and 32px — where the terrain bits are the four corners of the cell, so a complete set is 2⁴ = 16 tiles and not 47. The tiles are opaque, so one TileMapLayer is enough: the lower terrain is painted by the same set. Checked in Godot 4.3, 4.4 and 4.7 — 136/136 on each. ⬇ Download the corner-terrain pack (89 KB) · release notes · browse the files · why terrain does not connect across two layers: docs/why-terrain-does-not-connect-across-two-tilemaplayers.md.

the four corner terrains painted by Godot

New: a painted field that does not look tiled — examples/variants/: eight Match Sides TileSets — grass, stone, sand, water, 16px and 32px — with 19 tiles instead of 16. The three extra tiles are decorated copies of the interior tile that declare the same four peering bits, so set_cells_terrain_connect treats them as interchangeable and picks among them at random, weighted by each tile's probability (plain 1.0, each decorated one 0.35). That is a Godot 4 feature almost nobody wires. The gate paints a 24×24 square and counts the 400 interior cells: all four tiles turn up, the plain one takes the 48.8% its weight asks for, setting the decorated tiles to probability = 0 collapses the field back to one repeated tile, and set_cell ignores probability entirely. Checked in Godot 4.3, 4.4 and 4.7 — 128/128 on each. Every decorated tile keeps its outer ring of pixels identical to the plain one — the generator refuses to write one that does not — because a variant is only interchangeable if it meets its neighbours the same way. ⬇ Download the tile-variants pack (71 KB) · release notes · browse the files

two panels of the same grass tileset painted by Godot: on the left the decorated tiles are at probability 0 and the field is one tile repeated; on the right the engine mixes in pebbles, flowers and tufts

New: solid collision the physics server confirms — plus a checker for YOUR TileSet — examples/collision/: eight solid Match Sides TileSets — rock, brick, ice, wood, 16px and 32px — every one of the 16 tiles carrying a physics layer, a non-zero collision_layer and a full-cell polygon. The gate does not count polygons in the .tres: it paints the tiles into a live TileMapLayer and asks PhysicsDirectSpaceState2D — a dropped ray stops at the cell's top edge, the seam between two cells is solid, a one-cell hole really is empty, erasing a cell drops its collider, and collision_enabled = false removes every collider while the TileSet stays perfectly wired. Checked in Godot 4.3, 4.4 and 4.7 — 112/112 on each. The pack also ships check_tileset_collision.gd, which runs headless over your TileSet and names the four faults that look wired and collide with nothing: no physics layer, collision_layer = 0, a tile with no polygon, a polygon with fewer than 3 points. ⬇ Download the solid-collision pack (93 KB) · release notes · browse the files

a rock platform run, a brick wall with a hole and an ice floor with a pit, each outlined where the physics server says its collision ends

New: a floor the navigation server paths across the moment it is painted — plus a checker for YOUR TileSet — examples/navigation/: eight top-down floor + wall TileSets — dungeon, meadow, deck, cave, 16px and 32px — one Match Sides terrain set with a Floor and a Wall terrain, 17 tiles: 16 floor tiles each carrying a full-cell navigation polygon on a layer whose bitmask is 1 (what a NavigationAgent2D asks for by default), authored centred (-8,-8)..(8,8) and not (0,0)..(16,16), and one wall tile carrying collision and no navigation. The gate paints an 11×9 room with an inner wall into a live TileMapLayer and asks NavigationServer2D, only after map_get_iteration_id() has moved: one region per floor cell and none for the walls, a path that goes around the wall with not one sample inside a wall cell, endpoints at the exact centres of the start and goal cells, and — with the gap painted shut — a path that comes back shorter, not empty, so if path.is_empty() never fires. It also measures the two ways to paint it wrong from code: leaving out the 4th argument of set_cells_terrain_connect (default ignore_empty_terrains = true) painted 20 of the room's 99 cells, and walls-before-floor leaves 21 floor cells without their edge shading. Checked in Godot 4.3, 4.4 and 4.7 — 168/168 on each. The pack also ships check_tileset_navigation.gd, which runs headless over your TileSet and names the faults that leave an agent standing still or walking through walls: no navigation layer, a layers bitmask of 0, a floor tile with no polygon, a polygon that builds nothing, a polygon authored from (0,0) (half a tile off), a wall carrying a navigation polygon. ⬇ Download the walkable-floor navigation pack (192 KB) · release notes · browse the files

the same room painted four times by Godot, with the path NavigationServer2D returned drawn over it: around the inner wall, straight through a door, and stopping short against a closed gap

New: tiles that say what they are — move cost, walkable, damage, footstep — plus a checker for YOUR TileSet — examples/tiledata/: eight top-down ground TileSets — meadow, volcano, swamp, snow, 16px and 32px — one Match Sides terrain set with six ground kinds (path, ground, rough, slow, hazard, blocked), every tile carrying five custom data layers already filled in: ground (String), move_cost (float, never below 1.0), walkable (bool), damage (int) and footstep (String) — every value written, the falses and 0s included, because Godot reads a value never written as the type's default, the same as a real 0. The gate paints a 16×10 map into a live TileMapLayer, reads every cell back through get_cell_tile_data().get_custom_data(), and feeds an AStarGrid2D from it: the path takes the road over the bridge at the true minimum cost (a Dijkstra over the same data agrees), while the same grid fed walkable only cuts across 3 hazard cells. It also measures the ways it goes wrong without a word: local_to_map(global_position) on a moved, scaled layer found the right cell for 0 of 160 cells, a tile added after the layers reads ""/0.0/false, a move_cost of 0.0 is accepted and the path stops being the cheapest, a misspelt layer name returns null, the grid is a copy that keeps routing over a bridge you painted shut. Checked in Godot 4.3, 4.4 and 4.7 — 161/161 on each. The pack also ships tile_data_example.gd (the ground under a position, and an A* grid from the tiles) and check_tileset_custom_data.gd, which runs headless over your TileSet — and, with --scripts, your code — and names the faults that read back wrong: no custom data layer, a layer with no type or no name, a value that loaded as null, a tile added after the layers were filled, an empty String, a layer name your code asks for that no TileSet declares. ⬇ Download the tile custom-data pack (186 KB) · release notes · browse the files

the same 16 x 10 map painted four times by Godot — meadow, volcano, swamp, snow — with the path an AStarGrid2D fed the tiles' custom data returned (round the hazard, over the bridge) and the one it returns fed walkable only (through the hazard and the ford)

New: walls that cast 2D shadows the moment they are painted — plus a checker for YOUR TileSet and scene — examples/occluder/: eight top-down floor + wall TileSets — dungeon, crypt, forest, scifi, 16px and 32px — one Match Sides terrain set with a Floor and a Wall terrain, 17 tiles: 16 floor tiles with no occluder, and one wall tile carrying a full-cell occluder on an occlusion layer whose light_mask is 1 (what a PointLight2D's shadow_item_cull_mask ships with), authored centred (-8,-8)..(8,8), plus full-cell collision. One file works on 4.3, 4.4 and 4.7: the occluder is stored under the key 4.3 writes, which 4.4 and 4.7 still read — the key 4.4+ writes (occlusion_layer_0/polygon_0/polygon) loads on 4.3 as no occluder and no error, and re-saving the TileSet from 4.4+ (ResourceSaver.save) switches to it. Headless, the gate reads every tile back through the running version's API (get_occluder on 4.3, get_occluder_polygons_count/get_occluder_polygon on 4.4+) and finds the occluder on 44 of 44 wall cells and 0 of 73 floor cells of a painted room. The shadow itself cannot be seen headless (the dummy renderer draws nothing), so the gate also renders the room on a real OpenGL context and compares every floor pixel with line of sight from the light: 17,989 of 17,989 agree at 16px, 73,355 of 73,355 at 32px. It measures the traps too: shadow_enabled off (the default), an occlusion light_mask that shares no bit with shadow_item_cull_mask, no occlusion layer — 0 dark pixels each — and the 4.4 receiver rule (the floor's own light_mask is filtered too: 3,384 dark pixels on 4.3, 0 on 4.4 and 4.7). Checked in Godot 4.3, 4.4 and 4.7 — 181/181 on each (136 headless + 45 rendered). The pack also ships check_tileset_occluders.gd, which runs headless over your TileSet or scene and names the faults that leave a light shining through walls: no occlusion layer, a light_mask of 0, a wall with no occluder, a polygon authored from (0,0), the 4.4+ key in a project that must open in 4.3, a light with shadows off or no texture, a mask mismatch, occlusion_enabled off, the 4.4+ receiver mask. ⬇ Download the light-occluder pack (301 KB) · release notes · browse the files

the same room with a pillar rendered four times by Godot — dungeon, crypt, forest, scifi — each lit by one PointLight2D, with the hard shadow the wall tiles cast fanning out behind the pillar

New: animated water, every tile moving — examples/animated-water/: a 47-tile blob water-over-grass terrain where all 47 tiles are animated (4 frames, 0.2 s each), laid out so Godot accepts the animation — frame cells are not tiles, animation_columns set, mode DEFAULT so ripples line up across tile edges. 16px and 32px TileSets plus a painted pond scene. Checked in Godot 4.3, 4.4 and 4.7 — 23/23 on each: Connect still picks the canonical tile for every cell, and the frame on screen is read back from rendered pixels (0, 1, 2, 3, 12 rendered frames each), with a one-frame control that fails both. ⬇ Download the animated water pack (224 KB) · release notes · browse the files · why animated tiles freeze: docs/why-my-animated-tile-does-not-animate.md.

New: terrain transitions, no seam — examples/transitions/: grass over sand, sand over water (shoreline), stone over grass (paths), water over grass (ponds), grass over stone (overgrown floors), dirt over grass (dirt roads), grass over dirt (tilled ground), snow over grass (winter maps) and lava over stone (volcanoes, dungeons), eighteen wired TileSets (16px, 32px) with two terrains in one terrain set, so Connect paints one over the other and every edge and corner between them matches. Painted headless in Godot 4.3, 4.4 and 4.7 — 144/144 checks on each, with a control showing the same paint with a one-terrain set leaves 188 mismatched edges per set. ⬇ Download the transitions pack (18 wired TileSets, 516 KB) · release notes · browse the files. The zip is unpacked into an empty project and repainted in 4.3 and 4.7 on every suite run, so the download is checked, not just the folder.

Want the four terrains in ONE TileSet? Godot 4 can add a source to a TileSet you already have open, but nothing in the engine takes two .tres TileSet resources and gives you a third — so the usual answer is to paste them together in a text editor. That is the dangerous version for these files in particular: every .tres in this pack declares its atlas as [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_bsmith"], so a hand-pasted file has two sources/ entries resolving to the same sub-resource — one terrain is silently gone, in a file that loads with no error and no warning. https://blobsmith.lbwma.com/godot-tileset-merge/ does it in the browser: drop the files, get one TileSet with every terrain in it, ids renumbered, physics and custom-data layers deduped, terrain sets grouped by mode — plus the new source id and terrain index for each input, which is what the TileMapLayers you already painted are storing. Checked against this repository: three starter-pack terrains merged and loaded in Godot 4.3 and 4.7, each input also loaded alone so the engine's own reading is compared tile by tile, and the hand merge built and loaded too, to show it losing the terrain — 107 assertions (tres-merge, web-tres-merge).

Merging them yourself, in a script or by hand? What actually has to be remapped is the list: add_source(other.get_source(id)) is a move that empties the donor, duplicate(true) keeps both but copies every index unchanged — and a terrain set, a custom data layer or a physics layer is only meaningful inside the TileSet that owns it, so the number survives while the meaning changes. 22 checks, run them on your own build with docs/verify_tileset_merge.sh /path/to/godot; identical results on 4.2, 4.3, 4.4 and 4.7.

Coming from Godot 3 — Tilesetter, TilePipe2, or your own old project? Your Godot 3 tileset does not open empty in Godot 4 — it opens without the autotile. Godot 4 does have a compatibility path for format=2, and it is not the "no importer at all" the forums say: the sources, the texture and the single tiles arrive. Every autotile arrives as a source with zero tiles and margins (0,0), so even its region is gone, and the engine says so once, as a WARNING you probably never scrolled to. Two more things it does not say: a collision shape shared by several tiles is re-origined once per sharer, so the second tile's collision sits half a tile off and the third a whole tile off; and the source ids are re-issued, so a painted map is pointing at numbers that moved. 15 checks on your own build with docs/verify_godot3_tileset.sh /path/to/godot; identical on 4.3, 4.4 and 4.7.

node docs/convert_godot3_tileset.js old_tileset.tres does the conversion the engine skips — geometry exactly (the two region formulas are the same expression), bitmasks canonicalised into Godot 4 terrain bits, tile ids kept as source ids, each polygon re-origined from the tile that owns it — and reports every mask it had to change and every pair that collapsed onto one neighbourhood, by tile and subtile coordinate. It refuses instead of guessing on occluders, navigation polygons, priority maps, rotated shapes and tex_offset. No terminal? The identical rules run in the browser: https://blobsmith.lbwma.com/godot-3-tileset-to-godot-4/

The sheet is yours and you do not want a plugin? The same wiring runs in the browser: https://blobsmith.lbwma.com/godot-tileset-tres-generator/ — drop the PNG, get the .tres back. It reads the tile size off the image instead of asking (the classify_sheet pass from this repo, ported), and names the ambiguous readings rather than picking one. Before offering the download it scans the alpha of every slot and names the wired ones that came back fully transparent — a blank cell in a wired sheet is the failure the editor never reports: no error, no gap, just a hole that paints wrong later. What it cannot wire it refuses (wrong column count, missing rows, a tile size that does not divide the sheet) instead of writing a .tres that opens and misbehaves. Checked against this repository, not against itself: the generator reproduces all 16 starter-pack .tres byte for byte — both terrain layouts, and the page hands a stranger those same bytes: 164 assertions (test/web-tres-generator.test.js), the download stages in a real browser over HTTP, including a sheet with three slots cleared by hand and a 3x3 sheet that has to be refused. The list of files it must reproduce is read from the pack's manifest.json, so a tileset added to the pack cannot skip the comparison — that is how the eight 16-tile files came under it the day they were published.

TileMap is deprecated — convert the whole project

⬇ Download the converter (2 scripts, no dependencies, 29 KB) · release notes · or run them out of docs/ after cloning.

Node 18+, MIT, reads and writes only your .tscn and .gd files — no engine, no install, no network. The two blocks below are what they do, and what they refuse to do.

TileMap is deprecated since Godot 4.3, and the editor converts one scene at a time, by hand, and only scenes it can open. node docs/convert_tilemap_to_tilemaplayer.js /path/to/project converts the whole project in one pass: every TileMap becomes a Node2D with one TileMapLayer child per layer, layer_N/tile_data is re-encoded into tile_map_data, and every block it does not own comes back byte for byte. It writes nothing without --write, keeps a .tscn.bak when it does, and refuses rather than guesses — a script on the node, an unknown layer_N/ key, a duplicate layer name, an unchecked format. Over Godot's own demo projects at branch 4.2 (298 scenes): 14 nodes and 3 427 cells converted, 3 refused, all three carrying scripts. The engine reads both versions back cell by cell in docs/verify_tilemap_convert.sh — 640 checks green across 4.3, 4.4, 4.7 and a 4.2 → 4.7 cross-version pass. No terminal? The same file runs in the browser: https://blobsmith.lbwma.com/godot-tilemap-to-tilemaplayer/ — paste a .tscn, get the converted scene back. Full write-up: converting TileMap to TileMapLayer.

The three it refuses are the scripted ones, and those now have a second pass. A script that extends TileMap cannot extend the Node2D the node becomes, so the scene converter stops — but the port itself is mechanical: node docs/scan_tilemap_script.js /path/to/project takes the .gd and returns it call by call. The layer index argument becomes a layer node — given the scene (or --layers=Ground,Walls) set_cell(0, …) comes back as $Ground.set_cell(…) and not as a shrug — every removed method gets its replacement named, and anything that needs a human decision comes back as a question instead of a silent rewrite. --write applies only the rewrites marked safe and leaves the rest alone; the exit code is 1 while anything still needs a human, so it drops into a build script. No terminal? The same file runs in the browser: https://blobsmith.lbwma.com/godot-tilemap-script-to-tilemaplayer/. The replacement table is dumped from ClassDB on 4.2, 4.3, 4.4 and 4.7 rather than typed from the docs, and docs/verify_tilemap_script_api.sh re-checks it against your own build: 31 checks green on each engine that has TileMapLayer, plus a round trip where the tools port a scripted scene end to end (22 rewrites, 0 left for a human) and the engine confirms the ported script reads the converted scene exactly as the original read the original.

Features

  • One-click wiring — pick a sheet, press Generate TileSet, get a wired <sheet>_tileset.tres saved next to the PNG.
  • Two layouts — 47-blob (corners + sides, 8×6 tiles) or 16-tile (sides only, 8×2 tiles).
  • Automatic layout detection — the file browser reads the PNG dimensions and pre-fills tile size and mode; a mismatch is reported instead of guessed.
  • Correct terrain mode per layout — MATCH_CORNERS_AND_SIDES for 47-blob, MATCH_SIDES for 16-tile.
  • Per-tile peering bits — derived from each tile's canonical neighbor mask, so painting resolves cleanly.
  • Optional collision — full-square collision polygons on physics layer 0 (collision layer/mask 1).
  • Named terrain — one terrain with a configurable name and a preset color.
  • Scriptable core — BlobsmithWirerCore is a plain RefCounted with no editor dependency; call it from your own tooling or tests.
  • Headless verification script — builds, saves, reloads and actually terrain-paints a TileMapLayer to prove the output works.

Everything above is implemented in this repository; nothing here is aspirational.

Neighbor bit layout

Masks use a clockwise 8-neighbor encoding (bit → neighbor):

NW   N  NE        128   1   2
 W   ·   E   →      64   ·   4
SW   S  SE         32  16   8

A corner bit only counts when both of its adjacent side bits are present — a lone diagonal neighbor does not create a distinct tile. Collapsing every 8-bit combination to that rule yields exactly 47 canonical masks, which is why the blob layout has 47 tiles. blob47() returns those masks in ascending order; tile i in the sheet is expected at column i % 8, row i / 8.

That rule is a claim about what the engine does, so it was asked: a headless script paints all 256 neighborhoods with set_cells_terrain_connect() and reads the peering bits back off the tile Godot 4.7 chose — 256/256 agree, and the engine distinguishes exactly 47 tiles. Toggle the eight neighbors and see which of the 47 answers your case (and how many of the 256 share it) at https://blobsmith.lbwma.com/godot-autotile-47-tiles/. The same page takes a TileSet .tres and lists the neighborhoods your set cannot answer — measured, a missing tile produces no error and no empty cell: the engine silently substitutes a tile claiming a corner the area does not have.

Why a blob autotile has 47 tiles and not 256 is the long form of the paragraph above: the collapse rule and why it is about what the tile can draw, the closed-form count (a tile answers 2^(4 - adjacent side pairs) neighborhoods, which is why the totals are 7×16 + 8×8 + 16×4 + 16×1 = 256), the engine transcript, the silent-substitution experiment, why a sides-only terrain set is 16 tiles instead, and the full 256 → 47 table with each tile's cell in the sheet. docs/verify_blob47.gd is the sweep itself and takes your tileset — godot --headless --script docs/verify_blob47.gd -- res://your.tres prints every neighborhood your set cannot answer and the tile the engine substitutes for it.

Terrain painting put the wrong tile down — what the engine actually does settles the two answers search gives you, neither of which is measured. It is not random (same paint 30× → one tile, including the ambiguous cases), and it picks the tile that disagrees with the fewest peering bits among tiles belonging to the terrain you are painting — never leaving the cell empty and never borrowing from another terrain, which is exactly why you get an ugly tile instead of an error. Delete the fully-surrounded tile and the engine silently places the one that is a single corner bit off. Also: why the empty cells around your painted region are constraints, why ignore_empty_terrains is not the lever people think it is (a measured negative, stated as such), and how the terrain mode caps 47 tiles down to 16 without telling you. The score says how wrong the substitute is and never which tile you get: over all 47 holes the engine always lands on a minimum-mismatch tile, and not once is one tile alone at that minimum — between 3 and 8 tie every time. Separately measured on the same page: "I changed a cell and the neighbours did not update", the complaint behind three open engine issues (#64674, #69737, #89844). One rule covers all of it — terrain fitting travels one cell out and only across a shared edge. set_cell()/erase_cell() re-fit nothing and no refresh call makes them; set_cells_terrain_connect() does rewrite cells you did not pass, but never past the edge-adjacent ring, and a cell touching your terrain only at a corner is never connected in either direction. Same numbers on 4.3, 4.4 and 4.7. docs/verify_terrain_choice.gd asserts all 33 claims against your Godot build — docs/verify_terrain_choice.sh runs it in a throwaway project built from the tilesets in examples/starter-pack, so a fresh clone needs no arguments and no test project — and docs/predict_terrain_paint.js predicts a painted region from your .tres alone, no engine — exiting non-zero on the first cell your set cannot answer. The same logic runs, byte for byte, at https://blobsmith.lbwma.com/godot-terrain-wrong-tile/, where you paint a region in the page and it names the cells with no exact tile.

The autotile has a seam exactly where your two TileMapLayers meet is the companion case: the tileset is fine and the paint is fine, but the map is split across two layers. set_cells_terrain_connect() reads one layer — the one you called it on — so each half autotiles as though the other were empty space. A 2×2 block is four different corner tiles on one layer and two identical isolated strips when split down the middle; painting the second layer does not change one cell of the first, and reversing the order changes nothing. The fix is one layer, not one call: two separate set_cells_terrain_connect() calls on the same layer reach the single-call result byte for byte, so chunk-by-chunk and player-driven painting connect correctly as long as the calls land on the same node. When the split is not negotiable (z_index, collision, a foreground over the player), paint once and copy the finished cells across with set_cell(), which re-runs no terrain logic and keeps the connected tiles. 9 claims, identical on 4.3, 4.4 and 4.7; docs/verify_cross_layer_terrain.sh runs them in a throwaway project built from examples/starter-pack, so a fresh clone needs no arguments.

Thin lines between tiles — what actually causes them separates the four causes that every answer piles into one paragraph. The most repeated fix, Rendering → Quality → 2D → Enable Pixel Snap, is a Godot 3 setting that does not exist in Godot 4 — its replacement is two settings, both shipping off. The default canvas filter in a new project is Linear, not Nearest (and the same integer 1 means Linear in the project setting and Nearest on the node). Re-exporting your atlas with gutters is usually redundant: use_texture_padding is on by default and already spaces 16px tiles 18px apart in the copy the GPU samples — a hand-cut gutter only costs you grid cells. 26 claims, all asserted on a real build by docs/verify_tile_seams.gd, plus docs/find_tile_seam_causes.js — node docs/find_tile_seam_causes.js /path/to/your/project reads your project.godot, .tscn and .tres and tells you which of the four is yours.

The tiles are y-sorted and they still draw in the wrong order measures what decides draw order instead of repeating the advice that does not work. Ticking Y Sort Enabled and dragging Texture Origin — the two steps every answer gives, in that order — move the picture and never the sort key: at texture_origin.y = -64 the art is four rows from home and the tile below is still on top. The field that moves the key is TileData.y_sort_origin, and the key itself is the centre of the cell, cell.y * tile_size.y + tile_size.y/2 + y_sort_origin — which is why "set the origin to the tile's feet" is right in spirit and useless as an instruction. Two layers do sort against each other, per tile, but only with y_sort_enabled ticked on each layer and on the node they hang from; tick one and the pixels do not move at all. And a single tile left at TileData.z_index = 1 outranks the whole arrangement silently. Draw order is not a property you can print, so all 28 claims are measured by rendering each scene into a SubViewport and reading the contested pixel back: docs/verify_y_sort.gd, run by docs/verify_y_sort.sh — which needs xvfb-run, because the headless build swaps in a dummy renderer and there is no pixel to read. docs/find_y_sort_causes.js walks your own .tscn/.tres for those causes and cites the claim id behind each one.

Which painted tiles will a body walk straight through? node docs/find_tile_collision_gaps.js /path/to/project names them, from the text resources the editor already wrote — no engine, no project import, no dependencies. The failure is silent by design: a tile with no collision polygon is not an error, not a warning and not visibly different in the tile picker, and Visible Collision Shapes draws the shapes that are there, never the ones that are missing. On a 47-tile blob set that is 47 chances to miss one. Its rules — including what a physics layer does and does not imply, and how an alternative tile inherits (or does not inherit) a shape — are asserted against a real 4.7 build by docs/verify_tile_collision.gd (26 claims, run by docs/verify_tile_collision.sh). All 26 are written out — the six silent ways through, the collision_mask column that cannot cause any of them, and what a clean scan does not prove — in why your player walks through a painted tile.

No terminal? The same rules run in the browser: https://blobsmith.lbwma.com/godot-tilemap-collision-not-working/ — paste the .tres and get the same list, plus the six causes as toggles over a body that actually stops or drops. The page loads docs/collision-scan-core.js, the identical bytes this command loads, so the two cannot name different tiles for one file.

Painting the generated TileSet from outside the editor? The tile_map_data binary format documents the bytes a TileMapLayer stores its cells in — header, 12-byte cell record, transform flags, and the erased-cell and int16-truncation traps — with a headless script that re-verifies every claim against your Godot build. Have a buffer in front of you right now? https://blobsmith.lbwma.com/godot-tile-map-data/ decodes it in the browser — paste the PackedByteArray out of your .tscn and get the cell table back (coords, source id, atlas coords, alternative, flip/transpose), or type a cell table and get the bytes to paste in. Nothing is uploaded.

I changed one tile at runtime and every copy of it changed is the rule the flip case is one instance of, and it is the one that bites in gameplay code: layer.get_cell_tile_data(cell).set_custom_data("hp", 3) does not set the hp of a cell. A cell never owns a TileData — the TileSet does, and every cell drawn from that tile, in every TileMapLayer sharing that TileSet, is looking at the same object. The write reads back through all of them, and ResourceSaver.save() then writes your runtime value into the .tres under version control. Two open engine issues (#108067, #93327) are the same fact reported from opposite ends — too wide, and not durable. There is also no private copy to take: TileData extends Object, not Resource, and has no duplicate(). 22 checks, identical on 4.3, 4.4 and 4.7 (docs/verify_tile_data_sharing.sh, which also runs against your own .tres).

My agent will not walk on the tiles I painted is the eight causes with the guesswork taken out, and the headline is that the most repeated fix is not one: there is no number of frames to wait before the first query. On an idle machine the corridor answers on frame 1 (4.3), 2 (4.4) and 4 (4.7) — six runs each, identical, which is what makes the table look like a fact — and the same 4.7 binary under load answered on frames 2, 11, 13 and 20, crossing the values tabled for the other builds. Poll map_get_iteration_id() instead; the same code is correct on all three (N30). map_force_update() publishes the regions without making the map answer (N12/N12b), a polygon authored (0,0)-(16,16) lands half a tile down-right because the region sits at the cell centre (N13-N17), and a corridor blocked halfway comes back shorter, not empty, so if path.is_empty() never fires (N23/N24). 31 checks, 31/31 on 4.3, 4.4 and 4.7 (docs/verify_tile_navigation.sh); docs/find_tile_navigation_gaps.js scans your own .tres/.tscn/.gd for the five causes that are readable in a file.

A flipped tile is not a new tile — what the three transform bits actually buy you answers the two questions that pull in opposite directions: can I draw half a symmetric sheet and flip the rest? and did my collision flip too? A flip is a property of the cell, not the tile — three bits (4096, 8192, 16384) OR-ed into the alternative_tile argument of set_cell(), authoring nothing and creating no alternative. So the answer to the first is no, and not by a little: the terrain autotiler never emits a transform bit, measured with the mirror tile deliberately withheld — the cell that needed it got a middle tile pointing into empty space instead, the same silent substitution the terrain-choice page describes. Draw all 47. The second answer is the trap: a flipped cell's get_cell_tile_data() returns the same TileData object as the unflipped one and reports the collision polygon as authored, while the shape the physics server actually holds is mirrored. Both readings are true, so anyone who inspects TileData and concludes "collision does not follow the flip" has read a correct value and drawn a false conclusion. Also: why a terrain repaint erases a hand-placed flip, and why an authored alternative — not a transform bit — is the thing that can carry its own terrain bits, custom data or navigation. 33 claims, each with an id and four of them controls that fail if the check stops discriminating, asserted against 4.3, 4.4 and 4.7 by docs/verify_tile_transforms.gd, run by docs/verify_tile_transforms.sh — headless is enough, because the physics server is real in a headless build.

The tile under the mouse is the wrong one — what local_to_map actually takes is the click-picks-the-wrong-cell bug in every form it takes, each measured. local_to_map() wants the layer's local coordinates: a world position passed raw is off by the layer's offset ((7,3) instead of (1,0)) or drifts with its scale, and subtracting global_position breaks under a rotated parent. A Camera2D lives in the canvas transform, not in the node, so event.position — and to_local(event.position) — ignore it; make_input_local(event) and get_local_mouse_position() are right under a camera, an offset and a scale at once. Vector2i(pos / 16) truncates, so (-1,-1) lands in cell (0,0) while local_to_map floors; map_to_local returns the centre, not the corner; and on a 64×32 isometric layer local (0,0) is cell (-1,-1), with a new TileSet defaulting to STACKED layout. 33 checks, three of them controls, 33/33 on 4.3, 4.4 and 4.7 by docs/verify_mouse_to_cell.sh.

set_cell ran and nothing appeared — what the engine stores instead is the tile-placed-from-code-is-invisible bug, with "nothing" read back from rendered pixels. set_cell(cell) and set_cell(cell, 0) are erases — the defaults are -1 and (-1,-1). Atlas coords that were never created as a tile, a missing source id or a missing alternative are stored anyway (the cell is in get_used_cells()) and draw nothing, and neither the set_cell nor the draw prints an error; only get_cell_tile_data() returns null and complains. Removing and re-adding an atlas makes its id 1, not 0; a 2×2 tile exists only at its origin; create_tile() on an atlas with no texture creates nothing; and enabled = false keeps the cells and draws none. 30 checks, two of them controls, 30/30 on 4.3, 4.4 and 4.7 by docs/verify_set_cell.sh (needs xvfb-run).

get_custom_data returns null (or the wrong value) — what the engine reads is the tile-custom-data bug, measured with a real CharacterBody2D landing on a real floor. The body rests safe_margin above the floor's top edge, so the cell at its feet is the empty one above and its tile data is null; contact point minus normal finds the floor in every version. On 4.5+ get_coords_for_body_rid() returns the index of a 16×16 physics chunk, not the cell — (0,0) for a body on cell (2,2) — unless physics_quadrant_size = 1. Unset values read 0/false/"", a layer left at type Nil reads null, a wrong-case name reads null and prints an error, writes are not type-checked. And an engine bug: after add_custom_data_layer(0) or move_custom_data_layer() from code, lookup by name returns another layer's value until the TileSet is reloaded. 34/35/38 checks (the version-specific ones differ), four of them controls, all passing on 4.3, 4.4 and 4.7 by docs/verify_tile_custom_data.sh.

Animated tile not animating (or animating when it should not) is the tile-animation page, with the frame on screen read back from rendered pixels at a fixed 60 fps. Frames default to 1.0 s each and are read to the right of the tile (columns = 1 reads them downward); if those atlas cells are already tiles — what "create tiles in non-transparent areas" does — the frame count is refused and the error says "animation columns count". Frame cells stop being tiles, so set_cell to a frame coordinate draws nothing. speed = 0 is refused. In mode DEFAULT every cell shows the same frame, even one placed later; RANDOM_START_TIMES desyncs them. get_tree().paused and process_mode = DISABLED do not stop tile animation; Engine.time_scale does, and so does setting the frame count back to 1. 27 checks, two of them controls, 27/27 on 4.3, 4.4 and 4.7 by docs/verify_tile_animation.sh (needs xvfb-run).

My scene tile is not there (or I cannot find its node) is the Scenes Collection page. The scene id goes in the alternative slot and the first one is 1; a cell pointing at scene id 0 or 99 is stored and creates nothing, with no error. get_cell_tile_data is null on a scene cell. set_cell does not create the node — update_internals() or a later frame does; the node sits at map_to_local(cell), has no owner, and only the first copy keeps the scene's name. The layer replaces the nodes whenever it rebuilds (enabled toggled, a scene tile added to the TileSet), so state on the node is lost; queue_free() on the node leaves the cell, and set_cell with the same values will not bring it back. The nodes are not saved with the scene. 47 checks, one of them a control, 47/47 on 4.3, 4.4 and 4.7 by docs/verify_scene_tiles.sh (headless).

AStarGrid2D walks through my walls (or finds no path) is the grid-pathfinding page. update() throws away every solid and weight set before it — and every region, cell_size or offset change needs another update(), which wipes them again. A new grid has region (0, 0, 0, 0), so every query is "out of bounds". The default diagonal mode slips between two walls that touch at a corner. Points are the cell's top-left corner until offset = cell_size / 2, and they are layer-local. A solid start cell still gets a path in 4.3/4.4 and returns [] in 4.7; allow_partial_path to a solid target returns [] in 4.3. An isometric TileSet is created STACKED, which matches no AStarGrid2D cell shape — DIAMOND_RIGHT/DIAMOND_DOWN do. 45 checks, one of them a control, 45/45 on 4.3, 4.4 and 4.7 by docs/verify_astar_grid.sh (headless).

My tiles cast no shadow — what a PointLight2D needs from a TileSet is the 2D lights page. shadow_enabled is off by default, and then nothing casts. A new TileSet has no occlusion layer, and occluder polygons are relative to the tile's center — drawn 0..16 the shadow moves half a tile. The occlusion layer's light_mask has to share a bit with the light's shadow_item_cull_mask, and since 4.4 the lit floor's light_mask has to as well: the same scene that shadows on 4.3 stays lit on 4.4/4.7. 4.4 also added TileMapLayer.occlusion_enabled and several polygons per layer. 23 checks on 4.3 and 27 on 4.4 and 4.7, three of them controls, all passing, read from rendered pixels by docs/verify_tile_occlusion.sh (needs xvfb).

My isometric TileMap is not a diamond — shape vs layout is the isometric page. Setting tile_shape to Isometric leaves tile_layout at Stacked, which lays diamonds out in a rectangle; Diamond Right is the missing line. Stairs Right lands on the same points as Stacked at 64×32, and Tile Offset Axis moves nothing on an isometric TileSet. Tall art goes in texture_origin, not tile_size; local (0, 0) is cell (0, -1); and RIGHT_SIDE returns the cell you passed in — +x is TOP_RIGHT_SIDE. 24 checks, 24/24 on 4.3, 4.4 and 4.7 by docs/verify_isometric_layout.sh (headless).

set_cells_terrain_connect is slow in procedural generation is the generation-speed page. On a 128×128 noise map one call costs ~57–79 µs per cell, linear in the cells passed; one call per cell, or per 16×16 chunk, gives the identical map and costs about 4× and 1.1× that. With a complete 47-tile set, computing the neighbour mask yourself and calling set_cell gives the same map, 0 cells different, 38–56× faster. set_cells_terrain_path joins only consecutive cells. 16 checks, 16/16 on 4.3, 4.4 and 4.7 by docs/verify_terrain_connect_speed.sh (headless; speed asserted as ratios only).

Which tile did the player hit? — why local_to_map(collision point) gives the neighbour is the tile-hit page. get_collider() is the TileMapLayer, never a tile. The contact point sits on the tile's edge, and the right and bottom edges belong to the next cell: over 88 move_and_collide approaches from 8 directions, local_to_map(get_position()) names the hit tile 37 times, 0 of 11 from below or from the right, and every miss is an empty cell. Minus the normal gets 72/88 (rectangle) and 81/88 (capsule) — the rest are tile corners — and a half-pixel probe for a non-empty cell gets 88/88. get_coords_for_body_rid() is the cell on 4.3/4.4 and a 16×16 chunk on 4.7, where two tiles of one chunk share a body, unless physics_quadrant_size = 1. Standing on a seam gives one slide collision a frame, not two. 17 checks on 4.3 and 4.4 and 20 on 4.7, three of them controls, all passing by docs/verify_tile_hit.sh (headless).

I erased the tile and it still collides — erase_cell and the physics body it leaves behind is the mining page. Right after erase_cell() the cell reads -1, but ray, point, RayCast2D and move_and_collide all still hit it; without update_internals() rays see the change after exactly 1 frame (20/20), and an enabled RayCast2D reports it one physics frame late. update_internals() removes it on the same line on all three versions. On 4.7 the 8-tile floor is one body that the erase replaces with a new RID, and right after update_internals() rays miss the neighbours too — 0 of 8 floor cells hit for 1 frame, so a ground-check ray next to the mined tile reads false once — while move_and_collide is unaffected; inside body_shape_entered on 4.7 the call prints two "flushing queries" errors and still works. Stored RIDs are dead. 24 checks, 24/24 on 4.3, 4.4 and 4.7 by docs/verify_erased_tile.sh (headless).

I can jump up through the one-way tile but not drop down through it is the drop-through page. Holding down on a one-way platform does nothing, even at 2000 px/s, and floor_snap_length is not the reason: the tile lets a body go only once it is deeper than one_way_margin (a 0.9 px nudge comes back, 1.0 px drops; 3.9 and 4.0 at margin 4). Clearing the collision mask bit drops the player after 3 physics frames, 2 snap back, and restoring it inside the platform does not push the player up. Put platforms on their own TileSet physics layer: with one shared layer the same trick on solid ground leaves the player stuck inside it on 4.3/4.4 or sends it through. add_collision_exception_with(TileMapLayer) prints an error and does nothing; a server exception by collider RID covers one tile on 4.3/4.4 and a whole 16×16 chunk on 4.7. Flipped or transposed tiles keep blocking from above; a layer node rotated 180° blocks from below. 19 checks, 19/19 on 4.3, 4.4 and 4.7 by docs/verify_one_way_drop.sh (headless).

My box gets stuck between two tiles — RigidBody2D catching on TileMapLayer seams is the crate-and-ball page. On 4.3/4.4 a 62-tile floor is 62 physics bodies, and a rotation-locked box pushed at 120 px/s stops for good 0.2 px past a seam (25.2 / 121.2 / 345.2) where one StaticBody2D lets it reach 421.9 / 653.9; a ball is kicked upward at the seams. On 4.7 the same floor is 5 bodies (16×16 chunks): the box matches the control to the tenth of a pixel, but the ball is still kicked at the chunk borders (x = 256, 512). physics_quadrant_size = 64 removes those too; = 1 brings the 4.3 snag back. Friction 0 only helps. A CharacterBody2D stalled on 0 frames — floor, capsule, flush ceiling, floating along a wall. 8 checks on 4.3 and 4.4 and 13 on 4.7, two of them controls, all passing by docs/verify_seam_snag.sh (headless).

I changed the tile size and half the map went invisible is the two-fields page. TileSet.tile_size and TileSetAtlasSource.texture_region_size are both called tile size and only the second one re-cuts the sheet under tiles that already exist. Doubling it on a 4x2 sheet of 16px cells deletes no tile and prints nothing: the atlas grid drops to 2x1, six of the eight tiles keep regions that fall outside the texture, and has_tile still answers true for them. On screen those cells draw fully transparent, and the tiles that stayed inside draw the centre of four sheet cells instead of their own art. The map data is untouched — same source id, same atlas coords, still in get_used_cells() — so setting the number back restores everything exactly. Changing TileSet.tile_size instead breaks nothing and just centres 16px of art in a 32px cell. If the bigger region is what you want, the tiles that no longer fit have to be removed, and doing it in a loop over get_tile_id(i) silently removes only 4 of the 6. 19 checks, 19/19 on 4.3, 4.4 and 4.7 by docs/verify_tile_size_change.sh (needs xvfb-run: it reads back rendered pixels).

Stack

Piece Detail
Language GDScript (@tool)
Engine Godot 4.3+ (the dialog uses the static EditorInterface API, which exists on 4.2, but TileMapLayer does not — so the .tres it writes has nothing to paint on there)
Plugin entry EditorPlugin registered via plugin.cfg v1.0.0
Output TileSet .tres (ResourceSaver)
Verification Headless SceneTree script (uses TileMapLayer, so Godot 4.3+)

Architecture

                 PNG sheet (res://…/sheet.png)
                          │
         plugin.gd  ──────┤  EditorPlugin
         (Tools-menu dialog: browse, tile size,
          mode, terrain name, collision)
                          │  wire_and_save(...)
                          ▼
         wirer_core.gd  ── BlobsmithWirerCore  (RefCounted, no UI)
            detect_layout(w,h)  → { tile_size, sides_only }
            blob47() / blob16() → canonical masks, ascending
            build_tileset(...)  → TileSet + TileSetAtlasSource
                                   • add_terrain_set + mode
                                   • per-tile set_terrain_peering_bit
                                   • optional collision polygons
                          │  ResourceSaver.save
                          ▼
              <sheet>_tileset.tres  (next to the PNG)

The UI (plugin.gd) only collects parameters and reports status. All of the mask math and TileSet construction lives in wirer_core.gd, which has no reference to the editor and is exercised directly by the verification script.

Getting started

Prerequisites

  • Godot 4.3 or newer. Measured on 2026-08-20 by running the four engine scripts on each stable build: 4.3, 4.4 and 4.7 pass (15 + 23 checks, plus 16-mode and the nasty-name round-trip); 4.2 fails — TileMapLayer does not exist before 4.3, so neither the verification script nor the workflow the plugin tells you to use ("add a TileMapLayer, set its TileSet") is available there.
  • An autotile sheet in the expected layout: 8 columns, tiles in ascending canonical-mask order — a 47-blob sheet (8×6 tiles) or a 16-tile sheet (8×2 tiles). The sheet under examples/ is one such sheet.

Install

⬇ Download the addon (v1.1.0, 16 KB) · release notes

Unzip it in your project root. It creates addons/blobsmith_wirer/ and nothing else — no file lands anywhere but that folder, which a test asserts on every run by unpacking the published zip into an empty project. An example 47-blob sheet rides along in addons/blobsmith_wirer/examples/ so there is something to wire before you have drawn your own.

Or copy the folder out of a clone:

# from your Godot project root
cp -r /path/to/blobsmith-autotile-wirer/addons/blobsmith_wirer addons/

Then in the editor: Project → Project Settings → Plugins → enable Blobsmith Autotile Wirer.

Nothing to find on the editor's AssetLib tab. Searching the Godot Asset Library for this addon returns no result — it is not listed there. Checked 2026-09-01 against the Asset Library API for every 4.x from 4.3 to 4.7, with a control term that does return results, so the zero is ours and not the query's. The zip above is the current addon; the repository is the current everything.

Use it (editor)

  1. Project → Tools → "Blobsmith Autotile Wirer…"
  2. Browse to your .png sheet (tile size and mode auto-fill on selection).
  3. Set the terrain name and whether to add collision.
  4. Press Generate TileSet. A wired <sheet>_tileset.tres appears next to the PNG.
  5. Add a TileMapLayer, assign the generated TileSet, and paint in its Terrains tab.

If your sheet is not in this layout, step 2 now says what it is instead of just refusing. From the pixel size alone the dialog names the reading — 6 base tiles, the Godot 3 3×3-minimal set, 16 tiles in a 4×4 Wang square, the 47-blob count in the wrong arrangement, a plain grid — and, when a sheet has more than one honest reading (256×256 is 4×4 tiles at 64px and 16×16 at 16px), it says the second one out loud instead of picking silently. A sheet no square tile size divides is reported as exactly that: margins or separation between tiles, which this wirer does not read. The same line offers the free pack, because "set tile size manually" is useless advice when the file you have cannot be wired at any tile size. BlobsmithWirerCore.classify_sheet(width, height) is the same function, callable from a script.

Use it (script)

# false = 47-blob (corners+sides), true = 16-tile (sides only)
var out := BlobsmithWirerCore.wire_and_save(
    "res://tiles/grass_47blob_16px.png",  # sheet path
    16,       # tile size in px
    false,    # sides_only
    true,     # add full-square collision
    "Grass",  # terrain name
)
# out == "res://tiles/grass_47blob_16px_tileset.tres"  ("" on failure)

build_tileset() returns the TileSet in memory if you want to configure it further before saving.

Verify

The verification script (test_verify_addon.gd) is a headless SceneTree program. It hardcodes the sheet at res://tiles/grass_47blob_16px.png and the addon at res://addons/blobsmith_wirer/, so run it from a Godot project laid out that way:

# inside a project containing:
#   res://addons/blobsmith_wirer/         (the addon)
#   res://tiles/grass_47blob_16px.png     (copy from examples/)
godot --headless --script res://test_verify_addon.gd
# prints PASS/FAIL per check, then "ADDON VERIFY: ALL PASS"; exit code 0 on success

It checks the mask tables, layout detection, a full build from the sample sheet, a save/reload round-trip, an actual terrain paint on a TileMapLayer, and the 16-tile mode.

Engines it has actually been run on (Godot_v<x>-stable_linux.x86_64, headless):

Godot Result
4.2.stable fails to parse — no TileMapLayer
4.3.stable pass
4.4.stable pass
4.7.stable pass

Project structure

blobsmith-autotile-wirer/
├── addons/
│   └── blobsmith_wirer/
│       ├── plugin.cfg          # plugin manifest (name, version 1.0.0, entry script)
│       ├── plugin.gd           # EditorPlugin: Tools-menu item + generator dialog
│       └── wirer_core.gd       # BlobsmithWirerCore: mask math + TileSet builder (no UI)
├── docs/
│   ├── why-tiles-have-seams.md      # the four causes of a line between two tiles
│   ├── verify_tile_seams.gd         # 26 claims asked of a real engine (+ .sh runner)
│   ├── find_tile_seam_causes.js     # scans YOUR project for those causes, no deps
│   ├── merging-two-tilesets.md      # what a TileSet merge has to remap, and why it fails silently
│   ├── tile-map-data-format.md      # the TileMapLayer tile_map_data byte layout
│   ├── verify_tile_map_data.gd      # headless script proving every claim in that doc
│   ├── tile-map-data-fixtures.json  # 12 buffers a real 4.7 wrote + the cells it reads back
│   ├── dump_tile_map_fixtures.gd    # regenerates that file from your own Godot build
│   ├── check_js_buffers.gd          # hands buffers built elsewhere to a real TileMapLayer
│   ├── why-terrain-paints-the-wrong-tile.md  # what the engine picks when your set falls short
│   ├── why-terrain-does-not-connect-across-two-tilemaplayers.md  # connect reads one layer; the seam is the split
│   ├── verify_terrain_choice.gd     # 33 claims asked of a real engine
│   ├── verify_terrain_choice.sh     # runs them on your binary, no test project needed
│   ├── terrain-choice-core.js       # the choice logic the CLI and the web page share
│   ├── predict_terrain_paint.js     # predicts a painted region from YOUR .tres, no engine
│   ├── terrain-paint-fixtures.json  # neighbourhood masks dumped straight out of 4.7
│   ├── dump_terrain_paint_fixtures.gd  # regenerates that file from your own Godot build
│   ├── why-y-sort-draws-the-wrong-order.md  # what actually decides tile draw order
│   ├── verify_y_sort.gd             # 28 claims measured by reading rendered pixels (+ .sh runner, needs xvfb)
│   ├── find_y_sort_causes.js        # scans YOUR project for those causes, no deps
│   ├── ysort-scan-core.js           # the y-sort rules, filesystem-free (same bytes run in a browser)
│   ├── why-tiles-do-not-collide.md  # the six ways a body goes through a painted tile
│   ├── why-one-cell-changed-every-cell.md  # a cell never owns its TileData; the TileSet does
│   ├── verify_tile_data_sharing.gd   # 22 claims asked of a real engine (+ .sh runner, takes your .tres)
│   ├── why-tiles-are-not-walkable.md   # the eight causes of an agent that will not move
│   ├── verify_tile_navigation.gd     # 31 claims asked of a real engine (+ .sh runner)
│   ├── find_tile_navigation_gaps.js  # scans YOUR project for the five readable causes
│   ├── nav-scan-core.js              # those rules, filesystem-free
│   ├── converting-tilemap-to-tilemaplayer.md  # TileMap -> TileMapLayer, and what it refuses
│   ├── convert_tilemap_to_tilemaplayer.js    # converts a whole project, no engine, no deps
│   ├── tilemap-convert-core.js      # the rules; the browser page runs these same bytes
│   ├── scan_tilemap_script.js       # ports the GDScript the converter refuses, call by call
│   ├── tilemap-script-scan-core.js  # the ClassDB replacement table (same bytes run in a browser)
│   ├── verify_tilemap_script_api.sh # 31 checks per engine + a scripted round trip
│   ├── verify_tilemap_convert.gd    # the engine reads before and after, cell by cell
│   ├── verify_tilemap_convert.sh    # 640 checks: 4.3, 4.4, 4.7 and 4.2 -> 4.7
│   ├── find_tile_collision_gaps.js  # painted tiles with no collision polygon, from .tres/.tscn alone
│   ├── collision-scan-core.js       # those rules, filesystem-free (same bytes run in a browser)
│   ├── verify_tile_collision.gd     # 26 collision claims asked of a real engine (+ .sh runner)
│   ├── why-a-godot-3-tileset-does-not-open-empty.md  # what Godot 4 keeps, drops and displaces from a format=2 TileSet
│   ├── convert_godot3_tileset.js    # Godot 3 -> Godot 4 TileSet, with a report of what could not carry over
│   ├── tres3-convert-core.js        # those rules, filesystem-free (same bytes run in a browser)
│   ├── verify_godot3_tileset.gd     # 15 claims asked of a real engine (+ .sh runner)
│   ├── why-my-animated-tile-does-not-animate.md  # frame layout, 1 s default, shared clock, what pauses it
│   ├── verify_tile_animation.gd     # 27 claims read from rendered pixels at fixed 60 fps (+ .sh runner, needs xvfb)
│   ├── why-my-scene-tile-is-not-there.md  # scene id slot, late creation, node names, rebuilds that drop state
│   ├── verify_scene_tiles.gd        # 47 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-astargrid2d-walks-through-walls.md  # update() wipes solids, corner cutting, corner vs centre, iso layouts
│   ├── verify_astar_grid.gd         # 45 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-my-tiles-cast-no-shadow.md  # shadow_enabled, occlusion layer, centered polygon, masks (4.4 change)
│   ├── verify_tile_occlusion.gd     # 27 claims read from rendered pixels (+ .sh runner, needs xvfb)
│   ├── why-my-isometric-tilemap-is-not-a-diamond.md  # tile_layout, offset axis, texture_origin, origin, neighbour constants
│   ├── verify_isometric_layout.gd   # 24 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-set-cells-terrain-connect-is-slow.md  # cost per cell, per-cell/chunk loops, precomputed set_cell, path vs connect
│   ├── verify_terrain_connect_speed.gd  # 16 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-the-tile-you-hit-is-the-wrong-one.md  # collider is the layer, edge contact point, corners, RID vs 4.7 chunks, seams
│   ├── verify_tile_hit.gd           # 17/20 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-changing-the-tile-size-emptied-my-tilemap.md  # tile_size vs texture_region_size, tiles outside the texture, what the map keeps, how to get back
│   ├── verify_tile_size_change.gd   # 19 claims asked of a real engine, rendered pixels (+ .sh runner, needs xvfb)
│   ├── why-you-cannot-drop-through-the-one-way-tile.md  # holding down, one_way_margin depth, mask-bit frames, own physics layer, exceptions, flips vs rotation
│   ├── verify_one_way_drop.gd       # 19 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-the-erased-tile-still-collides.md  # stale body after erase_cell, update_internals, 4.7 chunk rebuild, callbacks, dead RIDs
│   ├── verify_erased_tile.gd        # 24 claims asked of a real engine, headless (+ .sh runner)
│   ├── why-bodies-catch-on-tile-seams.md  # RigidBody2D stuck/bouncing on seams, one body per cell vs 4.5+ chunks, physics_quadrant_size, CharacterBody2D
│   └── verify_seam_snag.gd          # 8/13 claims asked of a real engine, headless (+ .sh runner)
├── examples/
│   ├── starter-pack/           # 16 free wired TileSets (grass/stone/sand/water × 16/32px × 2 layouts)
│   │   ├── *_47blob_*.png      # the 8×6 sheets — Match Corners and Sides
│   │   ├── *_16sides_*.png     # the 8×2 sheets — Match Sides
│   │   ├── *.tres              # the already-wired TileSets — no plugin needed to use these
│   │   ├── manifest.json       # base, size, layout, tile count and mode the engine gate iterates over
│   │   ├── README.md           # import steps, what is checked, honest note on the art
│   │   └── starter_preview.png  # all eight 32px sets, painted by Godot with set_cells_terrain_connect
│   ├── transitions/            # 2 terrains in one terrain set, 16/32px
│   │   ├── grass_on_sand_*.png/.tres  # 48 tiles (47 grass-over-sand + full sand), every bit set
│   │   ├── sand_on_water_*.png/.tres  # shoreline: 47 sand-over-water + full water
│   │   ├── stone_on_grass_*.png/.tres # path: 47 stone-over-grass + full grass
│   │   ├── water_on_grass_*.png/.tres # pond: 47 water-over-grass + full grass
│   │   ├── grass_on_stone_*.png/.tres # overgrown floor: 47 grass-over-stone + full stone
│   │   ├── dirt_on_grass_*.png/.tres  # dirt road: 47 dirt-over-grass + full grass
│   │   ├── grass_on_dirt_*.png/.tres  # tilled ground: 47 grass-over-dirt + full dirt
│   │   ├── manifest.json       # base, size, tiles and the two terrain names per TileSet
│   │   └── verify_transitions.gd  # paints a lake of the top terrain in a real engine (+ .sh runner)
│   ├── platformer/             # side-view: blob ground + one-way platforms on physics layer 2
│   │   ├── platformer_*px.png/.tres  # 47 Ground terrain tiles + 3 one-way platform tiles
│   │   ├── platformer_demo.tscn  # painted level, player, camera — F6 to play
│   │   ├── player.gd           # walk, jump, drop through platforms (clears mask bit 2 for 6 frames)
│   │   └── verify_platformer.gd  # drives the real player through the real scene (+ .sh runner)
│   ├── animated-water/         # 47-blob water over grass, every water tile 4 frames of 0.2 s
│   │   ├── animated_water_*px.png/.tres  # 47 animated Water tiles (frames to the right) + 1 Grass tile
│   │   ├── animated_water_demo.tscn  # painted pond + camera — F6 to watch
│   │   ├── animated_water_preview.gif/.png  # the pond animated / frame 0
│   │   └── verify_animated_water.gd  # paints, audits the animation, reads frames back from pixels (+ .sh runner)
│   ├── isometric/              # 47-blob isometric (Diamond Right), 2:1 cells, diamond collision
│   │   ├── {grass,stone,sand,water}_iso47_*px.png/.tres  # 47 tiles each, 16px (32×16) and 32px (64×32)
│   │   ├── isometric_preview.png  # an island Godot painted with the 32px grass set
│   │   ├── manifest.json       # base, size, cell and sheet dimensions per TileSet
│   │   └── verify_isometric_pack.gd  # paints each set, compares against the square pack (+ .sh runner)
│   ├── godot-47blob-starter-pack.zip  # the same sixteen in one download
│   ├── godot-47blob-transitions-pack.zip  # the same eighteen in one download, LICENSE inside
│   ├── godot-platformer-starter-pack.zip  # the platformer pack in one download, LICENSE inside
│   ├── godot-animated-water-pack.zip  # the animated water pack in one download, LICENSE inside
│   ├── godot-isometric-47blob-pack.zip  # the isometric pack in one download, LICENSE inside
│   ├── grass_47blob_16px.png   # 128×96 sample sheet — 16px tiles, 47-blob layout
│   └── blobsmith-demo.gif      # the companion Blobsmith tool painting a sheet
├── test_verify_addon.gd        # headless SceneTree verification script
└── icon.png                    # 256×256 project icon

Status and limitations

  • Personal project, tagged 1.0.0. The core is covered by the verification script above; there is no CI running it for you.
  • The sheet must already be in the expected order. The plugin wires masks by tile position; it does not reorder or validate the pixels of an arbitrary sheet. Feed it a sheet whose tiles are laid out in ascending canonical-mask order.
  • Layouts: only the 47-blob and 16-tile arrangements are supported. Other autotile schemes are out of scope.
  • Collision is a single full-square polygon per tile — enough for solid terrain, not per-shape edge collision.
  • Single terrain, single terrain set. One terrain is created per run.
  • The verification script's paths are hardcoded to res://tiles/…; adjust them or place the sample sheet accordingly before running.

The examples/ sheet is produced by Blobsmith, a separate pixel-art tool that draws a 47-blob sheet from 6 base tiles. This plugin works with any sheet in the same layout.

License

Released under the MIT License.

More from the studio

About

Free MIT Godot 4 tilesets, already wired: 47-blob and 16-tile terrains (grass, stone, sand, water), transitions, animated water, isometric, hexagon (pointy/flat), top-down dungeon walls with faces, platformer kit. Plus an editor plugin that wires any autotile sheet into a TileSet, and a TileMap to TileMapLayer converter. Checked in 4.3/4.4/4.7.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages