Skip to content
Open
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
49 changes: 49 additions & 0 deletions src/import/RomExtractorGen3.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46049,6 +46049,9 @@ RomExtractorGen3.FRLG_NAMING = {
GFX = 0xE980E4, BG_MAP = 0xE982BC,
KB_MAPS = { upper = 0xE98398, lower = 0xE98458, symbols = 0xE98518 },
MENU_PAL = 0xE98024, KB_PAL = 0xE97FE4,
-- text.c gKeypadIconTiles is a 128x32 4bpp sheet (64 tiles). Naming's
-- WIN_BANNER loads GetTextWindowPalette(2), the third 16-colour row here.
KEYPAD_GFX = 0x1EA700, TEXT_WINDOW_PALS = 0x471DEC,
-- naming_screen.c's private rival sheet is a second exact copy of
-- gObjectEventPic_Blue (0x38A428) in the retail ROM. Its dedicated palette
-- sits between gNamingScreenKeyboard_Pal and gNamingScreenMenu_Pal in
Expand Down Expand Up @@ -46119,21 +46122,61 @@ function RomExtractorGen3:extractFireRedNaming()
end
pcall(layer, "bg", N.BG_MAP, true)
for key, at in pairs(N.KB_MAPS) do pcall(layer, "kb_" .. key, at, false) end

-- PrintControls does not draw button-shaped boxes. The {DPAD_ANY},
-- {A_BUTTON} and {B_BUTTON} control codes call DrawKeypadIcon, which blits
-- these exact pixels from gKeypadIconTiles with colour 0 keyed out. Pull
-- those three icons so the runtime can compose gText_MoveOkBack with the
-- cartridge's own small-font glyphs and keypad art.
pcall(function()
local raw = rom:bytes(N.KEYPAD_GFX, 64 * 32)
local px = RomGba.tiles4bpp(raw, 16, 4)
local palRaw = rom:bytes(N.TEXT_WINDOW_PALS + 2 * 32, 32)
local bannerPal = {}
for i = 0, 15 do
bannerPal[i] = { RomGba.bgr555(palRaw[i * 2 + 1] + palRaw[i * 2 + 2] * 256) }
end
local function keypad(key, tile, w, h)
local sx, sy = (tile % 16) * 8, math.floor(tile / 16) * 8
local img = ImageWriter.blank(w, h)
for y = 0, h - 1 do
for x = 0, w - 1 do
local v = px[sy + y + 1][sx + x + 1]
local col = v ~= 0 and bannerPal[v]
if col then
img:setPixel(x, y, col[1] / 255, col[2] / 255, col[3] / 255, 1)
end
end
end
save(key, img)
end
keypad("help_a", 0x00, 8, 12)
keypad("help_b", 0x01, 8, 12)
keypad("help_dpad", 0x22, 8, 12) -- CHAR_DPAD_NONE / {DPAD_ANY}
end)

for key, spec in pairs(N.SPRITES) do
pcall(function()
local at, w, h, bank = spec[1], spec[2], spec[3], spec[4]
local cols, rws = w / 8, h / 8
local raw = rom:bytes(at, cols * rws * 32)
local px = RomGba.tiles4bpp(raw, cols, rws)
local img = ImageWriter.blank(w, h)
-- SpriteCB_Cursor changes ONLY OBJ palette entry 1 while the keyboard
-- cursor idles. Keep those pixels in a separate white mask so the
-- runtime can replace that one colour every frame without tinting the
-- rest of the cartridge-ripped cursor art.
local pulse = key == "cursor" and ImageWriter.blank(w, h) or nil
for y = 1, h do
for x = 1, w do
local v = px[y][x]
local col = v ~= 0 and pal[bank * 16 + v]
if col then img:setPixel(x - 1, y - 1, col[1] / 255, col[2] / 255, col[3] / 255, 1) end
if pulse and v == 1 then pulse:setPixel(x - 1, y - 1, 1, 1, 1, 1) end
end
end
save(key, img)
if pulse then save("cursor_pulse", pulse) end
end)
end
-- RIVAL's naming icon is not the ordinary object-event sprite at runtime:
Expand Down Expand Up @@ -46177,9 +46220,15 @@ function RomExtractorGen3:extractFireRedNaming()
end)
local function c(i) local t = pal[i] return { t[1], t[2], t[3] } end
local constants = self._constants or {}
local cursorWord = rom:u16(N.MENU_PAL + 5 * 32 + 2) -- palette 5, entry 1
constants.gen3FRLGNaming = {
images = images,
rivalIcon = rivalIcon,
cursorPulseBase = {
cursorWord % 32,
math.floor(cursorWord / 32) % 32,
math.floor(cursorWord / 1024) % 32,
},
colors = {
fill = { upper = c(10 * 16 + 13), lower = c(10 * 16 + 14), symbols = c(10 * 16 + 15) },
key = { c(10 * 16 + 1), c(10 * 16 + 2) },
Expand Down
7 changes: 6 additions & 1 deletion src/import/RomImporter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,12 @@ 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.
local CACHE_FORMAT = "rom-cache-v360:"
-- 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.
local CACHE_FORMAT = "rom-cache-v361:"
-- 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
17 changes: 13 additions & 4 deletions src/render/Font.lua
Original file line number Diff line number Diff line change
Expand Up @@ -799,11 +799,20 @@ local function frameIndex(def)
return want
end

local function sheetFrame(tx, ty, tw, th)
local function sheetFrame(tx, ty, tw, th, frameOverride)
local def = state and state.def or {}
local quads, image = frameQuads(def)
if not quads then return false end
local set = quads[frameIndex(def)]
local want = tonumber(frameOverride)
if want then
want = math.floor(want)
local count = (def.frames and def.frames.count) or 1
if want < 1 then want = 1 end
if want > count then want = count end
else
want = frameIndex(def)
end
local set = quads[want]
if not set then return false end
local tile = (def.frames.tile) or 8
local x, y = tx * 8, ty * 8
Expand Down Expand Up @@ -844,11 +853,11 @@ local function drawnFrame(tx, ty, tw, th, style)
end

-- Draw a Game Boy style bordered box in tile coordinates.
function Font.drawBox(tx, ty, tw, th)
function Font.drawBox(tx, ty, tw, th, frameOverride)
if not FALLBACK.enabled and state and state.def then
-- the cartridge's own nine-slice first; the drawn rectangle is what a
-- dataset without one falls back to
if state.def.frames and sheetFrame(tx, ty, tw, th) then return end
if state.def.frames and sheetFrame(tx, ty, tw, th, frameOverride) then return end
if state.def.frame == "drawn" or state.def.frame == "sheet" then
return drawnFrame(tx, ty, tw, th, activeStyle)
end
Expand Down
18 changes: 16 additions & 2 deletions src/ui/Gen3OakSpeechFRLG.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ function Speech.new(game, onDone)
self.black = 1 -- whole-screen fade to black
self.picAlpha = 0 -- trainer pic + platform
self.picTarget = 0
self.picOffset = 0 -- slide left while the name list is up
-- Visual offset of BG2 trainer art + the platform OBJ sprites. Oak speech's
-- task variable counts DOWN to -60, but BG2 HOFS=-60 displays the bitmap
-- sixty pixels to the RIGHT, while gSpriteCoordOffsetX is explicitly +60.
-- Store the screen-space result here so draw() does not invert it again.
self.picOffset = 0
self.picScale = 1
self.picWhite = 0
self.nidoAlpha, self.nidoScale, self.nidoWhite = 0, 0, 0
Expand Down Expand Up @@ -199,6 +203,7 @@ end
function Speech:menu(labels, opts)
local Menu = require("src.ui.Menu")
local choice
opts = opts or {}
local items = {}
for i, label in ipairs(labels) do
items[i] = { label = label, onSelect = function()
Expand All @@ -207,6 +212,15 @@ function Speech:menu(labels, opts)
self.waitFrames = 1
end }
end
-- oak_speech.c passes WindowTemplate CONTENT coordinates to these menus.
-- Their frame is one tile outside the template on every side and every row
-- starts at y=1 with a 16-pixel pitch. A fresh FireRed save has Frame Type
-- 1 (optionsWindowFrameType=0), so keep the professor intro on that frame
-- even though this port deliberately persists desktop options across New
-- Game.
opts.gbaWindow = true
opts.th = opts.th or (#labels * 2)
opts.frameIndex = 1
opts.cancelable = opts.onCancelIndex ~= nil
if opts.onCancelIndex then
opts.onCancel = function() choice = opts.onCancelIndex; self.waitFrames = 1 end
Expand Down Expand Up @@ -366,7 +380,7 @@ function Speech:run()
while true do
box = self:ask(question)
question = "rivalNameAgain"
if self.picOffset > -60 then self:tween("picOffset", -60, 2) end
if self.picOffset < 60 then self:tween("picOffset", 60, 2) end
local labels = { clean(self.text.newName) or Strings("NEW NAME") }
for i = 1, 4 do labels[#labels + 1] = rivals[i] end
local pick = self:menu(labels, { tx = 2, ty = 2, tw = 12 })
Expand Down
40 changes: 35 additions & 5 deletions src/ui/Menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ function Menu.new(game, items, opts)
self.tx = opts.tx or 10
self.ty = opts.ty or 0
self.tw = opts.tw or 10
-- Gen 3 WindowTemplate coordinates describe the CONTENT rectangle. Its
-- standard frame is drawn one tile outside on every side. Most of this
-- engine's menus use the older convention where tx/ty/tw/th already name
-- the OUTER framed rectangle, so keep that as the default and opt the few
-- cartridge-window callers into the GBA convention explicitly.
self.gbaWindow = opts.gbaWindow or false
self.frameIndex = opts.frameIndex
-- grow the box to the widest label so longer (e.g. localized) labels don't
-- overflow the frame; nudge tx left to keep the box on-screen (20 tiles).
do
Expand All @@ -30,15 +37,15 @@ function Menu.new(game, items, opts)
end
end
local needed = widest + 3
if needed > self.tw then self.tw = needed end
if not self.gbaWindow and needed > self.tw then self.tw = needed end
-- ...ON THE SCREEN THAT IS ACTUALLY THERE. Twenty tiles is the Game Boy's
-- width and was written in here as a constant; Hoenn's screen is thirty,
-- so a box the cartridge places near the right-hand side was shoved left
-- by ten tiles. Theme.uiSize is the same answer every Gen 3 screen asks
-- for, and it returns the Game Boy's own 160 for Gen 1 and Gen 2, so
-- nothing there moves.
local cols = math.floor(select(1, Theme.uiSize()) / 8)
if self.tx + self.tw > cols then
if not self.gbaWindow and self.tx + self.tw > cols then
self.tx = math.max(0, cols - self.tw)
end
end
Expand Down Expand Up @@ -157,11 +164,16 @@ function Menu:draw()
-- edge (the START menu asks for "topright"). Only menus that ask for it
-- move; every other menu is placed exactly as before.
local r = self.anchor and self.game and self.game.renderer
local frameTx, frameTy, frameTw, frameTh = self.tx, self.ty, self.tw, self.th
if self.gbaWindow then
frameTx, frameTy = self.tx - 1, self.ty - 1
frameTw, frameTh = self.tw + 2, self.th + 2
end
if r and r.setUIAnchor then
r:setUIAnchor(self.tx * 8, self.ty * 8,
self.tw * 8, self.th * 8, self.anchor)
r:setUIAnchor(frameTx * 8, frameTy * 8,
frameTw * 8, frameTh * 8, self.anchor)
end
Font.drawBox(self.tx, self.ty, self.tw, self.th)
Font.drawBox(frameTx, frameTy, frameTw, frameTh, self.frameIndex)
love.graphics.setColor(0, 0, 0, 1)
local visible = (self.maxVisible and math.min(self.maxVisible, #self.items))
or #self.items
Expand All @@ -186,6 +198,24 @@ function Menu:draw()
-- overhang, which is nothing at all for an 8-pixel font: every Game Boy
-- menu in this engine draws where it always did, and a taller face lands
-- with its last descender flush on the inner edge.
if self.gbaWindow then
-- FireRed Menu_InitCursor + AddTextPrinterParameterized3: the selector is
-- at x=0 in the window, labels at x=8, both start one pixel down, and rows
-- are sixteen pixels apart in the Oak intro menus.
for row = 1, visible do
local item = self.items[self.scroll + row]
if not item then break end
local y = self.ty * 8 + 1 + (row - 1) * self.rowStep * 8
Font.draw(item.label, self.tx * 8 + 8, y)
end
local cursorRow = self.index - self.scroll
Font.drawCode(Theme.cursor, self.tx * 8,
self.ty * 8 + 1 + (cursorRow - 1) * self.rowStep * 8)
self:drawDescription()
love.graphics.setColor(1, 1, 1, 1)
return
end

local lastY = (self.ty + self.th - 2) * 8
local lift = math.max(0, (lastY + Font.glyphHeight())
- (self.ty + self.th - 1) * 8)
Expand Down
61 changes: 44 additions & 17 deletions src/ui/NamingScreen.lua
Original file line number Diff line number Diff line change
Expand Up @@ -767,25 +767,30 @@ function NamingScreen:drawFireRed(rec)
local bg = self:frlgImage("bg")
if bg then g.draw(bg, 0, 0) end

-- the banner: window (0,0) 30x2, "{DPAD}MOVE {A}OK {B}BACK" right-aligned
-- the banner: PrintControls renders the exact
-- "{DPAD_ANY}MOVE {A_BUTTON}OK {B_BUTTON}BACK" string in FONT_SMALL and
-- right-aligns it four pixels from the edge. The control codes are bitmap
-- keypad icons, not outlined button boxes.
g.setColor(0, 123 / 255, 197 / 255, 1)
g.rectangle("fill", 0, 0, 240, 16)
local white, darkGray = { 1, 1, 1, 1 }, { 98 / 255, 98 / 255, 98 / 255, 1 }
local hints = { { "+", Strings("MOVE") }, { "A", Strings("OK") }, { "B", Strings("BACK") } }
local help = {
{ self:frlgImage("help_dpad"), Strings("MOVE") .. " " },
{ self:frlgImage("help_a"), Strings("OK") .. " " },
{ self:frlgImage("help_b"), Strings("BACK") },
}
local faced = Font.hasFace and Font.hasFace("small") and Font.pushFace("small")
local width = 0
for _, h in ipairs(hints) do width = width + 12 + Font.width(h[2]) + 4 end
if faced then Font.popFace() end
local hx = 240 - 4 - width + 4
for _, h in ipairs(hints) do
g.setColor(white)
g.rectangle("line", hx + 0.5, 2.5, 10, 9, 3, 3)
text(h[1], hx + 2, 0, white, darkGray, true)
text(h[2], hx + 12, 0, white, darkGray, true)
local f2 = Font.hasFace and Font.hasFace("small") and Font.pushFace("small")
hx = hx + 12 + Font.width(h[2]) + 4
if f2 then Font.popFace() end
for _, h in ipairs(help) do width = width + 8 + Font.width(h[2]) end
local hx = 240 - 4 - width
for _, h in ipairs(help) do
g.setColor(1, 1, 1, 1)
if h[1] then g.draw(h[1], hx, 0) end
hx = hx + 8
text(h[2], hx, 0, white, darkGray, false)
hx = hx + Font.width(h[2])
end
if faced then Font.popFace() end

-- the question and the typed name, on the plate BG3 already draws
local entryInk = cc.entry and rgb(cc.entry[2]) or darkGray
Expand All @@ -799,9 +804,13 @@ function NamingScreen:drawFireRed(rec)
if glyph then text(Strings(glyph), x, 48 + 1, entryInk, entryShadow) end
if underscore then g.draw(underscore, x + 3 - 4, 60 - 4) end
end
local at = math.min(#self.glyphs + 1, self.maxLen)
if arrow and math.floor((self.blink or 0) / 30) % 2 == 0 then
g.draw(arrow, base + (at - 1) * 8 - 4, 56 - 4)
if arrow then
-- CreateTextEntrySprites puts this sprite at centre (base-5, 56), and it
-- stays there regardless of how many letters are already typed. Its
-- callback cycles x2={0,-4,-2,-1} every eight frames; it does not blink.
local wobble = { 0, -4, -2, -1 }
local wi = math.floor((self.blink or 0) / 8) % #wobble + 1
g.draw(arrow, base - 9 + wobble[wi], 56 - 4)
end

-- the page: BG1's frame, the window's fill, the keys
Expand Down Expand Up @@ -856,7 +865,25 @@ function NamingScreen:drawFireRed(rec)
-- the cursor, on a key
local cursor = self:frlgImage("cursor")
if cursor and self.row <= m.keyRows then
g.draw(cursor, 30 + (cols[self.col] or 0), 80 + (self.row - 1) * 16)
local x, y = 30 + (cols[self.col] or 0), 80 + (self.row - 1) * 16
g.setColor(1, 1, 1, 1)
g.draw(cursor, x, y)
local pulse = self:frlgImage("cursor_pulse")
local base5 = rec.cursorPulseBase
if pulse and type(base5) == "table" then
-- SpriteCB_Cursor advances 2/4/.../16/14/.../0, each value lasting
-- two frames. MultiplyInvertedPaletteRGBComponents moves green/blue
-- toward white by that amount and red by half as much.
local k = math.floor((self.blink or 0) / 2) % 16
local n = (k < 8) and ((k + 1) * 2) or ((15 - k) * 2)
local function brighten(v, amount)
v = tonumber(v) or 0
return (v + math.floor((31 - v) * amount / 16)) / 31
end
g.setColor(brighten(base5[1], math.floor(n / 2)),
brighten(base5[2], n), brighten(base5[3], n), 1)
g.draw(pulse, x, y)
end
end
g.setColor(1, 1, 1, 1)
end
Expand Down
Loading