Skip to content
Closed
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
23 changes: 23 additions & 0 deletions docs/mod-naming-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Gen1Recomp Gen-2 naming screen compatibility

API-2 mods may open `mod.ui.push(game, "Gen2NamingScreen", opts)`, the public
screen ID used by Anytime Rename 1.2.1. The adapter uses Gen2Recomped's native
keyboard and supports `type` (player, rival, mom, box, nickname), `prompt`,
`maxLength`, `initial`, `mon`, `monName` and `onDone(name)`.

The completion callback owns closing this screen. It runs while the naming
state is still on the stack; a caller may pop it and return to its existing
parent menu. The ordinary `NamingScreen` API remains different: it pops itself
before calling `onDone`. Mapping the ID directly to that class would pop two
states when an existing Gen1Recomp mod closes its own screen.

`text` reflects the editable name and may also be assigned. Initial values and
assignments are limited by glyph count, preserving UTF-8 and charmap tokens.
The adapter keeps the native keyboard's input, drawing and `ui.naming.grid`
hook; it does not reproduce Gen1Recomp's separate Gen-2 keyboard layout or
change native game save fields. Mods should use the native naming screen for
their target host when they do not need this compatibility contract.

Regression coverage: `tests/engine/gen2_mod_naming.lua`. External verification
uses the unchanged Anytime Rename 1.2.1 entry point and its 55-check suite;
its native-menu flow suite additionally runs six games in two languages.
28 changes: 28 additions & 0 deletions docs/native-gen2-namepick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Native Gen2 player-name choice

The player-name step in Gen2 Oak speech now opens `Gen2NamePick`, rather than
using the generic preset menu attached to the naming keyboard. Its original
player portrait moves from tile column 6 to 13, one tile per update, before
input is admitted. A preset moves the portrait back before the original Oak
step resumes. B cannot dismiss this required choice. The class and tile layout
follow Gen1Recomp's separate Gen2 NamePick owner; original source provenance is
retained in the implementation.

NEW NAME opens the existing native Gen2 keyboard through `Screens.push`, so
screen registry overrides still apply. It retains the actual gender and field
length without turning an eight-slot player field into box entry. START selects
END and A confirms; empty input keeps the actual supplied preset. Completion is
once-only, and a keyboard whose original caller was removed cannot commit to it.
The public picker constructor leaves dismissal to its caller. Native Oak owns
its picker dismissal, player-name write and original continuation.

Portraits and form-specific names use the engine's actual imported player data.
The class spelling `src.ui.gen2.NamePick` addresses this same native owner.
Ordinary non-Gen2 and rival naming retain their existing route. Screens with no
presets can still open the keyboard directly. No mod-specific name flow is added.

ROM-free tests cover input blocking during both directions of movement, input
wrap, B rejection, native keyboard/empty-name ownership and actual Oak dispatch
for Gold, Silver and Crystal. Exact-source GPU acceptance of the new picker,
ROM naming-program/menu-art import, remaining raster/header details and a full
unmodified-mod story/save/link playthrough are separate unfinished work.
48 changes: 48 additions & 0 deletions docs/native-gen2-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Native Gen2 naming owner

`Gen2NamingScreen` now owns Gen2 entry state, input and presentation rather
than adapting the Gen1 keyboard. The zero-based cursor addresses four letter
rows (five for a box) and a bottom row of CASE, DEL and END targets. SELECT
changes case, B deletes a complete font glyph, START selects END, and A on END
accepts. Filling the field also selects END and waits for confirmation. Spaces
are actual cells. Unicode and charmap ligatures occupy complete name slots.

The public constructor preserves caller-owned dismissal. The ordinary
`NamingScreen` constructor delegates to this same native owner on Gen2 while
preserving its existing pop-before-callback convention. Native scripts,
catches and Oak therefore use the real Gen2 model. Other generations retain
their existing constructors. Screen registry overrides remain authoritative.
The `src.ui.gen2.NamingScreen` class spelling resolves to this owner, so external
presenters can observe and draw its actual rows, cursor, text and field size.

Name presets temporarily make the naming screen transparent and suppress its
keyboard until NEW NAME is chosen; the original parent remains underneath.
Suppression belongs to the active instance, so class-level external presenters
cannot draw the inactive keyboard through their wrappers. NEW NAME restores
the original instance/class presentation methods.
Native imported sprite and optional menu-art records can supply the header,
backdrop and retained cursor quads. Mail entry keeps both sixteen-glyph rows
above its keyboard. Native Oak and rival/name-rater calls carry their actual
player/rival/Pokemon role instead of inferring it from a field length. Default board data is an engine fallback; a `data.gen2Naming` record
can supply normal/box upper/lower rows. `ui.naming.grid` retains its public
context and can replace those rows.

Font aliases for `<PK>`, `<MN>` and `<DOT>` resolve only to sequences actually
present in the active font. They neither assume retail glyph ordinals nor
replace explicitly authored aliases. Measurements, editing and drawing use
that same charmap.

ROM-free tests cover actual stack/menu input, name types and glyph limits,
caller/native completion ownership, preset underlay suppression, Gen1
retention and actual Loader clients through native API1/2.
GPU acceptance and full reference raster/helper parity remain separate:
ROM naming-program/menu-art extraction and exact cursor/header rendering are
still open. The separate native portrait selector is described in
`native-gen2-namepick.md`; its exact-source GPU acceptance remains open. This change does not
establish complete VASC, naming-story, save/link or full Gen1 parity.

This offer includes the shared Gen2 menu chrome already offered in PR #224
and indexed palette drawer from PR #202 so its native screen can run without
either branch having merged. Existing Gen1 provenance and license notices are
preserved. Original contributions are offered under the specific permission at
https://github.com/Roxas2712/Gen2Recomped/blob/7867399c753186778bd4b8816ba37573357e4b40/LICENSE-ROXAS2712.md.
42 changes: 42 additions & 0 deletions docs/native-gen2-save-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Native Gen2 START and save dialogue

Gen2 START has its own native `Gen2StartMenu` class. Its public
`src.ui.gen2.StartMenu` spelling identifies the actual field-menu owner.
It uses the cartridge row IDs, scrolling list, descriptions and quit prompts,
retains the same START instance below native submenus, and remembers the
cursor in the save. Gen1 keeps its existing generic menu path. The START
controller and reusable typewriter are adapted from the read-only reference
`535a5ec088c1ae1bf4d91dd3e35a56f64cf46d91`; existing rights remain applicable.

START opens `Gen2SaveMenu`, an actual state over the native text
printer and save pipeline. The public `src.ui.gen2.SaveMenu` spelling names
this same owner. Screen registry overrides retain authority over the native
`Gen2SaveMenu` entry.

An existing active save slot requires a second overwrite answer. B/NO cannot
write. The timed saving message uses MID text cadence without held-button
acceleration; the write follows sixteen post-text logic frames. The saved
message follows another thirty-two frames and retains a thirty-frame tail,
waiting for its returned save sound with a bounded host failure escape.
Completion and writing are each guarded against duplicate calls.

The default operation calls `Game:writeSave`, retaining save-write vetoes,
live overworld capture, metadata capture, mod notifications and the
existing safe serializer. Explicit alternate snapshots use `SaveData.save`;
an injected writer receives the selected snapshot. Failed or throwing writes
cannot produce a successful completion message. Slot detection uses the
same active-slot lookup as native saving; an indeterminate lookup asks for
overwrite confirmation. The panel counts native badge and owned-Dex records.

ROM-free integration tests exercise Gold/Silver/Crystal through actual stack,
text printer, START input, screen resolution and the native Game writer,
including refusal, overwrite, fixed timing, duplicate suppression, writer
failure and a real save-hook veto. Gen1 retains its existing dialogue path.
This upstream branch has ROM-free coverage only. Real-cartridge save/reload,
unchanged-mod menu presentation, exact reference raster/typewriter helper
coverage and full story/save/link parity remain unverified. Supplying native
START does not assert that other PC/menu owner contracts are complete.
This state does not change save-format or link-protocol numbers.

Original contributions by Roxas2712; existing upstream and third-party rights
and credits remain applicable. See NOTICE.md and LICENSE-ROXAS2712.md.
8 changes: 5 additions & 3 deletions src/core/ModCompat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
-- hook, not one module
-- src.world.gen2.Follower PikachuFollower is Yellow's, not a general
-- party follower
-- src.ui.gen2.Chrome no shared window-chrome module
-- src.ui.gen2.SaveMenu the save flow is part of StartMenu here
-- src.ui.gen2.BattleAnimView Gen2AnimPlayer is a player, not a view
-- src.battle.UIVisibility no equivalent
-- src.ui.PartyPanel no equivalent
Expand Down Expand Up @@ -67,6 +65,9 @@ ModCompat.ALIASES = {

-- save-side data
["src.core.gen2.Boxes"] = "src.pokemon.Boxes",
["src.ui.gen2.NamingScreen"] = "src.ui.Gen2NamingScreen",
["src.ui.gen2.NamePick"] = "src.ui.Gen2NamePick",
["src.ui.gen2.SaveMenu"] = "src.ui.Gen2SaveMenu",
["src.core.gen2.BugContest"] = "src.world.BugContest",
["src.core.gen2.ItemEffects"] = "src.inventory.ItemEffects",
["src.core.gen2.Save"] = "src.core.SaveData",
Expand All @@ -80,10 +81,11 @@ ModCompat.ALIASES = {
["src.battle.gen2.Effects"] = "src.battle.MoveEffects",
["src.battle.gen2.Mon"] = "src.pokemon.Pokemon",
["src.ui.gen2.BattleState"] = "src.battle.BattleState",
["src.ui.gen2.Chrome"] = "src.ui.Gen2Chrome",

-- menus. The Gold port names two of these differently rather than
-- namespacing them, so both spellings land on the one implementation.
["src.ui.gen2.StartMenu"] = "src.ui.StartMenu",
["src.ui.gen2.StartMenu"] = "src.ui.Gen2StartMenu",
["src.ui.gen2.OptionsMenu"] = "src.ui.OptionsMenu",
["src.ui.gen2.PartyMenu"] = "src.ui.PartyMenu",
["src.ui.gen2.PokedexMenu"] = "src.ui.PokedexMenu",
Expand Down
24 changes: 24 additions & 0 deletions src/core/SaveData.lua
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,30 @@ function SaveData.playSeconds(save)
+ ticks / 60
end

-- The native save owns a scalar play clock and an owned-species map. Expose
-- the reference UI summary without requiring callers to reinterpret either
-- representation, load a profile, or create a second save/time store.
function SaveData.summary(save, data)
if type(save) ~= "table" then return nil end
local caught = 0
for _, has in pairs((save.pokedex and save.pokedex.owned) or {}) do
if has then caught = caught + 1 end
end
local seconds = SaveData.playSeconds(save)
if seconds ~= seconds or seconds == math.huge or seconds == -math.huge then seconds = 0 end
seconds = math.max(0, math.floor(seconds))
local spawn = save.spawn
return {
name = save.player and save.player.name or "?",
badges = Badges.count(data, save, save.version),
caught = caught,
hours = math.floor(seconds / 3600),
minutes = math.floor(seconds / 60) % 60,
map = save.player and save.player.map or (save.position and save.position.map)
or (type(spawn) == "table" and spawn.map or spawn),
}
end

function SaveData.slotSummary(save, version)
if type(save) ~= "table" then return nil, nil end
local name = save.player and save.player.name or nil
Expand Down
12 changes: 10 additions & 2 deletions src/mods/Events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ function Events:on(name, callback, priority, owner)
local list = self.listeners[name] or {}
self.listeners[name] = list
local entry = { callback = callback, priority = priority or 0, owner = owner }
list[#list + 1] = entry
table.sort(list, function(a, b) return a.priority > b.priority end)
-- Stable priority insertion: equally ranked listeners run in subscription
-- order. Re-sorting with a priority-only comparator permuted existing peers
-- whenever another listener joined, allowing an earlier generic decorator
-- to overwrite a later screen-specific one after a settled screen push.
local index = #list + 1
while index > 1 and list[index - 1].priority < entry.priority do
list[index] = list[index - 1]
index = index - 1
end
list[index] = entry
return function()
for i, candidate in ipairs(list) do
if candidate == entry then table.remove(list, i) break end
Expand Down
10 changes: 10 additions & 0 deletions src/render/Font.lua
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ function Font.load(data)
end
end

-- Legacy screen cells spell ligatures with angle brackets. Bind only
-- to a sequence the active font actually owns, so relocated hack glyphs
-- and explicitly authored bracket spellings retain their authority.
for token, native in pairs({["<PK>"]="PK",["<MN>"]="MN",["<DOT>"]="."}) do
if not seqCode(token) then
local code=seqCode(native)
if code then bucket({seq=token,code=code})end
end
end

for _, entries in pairs(state.byFirstByte) do
table.sort(entries, function(a, b) return #a.seq > #b.seq end)
end
Expand Down
126 changes: 126 additions & 0 deletions src/render/Gen2PaletteDraw.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
-- Original contributions (c) 2026 Roxas2712; offered to UNDERdecoded under
-- https://github.com/Roxas2712/Gen2Recomped/blob/7867399c753186778bd4b8816ba37573357e4b40/LICENSE-ROXAS2712.md
-- Indexed image drawing, separate from the LCD post-process in GBCFX.
-- Palette resolution uses the native display/register state in PaletteFX.
local Palettes = require("src.render.PaletteFX")
local Version = require("src.core.GameVersion")
local M = {}
-- Consumers use this view to leave already-coloured pictures untouched.
-- Gen2 currently resolves native ROM colours regardless of the Gen1 mode.
-- Do not invent a second mutable display setting for the external renderer.
setmetatable(M, {__index = function(_, key)
if key == "mode" then
if Version.isGen2() then return "gbc" end
if Palettes.mode == "og" or Palettes.mode == "og_inv" then return "dmg" end
return Palettes.mode == "classic" and "classic" or "gbc"
end
end})
local compiler, shaders, failed = nil, {}, {}
local sent = setmetatable({}, {__mode = "k"})
local names = {"pal0", "pal1", "pal2", "pal3"}
local source = [[
extern vec3 pal0; extern vec3 pal1; extern vec3 pal2; extern vec3 pal3;
vec4 effect(vec4 tint, Image image, vec2 uv, vec2 screen) {
vec4 pixel = Texel(image, uv);
float shade = floor((1.0 - pixel.r) * 3.0 + 0.5);
vec3 rgb = shade < 0.5 ? pal0 : (shade < 1.5 ? pal1 : (shade < 2.5 ? pal2 : pal3));
return vec4(rgb, ALPHA) * tint;
}
]]

local function graphics() return love and love.graphics end
local function shader(keyed)
local g = graphics()
local newShader = g and g.newShader
if type(newShader) ~= "function" then return nil end
if compiler ~= newShader then
compiler, shaders, failed = newShader, {}, {}
end
local key = keyed and "keyed" or "normal"
if failed[key] then return nil end
if not shaders[key] then
local alpha = keyed and "(shade < 0.5 ? 0.0 : pixel.a)" or "pixel.a"
local ok, value = pcall(newShader, (source:gsub("ALPHA", alpha)))
if not ok or not value then failed[key] = true; return nil end
shaders[key] = value
end
return shaders[key]
end
function M.shader() return shader(false) end
function M.keyedShader() return shader(true) end
function M.available() return M.shader() ~= nil end

local function copy(colors)
if type(colors) ~= "table" then return nil end
local out = {}
for i = 1, 4 do
local color = colors[i]
if type(color) ~= "table" then return nil end
local row = {}
for c = 1, 3 do
local n = color[c]
if type(n) ~= "number" or n ~= n or n < 0 or n > 255 then return nil end
row[c] = n
end
out[i] = row
end
return out
end
function M.resolve(colors) return Palettes.effectiveColors(colors) end
function M.color(colors, index)
local palette = M.resolve(colors)
return palette and palette[index] or nil
end

local function upload(sh, colors)
-- Public shader objects can be edited externally. Upload every public call;
-- a cached last-send comparison could otherwise trust foreign uniforms.
sent[sh] = nil
for i = 1, 4 do
local row = colors[i]
sh:send(names[i], {row[1]/255, row[2]/255, row[3]/255})
end
sent[sh] = colors
end
local function use(colors, keyed, raw)
local g = graphics()
if not (g and g.setShader) then return false end
colors = copy(raw and colors or M.resolve(colors))
if not colors then return false end
local sh = shader(keyed)
if not sh then return false end
upload(sh, colors)
g.setShader(sh)
return true
end
function M.use(colors) return use(colors, false, false) end
function M.useRaw(colors) return use(colors, false, true) end
function M.useKeyed(colors) return use(colors, true, false) end
function M.clear()
local g = graphics()
if g and g.setShader then g.setShader() end
end

local function scoped(colors, body, apply)
local g = graphics()
local previous = g and g.getShader and g.getShader() or nil
-- Restoring only the shader object is insufficient: nested calls reuse it
-- and change its uniforms. Save the palette values as well as its identity.
local previousColors = previous and sent[previous]
local ok, applied = pcall(function()
local active = apply(colors)
body()
return active
end)
local restored, why = pcall(function()
if previousColors then upload(previous, previousColors) end
if g and g.setShader then g.setShader(previous) end
end)
if not ok then error(applied, 0) end
if not restored then error(why, 0) end
return applied
end
function M.with(colors, body) return scoped(colors, body, M.use) end
function M.withRaw(colors, body) return scoped(colors, body, M.useRaw) end
function M.keyedWith(colors, body) return scoped(colors, body, M.useKeyed) end
return M
6 changes: 5 additions & 1 deletion src/render/PaletteFX.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,7 @@ function PaletteFX.paperShade(data)
return c[1] / 255, c[2] / 255, c[3] / 255
end

function PaletteFX.effectiveColors(c)
function PaletteFX.effectiveColors(c, invert)
if not c then return nil end
local mode = PaletteFX.mode or "gbc"
local out = c
Expand All @@ -1176,6 +1176,10 @@ function PaletteFX.effectiveColors(c)
-- cave has to read dark in CLASSIC's pea greens and in plain DMG grays too
-- (#322). permute() is the identity (and returns `out` itself, which the
-- mod-graphics parity check leans on) while nothing has armed one.
-- An inverted font page reverses its settled display ramp before the
-- hardware shade register permutes it. Keep this on the native path so
-- Gen2 menu chrome cannot carry a second palette mode/register state.
if invert then out = { out[4], out[3], out[2], out[1] } end
return PaletteFX.permute(out, shadeMap)
end

Expand Down
Loading