Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 157 additions & 17 deletions src/import/RomExtractorGen3.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7237,28 +7237,107 @@ function RomExtractorGen3:extractFont()
-- rounded caps) in sTextWindowPalettes[0]; the user frames only border
-- menus. Baked as one 18-tile strip for Font.drawDialogueBox.
local dialogueRecord = nil
local signpostRecord = nil
local standardFrameRecord = nil
local fieldTextStyles = nil
local uiTextStyles = nil
if (self.manifest or {}).frlgItemMenu ~= nil then
local ok, err = pcall(function()
local GFX, PAL, N = 0x41F1C8, 0x471DEC, 18
local colors = RomGba.palette(self.rom:bytes(PAL, 32))
local raw = self.rom:bytes(GFX, N * 32)
local px = RomGba.tiles4bpp(raw, N, 1)
local image = ImageWriter.blank(N * 8, 8)
local MSG_GFX = self:symbol("gMenuMessageWindow_Gfx") or 0x41F1C8
local SIGN_GFX = self:symbol("gSignpostWindow_Gfx") or 0x470B0C
local PAL = self:symbol("gTextWindowPalettes") or 0x471DEC

local function bakeFrame(gfx, paletteId, count, name)
local colors = RomGba.palette(self.rom:bytes(PAL + paletteId * 32, 32))
local raw = self.rom:bytes(gfx, count * 32)
local px = RomGba.tiles4bpp(raw, count, 1)
local image = ImageWriter.blank(count * 8, 8)
for y = 1, 8 do
for x = 1, count * 8 do
local v = px[y][x]
-- Colour zero is transparent on the BG layer. Preserve that in
-- the baked strip so the sign/message frame can sit over the field
-- exactly like the cartridge rather than painting its transparent
-- holes with palette entry zero.
local c = v ~= 0 and colors[v + 1]
if c then
image:setPixel(x - 1, y - 1,
c[1] / 255, c[2] / 255, c[3] / 255, 1)
end
end
end
self:saveImage(image, "fonts/" .. name .. ".png")
local fill = colors[2]
return {
image = "assets/generated/fonts/" .. name .. ".png",
tile = 8, tiles = count,
fill = { fill[1] / 255, fill[2] / 255, fill[3] / 255 },
source = ("ROM:gfx %07X, gTextWindowPalettes[%d] %07X")
:format(gfx, paletteId, PAL + paletteId * 32),
}, colors
end

local colors
dialogueRecord, colors = bakeFrame(MSG_GFX, 0, 18, "frlg_dialogue")
-- MSGBOX_SIGN uses the exact same tile placement as a field dialogue
-- frame, but loads gSignpostWindow_Gfx and text-window palette 1 first.
-- The asset is 0x260 bytes = 19 tiles; the nineteenth tile is retained
-- even though the common frame path addresses the first eighteen.
signpostRecord = bakeFrame(SIGN_GFX, 1, 19, "frlg_signpost")
signpostRecord.layout = "frlg_signpost"

-- DrawStdFrameWithCustomTileAndPalette is the 9-tile fixed frame used by
-- the bag DEPOSIT ITEM tab and the Item-PC transient message/question
-- windows. It is NOT the configurable user frame and therefore must not
-- follow the player's Options -> Frame Type setting.
local STD_GFX = self:symbol("gStdTextWindow_Gfx") or 0x471A4C
-- LoadStdWindowGfx pairs these tiles with GetTextWindowPalette(3).
-- Item-PC and bag calls merely choose the destination palette bank.
local STD_PAL = PAL + 3 * 32
local stdColors = RomGba.palette(self.rom:bytes(STD_PAL, 32))
local stdRaw = self.rom:bytes(STD_GFX, 9 * 32)
local stdPx = RomGba.tiles4bpp(stdRaw, 9, 1)
local stdImage = ImageWriter.blank(9 * 8, 8)
for y = 1, 8 do
for x = 1, N * 8 do
local v = px[y][x]
local c = v ~= 0 and colors[v + 1]
for x = 1, 9 * 8 do
local v = stdPx[y][x]
local c = v ~= 0 and stdColors[v + 1]
if c then
image:setPixel(x - 1, y - 1, c[1] / 255, c[2] / 255, c[3] / 255, 1)
stdImage:setPixel(x - 1, y - 1,
c[1] / 255, c[2] / 255, c[3] / 255, 1)
end
end
end
self:saveImage(image, "fonts/frlg_dialogue.png")
local fill = colors[2]
dialogueRecord = {
image = "assets/generated/fonts/frlg_dialogue.png", tile = 8, tiles = N,
fill = { fill[1] / 255, fill[2] / 255, fill[3] / 255 },
source = ("ROM:gMenuMessageWindow_Gfx %07X, sTextWindowPalettes %07X"):format(GFX, PAL),
self:saveImage(stdImage, "fonts/frlg_standard_frame.png")
local stdFill = stdColors[2]
standardFrameRecord = {
image = "assets/generated/fonts/frlg_standard_frame.png",
tile = 8, tiles = 9,
fill = { stdFill[1] / 255, stdFill[2] / 255, stdFill[3] / 255 },
source = ("ROM:gStdTextWindow_Gfx %07X, gTextWindowPalettes[3] %07X")
:format(STD_GFX, STD_PAL),
}

local function rgb(i)
local c = colors[i + 1]
return { c[1] / 255, c[2] / 255, c[3] / 255, 1 }
end
-- AddTextPrinterDiffStyle: female NPCs are red, male NPCs blue, and
-- objects/system text dark gray; all three use the light-gray shadow.
-- These are palette indices, not invented RGB values, so the runtime can
-- reproduce ContextNpcGetTextColor without baking another font page.
fieldTextStyles = {
female = { text = rgb(4), shadow = rgb(3) },
male = { text = rgb(8), shadow = rgb(3) },
neutral = { text = rgb(2), shadow = rgb(3) },
mon = { text = rgb(2), shadow = rgb(3) },
}
-- bag.c/item_pc.c both use the same first ten standard text colours.
-- Their lower description strips deliberately switch to white text over
-- the blue background, while lists/submenus use dark gray on light paper.
uiTextStyles = {
white = { text = rgb(1), shadow = rgb(2) },
dark = { text = rgb(2), shadow = rgb(3) },
}
end)
if not ok then Logger.warn("gen3 frlg dialogue frame: %s", tostring(err)) end
Expand All @@ -7268,6 +7347,9 @@ function RomExtractorGen3:extractFont()
local perRow = 16
local cellW = math.floor(tonumber(font.cellWidth) or 8)
local cellH = math.floor(tonumber(font.cellHeight) or 11)
-- gFontInfos gives FireRed FONT_NORMAL a 14px runtime height.
local frlg = (self.manifest or {}).frlgItemMenu ~= nil
if frlg and tonumber(font.fontId) == 1 then cellH = 14 end
local top = math.floor(tonumber(font.inkTop) or 0)
local rows = math.ceil(count / perRow)

Expand Down Expand Up @@ -7508,7 +7590,58 @@ function RomExtractorGen3:extractFont()
-- Each is a page of its own -- own cell, own widths, own picture -- and they
-- are keyed by the ROLE the font id gives them, not by address.
local faces = {}
for _, face in ipairs(font.faces or {}) do
local faceDefs = {}
for _, face in ipairs(font.faces or {}) do faceDefs[#faceDefs + 1] = face end
-- FireRed's field dialogue has two extra Latin faces. They are not colour
-- aliases of FONT_NORMAL: text.c decompresses dedicated Male/Female glyph
-- sheets with their own width tables, then AddTextPrinterDiffStyle paints
-- them blue/red. The generic font discovery only needs normal/small for
-- menus, so append these exact named cartridge sheets for field dialogue.
if (self.manifest or {}).frlgItemMenu ~= nil then
-- FireRed keeps NormalCopy1, Normal, Male and Female Latin width tables as
-- byte-identical 512-byte runs. The retail manifest only names Normal, so
-- locate the next two identical runs after it; each corresponding full
-- glyph sheet starts exactly 0x8000 bytes before its width table. This is
-- the same relationship used by text.c, but remains relocation-safe.
local function nextMatchingWidths(from)
local n = math.min(count, 512)
local first = self.rom:u32(widths)
for at = math.max(0, math.floor(from or 0)), self.rom.size - n do
if self.rom:u32(at) == first then
local same = true
for i = 0, n - 1 do
if self.rom:u8(at + i) ~= self.rom:u8(widths + i) then
same = false; break
end
end
if same then return at end
end
end
return nil
end
local maleWidths = self:symbol("sFontMaleLatinGlyphWidths")
or nextMatchingWidths(widths + 1)
local femaleWidths = self:symbol("sFontFemaleLatinGlyphWidths")
or (maleWidths and nextMatchingWidths(maleWidths + 1))
local extra = {
{ role = "male", fontId = 4,
glyphs = self:symbol("sFontMaleLatinGlyphs")
or (maleWidths and maleWidths - 0x8000),
widths = maleWidths },
{ role = "female", fontId = 5,
glyphs = self:symbol("sFontFemaleLatinGlyphs")
or (femaleWidths and femaleWidths - 0x8000),
widths = femaleWidths },
}
for _, face in ipairs(extra) do
if face.glyphs and face.widths then
face.cellWidth, face.cellHeight = 8, 14
face.glyphBytes, face.tilesAcross = 0x40, 2
faceDefs[#faceDefs + 1] = face
end
end
end
for _, face in ipairs(faceDefs) do
local role = face.role
if role and face.glyphs ~= glyphs then
local fw = math.floor(tonumber(face.cellWidth) or cellW)
Expand Down Expand Up @@ -7631,6 +7764,10 @@ function RomExtractorGen3:extractFont()
-- the twenty border sets the OPTION screen's FRAME row picks between
frames = frameRecord,
dialogueFrame = dialogueRecord,
signpostFrame = signpostRecord,
standardFrame = standardFrameRecord,
fieldTextStyles = fieldTextStyles,
uiTextStyles = uiTextStyles,
-- the cursor and the more-below marker, which are drawn shapes in cells
-- the cartridge leaves blank; see the note above extractFont's page pass
symbols = { cursor = SYMBOLS.cursor,
Expand Down Expand Up @@ -44440,9 +44577,12 @@ function RomExtractorGen3:extractFireRedItemPc()
local cell = ty * S.COLS + tx
local e = map[cell * 2 + 1] + map[cell * 2 + 2] * 256
local tid = e % 1024
local flipX = math.floor(e / 1024) % 2 == 1
local flipY = math.floor(e / 2048) % 2 == 1
local bank = math.floor(e / 4096) % 16
if tid < tileCount and bank < S.PALETTE_BANKS then
RomExtractorGen3.partyTile(image, tiles, colors, tid, bank, tx * 8, ty * 8)
RomExtractorGen3.partyTile(image, tiles, colors, tid, bank,
tx * 8, ty * 8, flipX, flipY)
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion src/import/RomImporter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1222,11 +1222,19 @@ end
-- the one gate that forces a re-import, and main's change alters what a
-- FireRed cache CONTAINS -- so keeping v359 would let a FireRed cache
-- built before the tutor fix pass as current. v360 invalidates both.
-- v361: FireRed naming fidelity now extracts the cartridge's keypad icons and
-- the cursor-pulse mask/base colour. An older cache has none of those
-- files/fields, so without a format bump an updated runtime would keep
-- drawing the pre-fix handmade help icons and static selection cursor
-- until the user happened to force a ROM re-import manually.
-- v366: Gen 3 battle panels bake every user frame through gUserFrames' real
-- tile/palette pointers, so FireRed battles follow Frame Type 1-10.
-- (This was v339 on the FireRed branch before the merge with main's
-- v338-v360 chain.)
local CACHE_FORMAT = "rom-cache-v366:"
-- v367: FireRed bag / Item PC / field-dialogue fidelity: the Male/Female
-- dialogue faces and the OPTIONS frame art are baked from the
-- cartridge, so a cache built before this lacks them.
local CACHE_FORMAT = "rom-cache-v367:"
-- The completion marker is written under each version's cache prefix
-- (rom-cache.complete for Red, blue/rom-cache.complete for Blue).
local MARKER_PATH = "rom-cache.complete"
Expand Down
60 changes: 58 additions & 2 deletions src/render/Font.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1089,8 +1089,7 @@ function Font.dialogueBoxBounds(tx, ty, tw, th)
return tx*8, ty*8, tw*8, th*8
end

function Font.drawDialogueBox(tx, ty, tw, th)
local rec = Font.hasDialogueFrame() and state.def.dialogueFrame
local function prepareFrameStrip(rec)
if rec and dialogueSheet.path ~= rec.image then
local ok, img = pcall(Assets.image, rec.image)
dialogueSheet.path, dialogueSheet.image = rec.image, ok and img or nil
Expand All @@ -1110,6 +1109,11 @@ function Font.drawDialogueBox(tx, ty, tw, th)
dialogueSheet.quads = quads
end
end
return rec and dialogueSheet.quads ~= nil
end

local function drawDialogueRecord(rec, tx, ty, tw, th)
prepareFrameStrip(rec)
if not (rec and dialogueSheet.quads) then
return Font.drawBox(tx, ty, tw, th)
end
Expand All @@ -1134,6 +1138,12 @@ function Font.drawDialogueBox(tx, ty, tw, th)
for row = 0, th - 1 do
local flip = row >= half
local depth = flip and (th - 1 - row) or row
-- WindowFunc_DrawDialogueFrame uses a different lower-half set order for
-- MSGBOX_SIGN: set1, set2, set0 instead of set2, set1, set0.
if flip and rec.layout == "frlg_signpost" then
local signDepth = { [0] = 0, [1] = 2, [2] = 1 }
depth = signDepth[depth] or depth
end
local set = SETS[math.min(depth, 2) + 1]
local y = (ty + row) * 8
for col = 0, cols - 1 do
Expand All @@ -1149,4 +1159,50 @@ function Font.drawDialogueBox(tx, ty, tw, th)
end
end

function Font.drawDialogueBox(tx, ty, tw, th)
local rec = Font.hasDialogueFrame() and state.def.dialogueFrame
return drawDialogueRecord(rec, tx, ty, tw, th)
end

function Font.hasSignpostFrame()
local def = state and state.def
return not FALLBACK.enabled and def ~= nil and def.signpostFrame ~= nil
end

function Font.drawSignpostBox(tx, ty, tw, th)
local rec = Font.hasSignpostFrame() and state.def.signpostFrame
if not rec then return Font.drawDialogueBox(tx, ty, tw, th) end
return drawDialogueRecord(rec, tx, ty, tw, th)
end

function Font.drawStandardBox(tx, ty, tw, th)
local def = state and state.def
local rec = not FALLBACK.enabled and def and def.standardFrame or nil
if not (rec and prepareFrameStrip(rec) and dialogueSheet.quads) then
return Font.drawBox(tx, ty, tw, th, 1)
end
if tw < 2 or th < 2 then return Font.drawBox(tx, ty, tw, th, 1) end
local img, q = dialogueSheet.image, dialogueSheet.quads
local fill = rec.fill or { 1, 1, 1 }
love.graphics.setColor(fill[1], fill[2], fill[3], 1)
love.graphics.rectangle("fill", (tx + 1) * 8, (ty + 1) * 8,
math.max(0, tw - 2) * 8, math.max(0, th - 2) * 8)
love.graphics.setColor(1, 1, 1, 1)
for row = 0, th - 1 do
for col = 0, tw - 1 do
local tile
if row == 0 then
tile = col == 0 and 0 or (col == tw - 1 and 2 or 1)
elseif row == th - 1 then
tile = col == 0 and 6 or (col == tw - 1 and 8 or 7)
elseif col == 0 then
tile = 3
elseif col == tw - 1 then
tile = 5
end
if tile then love.graphics.draw(img, q[tile], (tx + col) * 8, (ty + row) * 8) end
end
end
end

return Font
21 changes: 18 additions & 3 deletions src/render/TextBox.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ function TextBox.new(game, text, onDone, opts)
local self = setmetatable({}, TextBox)
self.game = game
self.onDone = onDone
self.textFace = opts.face
local facePushed = self.textFace and Font.hasFace and Font.hasFace(self.textFace)
and Font.pushFace(self.textFace)
self.choice = opts.choice
self.defaultNo = opts.defaultNo
self.choiceNoSound = opts.noSound
Expand Down Expand Up @@ -88,10 +91,14 @@ function TextBox.new(game, text, onDone, opts)
self.scrollHoldFrames = tonumber(opts.scrollHoldFrames)
self.textX = opts.textX or (self.boxTx + 1) * 8
self.drawFrame = opts.drawFrame ~= false
self.signpost = opts.signpost == true
self.fillColor = opts.fillColor
self.textStyle = opts.style
self.promptArrow = opts.promptArrow or box.promptArrow
self.promptArrowAtPen = opts.promptArrowAtPen == true or box.promptArrowAtPen == true
-- GBA field text only draws the pen arrow for explicit page/scroll prompt
-- control codes; reaching the final terminator waits silently for A/B.
self.promptOnlyWhenWaiting = opts.promptOnlyWhenWaiting == true
self.promptArrowImage = nil
self.clipToBox = opts.clipToBox == true
self.uiWidth, self.uiHeight = opts.uiWidth, opts.uiHeight
Expand Down Expand Up @@ -133,6 +140,7 @@ function TextBox.new(game, text, onDone, opts)
self.done = false
self.blink = 0
self:beginLine()
if facePushed then Font.popFace() end
return self
end

Expand Down Expand Up @@ -623,7 +631,11 @@ function TextBox:draw()
r:setUIAnchor(x,y,w,h,"bottom")
end
if self.drawFrame then
Font.drawDialogueBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh)
if self.signpost and Font.drawSignpostBox then
Font.drawSignpostBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh)
else
Font.drawDialogueBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh)
end
elseif self.fillColor then
local c = self.fillColor
love.graphics.setColor(c[1] or 0, c[2] or 0, c[3] or 0, c[4] or 1)
Expand All @@ -637,6 +649,8 @@ function TextBox:draw()
self.boxTw * 8, self.boxTh * 8)
end
love.graphics.setColor(0, 0, 0, 1)
local facePushed = self.textFace and Font.hasFace and Font.hasFace(self.textFace)
and Font.pushFace(self.textFace)
if self.textStyle then Font.pushStyle(self.textStyle) end
if self.scrollPx and self.scrollPx > 0 then
self.scrollPx = self.scrollPx - (self.scrollStep or 2)
Expand Down Expand Up @@ -669,8 +683,8 @@ function TextBox:draw()
end
lastPen, lastY = pen, y
end
local prompt = self.waiting or (self.done and not self.choice and not self.auto
and not self.stay)
local prompt = self.waiting or (self.done and not self.promptOnlyWhenWaiting
and not self.choice and not self.auto and not self.stay)
if prompt and self.promptArrowAtPen and type(self.promptArrow) == "table"
and type(self.promptArrow.path) == "string" and lastPen and lastY then
if self.promptArrowImage == nil then
Expand All @@ -697,6 +711,7 @@ function TextBox:draw()
(self.boxTy + self.boxTh - 1) * 8 - 4)
end
if self.textStyle then Font.popStyle() end
if facePushed then Font.popFace() end
if self.clipToBox then
if oldScissor and oldScissor[1] ~= nil then
love.graphics.setScissor(oldScissor[1], oldScissor[2],
Expand Down
Loading
Loading