From 370ddea0b8fe1aa1a1444769c64dfe4557872e07 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 28 Jul 2026 15:14:48 -0400 Subject: [PATCH] fix the mf grass --- src/render/TileRenderer.lua | 69 ++++++++++++---- tests/drivers/grass_overlay_bug150_test.lua | 88 ++++++++++++++++++++- 2 files changed, 140 insertions(+), 17 deletions(-) diff --git a/src/render/TileRenderer.lua b/src/render/TileRenderer.lua index e10bff02..2970a68a 100644 --- a/src/render/TileRenderer.lua +++ b/src/render/TileRenderer.lua @@ -475,6 +475,13 @@ function TileRenderer.new(map, data) -- match the atlas's static tiles instead of showing raw grayscale gbcCtx = { tilesetId = map.tileset.id, mapId = map.id, key = "#gbc:" .. map.id, groupColors = PaletteFX.worldGroupColors(data, map.tileset.id, map.id, nil) } + -- ...and feeds the color-0-keyed single tiles the feet overdraw needs + -- (see getKeyedTile): same source image and palette groups, so keep the + -- context rather than re-deriving it per draw. + gbcCtx.imagePath = map.tileset.image + gbcCtx.perRow = map.tileset.tilesPerRow + self.gbcCtx = gbcCtx + self.gbcKeyed = {} end end -- a full-color atlas colors everything it paints, ring and border fill @@ -672,17 +679,48 @@ local function getColor0KeyShader() return color0KeyShader or nil end --- draw a cell's bottom tile row without touching the shader (the caller --- owns it). drawCellBottom wraps this with the color-0 key; tilt mode's --- upright pass wraps it with a color-0-keyed palette shader instead --- (PaletteFX.keyedShader) so the feet patch is colorized like the ground. +-- RED++ (COLORS=ADVANCED): the color-0 key, BAKED instead of tested for. +local function getKeyedTile(self, tile) + local ctx = self.gbcCtx + local cached = self.gbcKeyed[tile] + if cached ~= nil then return cached or nil end + local img = false + if ctx.groupColors and love.image and love.image.newImageData then + local group = PaletteFX.worldGroupAt(ctx.tilesetId, ctx.mapId, tile) + local colors = group and ctx.groupColors[group + 1] + local src = Assets.imageData(ctx.imagePath) + local ox = (tile % ctx.perRow) * 8 + local oy = math.floor(tile / ctx.perRow) * 8 + local out = love.image.newImageData(8, 8) + for py = 0, 7 do + for px = 0, 7 do + local r, g, b, a = src:getPixel(ox + px, oy + py) + -- read shade 0 off the RAW sheet, on recolorSample's own cutoff, so + -- the keyed pixels are exactly the ones the shader path keys + local shade0 = r > 0.83 + r, g, b, a = recolorSample(r, g, b, a, colors) + out:setPixel(px, py, r, g, b, shade0 and 0 or a) + end + end + img = love.graphics.newImage(out) + end + self.gbcKeyed[tile] = img + return img or nil +end + function TileRenderer:drawCellBottomRaw(cx, cy, camX, camY) local ty = cy * 2 + 1 for i = 0, 1 do local tx = cx * 2 + i - local quad = self.quads[self.map:tileAt(tx, ty)] - if quad then - love.graphics.draw(self.image, quad, tx * 8 - camX, ty * 8 - camY) + local tile = self.map:tileAt(tx, ty) + local keyed = tile and self.gbcCtx and getKeyedTile(self, tile) + if keyed then + love.graphics.draw(keyed, tx * 8 - camX, ty * 8 - camY) + else + local quad = self.quads[tile] + if quad then + love.graphics.draw(self.image, quad, tx * 8 - camX, ty * 8 - camY) + end end end end @@ -690,7 +728,9 @@ end -- redraw a cell's bottom tile row (tall grass hides the lower half of -- sprites standing in it, like the GB sprite-priority trick) function TileRenderer:drawCellBottom(cx, cy, camX, camY) - local shader = getColor0KeyShader() + -- the RED++ path is pre-keyed; the white test would be a no-op there at + -- best, and a false hit on some other group's near-white color 0 at worst + local shader = not self.gbcCtx and getColor0KeyShader() or nil if shader then love.graphics.setShader(shader) end self:drawCellBottomRaw(cx, cy, camX, camY) if shader then love.graphics.setShader() end @@ -712,13 +752,6 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors) end end --- Window cover for the static tile layer. Refill the reusable window batch --- (and the per-entry animated batches) only when the camera has scrolled past --- what they already cover; a small margin keeps small scrolls free. Cost --- scales with the view, never the map -- crossing a seam or warping in builds --- nothing. The beyond-body area (what the old 3-block ring drew) is painted --- by :drawBorderFill, whose world-aligned border-block tiling is identical --- there, so only body tiles are gathered here. local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills function TileRenderer:ensureWindow(camX, camY, vw, vh) @@ -873,6 +906,12 @@ end -- atlas -- is unique to this map (gbcAtlasCache is keyed by map id). function TileRenderer:release() self:releaseBatches() + if self.gbcKeyed then + -- baked per instance, shared with nobody (see getKeyedTile) + for _, img in pairs(self.gbcKeyed) do safeRelease(img) end + self.gbcKeyed = nil + self.gbcCtx = nil + end if self.gbcAtlas and self.image then local key = self.map.tileset.image .. "#gbc:" .. self.map.id if gbcAtlasCache[key] == self.image then gbcAtlasCache[key] = nil end diff --git a/tests/drivers/grass_overlay_bug150_test.lua b/tests/drivers/grass_overlay_bug150_test.lua index aec213ce..8c54bf93 100644 --- a/tests/drivers/grass_overlay_bug150_test.lua +++ b/tests/drivers/grass_overlay_bug150_test.lua @@ -26,8 +26,20 @@ -- * the ROUTE terrain palette still carries BOTH grass green (173,230,90) and -- light-blue (165,214,255), so the grass field keeps its green+blue dither. -- --- Screenshots (SHOT_DIR): grass_bug150_sgb.png (the reported view) and --- grass_bug150_ogred.png (OG RED reference -- green character, red terrain). +-- The SECOND half of #150 is the feet overdraw itself. Tall grass hides the +-- lower half of whoever stands in it (the GB OBJ-priority trick: an OBJ shows +-- through BG colour 0 and hides under colours 1-3), which the port reproduces +-- by redrawing the cell's bottom tile row over the sprite with shade 0 keyed +-- to alpha. Every mode but ADVANCED still has DMG white sitting in shade 0 at +-- draw time, so a white test finds it; ADVANCED bakes the real per-tile GBC +-- palette into the atlas first (TileRenderer.getGbcAtlas), which turns the +-- grass tile's shade 0 into its palette's light green -- the white test then +-- never fires and the patch paints an opaque block over the player. Hence the +-- gap-pixel gate below, run in BOTH modes so the two can't drift apart again. +-- +-- Screenshots (SHOT_DIR): grass_bug150_sgb.png (the reported view), +-- grass_bug150_advanced.png (the ADVANCED view) and grass_bug150_ogred.png +-- (OG RED reference -- green character, red terrain). -- -- Run: POKEPORT_DRIVER=tests/drivers/grass_overlay_bug150_test.lua \ -- POKEPORT_IDENTITY=bug150 POKEPORT_TOUCH=0 love . @@ -49,6 +61,31 @@ return function(game) return false end + -- How many of the 16x8 feet-overdraw pixels let the thing underneath show + -- through, measured the way the screen does it: render the cell's bottom + -- tile row over an opaque magenta field and count the magenta survivors. + -- Mode-agnostic on purpose -- it asks what reached the framebuffer, not + -- which of the two keying paths (shader or baked alpha) produced it. + local function grassGapPixels(ow) + local p = ow.player + local canvas = love.graphics.newCanvas(16, 8) + love.graphics.setCanvas(canvas) + love.graphics.clear(1, 0, 1, 1) + love.graphics.setColor(1, 1, 1, 1) + -- camera placed so the cell's bottom tile row lands at the canvas origin + ow.map.renderer:drawCellBottom(p.cellX, p.cellY, p.cellX * 16, p.cellY * 16 + 8) + love.graphics.setCanvas() + local id = canvas:newImageData() + local n = 0 + for y = 0, 7 do + for x = 0, 15 do + local r, g, b = id:getPixel(x, y) + if r > 0.9 and g < 0.1 and b > 0.9 then n = n + 1 end + end + end + return n + end + -- a party + starter flag so the overworld is fully usable game.save.flags.EVENT_GOT_STARTER = true local Pokemon = require("src.pokemon.Pokemon") @@ -70,6 +107,13 @@ return function(game) U.wait(40) -- let the grass/flower tile animation cycle U.shot(game, DIR .. "/grass_bug150_sgb.png") + -- the feet overdraw is see-through in SGB (the mode the reporter compared + -- ADVANCED against), so this side of the gate holds before AND after + local sgbGaps = grassGapPixels(ow) + check(sgbGaps > 0, + "SGB grass feet overdraw shows the sprite through its gaps (" + .. sgbGaps .. "/128 px)") + -- === render-decision gate: fails before the fix, passes after ========= check(PaletteFX.usesSpriteObp("gbc") == false, "SGB owns no object palette (it cannot colour an OBJ apart from the BG)") @@ -100,7 +144,47 @@ return function(game) check(hasColor(terrain, 165, 214, 255), "ROUTE terrain palette still contains light-blue (grass keeps its blue dither)") + -- === ADVANCED (RED++): the same overdraw, over a baked true-colour atlas == + -- setMode drops every cached Map/TileRenderer and reloads the visible one, + -- so re-read the state's map before touching its renderer. + game.save.options.colors = "redpp" + PaletteFX.setMode("redpp") + U.wait(20) + ow = game.overworld + check(ow.map.renderer.gbcAtlas ~= nil, + "ADVANCED baked the per-tile GBC atlas for ROUTE_1") + local advGaps = grassGapPixels(ow) + check(advGaps > 0, + "ADVANCED grass feet overdraw shows the sprite through its gaps (" + .. advGaps .. "/128 px)") + -- the gaps must be the SAME pixels the other modes key -- a baked-in green + -- shade 0 is what #150 saw, so the count has to match SGB's exactly + check(advGaps == sgbGaps, + "ADVANCED keys the same shade-0 pixels SGB does (" .. advGaps + .. " vs " .. sgbGaps .. ")") + U.shot(game, DIR .. "/grass_bug150_advanced.png") + + -- ROUTE_1 is the OVERWORLD tileset; the other two tilesets that own a grass + -- tile (FOREST $20, PLATEAU $45) file it under the very same pack group 2, + -- so all three baked the same green over shade 0 and all three broke + -- together. One tileset passing proves nothing about the other two. + for _, spot in ipairs({ { "VIRIDIAN_FOREST", 6, 6, "FOREST" }, + { "ROUTE_23", 10, 44, "PLATEAU" } }) do + local id, cx, cy, tsId = spot[1], spot[2], spot[3], spot[4] + U.teleport(game, id, cx, cy, "down") + U.wait(10) + ow = game.overworld + check(ow.map.tileset.id == tsId + and ow.map:isGrassCell(ow.player.cellX, ow.player.cellY), + id .. ": player stands on " .. tsId .. " tall grass") + check(ow.map.renderer.gbcAtlas ~= nil, id .. ": ADVANCED baked its atlas") + local gaps = grassGapPixels(ow) + check(gaps > 0, id .. ": ADVANCED grass feet overdraw shows the sprite " + .. "through its gaps (" .. gaps .. "/128 px)") + end + -- OG RED reference for the human diff (green character over red terrain) + U.teleport(game, "ROUTE_1", 10, 6, "down") game.save.options.colors = "ogred" PaletteFX.setMode("ogred") U.wait(20)