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