diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index eb2dbb9..e9b4c39 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -64,6 +64,26 @@ end local ChunkMesher = {} +-- Which drawn row a FLAT-topped volume's top face wears at depth `ty`. +-- +-- A structure is usually deeper than the art that draws it, so the rows +-- cycle and the drawing repeats down the top. That is right for art which +-- genuinely repeats -- the Safari Zone's fence alternates two tiles the +-- whole way down -- and wrong for a RIM over a uniform body: a cliff +-- mound's first row is its top edge, and cycling lays that edge again +-- every second tile, striping a plateau with rims it should not have. +-- +-- Where Structures found the body uniform, the rim is laid once at the +-- north edge and the body held after it. Everything else cycles as before. +function ChunkMesher.flatTopRow(run, ty) + local m = math.min(2, run.extent) + local d = ty - run.north + if run.topUniform then + return run.north + math.min(d, m - 1) + end + return run.north + (d % m) +end + -- Ring of border blocks meshed around the body, matching the width -- TileRenderer draws so the two modes end at the same place. local RING = 3 @@ -528,8 +548,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink) { x0 + 8, neY, z0 }, { x0, nwY, z0 } }, { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95) elseif run then - local m = math.min(2, run.extent) - local topTile = map:tileAt(tx, run.north + ((ty - run.north) % m)) + local topTile = map:tileAt(tx, ChunkMesher.flatTopRow(run, ty)) topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE) else local topTile = tile diff --git a/lib/Structures.lua b/lib/Structures.lua index 2c99c8d..8950380 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -2225,6 +2225,43 @@ function Structures.buildVolume(S, map, tiles) -- whether the region's dominant columns are flat repeats (a cliff -- mound's plateau) rather than drawn facades (a house's front) local modeRepeat = (repeatVotes[modeH] or 0) * 2 > modeN + + -- Whether this REGION's tops are a rim over a uniform body -- what every + -- cliff mound is drawn as: a top edge, then the same rock the whole way + -- down. The top face may then lay that rim once along its north edge and + -- hold the body after it, instead of cycling the rim back every second + -- tile and striping a plateau with edges it should not have. + -- + -- Answered per column AND per region, because each catches what the + -- other misses. A mound is one structure many columns wide, and the + -- columns carrying its cave mouth read differently from their neighbours + -- (their drawing ends in the mouth's own tiles): per column alone, those + -- kept cycling while the rest held, leaving rim stubs above the doorway. + -- But a region vote alone silences a genuine rim-over-body column that + -- happens to stand in a region of repeating art -- three of them in the + -- Safari Zone. A column holds if EITHER says so. + -- + -- Art that genuinely repeats is not uniform and keeps cycling: the + -- Safari Zone's fence alternates two tiles the whole way down, and there + -- the repeat IS what the drawing says. + local uniformVotes, uniformTotal = 0, 0 + for _, r in ipairs(runs) do + local run = r.run + if run.extent > 2 then + uniformTotal = uniformTotal + 1 + local body = map:tileAt(r.tx, run.north + 1) + local uniform = true + for d = 2, run.extent - 1 do + if map:tileAt(r.tx, run.north + d) ~= body then + uniform = false + break + end + end + run.ownUniform = uniform + if uniform then uniformVotes = uniformVotes + 1 end + end + end + local regionUniform = uniformTotal > 0 and uniformVotes * 2 > uniformTotal for _, r in ipairs(runs) do local run = r.run local h = run.unit * 8 @@ -2272,6 +2309,7 @@ function Structures.buildVolume(S, map, tiles) run.rise = roofRows * 8 run.peak = h run.h = h - run.rise -- facade height: what sides build to + run.topUniform = run.ownUniform or regionUniform for ty = run.north, run.front do S.runs[keyOf(r.tx, ty)] = run end diff --git a/tests/flat_top_test.lua b/tests/flat_top_test.lua new file mode 100644 index 0000000..14035c0 --- /dev/null +++ b/tests/flat_top_test.lua @@ -0,0 +1,95 @@ +-- A flat top must not stamp its rim twice. +-- +-- ChunkMesher.flatTopRow decides which drawn row a flat-topped volume's top +-- face wears at each depth. Where the drawing is a RIM over a uniform body +-- -- every cliff mound in the game, and the mound the Diglett's Cave mouth +-- is cut into -- the rim belongs at the plateau's north edge and nowhere +-- else. Cycling the first two rows lays it again every second tile. +-- +-- The invariant: on such a run the sampled row never goes BACKWARDS as ty +-- moves south. Art that genuinely repeats (the Safari Zone's fence +-- alternates two tiles the whole way down) is exempt: there the repeat is +-- what the drawing says, and the run is not rim-over-body. +-- +-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/flat_top_test.lua lovec . +return function(game) + local U = dofile("tests/drivers/util.lua") + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local ChunkMesher = V and V.require("ChunkMesher") + if not (Structures and ChunkMesher and ChunkMesher.flatTopRow) then + print("[flattop] FAIL mod, Structures or ChunkMesher.flatTopRow missing") + love.event.quit(1) + return + end + local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end + + local MAPS = {} + for id in pairs((game.data and game.data.maps) or {}) do + MAPS[#MAPS + 1] = id + end + table.sort(MAPS) + + local checked, offenders, examples = 0, 0, {} + for _, mapId in ipairs(MAPS) do + U.teleport(game, mapId, 5, 5, "up") + U.wait(6) + local ow = game.overworld + if ow and ow.map and ow.map.def and ow.map.def.id == mapId then + local map = ow.map + local S = Structures.forMap(map) + local seen = {} + for tx = 0, map.def.width * 4 - 1 do + for ty = 0, map.def.height * 4 - 1 do + local run = S.runs[keyOf(tx, ty)] + local sig = run and (tostring(run) .. ":" .. tx) + if run and not seen[sig] and (run.rise or 0) == 0 then + seen[sig] = true + local ext = run.front - run.north + 1 + -- rim over a uniform body: the shape the rim must not repeat on + local uniform = ext > 2 + if uniform then + local body = map:tileAt(tx, run.north + 1) + for d = 2, ext - 1 do + if map:tileAt(tx, run.north + d) ~= body then + uniform = false + break + end + end + end + if uniform then + checked = checked + 1 + local prev = -1 + for ty2 = run.north, run.front do + local row = ChunkMesher.flatTopRow(run, ty2) + if row < prev then + offenders = offenders + 1 + if #examples < 5 then + examples[#examples + 1] = ("%s tx=%d north=%d ext=%d " + .. "went back to row %d at ty %d") + :format(mapId, tx, run.north, ext, row, ty2) + end + break + end + prev = row + end + end + end + end + end + end + end + + print(("[flattop] %d rim-over-body runs checked, %d repeat their rim") + :format(checked, offenders)) + for _, e in ipairs(examples) do print("[flattop] " .. e) end + if offenders > 0 then + print("[flattop] FAIL") + love.event.quit(1) + else + print("[flattop] PASS") + love.event.quit(0) + end +end