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
88 changes: 88 additions & 0 deletions LICENSE-ROXAS2712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Roxas2712 — Fork Contributions

Version 1.0 · Effective 25 October 2026

Copyright © 2026 Roxas2712, to the extent of rights held.
All rights reserved except as expressly granted below or already granted.

## 1. Scope and ownership

This notice applies only to original contributions made by Roxas2712
(GitHub: Roxas2712) to `Roxas2712/Gen2Recomped`, including original additions
within modified files, insofar as he owns the relevant rights. Git history,
pull-request diffs and source provenance identify contributions; a commit
author or a whole-file change alone does not establish ownership of every
part of that file.

It does not claim ownership of Cedric's work, third-party engine work,
other contributors' work, third-party dependencies, or ROM-derived
content. Their applicable licences and notices remain in force. It does not
grant permission to distribute the complete combined fork where another
rights holder's permission is required.

## 2. General permission and reserved rights

For contributions covered by this notice and not already licensed more
broadly, you may read and study the published source and make private,
non-commercial modifications and builds for your own use. This grants no
general permission to redistribute, sublicense, sell, or offer those
contributions as a hosted service. Other uses require Roxas2712's
written permission, except for the specific permission below and rights
available independently under applicable law.

Keep the relevant copyright, licence and attribution notices. Do not
represent Roxas2712's contributions as exclusively authored by someone
else. Public source availability alone does not put them in the public
domain or grant an open-source licence.

## 3. Specific permission for Cedric / UNDERdecoded

Roxas2712 expressly grants Cedric (Ced; GitHub: UNDERdecoded /
UNDERdecodedHD) a non-exclusive, worldwide, perpetual, royalty-free
permission to use, reproduce, integrate, modify and distribute the covered
contributions that Roxas2712 submits or expressly offers through pull
requests to `UNDERdecoded/Gen2Recomped`, for Cedric's Gen2Recomped /
RetroRecompiled project. This includes its renamed continuation maintained
by Cedric, its official builds, and its commercial operation.

Cedric may license the incorporated contributions to recipients as part of
that project, subject to preserving this permission's applicable credit and
notice requirements. This permission is not a copyright assignment, is not
exclusive, and does not cover unpublished work, separate repositories,
ModCompass service operation, credentials, or content not offered in those
pull requests. Roxas2712 retains his own rights to his contributions.

For uses relying on this permission:

- retain the relevant source copyright and licence notices;
- give visible credit in the project's README or equivalent public project
page, and in the application's existing About / Credits view, if present;
- use at least the following credit, or a substantially equivalent one:

> Contributions by Roxas2712 —
> https://github.com/Roxas2712

A maintained consolidated contributor credit is sufficient; a separate
credit on every screen or for every change is not required. Adapted
contributions must not be attributed exclusively to another author. Other
contributors' credits must also remain intact.

## 4. Earlier licences and permissions remain effective

This notice does not revoke, narrow, or retrospectively add conditions to
any permission already validly granted, including MIT / GPL grants,
separate agreements, or permissions arising from previously submitted
pull requests. Where an earlier grant permits a use independently of this
notice, its existing terms govern that use. For such earlier uses, the
visible credit above is requested to the extent it is not already required.

For subsequent contributions, this is the default statement of Roxas2712's own reserved rights and the specific PR permission, subject to any
applicable existing licence obligations or separately agreed terms. It
does not change another rights holder's licence or guarantee compatibility
of a particular combination of code.

## 5. No warranty

To the extent permitted by applicable law, these contributions are provided
as is, without warranty. No support obligation, endorsement, or rights to
third-party code, game assets, names, or trademarks are granted.
72 changes: 72 additions & 0 deletions docs/native-battle-animation-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Native battle animation bridge

`src.core.game3.battle.anim` presents the admitted GBA battle through its native
`BattleState`, `Gen3MoveAnim` and `Gen3BallAnim` owners. It does not load another
battle engine or rewrite a mod package. FireRed, LeafGreen, Emerald, Ruby and
Sapphire use the same boundary and their own imported identifiers and records.

## Shared clock and ownership

The native update opens one presentation phase. A source `Anim.update()` can
advance that phase while a custom ending or pose holds the combat update.
Repeated source calls and a later native base call consume the same display
steps. The native dt drives sprite animations. Outside that phase, source
updates cannot manufacture frames.

Move/ball effects, visual waits, HP drain and EXP fill remain native operations.
Only the native combat update consumes command rows, input and outcomes.
Source-launched clips occupy the native animation player and hold the command
queue until that clip finishes. A presentation serial distinguishes a finished
source clip from a newer native animation on the same player. Reset/unload
cancels only a clip still owned by that source admission.

`present(id)` has stable identity for an admitted battler. It observes native
display HP/maximum/level/status/EXP, visibility, offsets and affine effects.
Source visual overrides compose around the existing native sprite draw;
reading a native effect never applies it a second time. `shownBattler` carries
an owned presentation override without replacing the native combatant.
Replacing the session, save, data or catalogue retires old poses and callbacks.

`GameVersion.layout(id)` also reports the registered GBA family (`frlg` or
`rse`) so an unchanged field companion probe can recognize the native host.
It supports current-version and colon queries without changing cache/save
selection; an explicit registered layout takes precedence.

## Geometry and screen effects

`anim_coords` exposes authored anchors and ordering for singles and doubles.
Current imported cartridge coordinates take precedence over the standard
240x160 anchors. Temporary side bindings restore on success and failure and
preserve trailing nil arguments/results.

Supported screen effects are inversion, fade to white/black, darkening,
grayscale and blending to a custom colour. Repeated begin calls reapply within
one guarded render seam; nested seams restore their caller's shader. The native
graphics guard restores render targets, transforms, shaders and leaked pushes
after an error. Synthetic asymmetric sprites test orientation as well as colour.

## Current boundary

This bridge implements native move launch/observation, source pose tweens,
display ratios, sequence observations, screen effects and coordinate bindings.
It deliberately does not advertise a complete presentation API version.
Authored animation commands, mutable VM/sprite/task/palette callbacks,
particle bands, trainer/intro/switch/catch stage control, source HP/EXP tween
commands and complete custom UI captures still require integration. A module
being importable is not evidence of a complete mod boot or playable campaign.

## Validation

- `tests/engine/native_battle_animation.lua`: all five editions; shared clocks,
owned clip completion, held command queues, stale poses/callbacks and 180-frame
native trajectory comparisons per edition with and without the bridge.
- `tools/battle-animation-runtime/lifecycle-check.lua`: SDK entry failure,
teardown and native peer callback identities.
- `tools/battle-animation-runtime/coords-check.lua`: optional local contract
oracle supplied through `BATTLE_COORDS_ORACLE`; no external engine is bundled.
- `tools/battle-animation-runtime/host`: actual Metal/OpenGL sprite and screen
effect rendering, pixel comparisons and graphics error restoration.

Local qualification runs use the repository's QA storage manager, frozen source
copies and dedicated LOVE identities. They use synthetic imported data and art;
they do not open player profiles, save files or ROM originals.
75 changes: 75 additions & 0 deletions docs/qa/native-foundation/gba-battle-source-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Native GBA battle source boundary

This is an incremental presentation and observation bridge over the existing
`BattleState`. It does not establish complete unchanged-package boot or play.

## Implemented boundaries

- The admitted native stack owns battle identity, input, combat, RNG and exit.
- Stable source observations translate cartridge species/move/item/ability/type
identifiers and flattened Pokemon fields. Trainer class and trainer id remain
different fields. HP/status changes and PP transactions reach native Pokemon;
unsupported move replacements fail before an update.
- Native start notifications, update, UI pump, whole-screen drawing and the
background pass have independent scopes. Repeated base calls consume each
native body once. Calls outside an update scope cannot tick combat.
- Every queued native update/draw rechecks the original admission after source
hooks, including the inner UI/background hooks. Save, dataset, loader, stack,
edition and imported catalogue replacement retire the old binding; restoring
an earlier pointer cannot revive that binding. Native method bodies execute
outside a source callback owner.
- Explicit battle/UI captures suppress the repeated native screen draw within
the same renderer frame and render target. A separate preview target cannot
suppress the main screen. Capturing never advances combat.
- LuaJIT `pairs`/`next` expose typed battle fields, preserving nested typed
identity. Iterator registry values are weak, so an unused observation cannot
retain its own weak key through a closure.
- Covered battles remain observable, but cannot update or pop their cover.
Mod retirement invalidates its source views without cancelling native combat.
Completed views keep detached observations and reject further native writes.
- Basic source wild/trainer requests reuse the native encounter admission and
cleanup provider when present. Authored source parties, extended options,
headless simulation and source-controlled link termination remain unsupported.
Constructor-request acceptance is not yet independently qualified here.
- Pokemon observations use native imported records. Height and weight convert
metres/kilograms to cartridge decimetres/hectograms. Static pictures are owned
by the source service; native animation frames remain borrowed.
- Separate presentation players reuse native `MonAnim`/`PicAnim` timing. Their
timing is the existing native implementation, not a proof of exact source
sprite callbacks. They cannot consume combat commands.

Older native controllers without HUD visibility or move-grid query methods
return an explicit unavailable reason for those observations. This does not
advertise the complete presentation protocol on that host.

## Qualification

The focused engine suites cover all five registered GBA editions using
synthetic imported data and actual native constructors, stack admissions and
exit. They exercise repeated base calls, covered states, UI input/draw scopes,
owner/data/edition retirement, numeric identity, PP and detached final views.
View-only collection checks isolate this bridge from the host's global facade
cache; they do not certify the entire host's Game/save collection policy.

`tools/battle-source-runtime/host` draws the actual native `Gen3Battle` on a
GPU. It compares default and wrapped pixels, replaces only the field pass,
checks that the native command-area pixels remain identical, and checks
graphics restoration after a failing callback. Artwork and battle records are
synthetic; these are not original-package or cartridge gameplay checks.

## Remaining presentation work

The UI module deliberately does not advertise the complete sprite/particle
presentation protocol. Native animation VM, effect, healthbox, trainer, ball,
text, catch, switch and intro presentation interfaces still need their own
bindings. So do complete party/save collection schemas and authored source
Pokemon operations. Original-package initialization, real play sequences,
source effect timing/art parity and physical target acceptance remain open.

The last unchanged-package loader probe advances beyond battle, Pokemon,
motion and UI imports to the missing `src.core.game3.battle.anim` interface.

Older native hosts also retire already-loaded battle, Pokemon-picture and
motion services at loader unwind, even when their stream service is not yet
driven by loader retirement. This releases observations and owned pictures;
it does not pop or finish the actual native battle.
112 changes: 99 additions & 13 deletions src/battle/BattleState.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ local TEXT_AREA_ROWS = { 112, 128 }
local BattleState = {}
BattleState.__index = BattleState
BattleState.isOpaque = true
-- Native source observations identify this admitted controller directly.
BattleState.isBattleState = true

-- pokered prints the battle lines itself (engine/battle/core.asm and the
-- move-effect banks), and the importer extracts every one of them, so the
Expand Down Expand Up @@ -2593,11 +2595,9 @@ function BattleState:beginMsgLine()
self.shown[#self.shown + 1] = {}
end

function BattleState:updateQueue()
if self.waitingUI then
if self.game.stack:top() ~= self then return true end
self.waitingUI = nil
end
-- Only display controllers live here. Command holds, HP awards, turn rows
-- and completion remain in updateQueue, even while a mod holds presentation.
function BattleState:updateMovePresentation()
-- HOENN'S PARTICLES TICK BEFORE THE QUEUE'S OWN HOLD, and the whole
-- reason the animations looked broken is that they used to tick after it.
--
Expand Down Expand Up @@ -2648,11 +2648,16 @@ function BattleState:updateQueue()
if not anim.caught then self.gen3Ball = nil end
end
end
end

-- Display-only waits and bar completion. Returning idle grants no command;
-- only updateQueue may consume a battle row after this gate.
function BattleState:updateQueuePresentation()
self:updateMovePresentation()
local source = GameVersion.isGen3() and package.loaded['src.core.game3.battle.anim']
if source and source._holdsNativeQueue(self) then return true end
-- a queued hold (faint slide, hit blink) counts down before the next row
if self.waitFrames and self.waitFrames > 0 then
self.waitFrames = self.waitFrames - 1
return true
end
if self:stepPresentationWait() then return true end
-- WaitForSoundToFinish (home/delay.asm:15-20) blocks until the sfx has
-- actually stopped sounding, which is how the original gives a sound its
-- own clear window instead of letting the next beat play over it
Expand Down Expand Up @@ -2682,6 +2687,15 @@ function BattleState:updateQueue()
if self:stepExpBar() then return true end
self.expFilling = nil
end
return false
end

function BattleState:updateQueue()
if self.waitingUI then
if self.game.stack:top() ~= self then return true end
self.waitingUI = nil
end
if self:updateQueuePresentation() then return true end
if self.animPlaying then
self.animPlayer:update()
if self.animPlayer.pollEffects and self.applyAnimEffect then
Expand Down Expand Up @@ -4270,17 +4284,36 @@ function BattleState:touchpressed(_, px, py)
end
return true
end
function BattleState:updateBattlerPresentation(dt)
for _, b in activeBattlers(self) do
if b and b.picAnim then b.picAnim:update(dt) end
if b and b.monAnim then b.monAnim:update(dt) end
end
end

function BattleState:stepPresentationWait()
if self.waitFrames and self.waitFrames > 0 then
self.waitFrames = self.waitFrames - 1
return true
end
return false
end

-- Advance an ending/pose hold without executing battle commands or input.
function BattleState:updatePresentation(dt)
self:tickFx()
self:updateBattlerPresentation(dt)
self:updateQueuePresentation()
end

function BattleState:update(dt)
self:tickFx()
-- one sample a second into the fight, while it is still on screen
self._stadiumReportTicks = (self._stadiumReportTicks or 0) + 1
if self._stadiumReportTicks == 60 then self:reportStadiumBattle("mid") end
-- Crystal pic animations run off the wall clock like every other battle
-- pic effect; a Gold/Silver battler has no picAnim and this is a no-op.
for _, b in activeBattlers(self) do
if b and b.picAnim then b.picAnim:update(dt) end
if b and b.monAnim then b.monAnim:update(dt) end
end
self:updateBattlerPresentation(dt)
local input = self.game.input

-- L PUTS THE BOTTOM SCREEN AWAY, AND BRINGS IT BACK.
Expand Down Expand Up @@ -12855,4 +12888,57 @@ function BattleState:drawClassic()
end
end

-- Source wrappers observe the real GBA owner. Unrequested modules and other
-- generations keep the direct native path; there is no additional clock.
do
local update, draw, field, exit, enter = BattleState.update, BattleState.draw,
BattleState.drawBattleField, BattleState.exit, BattleState.enter
local Clock = require('src.battle.PresentationClock')
for _, name in ipairs{'tickFx','updateBattlerPresentation','updateMovePresentation',
'stepHPDrain','stepExpBar','stepPresentationWait','updateQueuePresentation'} do
local body = BattleState[name]
BattleState[name] = function(self, ...)
return Clock.once(self, name, body, ...)
end
end
BattleState._drawNative = draw
local battlerDraw = BattleState.drawBattlerPic
function BattleState:drawBattlerPic(battler, ...)
local anim = GameVersion.isGen3() and package.loaded['src.core.game3.battle.anim']
if anim then return anim._drawBattler(self, battler, battlerDraw, ...) end
return battlerDraw(self, battler, ...)
end
function BattleState:enter()
enter(self)
local source = GameVersion.isGen3() and package.loaded['src.core.game3.battle']
if source then return source._nativeEnter(self) end
end
function BattleState:update(dt)
local source = GameVersion.isGen3() and package.loaded['src.core.game3.battle']
local anim = source and package.loaded['src.core.game3.battle.anim']
if anim then
return Clock.run(self, dt, function()
return source._nativeUpdate(self, dt, update)
end)
end
if source then return source._nativeUpdate(self, dt, update) end
return update(self, dt)
end
function BattleState:draw()
local source = GameVersion.isGen3() and package.loaded['src.core.game3.battle']
if source then return source._nativeDraw(self, draw) end
return draw(self)
end
function BattleState:drawBattleField()
local source = GameVersion.isGen3() and package.loaded['src.core.game3.battle']
if source then return source._nativeField(self, field) end
return field(self)
end
function BattleState:exit()
local source = package.loaded['src.core.game3.battle']
if source then source._nativeExit(self) end
return exit(self)
end
end

return BattleState
Loading