Matching
games/tbs/src/MATCHING.md on GitHub
This page owns what the folder guides share about matching: how the build is judged, what the markers in the source mean, and the plain-C spellings that some sites keep because they are what compiles to the original bytes. Each guide points here instead of repeating it, and keeps its own pages for how its code works.
How a build is judged
make compare-editions builds all twelve editions and checks each ROM's
SHA-1 against its line of rom.sha1 (the compare-% rule of the
Makefile); each edition that matches prints a line ending : OK. Its
output is long, so send it to a log and count those lines: twelve is a pass.
make compare-tbs checks the six ☀️ editions, make compare-tbs-en one.
The judgement is a hash of the whole image, because the hash is the only
thing from the original cartridge the build is allowed to see: the reference
ROM judges the build and never feeds it. That makes the check all or
nothing. One wrong byte anywhere fails its edition, and the hash does not say
where. To find the byte, compile the one object you changed to assembly with
the Makefile's command and diff it against a baseline with .L label
numbers ignored, or use the optional alchemy-devtools diff on a function.
The ☀️ editions need no ROM. The ⚓️ editions need your own ROMs under
roms/, because what is not yet C comes from .incbin "baserom.gba"
scaffolds under recon/tla, which never count.
A change made on purpose, such as a mod or a fixed bug, changes the bytes and
so fails the comparison. That is expected: keep such work apart from the
matching tree, or behind BUGFIX.
Markers in the source
-
/* FAKEMATCH(kind): the form kept; as <plain form>, N of M instructions differ in tbs-en. */marks a device that steers the compiler towards the original instructions, inside the function it steers. N is measured over the whole function against the edition named. The kinds:wrapper: a call made through a helper, a cast or a library routine in place of the plain call or operator;volatile: a qualifier that forces a load or store to stay where it is written;temporary: a variable that holds a value the plain form would not name;dead: code that emits nothing and only changes allocation or order;one-pass: a block or loop that runs once (do { } while (0)), whose loop notes end a CSE block and act as a scheduling barrier;barrier: an emptyasm, which emits nothing but counts four bytes when literal pools are placed;pin: a variable fixed to a register.
A device comes only after reshaping the code has failed, and removing one is humanizing. Some older tags have no kind yet (
/* FAKEMATCH: ... */); they say the same thing. -
// DEAD: whymarks code the ROM keeps that cannot change the result, such as a test that is always true, where the construct's own compare, branch or loop is in the ROM. It is then the source's form, not a device. -
// BUG: what goes wrongmarks a bug of the original game, kept as it is; the fix, where there is one, is behindBUGFIX(include/config.h), which is off. -
// UB:marks undefined behaviour kept in the form that matches; the defined form is behindUBFIX, whichmake modernandBUGFIXturn on. -
CAMELOT_ASM("proof")marks the few proven handwritten helpers inside C (Dma_Set,Transform_SetIdentity,Iwram_Call2,WaitFrames' stack switch,MulQ16, and theswi 2andswi 3BIOS calls). It expands to nothing; it is there to be read. -
UNUSEDmarks a function nothing calls, kept because its bytes are in the ROM. -
Assembly files open with
@ C2:lines that prove why no C compiler of the set would produce them (an instruction, frame or pattern agscc cannot emit).
Spellings that match at different sites
Each idea has one preferred spelling. Where a site matches only with a plain-C equivalent (an index or pointer form, a macro or its expansion, a cast), that site keeps the equivalent, untagged, and it is listed here. These are one idea each, and can compile differently.
Everywhere
p[n]and*(p + n), and&p[n]and&*(p + n).- A function whose result nothing reads keeps its
s32return wherevoidchanges its epilogue;UBFIXgives thevoidform. An update hook that the bytes show returning a value does the same, with// UB:above it. - Palette RAM through
BG_PLTT_COLOR, which is volatile, or through a plain pointer to it such as(u16 *)&BG_PLTT_COLOR(14, 0). - A work slot as
WORK_SLOT(n), asgWorkSlot[n], or through a named pointer indexed past it ((void **)&gCameraWork), which is undefined behaviour with its defined form behindUBFIX. - A message after a named one as
MsgName + n, such asMsgSaveDataCorrupted + 2forMsgSaveRecoveryFailed, where the ROM adds to the register holding the first.
Runtime
Vector_AddPolarOffsetwalks positions with a byte cursor so both struct and array positions are valid. Its two one-coordinate steps retain the native writeback; combining them changes register allocation and stores.- A register write as
Io_Write16(value, ®_X), which builds the value in a word first and keeps the store where it is written, or asREG_X = value. - Code copied to RAM is called through a cast function pointer, or through
_call_via_r3, the compiler library's call through r3, with the code's address as its fourth argument. - A DMA control word is written with its flags above its count:
(DMA_ENABLE | DMA_32BIT) << 16 | words, orDMA_ENABLE << 16 | halfwords.
Field
*&gProjection.centerXforgProjection.centerX(field/vertices.c), and*(s32 *)&metadata->framesfor the frames pointer (AnimationObject_Allocate).(mapState->anim + channelIndex)->formapState->anim[channelIndex].(MapAnimation_Update).Scheduler_AddOrUpdateCallbackcalled through a cast to avoidfunction (Map_LoadLayeredScene, with the declared call behindUBFIX).(u16)~(...)for a halfword mask (Map_SetShownLayers).- Three loops written with
goto(Sprite_ComposeAnimationFrame,AnimationObjects_SelectAnimation), with their loop forms measured in the comments beside them. The othergotos (the wander retries, the step searches, the map effects' script loops and the debug loops) stay until their loops are found.
Field objects
Not all of these sites have been measured; each keeps its spelling until it is:
- the field work:
(struct FieldWork *)gEventWork,WORK_SLOT(HEAP_SLOT_EVENT), orRuntime_AllocateBlock(HEAP_SLOT_EVENT, sizeof(struct FieldWork)), which returns the existing block; - the effect scene:
gEffectWorkor*(gWorkSlot + HEAP_SLOT_FX); - the map's view:
gMapWork[0]orWORK_SLOT(8).
Measured:
((16 - blend) << 8) | blendinobject/psynergy_effects/world_map.cisBLEND_ALPHA(blend, 16 - blend)with its operands swapped; the macro changes the registers.
Scenes
FixedPoint_Distancekeeps its coordinate-address interface with byte views derived from whole actors. Its byte cursor reaches the actual signed coordinate members without crossing scalar bounds or copying them into temporary arrays.- Altin's hop countdowns read and decrement
FieldActor.workthrough its corresponding signed halfword type, then reset it through the declared unsigned member. Changing either spelling changes native instructions; both views are valid aliases. - Calling a helper before its declaration, or with its exact
voidprototype in scope. In the affected Colosso and Kolima Forest bend helpers, adding the prototype changes the argument setup order. Their unit headers provide the correct declarations underUBFIX; the matching build keeps the call before the declaration. - A field read or written through its offset,
*(T *)((u8 *)p + offsetof(struct FieldActor, field)), wherep->fieldgoes through FieldActor's single/group union and moves a load (scene/common/effect_spawn.h,scene/common/mercury_lighthouse_pillar.h,scene/crossbone_isle_pillar_rooms.c). - An actor read as
struct FieldEffect, union FieldObject's effect view of the same object, whose sprite pointer is a plain member: through FieldActor's sprite union the flags address moves after the next store (SolSanctum_StartFloatingLightsinscene/common/sol_sanctum_elemental_star_ring_setup.h). - Byte-addressed field stores with
sizeofandoffsetofrather than direct struct members (Colosso_RestoreCompetitorandTolbiShipInteriors_SelectPlacements). These valid byte views keep the native address bases and registers. - The same byte view for
Xian_SpawnTrainingParticleskeeps the actor's Z load ahead of the options-script store. Direct members move it after the store and the following constant load in the English build. - The Goma stump turns read
AnimationObject.rotationthrough its corresponding signed halfword type, keeping the negative literal. Their update-hook clears retain the native integer alias outsideUBFIX; the defined pointer store swaps with the sprite-pointer load. - The native
u32view ofgMapWork[0](SceneActor_MoveAndRedraw). It aliases a pointer field;UBFIXreads the pointer directly. - A command of
event.hand the service it names; each site keeps the one it matches with.
Windows
- The work block as
UI_WORKor asgWindowWork[0], and a neighbouring heap block asgWindowWork[n]. - A tilemap cell as
work->tilemap + y * 32 + x, or as a byte offset added to(u32)work->tilemap.
Menus
- A unit copied with the resident IWRAM routine as
Iwram_CopyWords, through_call_via_r3(..., IwramCopyWords)(Owner_Copy), or throughItem_CopyWords(tagged). - A neighbouring heap block as
cache[HEAP_SLOT_EVENT - HEAP_SLOT_MENU_CONTROL]from the menu control slot.
Battles
- An action slot's first halfword is cleared through a cast,
*(u16 *)&member->actionSlots[i] = 0, where the field store loads its zero differently (battle/battle/debug.c). BLEND_ALPHA(16, 16) - stepandstep + BLEND_ALPHA(0, 16)where the macro's|form changes the code (background.c).- Work slots through
(void **)&gCameraWorkand(void **)&gBattleWorkindexed past them (see Everywhere).
Battle effects
- A work slot is read as
WORK_SLOT(n)orgWorkSlot[n], through a base pointer asslots[n - base],EFFECT_SLOTorCANVAS_SLOT, and as a blitter asEFFECT_BLITTERS[n]or(DrawRectangle)gWorkSlot[n].
Game rules
- A unit as
UnitGet(owner), or as the far-callUnitGetFar(owner)from other banks. - An element pair array as
s16 [4][2]where the matching code indexes it, or throughstruct BattleElementStatswithUBFIX.
Sound
- The driver's command table is called through a pointer read from the
table's entry (
*(&gSound_CommandTable[n])); indexing it directly adds an instruction withold_agbcc. engine.ckeeps the labelled jumps its matching forms need.