mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:00:51 +02:00
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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user