DramaticShapeVoxelMod archival mirror · read-only
8b73cbd54d57cfa6189a86aa572a016508a52f36

keep the block-edit hook in the mod, not in the engine

DramaticShape · 2026-07-27 · parent d229622b09

keep the block-edit hook in the mod, not in the engine

Cut, the tree regrowth on re-entry and the card-key door stamps all write
the block layer directly without emitting world.block_replaced, so
meshes derived from that layer went stale unannounced. An earlier cut of
this work made the engine announce each one -- the wrong place: it edits
the game for one mod's benefit, and every future path that writes a
block has to remember to do the same.

They all funnel through one choke point, so wrap Map:setBlock from the
mod instead. Map is a plain metatable shared by every instance, so this
covers paths written after this mod too. The write is read back rather
than trusted: setBlock ignores an out-of-bounds write, and a stamp that
rewrites a block with the value it already held is not an edit and must
not throw the mesh away.

src/world/OverworldController.lua is reverted to stock.

Changed files

main.lua                      | 40 ++++++++++++++++++++++++++++++++++++++++
 tests/dramatic_shape_test.lua | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 78 insertions(+)

Diff

diff --git a/main.lua b/main.lua
index fd9a5a7..2d19fab 100644
--- a/main.lua
+++ b/main.lua
@@ -353,6 +353,46 @@ mod.events:on("world.block_replaced", function(payload)
   if mapId then ChunkMesher.refresh(mapId) end
 end)
 
+-- The event above is the ANNOUNCED edit -- OverworldState:replaceBlock
+-- emits it, which is the path Victory Road's barriers and a script's
+-- replaceBlock take. Several edits do not go through it:
+--
+--   Cut          swaps the tree block and rebuilds the 2D renderer
+--   the regrowth restores those blocks when the map is re-entered
+--   card-key doors are stamped closed on floor load
+--
+-- all of them writing the block layer directly. Meshes derived from that
+-- layer went stale with no announcement -- the cut tree stayed standing,
+-- and after a round trip through a door the stump stayed cut because this
+-- map's mesh survives in the cache (that is what prevLive is for).
+--
+-- The engine could announce each of those, and an earlier cut of this
+-- work changed it to. That is the wrong place: it edits the game for one
+-- mod's benefit, and every future path that writes a block has to
+-- remember to do the same. They all funnel through ONE choke point --
+-- Map:setBlock -- so wrap that from here instead. Map is a plain
+-- metatable shared by every map instance, so this covers all of them,
+-- including paths written after this mod.
+--
+-- Read back rather than trust the argument: setBlock silently ignores an
+-- out-of-bounds write, and a stamp that rewrites a block with the value
+-- it already held (the door code guards for this, the regrowth does not)
+-- is not a change and must not throw the mesh away.
+do
+  local Map = require("src.world.Map")
+  if not Map.dramaticShapeBlockHook then
+    local setBlock = Map.setBlock
+    Map.setBlock = function(self, bx, by, block)
+      local before = self:blockAt(bx, by)
+      setBlock(self, bx, by, block)
+      if self.id and self:blockAt(bx, by) ~= before then
+        ChunkMesher.refresh(self.id)
+      end
+    end
+    Map.dramaticShapeBlockHook = true
+  end
+end
+
 -- A reloaded map is rebuilt from scratch (warps that re-enter the same map,
 -- hot reload), so its mesh is stale for the same reason -- with one
 -- exception, and it is the common one.
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 5447507..9612840 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -561,6 +561,44 @@ T.eq(#dropped, 2, "a replaced block does not drop the mesh outright")
 T.eq(#refreshed, 1, "it refreshes the mesh in place instead")
 T.eq(refreshed[1], "PALLET_TOWN", "and refreshes exactly the edited map")
 
+-- ------- and the edits the engine does NOT announce
+--
+-- Cut swaps its tree block, the regrowth restores it on re-entry, and the
+-- card-key doors are stamped on floor load -- all writing the block layer
+-- directly, none of them emitting world.block_replaced. The mod wraps
+-- Map:setBlock rather than asking the engine to announce each one (an
+-- earlier cut changed the engine, which is the wrong place: it edits the
+-- game for one mod, and every future block write has to remember).
+--
+-- These run through a REAL Map, so they also pin that the wrap survives
+-- whatever the engine does to that method.
+
+local Map = require("src.world.Map")
+local function fakeMap(id)
+  return setmetatable({
+    id = id,
+    def = { width = 2, height = 2, blocks = { 1, 1, 1, 1 }, borderBlock = 0 },
+  }, Map)
+end
+
+local m = fakeMap("ROUTE_2")
+refreshed = {}
+
+m:setBlock(0, 0, 9)
+T.eq(#refreshed, 1, "a direct setBlock refreshes the mesh -- this is Cut's path")
+T.eq(refreshed[1], "ROUTE_2", "and names the map that was edited")
+T.eq(m:blockAt(0, 0), 9, "and the block really changed")
+
+-- the regrowth rewrites every block it recorded, changed or not; a write
+-- that changes nothing must not throw the mesh away
+m:setBlock(0, 0, 9)
+T.eq(#refreshed, 1, "rewriting a block with the value it already held is not an edit")
+
+-- setBlock silently ignores an out-of-bounds write, so there is nothing
+-- to rebuild for one
+m:setBlock(99, 99, 3)
+T.eq(#refreshed, 1, "an out-of-bounds write refreshes nothing")
+
 ChunkMesher.invalidate = realInvalidate
 ChunkMesher.refresh = realRefresh