Skip to content

Instantly share code, notes, and snippets.

@beanieaxolotl
Last active February 9, 2025 07:40
Show Gist options
  • Save beanieaxolotl/6a66f0b9cb1444c82263f6025c4d22a0 to your computer and use it in GitHub Desktop.
Save beanieaxolotl/6a66f0b9cb1444c82263f6025c4d22a0 to your computer and use it in GitHub Desktop.
WIP documentation of the A2M GBA game engine
A2Mx Game Engine
> The A2Mx game engine is a 2D engine that has been used for platformer, isometric and top-down games, with support for SRAM saving (or passwords), and cutscenes. It was developed by Stéphane Hockenhull[1] (https://rv6502.ca/), with the tools being developed by Benoit Hubert[2].
> A2Mx is not the confirmed name of the engine, but it could be judging by the names "A2M4" and so on appearing in the ROM.
> Known games
A2M0:
- Enchanted - Once Upon Andalasia (last game)
A2M1:
- Kim Possible 2: Drakken's Demise
- Happy Feet
- everGirl
- Scooby-Doo: Mystery Mayhem (debut)
A2M2:
- Lizzie McGuire 2 - Lizzie Diaries
A2M4:
- Chicken Little
A2M5:
- Ed Edd & Eddy: The Mis-Ed-Ventures
A2M7:
- The Sims 2: Pets
A2MR:
- Monster House
A2MT:
- Drake & Josh
A2M4:
Collision tiles:
These are stored as 8bpp 16x16 tiles (although treated as a 1bpp array)
For each pixel:
00 - Air
01 - Solid ground
Entirely empty or solid tiles (with tile indexes of 00 and 01) are generated at runtime.
Debug menu:
- The debug strings are stored in plain text, as opposed to the internal encoding seen in the dialog text. These are the known strings:
> Level
> MapX
> MapY
> FX Player
> Music Player
> Obj Viewer
> Asset Viewer
- The debug menu code is present in the game's ROM, but no remaining code seems to call the relevant functions.
Sound engine:
- All of the above games use Shin'en's GAX Sound Engine for their audio mixing. The only games that don't use this engine also don't use Stéphane's game engine (i.e High School Musical). Refer to the GAX documents for a more thorough overview of how it works (to do).
For example, in Chicken Little, the data is stored like this, directly after the debug menu strings:
u32[*] - Pointers to each subsong
(These have been observed to be in any order. One example of this is Monster House)
GAX_Blob - Music data (music.o)
GAX_Blob - SFX data (fx.o)
To-do:
- Where is the FX pointer?
- Chicken Little's FX mapping differs from the internal FX mapping in the blob.
Save RAM:
- The save data format is highly customizable, and allows up to 3 save slots at a time. The smallest save size I have seen is in Chicken Little (0xA0 bytes), while the largest is Monster House (0xF7 bytes).
- Before every save file is a 16-byte header, which is also used for checking the integrity of all of the saves on the SRAM chip (for detecting corruption or intentional tampering):
- u16 - Checksum
- u16 - ??
- u32 - ??
- char[4] - Game engine identifier
- u8 - ?? (unused?)
- u8 - ??
- u8 - ?? (unused?)
- u8 - ?? (unused?)
Passwords:
Only spotted in Drake & Josh so far.
In that game, the password is composed of 4 entries,
with 8 icons each.
To do: everything else
Dialogue system:
Text data:
u16 - Global font index
This is the font to use for the entire text stream.
For example, Chicken Little uses these values for its fonts:
> 0x0000 - Font (Menus)
> 0x0100 - Font (Cutscenes)
> 0x0300 - Alien characters
u16 - Global palette index
u16 - Unused (always set to 0)
u16 - Portrait index
> This is unused in A2M games that use the talking-heads cutscene system.
Encoded text stream data begins here:
The text data is remapped in a certain way:
If a character is lowercase, the character is XORed by 32.
If a character is uppercase or is a symbol (i.e punctuation),
the character is incremented by 32.
Any special characters are remapped using a table?
Any special characters with a resulting value more than 0x7A
has a 0x80 byte appended at the start.
Any Japanese characters start from 0x80C0. The small characters are seperated into 80F5-80FF.
> For example:
0x80C1 = あ
0x80C2 = い
0x80C3 = う
0x80C4 = え
0x80C5 = お
0x80C6 = か
...
0x80D3 = て
The alternate characters are 8101+.
> For example:
0x8101 = が
Every space is the byte string 0xFFFC.
u16 - 0xFFFE -> this ends the text stream.
This is padded with 0x00 to the nearest dword
Dialog data:
u16 - Frames to wait before executing
u8 - Skips command if not 0
u8 - Always set to 5. This is treated as a magic number,
and is checked for every dialog command. If this isn't
set to 5, the command is ignored
u8 - Command type
00 -> Create text
01 -> Create mugshot actor
02 -> Hide mugshot actor
03 -> Create text (w/ dialog box)
04 -> ~~
05 -> End cutscene
06 -> ~~
07 -> ~~
0A -> Load level ID
0C -> Play sound ID
0D -> Fade out to black
0E -> Fade in from black
0F -> Stop sound ID
11 -> Related to screen variables
12 -> Related to screen variables
13 -> Set music update boolean
14 -> Play music ID
<Command types>
Create text (0x00):
u8 - Character ID
u8 - Dialog line index
u8 - Font offset (broken?)
u8 - Unused
Create mugshot actor (0x01):
u8 - Character mugshot index
> Any dialog that is displayed after
the mugshot is loaded must have its ID
match this index. If not the cutscene system
will skip past it until the bytestream is valid.
u8 - Mugshot direction
0 -> left
1 -> right
u8 - Reserved
u8 - Unknown (always set to 0)
Hide mugshot actor (0x02):
u8 - Character mugshot index
bool - Whenever or not to slide the sprite off-screen
> Typically set to 0 in most cases
Create textbox (0x03):
u8 - Character ID
u8 - Dialog line index
u8 - Font offset (broken?)
u8 - Dialog box style
?? (0x04):
> unknown
?? (0x05):
u8 - Reserved
u16 - Set to 0x2 if ending a cutscene
?? (0x06):
> unknown
?? (0x07):
> unknown
Load level ID (0x0A):
u8 - Level index
Play sound ID (0x0C):
u8 - Sound effect index to play
?? (0x0D):
> No parameters
?? (0x0E):
> No parameters
Stop sound ID (0x0F):
u8 - Sound effect index to stop
Related to screen variables (0x11, 0x12):
u8 - Unknown
Set music update boolean (0x13):
bool -> Truth value to update the boolean to
Play music ID (0x14)
u8 -> Music track index
Cutscene data:
To do: everything!
This is seen in games such as Monster House and The Ant Bully.
Use cases for this are ingame cutscenes w/ dialog boxes.
Levels:
Tilemap data:
u8 - Tile packing type bitfield
bit 0 -> Compress tilemap (used for screens)
bit 1 -> Compress tilemap (used in levels)
bit 2 -> ~~
bit 7 -> ~~ (Always set?)
u8 - Tile-related
00 -> Shifts tilemap to the right slightly
01 -> ~~
02:FF -> Tile 0 with palette 0 across the screen
u8 - Tile width
u8 - Tile height
u16 - Map width
u16 - Map height
> If the map width/height is less than what it's
designed for, The display width/height is modulo'd by this number
u8 - Tilemap properties
bit 0:1 -> Priority index (0 to 3)
bit 2:3 -> Tileset index
bit 4:6 -> Unused
bit 7 -> 8bpp mode
u8 - Unknown
u8 - Tile data read offset
u8 - Reserved
If packing bit 1 is set:
u32 - Tilemap address (not ROM address)
> Calculated and displayed correctly if the address is word-aligned
<Unused here>
> u16 - Height in pixels
> u8 - Unused
> u8 - Width in pixels
u32 - Mostly (if not always) set to 0x00000000
u8 - ~~ (Decoding bitfield?)
u8 - ~~
u8 - ~~
<Tilemap data (Compressed)>
u8 - Number of commands to decode
u8 - Uncompressed size in bytes?
- The non compressed entries are just u8 indexes into the metatile data
- Compression is unknown (pretty unintuitive as to how it works)
> Padded to nearest dword
If not:
u32 - Tilemap address (not ROM address)
> Calculated and displayed correctly if the address is word-aligned
<Unused here>
> u16 - Height in pixels
> u8 - Unused
> u8 - Width in pixels
u32 - Mostly (if not always) set to 0x00000000
u8[*] - Tile index
> Number of indexes is calculated using:
map width * map height
Metatile definition data:
u16 - Subtile properties
-> YYYYXXXXXXXXXXXX
bits 0-11 -> tile ID
bits 12-15 -> palette ID
For example: if a 16x16 metatile rule is described, a single metatile takes up a single dword, and is formatted like this:
[A] [B]
[C] [D] ...
The tiles are stored in ascending, rightwards order here.
If the header declares absolute tile indexing:
u8 - Palette index (0x00-0x3F)
The resulting index into the palette RAM is calculated as is:
> (a & 0x40) / 4
u8 - Reserved
u16 - Absolute index into tile data
Collision map:
The collision data is stored as an 8bpp bitmap array. Each entry is an entry in the level's collision map. Each entry is also represented in-game as a 16x16 collision block. The size of the data is the W tile count * the H tile count.
For example: These are the values that Chicken Little uses:
0x00 -> nothing
0x01 -> ground tile
0x2c -> jump thru solid (left)
0x30 -> jump thru solid
0x2e -> jump thru solid (right)
0x37 -> bouncy tile
0x58, 0x50 -> climbable tile (not solid)
0x2c, 0x30 -> climbable tile (solid)
Object list (WIP):
To do: Where are the positions stored, and how?
Object property chunk (WIP):
...
u16 -> Object type
u16 -> Object params / state value
u16[2] -> set to 0xFFFF if not in use, unknown purpose
u16 -> Object ID (read by the game, but not used)
u16 -> ?
u16 -> ?
u16 -> Count of objects
...
Footer (WIP):
Unknown data[*]:
s16 - X
s16 - Y
u16 - Unused
u16 - Packet ID
Unknown pointers:
u32*[*] - ~~
Layer setting[*]:
u8 - Tilemap ID
u8 - Bitfield(?)
-> bit 0 - Disable horizontal scrolling
-> bit 1 - Disable all scrolling
-> bit 3 - Enable constant scrolling
u16 - Reserved
s16 - Param #1
s16 - Param #2
char[16] -> Level name (i.e School01a, Logo_A2M, etc.)
-> padded with 0x00
<8 bytes, this depends on size>
u16 - Global scanline effect params
u16 - ~~
bool - Use absolute tile indexing
bool - ~~
u16 - ~~
u16 - Layer count
u16 - Reserved
u32 - ~~
u32 - ~~
u32 - ~~
u16 - ~~
u16 - Tileset index
u16 - Tileset-related (unknown)
u16 - Palette index
u32 - Reserved
u32 - ~~
u32 - ~~
u32* - ~~
u32 - ~~
u16 - Pointer list width
u16 - Pointer list height
u32 - Pointer list address (relative)
u32* - ~~
u32 - Layer setting data start offset
-> In Chicken Little, the offset is calculated by adding this offset to the ROM address 0x81D8474
u32[2] - ~~
u32 - Unused
u32* - ~~
Character controller (WIP):
> Notes:
- In Chicken Little, there is a u32 constant for velocity changes, which is treated as if it were a float.
- These are the max velocity speed for specific movement actions, like moving in-air, moving left or right, moving then jumping, etc.
- Collision tiles are interacted with per-pixel.
- Some games use fixed point 16.16 floats for positions[3].
Palettes:
Stored in the general GBA color format, but the first color of each row is black instead of pink/magenta. This doesn't matter as that color is simply ignored.
Objects / animations:
Palettes seem to be not a part of this data, and are called seperately.
This allows for calling of the same object with different display properties (i.e transparent tutorial player NPCs like those seen in Chicken Little or Kim Possible 2)
<Unknown data>
Sprite graphic data:
- Stored in 4bpp or 8bpp little-endian tiles (8x8 px per tile).
- This stores every tile of an animation frame
- This is padded with 0x00 to the nearest dword
Animation data:
Keyframe:
u16 - Keyframe chunk size
u32 - Sprite graphic data offset
> Seems to be different for every frame. This suggests that
this is remapping the graphics data onto every subsprite.
> This is also not a ROM address, which implies there is
some offset calculation going on to convert it into a ROM pointer
u16 - ?
Sprite placement:
s16 - Sprite offset (X)
s16 - Sprite offset (Y)
u16[2] - ?
u8 - Subsprite count
> Number of sprites in a given animation frame.
> Maximum is ~97 sprites
s8 - Frame speed
u8 - Frame ID
bool (u8) - Is a sound playing back on this keyframe?
u8 - ?
u8 - Known values are 8 and 0
Subsprite placement:
For each subsprite:
s8 - Subsprite Y position
u8 - Subsprite properties #1
bit 0 - Transform
bit 1 - Visibility
bit 2 - Blend effect
bit 3 - Window
bit 4 - Mosaic
bit 5 - Bit depth
0 -> 4bpp
1 -> 8bpp
bit 6 - Double/halve tile height
(i.e 8x8 -> 8x16, 32x32 -> 32x16)
bit 7 - Double/halve tile width
(i.e 8x8 -> 16x8, 32x32 -> 16x32)
> Note: When both bits 6 and 7
are set, they don't do anything
u8 - Subsprite X position
u8 - Subsprite properties #2
bit 0 - Off-screen(?)
bit 1 - *unused*
bit 2 - *unused*
bit 3 - *unused*
bit 4 - Horizontal mirror
bit 5 - Vertical mirror
bit 6-7 - Tile multiplier
0 - 8x8
1 - 16x16
2 - 32x32
3 - 64x64
~
If a sound is defined:
u8 - Set to 0x02
u8 - If this is more than 0x0 there is no sound
u16 - SFX index
> In Chicken Little, this uses a different
SFX mapping than observed in the original GAX FX blob.
Check me: This and the sound effect player menu rely on a
remapping of the GAX sound effect bank. Is this
done in just Chicken Little or does this occur in
other games as well?
If not, then:
u16 - ?? (seems to depend on the subsprite count over every frame of the animation?)
~
u16 - Object script (for every frame)
u16 - *unused*
u16 - Palette offset
u16 - Object script (on init)
u8 - Interactibility with objects?
> in Chicken Little, 0x3 means that the object can interact with acorns
u8 - Animation count
u8 - Number of animations to fetch on init
u8 - Starting animation index offset
<For each animation>
u32 - Relative pointer to animation data
u8 - Animation index
u8 - ??
u32 - Pointer to script (on animation init?)
u16 - ??
Notes:
> The A2Mx Game Engine did not debut on Ice Age for the GBA. The engine used in that game was instead done by Vincent Van Eeckhout, with the tools developed by Simon Chouinard. Stéphane's game engine actually debuted in 2003 on Scooby-Doo!: Mystery Mayhem for the same system.
[1] - https://www.mobygames.com/game/159188/ed-edd-n-eddy-the-mis-edventures/credits/gameboy-advance/
[2] - https://www.mobygames.com/game/205630/disneys-chicken-little/credits/gameboy-advance/
[3] - https://tasvideos.org/Forum/Topics/17310
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment