To open a record, select a node in the graph or a title in the index.

[[17358653195271|speedcylinder global spawner]]

types : mods
keywords :

📂 View source on GitHub


SpeedCylinder Global Spawner

Overview

Spawns a SpeedCylinder (Pendulum) at Player 1's position in any level, not just Level 6 (Up). Pure CE AutoAssembler script — no bass.dll proxy required.

Files

  • SpeedCylinderSpawn.CEA — CE AutoAssembler script (pure CEA, no Lua)

Installation

  1. Copy SpeedCylinderSpawn.CEA to your Cheat Engine scripts folder
  2. In CE: File → Load → select SpeedCylinderSpawn.CEA
  3. Enable the script

Usage

  1. Add the address SpawnFlag to your CE address list (Advanced → Add Address, or type "SpawnFlag" in the address bar after enabling the script)
  2. Enter a race or arena (any level works)
  3. Set SpawnFlag to 1 — a SpeedCylinder spawns at Player 1's current position
  4. The flag auto-resets to 0 after spawning
  5. Set to 1 again to spawn another

How It Works

The script hooks Scene_UpdateBallsAndState (0x41B540) which runs every frame. When SpawnFlag is set:

  1. Checks if a SpeedCylinder mesh is already loaded (Board+0x4788, only set on Up Race)
  2. If not, loads the mesh from levels\levelup-speedcylinder via MeshWorld_ctor
  3. Finds Player 1's ball in the all_balls_list and reads its position
  4. Allocates a Pendulum struct (0x150C bytes) and calls Pendulum_ctor
  5. Registers the new SpeedCylinder in the Board's mechanical objects list (Board+0x2578)

Verified Addresses (Ghidra, 2026-06-22)

Address Function Convention
0x41B540 Scene_UpdateBallsAndState __thiscall (ECX=Board)
0x4BA57B operator_new __cdecl (RET)
0x461510 MeshWorld_ctor __thiscall (RET 0x8, 2 params)
0x436A20 Pendulum_ctor __thiscall (RET 0x18, 6 params)
0x453810 AthenaList_Append __thiscall (RET 0x4, 1 param)
0x4D1140 "levels\levelup-speedcylinder" String constant
0x5341E0 App global pointer Runtime global

🔗 Related Documents

[[23063636100492|global toob race object spawne]]

types : objects
keywords :

📂 View source on GitHub


Global Toob Race Object Spawner

Spawns any of the 10 Toob Race objects at Player 1's position in any level.

Usage

  1. Set ObjectType (1-10) to select which object to spawn:
    • 1 = SPINNY (rotating platform)
    • 2 = LOOPER (looping track)
    • 3 = GEAR (spinning gear)
    • 4 = BIGGEAR (big gear)
    • 5 = ROTATOR (generic rotator, uses Blockdawg1 mesh)
    • 6 = SPEEDCYLINDER (swinging pendulum)
    • 7 = LIFTER (lifter elevator)
    • 8 = TIMEBUTTON (timed button)
    • 9 = BLOCKDAWG1 (block dog enemy 1)
    • 10 = BLOCKDAWG2 (block dog enemy 2)
  2. Set SpawnObject to 1 to spawn at player position
  3. Objects update automatically via vtable[0x2C] each frame

How It Works

The script bypasses the factory chain (CreateSpinny → CreateLevelObjects → etc.)
and calls constructors directly with our own MeshWorld pointer.

Key advantage over Drawbridge/PopCylinder scripts: No JIT board slot injection needed.
All Toob constructors take the mesh pointer as the last stack parameter, so we pass
our own MeshWorld directly without touching any board+0x43xx slots.

Toob Race Board Mesh Slots

Set by LevelBoard_Toob_ctor (0x41F4B0):

Offset Mesh File String Addr Used By
board+0x436C Levels\Level8-Spinny 0x4D0E38 LOOPER
board+0x4370 Levels\Level8-Saw 0x4D0E24 GEAR
board+0x4374 Levels\Level8-Fallout 0x4D0E0C BIGGEAR
board+0x4378 Levels\Level8-Blockdawg1 0x4D0DF0 ROTATOR
board+0x437C Levels\Level8-Blockdawg2 0x4D0DD4 BLOCKDAWG2

Additional slots set during LoadRaceData (not by board ctor):

  • board+0x4784 = LIFTER mesh (runtime, unknown file)
  • board+0x4788 = SPEEDCYLINDER mesh (runtime, unknown file)
  • board+0x478C = TIMEBUTTON mesh (runtime, unknown file)
  • board+0x47E0 = SPINNY mesh (runtime, unknown file)

For objects with unknown mesh files (SPEEDCYLINDER, LIFTER, TIMEBUTTON),
the script uses Level8-Spinny as a fallback mesh.

Constructor Summary

Object Constructor Addr Alloc RET CollisionLevel Offset
SPINNY Rotator_ctor 0x435940 0x1508 0x14 +0x10D4
LIFTER Rotator_ctor_sound 0x436920 0x10F4 0x18 +0x10E0
SPEEDCYLINDER Pendulum_ctor 0x436A20 0x150C 0x18 +0x10E0
TIMEBUTTON Rotator_ctor_nosound 0x436C10 0x10E8 0x14 +0x10E0
LOOPER Looper_ctor 0x435800 0x1500 0x14 +0x10D4
GEAR Gear_ctor 0x437590 0x1514 0x20 +0x10D4
BIGGEAR Gear_ctor 0x437590 0x1514 0x20 +0x10D4
ROTATOR Rotator_ctor 0x435940 0x1508 0x14 +0x10D4
BLOCKDAWG1 Blockdawg_ctor 0x43C310 0x1154 0x18 +0x10D4
BLOCKDAWG2 Blockdawg_ctor 0x43C310 0x1154 0x18 +0x10D4

Factory Chain (How the Game Creates Toob Objects)

LoadRaceData("TOOBRACE")
  → reads MESHWORLD ref objects
  → for each ref, calls factory chain:
    CreateBumper (0x40FA20)     → BUMPER
    CreateSpinny (0x4143D0)     → SPINNY (→ Rotator_ctor, mesh from board+0x47E0)
    CreateSpeedCylinder (0x4117B0) → LIFTER, SPEEDCYLINDER, TIMEBUTTON
    CreateMechanicalObjects (0x417FE0) → LOOPER, GEAR, BIGGEAR, ROTATOR
    CreateLevelObjects (0x4121D0) → BRIDGE, TIPPER, BONK, POPCYLINDER, BLOCKDAWG, CATAPULT, GLUEBIE

Toob Race Collision Handler Events

The Toob collision handler at vtable[0x1D] (0x410020) handles:

  • N:SPINNY, N:SAWTEETH, N:BUMPER — collision with rotating objects
  • E:ALERTSAW2, E:BRANCH — event triggers
  • PILLAR, MAGNIFYER, CLOUDSCAPE — special objects
  • POPCYLINDER, TRAPDOOR — pop/door effects
  • E:PEGS, E:TRAPPOP, E:NOPEGS — peg state machine
  • E:HEATON, E:HEATOFF — heat/lava toggle
  • E:LIMIT — boundary limit

Object Registration

All objects are registered in:

  • board+0x2578 — active objects list (update + render)
  • board+0x10EC — collision level list
  • board+0x8B0+0x18 — collision dispatch list
  • board+0x8AC→+0x480→+0x1C — render list (post-alpha)

Notes

  • BLOCKDAWG objects need a DAWGPATH ref for movement. With pathId=0 they stay stationary.
  • GEAR/BIGGEAR use dual-position params (X,Y,Z + X2,Y2,Z2). Script uses same position for both.
  • SPEEDCYLINDER/LIFTER/TIMEBUTTON use fallback mesh (Level8-Spinny) since their actual
    mesh files are loaded at runtime via board+0x4784/0x4788/0x478C and the filenames
    are not known at compile time.

🔗 Related Documents

[[24542692255965|jump_mod v22]]

types : mods
keywords :

📂 View source on GitHub


jump_mod v22

Press SPACE to jump (Player 1 only, raycast ground detection).

What changed from v20

v22 adds countdown and race-end gating:

  1. Countdown gate: Before allowing a jump, checks Scene+0x3A4C
    (countdown_done flag). This flag is set to 1 by Scene_HandleRaceEnd
    (0x41B130) when all 3 Ready/Set/Go phases complete. If 0, the game
    itself blocks all input in Scene_vmethod31 (0x41AC70) — the jump
    mod now mirrors this behavior.

  2. Race-end gate: Checks ball+0x14C (freeze flag). Set to 1 by
    Scene_HandleRaceEnd at 0x41B40D when the race timer expires
    (player touches the goal). Also checked by the game's Ball_Update
    at 0x4060A1. When set, the ball is frozen and jumping is blocked.

Both gates are checked in the input thread BEFORE running the raycast,
so denied jumps don't waste a raycast call.

What changed from v17

v20 changed ground detection from a fixed epsilon to a slope-aware
threshold (radius × 1.45):

On a slope of angle θ, a straight-down raycast hits at distance r/cos(θ)
from the ball center, not r. Using radius * 1.45 covers slopes up to
45° (cos(45°) ≈ 0.707, r/0.707 ≈ 1.414r). The 1.45 factor gives a small
safety margin beyond the theoretical minimum of √2 ≈ 1.414.

Features

  • Raycast ground detection: Casts a ray straight down from the ball
    position using the game's own Mesh_FindClosestCollision (0x465D90).
    If the hit point is within radius × 1.45 of the ball Y, the ball is
    grounded and can jump.
  • Countdown gating: No jumping during Ready/Set/Go countdown
    (Scene+0x3A4C == 0).
  • Race-end gating: No jumping after touching the goal
    (ball+0x14C == 1).
  • Airborne denial: Can't jump while in the air (raycast misses).
  • Edge detection: SPACE uses rising-edge detection (one jump per keypress).
  • Safety checks: Won't jump if ball pointer not yet captured or
    during fall/respawn.

Hook points

Hook Address Original bytes Purpose
Phase 15 cave 0x407BB4 8B 4C 24 1C 8B 11 (6 bytes) Jump impulse application

The input thread polls the keyboard (DIK_SPACE at KeyboardDevice+0x45)
every 16ms. On rising-edge keypress, it checks countdown/race-end gates,
then runs the raycast. If grounded, sets g_want_jump=1. The Phase 15
cave checks this flag and adds an upward impulse to ball+0x174 (Y force
accumulator) if set.

Files

  • jump_mod_raycast.c — C source code (BASS proxy + raycast + gates)
  • bass.dll — Compiled DLL (MinGW cross-compiled, PE32 i386)
  • jump_mod_v22.zip — Distribution archive

Build

i686-w64-mingw32-gcc -shared -o bass.dll jump_mod_raycast.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

🔗 Related Documents

[[32519525145467|no-pause mod v2]]

types : mods
keywords :

📂 View source on GitHub


No-Pause Mod v2

Prevents the pause menu from appearing via any input method — ESC key, right-click, or Win32 message pump. The game continues running normally with no pause overlay, no physics freeze, and no camera stop.

Why v1 Didn't Work

v1 only patched one of three code paths that trigger pause. Pressing ESC still paused the game through the Win32 message pump path, and right-clicking paused through the mouse event handler path.

The Three Pause Paths

Scene_CreateGameOverMenu (0x40a920) creates the pause overlay and sets scene+0x874 = 1 (pause flag). When this flag is set, GameUpdate (0x469cf0) skips calling Scene_Update on the scene — freezing all physics and game logic.

There are three independent code paths that call Scene_CreateGameOverMenu:

Path 1: DirectInput ESC Poll (Scene_Update)

Scene_Update (0x419c00)
  → Input_CheckKeyCombo(app, 2)    ; checks ESC via DirectInput
  → if pressed: Scene_CreateGameOverMenu(scene, 1)

Gating conditions: game state not in {3,4}, scene+0x220 == 0, demo timer inactive.

Path 2: Right-Click Mouse Handler (vtable[5])

App_OnMouseDown(param_3=1=right button)
  → UIWidget_HitTest → vtable[5] on Scene
  → thunk 0x4130A0 checks param_3==1, App+0x238, profile+0x95
  → if conditions met: Scene_CreateGameOverMenu

This thunk appears in 32 scene-object vtables — all scene types inherit this pause-on-right-click behavior.

Path 3: Win32 Message Pump ESC (vtable[8])

WndProc → key message dispatch → vtable[8] on Scene
  → thunk 0x40B400 checks param == 0x1B (VK_ESCAPE = 27)
  → if match: JMP Scene_CreateGameOverMenu (tail call)

This is a separate ESC detection path from Path 1. Path 1 uses DirectInput polling; Path 3 uses the Win32 message pump. Both fire on ESC press. This thunk also appears in 32 vtables.

The Patches

Three single-byte patches, one per path:

Path Address Original Patched Effect
1 — DirectInput ESC 0x419d5b 74 09 (JZ) EB 09 (JMP) Always skip pause creation in Scene_Update
2 — Right-click 0x4130b5 74 17 (JZ) EB 17 (JMP) Always skip pause in vtable[5] right-click thunk
3 — Message pump ESC 0x40b405 75 0D (JNZ) EB 0D (JMP) Always skip pause in vtable[8] message handler thunk

All three convert conditional jumps to unconditional jumps, causing the pause creation code to always be skipped.

How Pause Works (for reference)

  1. Scene_CreateGameOverMenu (0x40a920) creates a PauseMenu or PauseArenaMenu overlay and sets scene+0x874 = 1
  2. GameUpdate (0x469cf0) iterates scene objects each frame. For each object, it checks obj[0x21d] (byte at scene+0x874). When the flag is 1, it skips calling Scene_Update (vtable[1]) — freezing all game logic
  3. When the player clicks "RESUME" in the pause menu, PauseMenu_HandleButtonClick sets scene+0x874 = 0 — unfreezing the game

Installation

  1. Extract the zip into your Hamsterball game folder (next to Hamsterball.exe)
  2. Run install.bat
  3. Launch the game

Uninstallation

Run uninstall.bat to restore the original bass.dll.

Technical Details

  • Mod type: BASS.dll proxy (v3 lazy loader pattern)
  • Patch type: Three single-byte patches — conditional → unconditional jumps
  • Patch addresses: RVA 0x19d5b, 0x130b5, 0x0b405 (VA 0x419d5b, 0x4130b5, 0x40b405)
  • Side effects: None — ESC and right-click are simply ignored during gameplay. No menu, no pause, no freeze.

🔗 Related Documents

[[35033317086156|universal ref loader]]

types : docs
keywords :

📂 View source on GitHub


Universal Ref Loader — Design Document

Created: 2026-06-23
Session: RodentRacer investigation into ref-loading system
Purpose: Enable loading ANY object ref into ANY race level via a DLL mod, and fix the multi-instance problem for static-mesh objects.


Table of Contents

  1. Architecture Overview
  2. How Ref Loading Works
  3. Two Board Systems: Race vs Arena
  4. Factory Dispatch via vtable[33]
  5. Board Constructor Mesh Pre-Loading
  6. Object Categories: Allocating vs Static vs Configuring
  7. The Multi-Instance Problem
  8. The Mesh Dependency Problem
  9. The Difficulty Gate Problem
  10. The universal ref loader Design (Option B)
  11. Clone-on-Return for Static Mesh Objects (Option A)
  12. Complete Object Dependency Table
  13. Complete vtable[33] Mapping
  14. Complete Board Slot → Mesh File Mapping
  15. Key Function Addresses
  16. Implementation Plan
  17. Build & Test

1. Architecture Overview

Hamsterball loads level objects in a two-layer pipeline:

MESHWORLD file (Section 1: ref points)
  ↓
Scene_CreateDynamicObjects (0x40C430) — iterates ref points
  ↓  For each ref:
  ↓  calls board->vtable[33](refName, &outObj, &outCol, refEntry) at 0x40C4BA
  ↓
Factory Function (level-specific, via vtable[33])
  ↓  Matches ref name via __strnicmp
  ↓  Allocates object, calls constructor, sets position
  ↓  Returns object pointer in outObj
  ↓
Scene adds object to active list (+0x335), sets position from MESHWORLD data
  ↓  Calls object vtable[22] (Init) and vtable[21] (SetTimer)

Scene_CreateDynamicObjects (0x40C430) — Full Decompiled Flow

void __fastcall Scene_CreateDynamicObjects(int *param_1)
{
    // Iterate through MESHWORLD Section 1 ref points
    // For each ref (puVar4 = ref entry with name, x, y, z, ...):
    
    local_58 = NULL;  // output object pointer
    local_54 = 0;     // output secondary object pointer (trigger zones)
    
    // Call the Board's factory function (vtable[33], offset +0x84)
    (*(board->vtable[33]))(*puVar4, &local_58, &local_54, puVar4);
    
    if (local_58 != NULL) {
        // Object was created successfully
        Gfx_ScaleX(_DAT_004cf44c - puVar4[5]);           // set scale
        Gfx_SetPosition(puVar4[1], puVar4[2], puVar4[3]); // set position from MW data
        
        AthenaList_Append(scene + 0x335, local_58);       // add to dynamic object list
        AthenaList_Append(mw_parser + 0x1c, local_58);    // add to MW object list
        
        (*local_58->vtable[22])();    // Init/Setup
        (*local_58->vtable[21])(timer); // SetTimer
        
        if (local_54 != 0) {
            AthenaList_Append(scene + 0x43B, local_54);  // add trigger zone
            AthenaList_Append(scene_physics + 0x18, local_54);
        }
    }
    
    // Advance to next ref point
}

Key insight: The factory returns a pointer in local_58. If it's non-NULL, the scene adds it to the dynamic object list and initializes it. The position comes from the MESHWORLD ref data (puVar4[1], puVar4[2], puVar4[3]).


2. How Ref Loading Works

MESHWORLD Section 1 Ref Points

Each ref point in a MESHWORLD file contains:

  • Name (string, e.g. "CATAPULT", "WATERWHEEL", "N:BUMPER")
  • Position (3 floats: x, y, z)
  • Rotation (3 floats: x, y, z)
  • Material (4×4 float matrix + Power + reflection + texture)
  • Extra params (passed as param_4 to the factory)

Ref Name → Factory Matching

The factory function uses __strnicmp to match ref name prefixes:

  • "CATAPULT" matches any ref starting with "CATAPULT" (case-insensitive)
  • "N:" prefix refs are handled by a separate N:/E: handler at 0x40C5D0
  • "E:" prefix refs are event triggers (score, gravity, etc.)

Two Passes

  1. vtable[33] factory — matches bare ref names (CATAPULT, TIPPER, etc.)
  2. N:/E: handler — called from within the factory for N: and E: prefixed refs

The handler is called from 25 sites within the vtable[33] factory functions.


3. Two Board Systems: Race vs Arena

Hamsterball has TWO independent Board systems, selected by App+0x237:

Mode App+0x237 Board Constructors vtable Range Switch Function
Race 1 (non-zero) 0x422xxx 0x4D1428–0x4D2298 0x426780 (jump table at 0x426AB0)
Arena 0 0x41Cxxx 0x4D04A8–0x4D21C0 0x427080 (switch at 0x427140)

Critical: Race factories handle FAR FEWER ref types than Arena factories. Levels 1, 2, 3, 9, 10, 12, 14 use ONLY the base race factory (0x4133E0) which handles only PLATFORM, STANDS, N:BUMPER. Their level-specific objects (tippers, bridges, waterwheels) are NOT created in race mode — they're loaded via the Arena Board system or are part of static geometry.

The universal ref loader should hook at 0x40C4BA (the vtable[33] call site inside Scene_CreateDynamicObjects), which intercepts BOTH Race and Arena modes.


4. Factory Dispatch via vtable[33]

The Dispatch Call Site

At address 0x0040C4BA inside Scene_CreateDynamicObjects:

; *board is in a register, vtable[33] is at offset +0x84
mov eax, [board]          ; get vtable pointer
call [eax + 0x84]        ; call vtable[33] factory
; args: refName, &outObj, &outCol, refEntry

Factory Function Signature

void __thiscall FactoryFunc(
    Board* this,           // ECX = board pointer
    const char* refName,   // [ESP+4] = ref name string from MESHWORLD
    void** outObj,         // [ESP+8] = output: created object pointer
    void** outCol,         // [ESP+0xC] = output: secondary object (trigger zone)
    RefEntry* refEntry     // [ESP+0x10] = full ref entry (name, pos, rot, material)
);

Complete vtable[33] Mapping

Race Board Factories

# Race Level Constructor Board Vtable Factory Addr Refs Handled
1 Warm-up 0x4224A0 0x4D1428 0x4133E0 PLATFORM, STANDS (base only)
2 Beginner 0x422550 0x4D14F0 0x4133E0 PLATFORM, STANDS (base only)
3 Intermediate 0x4226E0 0x4D15C0 0x4133E0 PLATFORM, STANDS (base only)
4 Dizzy 0x422790 0x4D1680 0x4143D0 SPINNY, MACE, CATAPULT, TURRET, LIFTER, FAN + base
5 Tower 0x4228C0 0x4D1740 0x414680 MACE, CATAPULT, TURRET, LIFTER, FAN + base
6 Up 0x422B10 0x4D17F8 0x414A20 LIFTER, FAN, WOBBLY + base
7 Neon 0x424860 0x4D1EC8 0x4173B0 NEONPLATFORM, DFLOOR, TRODE + base
8 Expert 0x423060 0x4D18C8 0x414BD0 FAN, WOBBLY + base
9 Odd 0x423220 0x4D1980 0x4133E0 PLATFORM, STANDS (base only)
10 Toob 0x4234E0 0x4D1A40 0x4133E0 PLATFORM, STANDS (base only)
11 Wobbly 0x423690 0x4D1B18 0x415460 WOBBLY, PILLAR, POPCYLINDER + base
12 Glass 0x424B60 0x4D2048 0x4133E0 PLATFORM, STANDS (base only)
13 Sky 0x423BF0 0x4D1BD8 0x415A30 POPCYLINDER + base
14 Master 0x424380 0x4D1C80 0x4133E0 PLATFORM, STANDS (base only)
15 Impossible 0x424EC0 0x4D2298 0x418760 GEAR + base

Arena Board Factories

# Arena Level Constructor Board Vtable Factory Addr Refs Handled
1 Warm-up Arena 0x41CA40 0x4D04A8 0x419750 (none — NoOp)
2 Beginner Arena 0x4200E0 0x4D1098 0x419750 (none — NoOp)
3 Intermediate Arena 0x41CB20 0x4D05A0 0x40A550 BRIDGE, TIPPER, WATERWHEEL, SWIRL, GLUEBIE, SMASHER1, SMASHER2
4 Dizzy Arena 0x41D060 0x4D0890 0x40A5F0 TIPPER, WATERWHEEL, SWIRL, GLUEBIE, SMASHER1, SMASHER2
5 Tower Arena 0x41E340 0x4D0A08 0x40D7C0 CATAPULT, MACE, DRAWBRIDGE, WINDMILL, TRAPDOOR, CHOMPER, BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL
6 Up Arena 0x420390 0x4D11A0 0x4117B0 LIFTER, SPEEDCYLINDER, TIMEBUTTON, TarBubble, BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT
7 Neon Arena 0x424440 0x4D1DF0 0x416910 NEONPLATFORM, DFLOOR1-4, TRODE, N:NEONPLATFORM, E:ZOOP, E:LIGHTSOFF, E:LIGHTSON, FLICKNING, N:BUMP, N:GLASS, N:TENBONUS1
8 Expert Arena 0x41EA40 0x4D0B00 0x40E250 BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL, E:SCORE, E:BELL, LIFTER, E:GRAVITY
9 Odd Arena 0x41ED80 0x4D0BC0 0x40EC40 LIFTER, E:GRAVITY, WOBBLY1-7, WAVY1, N:SQUAREWOBBLY, N:WAVY, SPINNY
10 Toob Arena 0x41F4B0 0x4D0E78 0x40FB30 SPINNY, FALLOUT1, BLOCKDAWG1-3, E:ALERTSAW2, E:BRANCH, N:SPINNY, N:SAWTEETH, N:BUMPER, PILLAR, MAGNIFYER
11 Glass Arena 0x41F110 0x4D0D38 0x40F420 WOBBLY1-7, WAVY1, N:SQUAREWOBBLY, N:WAVY, SPINNY, FALLOUT1, BLOCKDAWG1-3, E:ALERTSAW2, E:BRANCH, N:SPINNY
12 Wobbly Arena 0x424A90 0x4D1F90 0x40AD80 SMASHER1, SMASHER2, SECRETUNLOCK, SECRET, BADBALL
13 Sky Arena 0x41F930 0x4D0FC8 0x410AD0 POPCYLINDER, TRAPDOOR, N:BUMPER, VAC-IN, LIFTER, SPEEDCYLINDER, TIMEBUTTON
14 Master Arena 0x4206D0 0x4D12B0 0x4121D0 BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT, GLUEBIE, N:SPINNER, N:BUMPER, E:LAUNCH
15 Impossible Arena 0x424C20 0x4D21C0 0x417FE0 LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM, N:BOUNCE, N:ONROTATOR, N:ONGEAR

Key insight: Levels 1, 2, 3, 9, 10, 12, 14 Race factories handle ONLY PLATFORM, STANDS. Their interesting objects (tippers, bridges, waterwheels) are NOT created in race mode by vtable[33] — they're loaded via the Arena Board system or are static geometry.


5. Board Constructor Mesh Pre-Loading

Each Board constructor loads specific mesh files into board+0x4xxx slots BEFORE the factory runs. The factory then reads these slots to get the mesh data needed by each object's constructor.

How It Works

// Inside LevelBoard_Dizzy_ctor (Dizzy Race, 0x41D060):
pvVar1 = operator_new(0x10D0);
pvVar1 = MeshWorld_ctor(pvVar1, App+0x174, "Levels\\Level3-WaterWheel");
*(void**)(board + 0x4BA8) = pvVar1;  // store mesh at board+0x4BA8

pvVar1 = operator_new(0x10D0);
pvVar1 = CollisionLevel_ctorWithLevel(pvVar1, board+0x4BA8);
*(void**)(board + 0x4BAC) = pvVar1;  // store collision at board+0x4BAC

The Conflict Problem

The same board+0x4xxx slot is reused for different meshes on different levels. For example:

Board Slot Beginner Intermediate Tower Expert Impossible Master Up
+0x436C (empty) Level2-Bridge Level4-Catapult (empty) Looper BreakBridge1 (empty)
+0x4370 (empty) (collision) Level4-Drawbridge (empty) BigGear BreakBridge2 (empty)
+0x4374 (empty) (empty) YellowLink(MeshNode) (empty) Rotator PopCylinder2 (empty)
+0x4378 (empty) (empty) Level4-Mace Level5-Bridge Rotator PopCylinder (empty)
+0x437C (empty) (empty) Level4-Windmill Collision Pendulum (empty) (empty)

You cannot simply "load all meshes" into board slots — you'd overwrite one mesh with another.

Solution: Just-In-Time Mesh Injection

The DLL maintains its own private array of all 46 object meshes loaded from disk. When the factory needs a specific mesh, the DLL:

  1. Saves the current board slot value
  2. Writes the correct mesh pointer into the slot
  3. Calls the constructor (which reads the slot)
  4. Restores the original board slot value

6. Object Categories: Allocating vs Static vs Configuring

All 46 verified ref types fall into three categories:

Category 1 — Allocating Objects (MULTI-INSTANCE SAFE)

The factory allocates a new object each time, calls a constructor, and appends it to the active object list (board+0x2578). Multiple instances work fine — each gets independent state. They share the pre-loaded mesh at board+0x4xxx (read-only), which is fine for rendering.

Object Alloc Size Constructor Board Mesh Slot (READ) AthenaList Difficulty Gate
TIPPER 0x1104 Tipper_ctor +0x4394, +0x4398 +0x2578 YES (App+0x23C≠0)
BONK 0x1200 Bonk_ctor (self-loads) +0x2578 YES (App+0x23C≠0)
BREAKBRIDGE1 0x1100 BreakBridge_ctor +0x5410 +0x2578 No
BREAKBRIDGE2 0x1100 BreakBridge_ctor +0x5414 +0x2578 No
POPCYLINDER 0x10E8 PopCylinder_ctor +0x5420 +0x2578, +0x5428 No
BLOCKDAWG1 0x1154 Blockdawg_ctor +0x5840 +0x2578 YES (App+0x23C≠0)
BLOCKDAWG2 0x1154 Blockdawg_ctor +0x5844 +0x2578 YES (App+0x23C≠0)
CATAPULT 0x1108 Catapult_ctor +0x5848 +0x2578, +0x584C No
GLUEBIE 0x110C Gluebie_ctor +0x607C +0x6080, +0x2578 YES (App+0x23C≠0)
LIFTER 0x10F4 Rotator_ctor_sound +0x4784 (Up) / +0x47E0 (Odd) +0x2578 No
SPEEDCYLINDER 0x150C Pendulum_ctor +0x4788 +0x2578 No
TIMEBUTTON 0x10E8 Rotator_ctor_nosound +0x478C +0x2578 No
LOOPER 0x1500 Looper_ctor +0x436C +0x2578 No
GEAR 0x1514 Gear_ctor +0x4370 +0x2578 No
BIGGEAR 0x1514 Gear_ctor +0x4374 +0x2578 No
ROTATOR 0x1508 Rotator_ctor +0x4378 +0x2578 No
PENDULUM 0x1504 Pendulum_ctor +0x437C +0x2578 No
FAN 0x1188 TowerLevel_Ctor (none — procedural) +0x2578 YES (App+0x23C≠0)
SAWBLADE 0x111C Sawblade_Level_Ctor (none) +0x2578 YES (App+0x23C≠0)
BRIDGE-Expert 0x10FC Spinner_Level_ctor +0x4378 +0x2578 (conditional) No
JUDGE 0x1100 Gear_Level_ctor (none) +0x4BBC No
BELL 0x10E8 Tipper_Level_Ctor (none) +0x2578 No
DRAWBRIDGE 0x113C Glass_Level_ctor +0x4370 +0x2578 No
MACE 0x110C (0x438750) +0x4378 +0x2578 No
TRAPDOOR 0x10F8 (0x438290) (none) +0x2578 No
TURRET 0x10D0 Stands_ctor +0x43B4 (vtable dispatch) No
SPINNY 0x1508 Rotator_ctor +0x47E0 +0x2578 No
SMASHER1 +0x2578 No
SMASHER2 +0x2578 No
WAVY +0x2578 No
WOBBLY GameLevel_ctor +0x2578 No

Category 2 — Static Mesh Return Objects (MULTI-INSTANCE BROKEN)

The factory does NO allocation, NO constructor, NO list append. It returns the pre-loaded mesh pointer directly. Multiple refs all point to the SAME object in memory → only one renders (at the last position set).

Object Board Slot (returned) Level Description
WATERWHEEL +0x4BA8 Dizzy Spinning floor mesh
SWIRL +0x4BC4 Dizzy Swirl mesh
CHOMPER +0x4390 Tower MeshNode visual only (no collision/game logic)
WINDMILL +0x437C Tower CollisionLevel only (no game object/render)
PILLAR (static) Sky Static mesh + collision

Category 3 — Configuring Objects

The factory doesn't create anything — it moves/reconfigures the existing mesh.

Object Board Slot Description
BRIDGE (base) +0x436C Configures existing bridge mesh position/collision

7. The Multi-Instance Problem

Root Cause

When you add two WATERWHEEL refs to a Dizzy Race MESHWORLD file:

  1. Scene_CreateDynamicObjects calls vtable[33] for the first WATERWHEEL ref
  2. The factory returns board+0x4BA8 (the pre-loaded mesh pointer) in local_58
  3. The scene adds it to the dynamic list, sets position A, calls Init
  4. Scene_CreateDynamicObjects calls vtable[33] for the second WATERWHEEL ref
  5. The factory returns board+0x4BA8 again (THE SAME pointer) in local_58
  6. The scene adds it to the list again, sets position B, calls Init
  7. Result: Both list entries point to the same MeshWorld object. The second Init overwrites the first position. Only one renders (at position B).

The Fix: Clone-on-Return (Option A — CHOSEN)

When the universal ref-loader DLL detects a Category 2 object, it calls Level_CloneTree (0x466060) to create an independent copy before returning it.

// In the DLL factory hook, when matching WATERWHEEL/SWIRL/CHOMPER/WINDMILL/PILLAR:
void* mesh = *(void**)(board + boardOffset);  // pre-loaded mesh
void* clone = Level_CloneTree(mesh, board);     // 0x466060, thiscall
*outObj = (int)clone;                          // return the clone, not the original

Level_CloneTree (0x466060) — What It Does

undefined4* __thiscall Level_CloneTree(void* this, int param_1)
{
    // 1. Allocate new 0x10D0-byte object (same size as MeshWorld)
    undefined4* clone = operator_new(0x10D0);
    
    // 2. Call Level_ctor with parent's D3D device
    Level_ctor(clone, *(undefined4*)((int)this + 4));
    
    // 3. Set vtable to PTR_Level_DeletingDtor2 (0x4D9068)
    *clone = &PTR_Level_DeletingDtor2_004d9068;
    
    // 4. Copy parent's SceneObject (this+0x480)
    clone[0x120] = *(undefined4*)((int)this + 0x480);
    clone[0x121] = 1;  // flag: has scene object
    
    // 5. Copy parent's world matrix (this+8)
    clone[2] = *(undefined4*)((int)this + 8);
    
    // 6. Copy collision flag (this+0x430)
    *(char*)((int)clone + 0x10C) = *(char*)((int)this + 0x430);
    
    // 7. Set parent (param_1 = board)
    clone[0x11F] = param_1;
    
    // 8. Copy timer pointer (param_1 + 0x434)
    clone[0x10D] = *(undefined4*)(param_1 + 0x434);
    
    // 9. Recursively clone children (spatial tree)
    // For each child in this+0x424:
    //   Level_CloneTree(child, param_1) → append to clone's child list
    
    return clone;
}

Alternative Options (kept for reference)

Option B — Re-load from file each time:

void* mesh = operator_new(0x10D0);
MeshWorld_ctor(mesh, *(int*)(*(int*)(board + 0x878) + 0x174), "Levels\\Level3-WaterWheel");
*outObj = (int)mesh;

Slower (re-parses file) but fully independent. Works even if board slot is empty (useful for cross-level loading).

Option C — Use Stands_ctor pattern:

void* obj = operator_new(0x10D0);
Stands_ctor(obj, *(void**)(board + 0x4BA8));  // clones spatial tree from parent
*outObj = (int)obj;

This is what the game itself uses for TURRET, MACE, DRAWBRIDGE — objects that need independent instances sharing a mesh.


8. The Mesh Dependency Problem

Problem

Each Board constructor loads specific mesh files into board+0x4xxx slots. The same slot holds different meshes on different levels. The factory's constructors READ these slots to get mesh data.

If a level's constructor didn't load a mesh into the required slot, the slot is NULL. The factory does NOT null-check — it dereferences the pointer directly, causing an access violation (crash).

Solution: Just-In-Time Mesh Injection

The universal ref-loader DLL maintains a private array of all object meshes loaded from disk. When dispatching to a factory:

  1. Before calling the constructor: Save the current values of all board slots the constructor will read
  2. Write the correct mesh pointers from the private array into those slots
  3. Call the constructor (which reads the slots)
  4. Restore the original board slot values (so the level's own objects aren't broken)

Which Objects Need Which Board Slots

Object Required Board Slot (READ) Mesh File Path
BRIDGE (base) +0x436C Level2-Bridge (varies by level)
TIPPER +0x4394 (mesh), +0x4398 (visual) Level3-Tipper
WATERWHEEL +0x4BA8 (static return) Level3-WaterWheel
SWIRL +0x4BC4 (static return) Level3-Swirl
CHOMPER +0x4390 (static return) Meshes\Chomper
WINDMILL +0x437C (collision only) Level4-Windmill
DRAWBRIDGE +0x4370 Level4-Drawbridge
MACE +0x4378 Level4-Mace
TURRET +0x43B4 Level4-Turret
CATAPULT +0x5848 Level4-Catapult
BREAKBRIDGE1 +0x5410 Level10-Bridge1
BREAKBRIDGE2 +0x5414 Level10-Bridge2
POPCYLINDER +0x5420 Level9-PopCylinder1
BLOCKDAWG1 +0x5840 Level8-Blockdawg1
BLOCKDAWG2 +0x5844 Level8-Blockdawg2
GLUEBIE +0x607C Level3-Gluebie
LOOPER +0x436C LevelImpossible-Looper
GEAR +0x4370 Gear
BIGGEAR +0x4374 BigGear
ROTATOR +0x4378 Rotator
PENDULUM +0x437C Pendulum
SPINNY +0x47E0 Level8-Spinny
LIFTER (Up) +0x4784 LevelUp-Lifter
SPEEDCYLINDER +0x4788 LevelUp-SpeedCylinder
TIMEBUTTON +0x478C LevelUp-Button
BRIDGE-Expert +0x4378 Level5-Bridge
BONK (self-loads levels\level5-bonk) Level5-Bonk
FAN (none — procedural)
SAWBLADE (none)
JUDGE (none)
BELL (none)
TRAPDOOR (none)

Objects That DON'T Need Pre-Loaded Meshes

These objects work immediately without any mesh injection:

  • BONK — self-loads levels\level5-bonk via MeshWorld_ctor inside Bonk_ctor
  • FAN — procedural animation, no mesh at all
  • SAWBLADE — self-contained (no mesh read)
  • JUDGE — no mesh read (calls Level_ctor base)
  • BELL — no mesh read
  • TRAPDOOR — no mesh dependency

9. The Difficulty Gate Problem

Problem

Some objects are gated behind App+0x23C != 0:

  • TIPPER — only created when App+0x23C != 0
  • BONK — only created when App+0x23C != 0
  • BLOCKDAWG1/2 — only created when App+0x23C != 0
  • GLUEBIE — only created when App+0x23C != 0
  • FAN — only created when App+0x23C != 0
  • SAWBLADE — only created when App+0x23C != 0

What App+0x23C Is

App+0x23C is a difficulty enum (not a simple boolean):

  • 0 = Pipsqueak (easiest) — gated objects don't spawn
  • 1 = Normal — gated objects spawn
  • 2 = Frenzied/Hard — gated objects spawn

The absolute address is 0x4FD8B4 (App singleton at 0x4FD680 + 0x23C).

Solution

The DLL temporarily sets App+0x23C = 1 during factory dispatch for gated objects, then restores the original value afterward:

int originalDiff = *(int*)(app + 0x23C);
*(int*)(app + 0x23C) = 1;  // force Normal difficulty
// ... call factory ...
*(int*)(app + 0x23C) = originalDiff;  // restore

10. The Universal Ref Loader Design (Option B)

Overview

A bass.dll proxy that hooks the vtable[33] dispatch at 0x0040C4BA and replaces it with a universal factory that:

  1. Tries all 13 Arena factories in sequence (each factory's __strnicmp will match its own refs)
  2. For each factory, injects the required meshes into board slots before calling
  3. For Category 2 objects (WATERWHEEL, etc.), clones the mesh instead of returning the same pointer
  4. For difficulty-gated objects, temporarily sets App+0x23C = 1
  5. Restores all board slots and App+0x23C after the factory returns

Hook Point

; Original at 0x0040C4BA:
call [eax + 0x84]  ; board->vtable[33](refName, &outObj, &outCol, refEntry)

; DLL patches this to:
jmp universal_factory_dispatch

Universal Factory Dispatch (Pseudocode)

void universal_factory_dispatch(
    Board* board, const char* refName, 
    void** outObj, void** outCol, RefEntry* refEntry)
{
    // 1. Try the original level's factory first
    original_factory(board, refName, outObj, outCol, refEntry);
    if (*outObj != NULL) {
        // Original factory handled it — check if it's a static-mesh return
        if (is_static_mesh_object(refName)) {
            // Clone it for multi-instance support
            *outObj = Level_CloneTree(*outObj, board);
        }
        return;
    }
    
    // 2. Original factory didn't handle it — try all other factories
    for (int i = 0; i < NUM_FACTORIES; i++) {
        FactoryFunc* factory = factories[i];
        
        // Check if this factory handles the ref name
        if (!factory_handles_ref(factory, refName))
            continue;
        
        // Get the mesh slots this factory needs
        int* slots = get_required_slots(factory);
        
        // Save original slot values
        void** saved = save_board_slots(board, slots);
        
        // Inject correct meshes
        inject_meshes(board, slots);
        
        // Handle difficulty gate
        int savedDiff = *(int*)(app + 0x23C);
        if (is_difficulty_gated(refName))
            *(int*)(app + 0x23C) = 1;
        
        // Call the factory
        factory(board, refName, outObj, outCol, refEntry);
        
        // Restore slots and difficulty
        restore_board_slots(board, slots, saved);
        *(int*)(app + 0x23C) = savedDiff;
        
        if (*outObj != NULL) {
            // Check for static-mesh return
            if (is_static_mesh_object(refName)) {
                *outObj = Level_CloneTree(*outObj, board);
            }
            return;
        }
    }
    
    // No factory handled it — return NULL (object not created)
    *outObj = NULL;
}

Private Mesh Array

At board constructor time (hooked via a separate hook), the DLL loads all 46 object meshes from disk into a private array:

struct MeshEntry {
    const char* refName;    // "WATERWHEEL", "CATAPULT", etc.
    const char* meshPath;   // "Levels\\Level3-WaterWheel", etc.
    void* mesh;              // loaded MeshWorld*
    void* collision;         // loaded CollisionLevel*
    int boardSlot;           // which board+0x4xxx slot to inject into
};

MeshEntry meshDatabase[] = {
    {"CATAPULT",    "Levels\\Level4-Catapult",    NULL, NULL, 0x5848},
    {"TIPPER",      "Levels\\Level3-Tipper",       NULL, NULL, 0x4394},
    {"WATERWHEEL",  "Levels\\Level3-WaterWheel",   NULL, NULL, 0x4BA8},
    // ... all 46 entries
};

11. Clone-on-Return for Static Mesh Objects (Option A)

Which Objects Need Cloning

Object Board Slot Why It Needs Cloning
WATERWHEEL +0x4BA8 Factory returns mesh pointer directly, no alloc
SWIRL +0x4BC4 Factory returns mesh pointer directly, no alloc
CHOMPER +0x4390 Factory returns MeshNode pointer, no alloc
WINDMILL +0x437C Factory returns CollisionLevel, no alloc
PILLAR (static) Returns static mesh + collision
BRIDGE (base) +0x436C Factory configures existing mesh, no alloc

Clone Implementation

// Called after the factory returns, if the ref is a Category 2 object
void* clone_static_mesh(void* originalMesh, Board* board) {
    // Level_CloneTree is at 0x466060, thiscall (ECX = originalMesh)
    // It allocates 0x10D0 bytes, copies spatial tree recursively
    typedef void* (__thiscall *CloneTreeFn)(void* this, int param_1);
    CloneTreeFn cloneTree = (CloneTreeFn)0x466060;
    return cloneTree(originalMesh, (int)board);
}

Detection Logic

bool is_static_mesh_object(const char* refName) {
    return _strnicmp(refName, "WATERWHEEL", 10) == 0
        || _strnicmp(refName, "SWIRL", 5) == 0
        || _strnicmp(refName, "CHOMPER", 7) == 0
        || _strnicmp(refName, "WINDMILL", 8) == 0
        || _strnicmp(refName, "PILLAR", 6) == 0
        || _strnicmp(refName, "BRIDGE", 6) == 0;  // base BRIDGE, not Expert
}

12. Complete Object Dependency Table

All 46 Verified Ref Types (from MESHWORLD binary parsing)

Object Factory Alloc Size Constructor Board Mesh Slot (READ) AthenaList(s) Difficulty Gate Multi-Instance? Needs Clone?
BRIDGE (base) CreateLevelObjects N/A None (configures mesh) +0x436C No NO YES
TIPPER CreateLevelObjects 0x1104 Tipper_ctor +0x4394, +0x4398 +0x2578 YES YES No
BONK CreateLevelObjects/CreateExpertLevelObjects 0x1200 Bonk_ctor (self-loads) +0x2578 YES YES No
BREAKBRIDGE1 CreateLevelObjects 0x1100 BreakBridge_ctor +0x5410 +0x2578 No YES No
BREAKBRIDGE2 CreateLevelObjects 0x1100 BreakBridge_ctor +0x5414 +0x2578 No YES No
POPCYLINDER CreateLevelObjects 0x10E8 PopCylinder_ctor +0x5420 +0x2578, +0x5428 No YES No
BLOCKDAWG1 CreateLevelObjects 0x1154 Blockdawg_ctor +0x5840 +0x2578 YES YES No
BLOCKDAWG2 CreateLevelObjects 0x1154 Blockdawg_ctor +0x5844 +0x2578 YES YES No
CATAPULT CreateLevelObjects 0x1108 Catapult_ctor +0x5848 +0x2578, +0x584C No YES No
GLUEBIE CreateLevelObjects 0x110C Gluebie_ctor +0x607C +0x6080, +0x2578 YES YES No
LIFTER CreateUpLevelObjects/CreateLifter 0x10F4 Rotator_ctor_sound +0x4784 or +0x47E0 +0x2578 No YES No
SPEEDCYLINDER CreateUpLevelObjects 0x150C Pendulum_ctor +0x4788 +0x2578 No YES No
TIMEBUTTON CreateUpLevelObjects 0x10E8 Rotator_ctor_nosound +0x478C +0x2578 No YES No
LOOPER CreateMechanicalObjects 0x1500 Looper_ctor +0x436C +0x2578 No YES No
GEAR CreateMechanicalObjects 0x1514 Gear_ctor +0x4370 +0x2578 No YES No
BIGGEAR CreateMechanicalObjects 0x1514 Gear_ctor +0x4374 +0x2578 No YES No
ROTATOR CreateMechanicalObjects 0x1508 Rotator_ctor +0x4378 +0x2578 No YES No
PENDULUM CreateMechanicalObjects 0x1504 Pendulum_ctor +0x437C +0x2578 No YES No
FAN CreateExpertLevelObjects 0x1188 TowerLevel_Ctor (none) +0x2578 YES YES No
SAWBLADE CreateExpertLevelObjects 0x111C Sawblade_Level_Ctor (none) +0x2578 YES YES No
BRIDGE-Expert CreateExpertLevelObjects 0x10FC Spinner_Level_ctor +0x4378 +0x2578 (conditional) No YES No
JUDGE CreateExpertLevelObjects 0x1100 Gear_Level_ctor (none) +0x4BBC No YES No
BELL CreateExpertLevelObjects 0x10E8 Tipper_Level_Ctor (none) +0x2578 No YES No
DRAWBRIDGE CreateTowerObjects 0x113C Glass_Level_ctor +0x4370 +0x2578 No YES No
MACE CreateTowerObjects 0x110C (0x438750) +0x4378 +0x2578 No YES No
TRAPDOOR CreateTowerObjects 0x10F8 (0x438290) (none) +0x2578 No YES No
TURRET CreateTowerObjects 0x10D0 Stands_ctor +0x43B4 (vtable) No PARTIAL No
CHOMPER CreateTowerObjects NONE NONE (static) +0x4390 No NO YES
WINDMILL CreateTowerObjects N/A CollisionLevel only +0x437C No NO YES
WATERWHEEL CreateDizzyObjects NONE NONE (static) +0x4BA8 No NO YES
SWIRL CreateDizzyObjects NONE NONE (static) +0x4BC4 No NO YES
SPINNY CreateSpinny/CreateMechanicalObjects 0x1508 Rotator_ctor +0x47E0 +0x2578 No YES No
SMASHER1 CreateWobblyObjects +0x2578 No YES No
SMASHER2 CreateWobblyObjects +0x2578 No YES No
WAVY CreateOddObjects +0x2578 No YES No
WOBBLY CreateOddObjects/CreateUpObjects GameLevel_ctor +0x2578 No YES No
PILLAR CreateSkyObjects N/A NONE (static) (static) No NO YES
SIGN-TARPIT (separate dispatch) No YES No
TARBUBBLE CreateUpLevelObjects No YES No
LAUNCH (separate dispatch) No YES No
MOUSETRAP (separate dispatch) No YES No
NEONPLATFORM CreateNeonObjects +0x4374 +0x2578 No YES No
DFLOOR1-4 CreateNeonObjects +0x4378-0x4384 +0x2578 No YES No
TRODE CreateNeonObjects +0x4388 +0x2578 No YES No
FLICKNING CreateNeonObjects +0x2578 No YES No
POPCYLINDER (Sky) CreateSkyObjects 0x10E8 PopCylinder_ctor +0x5420 +0x2578, +0x5428 No YES No

13. Complete vtable[33] Mapping

See Section 4 above for the full Race and Arena vtable[33] mapping tables.


14. Complete Board Slot → Mesh File Mapping

Board Offset Type Description Used By Levels
+0x2578 AthenaList Active game objects list (ALL objects) All factories
+0x436C MeshWorld* Bridge/Tipper/Catapult/Spinny/Looper mesh (varies) Dizzy, Tower, Expert, Master, Impossible
+0x4370 CollisionLevel*/MeshWorld* Collision for +0x436C / or secondary mesh Dizzy, Tower, Expert, Master
+0x4374 MeshWorld* Gluebie (Dizzy) / 2PBridge (Master) / Fallout (Toob) Dizzy, Master, Toob
+0x4378 AthenaList*/MeshWorld* Dizzy: AthenaList / Tower: Mace / Expert: Bridge / Toob: Blockdawg1 Dizzy, Tower, Expert, Toob
+0x437C CollisionLevel*/MeshWorld* Dizzy: collision / Tower: Windmill / Expert: collision Dizzy, Tower, Expert
+0x4380 AthenaList Expert: Spinner bridge list 1 Expert
+0x4388 float Master: 0x42340000 (40.0f) Master
+0x438C Vec3List[8] Toob: vector array (0x418 × 8) Toob
+0x4390 MeshNode* Tower: Chomper visual mesh Tower
+0x4394 MeshWorld* Tipper mesh Dizzy, Master
+0x4398 CollisionLevel* Tipper visual/collision Dizzy, Master
+0x439C Vec3List[4] Master: vector array (0x418 × 4) Master
+0x43A0-A8 float[3] Tower: zero-init (0, 0, 0) Tower
+0x43B4 MeshWorld* Tower: Turret mesh Tower
+0x43B8 AthenaList Tower: object list Tower
+0x4784 MeshWorld* Up: Lifter mesh Up
+0x4788 MeshWorld* Up: SpeedCylinder mesh Up
+0x478C MeshWorld* Up: TimeButton mesh Up
+0x4790 AthenaList Dizzy: secondary list Dizzy
+0x4798 AthenaList Expert: Spinner bridge list 2 Expert
+0x47D0 AthenaList Tower: list 2 Tower
+0x47E0 MeshWorld* Spinny/Gear/Lifter mesh Impossible, Spinny, Lifter
+0x47E4 AthenaList Tower Arena: list Tower Arena
+0x4BA8 MeshWorld* Dizzy: WaterWheel mesh Dizzy
+0x4BAC CollisionLevel* Dizzy: WaterWheel collision Dizzy
+0x4BC4 MeshWorld* Dizzy: Swirl mesh Dizzy
+0x4BC8 CollisionLevel* Dizzy: Swirl collision Dizzy
+0x4BE8 AthenaList Tower: list 3 Tower
+0x4FD4 void* Expert: BELL object pointer Expert
+0x5000 AthenaList Tower: list 4 Tower
+0x540C void* Bonk object pointer Dizzy, Master
+0x5410 MeshWorld* BreakBridge1 mesh Master
+0x5414 MeshWorld* BreakBridge2 mesh Master
+0x5418 void* BreakBridge1 object pointer Master
+0x541C void* BreakBridge2 object pointer Master
+0x5420 MeshWorld* PopCylinder mesh Master, Sky
+0x5424 MeshWorld* PopCylinder2 mesh (Master only) Master
+0x5428 AthenaList PopCylinder list Master
+0x5840 MeshWorld* BlockDawg1 mesh Master, Toob
+0x5844 MeshWorld* BlockDawg2 mesh Master, Toob
+0x5848 MeshWorld* Catapult mesh Master
+0x584C AthenaList Catapult list Master
+0x607C MeshWorld* Gluebie mesh Master
+0x6080 AthenaList Gluebie list Master
+0x868 char* Board display name string All
+0x870 int Race ID from App+0x14+0x1DC All
+0x878 App* App pointer All
+0x8AC Scene* Scene pointer All
+0x4344 char* Display/theme string All

15. Key Function Addresses

Address Function Description
0x40C430 Scene_CreateDynamicObjects Iterates MESHWORLD ref points, calls factory
0x40C4BA (call site) vtable[33] dispatch call inside Scene_CreateDynamicObjects
0x40C5D0 N:/E: Handler Handles N: and E: prefixed ref names
0x4133E0 BaseFactory Base race factory (PLATFORM, STANDS)
0x419750 NoOpFactory Warm-up/Beginner Arena factory (no-op)
0x40A550 CreateBeginnerObjects Beginner Arena factory
0x40A5F0 CreateDizzyObjects Dizzy Arena factory
0x40D7C0 CreateTowerObjects Tower Arena factory
0x4117B0 CreateUpLevelObjects Up Arena factory
0x416910 CreateNeonObjects Neon Arena factory
0x40E250 CreateExpertLevelObjects Expert Arena factory
0x40EC40 CreateOddObjects Odd Arena factory
0x40FB30 CreateToobObjects Toob Arena factory
0x40F420 CreateGlassObjects Glass Arena factory
0x40AD80 CreateWobblyObjects Wobbly Arena factory
0x410AD0 CreateSkyObjects Sky Arena factory
0x4121D0 CreateLevelObjects Master Arena factory (most inclusive)
0x417FE0 CreateMechanicalObjects Impossible Arena factory
0x4143D0 CreateSpinny Dizzy Race factory
0x414680 CreateTowerRaceObjects Tower Race factory
0x414A20 CreateUpRaceObjects Up Race factory
0x414BD0 CreateExpertRaceObjects Expert Race factory
0x4173B0 CreateNeonRaceObjects Neon Race factory
0x415460 CreateWobblyRaceObjects Wobbly Race factory
0x415A30 CreateSkyRaceObjects Sky Race factory
0x418760 Scene_CreateObject_Gear Impossible Race factory
0x466060 Level_CloneTree Clones a Level/MeshWorld object (thiscall)
0x461510 MeshWorld_ctor Creates MeshWorld from file (alloc + parse)
0x465080 CollisionLevel_ctorWithLevel Creates CollisionLevel from MeshWorld
0x462850 Stands_ctor Base constructor — clones SpatialTree from parent
0x453810 AthenaList_Append Append object to AthenaList
0x453210 AthenaList_Init Initialize an AthenaList
0x4BA57B operator_new Jump to malloc (0x10D0 = MeshWorld size)
0x4C7677 __strnicmp Case-insensitive string compare
0x4FD680 App singleton Global App object
0x4FD8B4 App+0x23C Difficulty enum (0=Pipsqueak, 1=Normal, 2=Frenzied)

16. Implementation Plan

Phase 1: Build the Universal Factory Hook

  1. Create a bass.dll proxy that patches 0x0040C4BA with a JMP to the DLL's dispatch function
  2. The dispatch function intercepts the factory call and tries all 13 Arena factories
  3. For each factory attempt, save/restore board slots and inject meshes

Phase 2: Just-In-Time Mesh Injection

  1. Build a mesh database mapping all 46 ref names → mesh file paths + board slot offsets
  2. At board constructor time, load all meshes from disk into a private array
  3. During factory dispatch, inject the correct mesh into the board slot before calling the constructor

Phase 3: Clone-on-Return for Static Mesh Objects

  1. Detect Category 2 objects (WATERWHEEL, SWIRL, CHOMPER, WINDMILL, PILLAR, BRIDGE-base)
  2. After the factory returns a static mesh pointer, call Level_CloneTree (0x466060) to clone it
  3. Return the clone instead of the original

Phase 4: Difficulty Gate Bypass

  1. Detect difficulty-gated objects (TIPPER, BONK, BLOCKDAWG, GLUEBIE, FAN, SAWBLADE)
  2. Before calling the factory, save App+0x23C and set it to 1
  3. After the factory returns, restore App+0x23C

Phase 5: Crash Test

  1. Compile with MinGW: i686-w64-mingw32-gcc -shared -o bass.dll mod.c -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias
  2. Crash test via hbtestd or Wine/Xvfb: launch game, wait 35s, check if process alive
  3. Copy to mods/universal-ref-loader/ with README

17. Build & Test

Build Command

i686-w64-mingw32-gcc -shared -o bass.dll universal_ref_loader.c \
    -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
    -Wl,--add-stdcall-alias

Crash Test

# Via hbtestd MCP (preferred):
mcp_hbtestd_test_dll_mod(path="universal_ref_loader.zip", target_dll="bass.dll", timeout=10)

# Via Wine/Xvfb:
DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 timeout 35 wine Hamsterball.exe
# Check if process is alive after 35s (catches stack corruption, wrong hook addresses)

Deliverables

  • mods/universal-ref-loader/bass.dll — compiled DLL
  • mods/universal-ref-loader/universal_ref_loader.c — source code
  • mods/universal-ref-loader/README.md — documentation
  • mods/universal-ref-loader/hamsterball-universal-ref-loader.zip — packaged mod
  • Update mods/README.md catalog

Appendix: Existing Related Files

File Description
docs/REF_LOADING_SYSTEM.md Original ref-loading system documentation (needs update with clone findings)
docs/VERIFIED_REFS_BY_LEVEL.md Verified 46 unique ref types from MESHWORLD binary parsing
docs/objects/OBJECT_FACTORY_SYSTEM.md Factory system overview
docs/objects/LEVEL_LOCKED_OBJECTS.md Level-locked objects and their board slot dependencies
docs/objects/TOWER_OBJECT_ANALYSIS.md Dizzy/Tower factory analysis with spawnability verdicts
docs/gameplay/ARENA_HAZARD_SYSTEM.md Arena hazard system (difficulty gating)
analysis/factory_objects_comprehensive.md Comprehensive factory object analysis with all board offsets
analysis/ghidra/decompilations/scene/Scene_CreateDynamicObjects_0040c430.c Full decompilation of the dispatch loop
analysis/ghidra/decompilations/batch_auto/Level_CloneTree_0x00466060.c Full decompilation of the clone function
analysis/ghidra/decompilations/batch_auto/Stands_ctor_0x00462850.c Stands_ctor (base constructor with SpatialTree clone)
mods/universal-ref-loader/ Existing universal ref loader mod (needs rebuild with new design)

🔗 Related Documents

[[35033317086156|universal ref loader]]

types : mods
keywords :

📂 View source on GitHub


Universal Ref Loader — Hamsterball DLL Mod v3

What It Does

This mod patches the game's level-object dispatch system to allow any object ref to be loaded into any level. Normally, each level only loads refs that its own Board vtable[33] factory recognizes (e.g. SpeedCylinder only in Up levels, Bonk only in Expert/Master levels, Gears only in Impossible level).

The mod hooks the vtable[33] dispatch at 0x0040C4BA (inside Scene_CreateDynamicObjects at 0x40C430) and replaces it with a universal factory that:

  1. Tries the original factory first — preserves normal behavior for all existing refs
  2. Falls through to all 13 Arena factories if the original didn't handle the ref
  3. JIT mesh injection — if a factory needs a board mesh slot that's NULL (not loaded by the current level), loads the mesh from disk on the fly
  4. Clones static-mesh objects (WATERWHEEL, SWIRL, BRIDGE-base) via Level_CloneTree for multi-instance support
  5. Bypasses difficulty gates — temporarily sets App+0x23C = 1 (Normal) for gated objects (TIPPER, BONK, BLOCKDAWG, GLUEBIE, FAN, SAWBLADE, MACE)
  6. Safety checks board slots — skips factories whose required mesh slots can't be filled (prevents crashes)

How to Use

  1. Rename original bass.dll to bass_real.dll (for audio passthrough)
  2. Copy this mod's bass.dll to your Hamsterball game directory
  3. Add ref names to MESHWORLD Section 1 in any level file
  4. The mod will try all Arena factories to create the object, loading meshes from disk as needed

JIT Mesh Injection

When a factory reads a board+0x4xxx slot that's NULL (because the current level's Board constructor didn't load that mesh), the mod:

  1. Looks up the board offset in the mesh database (maps offsets → file paths)
  2. Calls MeshWorld_ctor(mem, d3dDevice, path) to load the mesh from disk
  3. Optionally calls CollisionLevel_ctorWithLevel(mem, mesh) for collision
  4. Writes the loaded mesh pointer into the board slot
  5. Calls the factory (which reads the slot)
  6. Restores the original NULL value after the factory returns

Meshes are cached so each file is only loaded once per session.

Mesh Database (verified from decompiled board constructors)

Board Slot Mesh File Collision? Used By
+0x436C Level3-Tipper / Level4-Catapult / LevelImpossible-Looper +0x4370 Dizzy, Tower, Impossible
+0x4370 Level4-Drawbridge / LevelImpossible-Gear Tower, Impossible
+0x4374 Level3-Gluebie / LevelImpossible-BigGear Dizzy, Impossible
+0x4378 Level4-Mace / Level5-Bridge / LevelImpossible-Rotator Tower, Expert, Impossible
+0x437C Level4-Windmill / LevelImpossible-Pendulum Tower, Impossible
+0x4394 Level3-Tipper +0x4398 Master
+0x4BA8 Level3-WaterWheel +0x4BAC Dizzy
+0x4BC4 Level3-Swirl +0x4BC8 Dizzy
+0x4784 LevelUp-Lifter Up
+0x4788 LevelUp-SpeedCylinder Up
+0x478C LevelUp-Button Up
+0x47E0 Level8-Spinny Toob/Odd/Glass
+0x5410 Level10-Bridge1 Master
+0x5414 Level10-Bridge2 Master
+0x5420 Level9-PopCylinder1 Master, Sky
+0x5840 Level8-BlockDawg1 Master, Toob
+0x5844 Level8-BlockDawg2 Master, Toob
+0x5848 Level4-Catapult Master
+0x607C Level3-Gluebie Master

Verified Features

Feature Status Details
Hook point ✅ Verified CALL [EAX+0x84] at 0x0040C4BA, 6 bytes FF 90 84 00 00 00
Factory addresses ✅ Verified All 30 factories (15 Arena + 15 Race) confirmed via vtable[33] reads
Level_CloneTree ✅ Verified __thiscall at 0x466060, allocs 0x10D0, recursive spatial tree clone
MeshWorld_ctor ✅ Verified __thiscall at 0x461510, params: (mem, d3dDevice, path)
CollisionLevel_ctor ✅ Verified __thiscall at 0x465080, params: (mem, sourceMesh)
Board slot offsets ✅ Verified All offsets cross-referenced from decompiled factory code
Difficulty gate ✅ Verified board+0x878 → App+0x23C != 0 confirmed in 7 factory handlers
D3D device path ✅ Verified *(board+0x878) + 0x174 (App+0x174)
BASS proxy ✅ v3 pattern Lazy loader, no DllMain deadlock, stubs if bass_real.dll missing
Crash test ✅ Passed 14.12s runtime, no crash (hbtestd)

Static-Mesh Cloning

Objects like WATERWHEEL and SWIRL return the same board slot pointer for every ref — only one instance renders. This mod calls Level_CloneTree to create independent copies:

Object Board Slot Clone Status
WATERWHEEL +0x4BA8 ✅ Cloned
SWIRL +0x4BC4 ✅ Cloned
BRIDGE (base) +0x436C ✅ Cloned
WINDMILL +0x437C ⚠️ Not cloned (complex: creates CollisionLevel + attaches)

Factory Dispatch Order

Factories are tried in order of inclusiveness (most ref types first):

  1. Expert — BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL (no mesh deps, self-loading)
  2. Wobbly — SMASHER1, SMASHER2 (configuring only)
  3. Master — BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT, GLUEBIE
  4. Tower — CATAPULT, MACE, DRAWBRIDGE, WINDMILL, TRAPDOOR, CHOMPER, TURRET
  5. Impossible — LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
  6. Up — LIFTER, SPEEDCYLINDER, TIMEBUTTON
  7. Dizzy — TIPPER, WATERWHEEL, SWIRL, GLUEBIE
  8. Beginner — BRIDGE (base)
  9. Neon, Odd, Toob, Glass, Sky — level-specific objects

Limitations

  • WINDMILL: Returns static mesh + creates CollisionLevel with attach — not cloned yet (complex multi-object creation).
  • N:/E: prefixed refs: Handled by the original factory's N:/E: handler, not by the universal dispatch.
  • Slot ambiguity: Some board offsets map to different mesh files on different levels (e.g. 0x436C = Tipper on Dizzy but Catapult on Tower). The mesh DB tries all entries; the first that loads successfully is used.

Build

i686-w64-mingw32-gcc -shared -o bass.dll universal_ref_loader.c \
  -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Technical Details

  • Hook point: 0x0040C4BA (CALL [EAX+0x84]CALL universal_factory + NOP)
  • Calling convention: __thiscall (ECX=board), 4 stack args
  • JIT mesh loading: MeshWorld_ctor (0x461510) + CollisionLevel_ctorWithLevel (0x465080)
  • D3D device: *(board+0x878) + 0x174 (App+0x174)
  • BASS proxy: v3 lazy loader pattern (LoadLibraryA on first BASS call, not in DllMain)
  • All addresses verified via GhidraMCP decompilation + vtable memory reads (June 2026)

See docs/UNIVERSAL_REF_LOADER_DESIGN.md for the complete reverse engineering analysis.


🔗 Related Documents

[[37189490095032|reverse engineering playbook]]

types : playbook

📂 View source on GitHub


Reverse Engineering Playbook

A program-agnostic guide for analyzing compiled binaries. The Hamsterball project is used as a running example, but the methodology applies to any compiled program (games, applications, malware, firmware).

Quick Start

# File What it covers
1 00-MINDSET.md How to approach an unknown binary
2 01-TARGETS-AND-TOOLS.md PE, ELF, Mach-O; static vs dynamic tools
3 02-BINARY-RECON.md Strings, imports, exports, sections, entropy
4 03-GHIDRA-SETUP.md Import and auto-analyze any binary
5 04-NAMING-STRATEGY.md How to name unknown functions and data
6 05-DECOMPILATION.md Reading and cleaning decompiled output without distorting it
7 06-CALLING-CONVENTIONS.md cdecl, stdcall, fastcall, thiscall, x64, ARM64
8 07-MEMORY-LAYOUT.md Recovering structs and globals
9 08-OOP-PATTERNS.md Vtables, RTTI, constructors, virtual dispatch
10 09-DYNAMIC-ANALYSIS.md Debuggers, hooking, tracing
11 10-SUBSYSTEM-PATTERNS.md Rendering, input, audio, file formats
12 11-VERIFICATION.md Proving your findings are right
13 12-TROUBLESHOOTING.md Common dead ends and how to escape
14 case-studies/hamsterball.md How this playbook was applied to Hamsterball

Running Example

Throughout this playbook, Hamsterball.exe (PE32, MSVC 2003, D3D8 + DInput8 + BASS) illustrates each step. See the case study for the full project-specific details.


🔗 Related Documents

[[39040799821588|ball object]]

types : objects
keywords :

📂 View source on GitHub


Ball Object — Complete Modder's Reference

Verified via direct Ghidra decompilation of Hamsterball.exe (Athena engine, PE32 i386).
All offsets below were extracted from the live decompiled code via the GhidraMCP headless server.
This document replaces / supplements BALL_OBJECT.md with authoritative, source-verified data.


Quick Stats

Property Value
Primary update function Ball_Update @ 0x00405E00
Constructor Ball_ctor2 @ 0x004039E0
Total struct size 0x0C98 bytes (3,224 bytes)
Physics body (nested) CollisionMesh @ this + 0x1A4 (size 0xCB0)
VTable (Ball) 0x004CF3A0

Table of Contents

  1. Position & Velocity
  2. Ball-Related Functions (Complete List)
  3. What You Can Do With the [[39040799821588|ball object]]
  4. Modding Recipes
  5. Verified Memory Layout (Key Fields)

Position & Velocity

Verified by Ghidra decompilation of Ball_ctor2 (0x004039E0)

// Ball_ctor2 initializes these fields to zero:
*(undefined4 *)((int)this + 0x164) = 0;   // pos_x
*(undefined4 *)((int)this + 0x168) = 0;   // pos_y
*(undefined4 *)((int)this + 0x16c) = 0;   // pos_z
*(undefined4 *)((int)this + 0x170) = 0;   // vel_x ← **VERIFIED**
*(undefined4 *)((int)this + 0x174) = 0;   // vel_y ← **VERIFIED**
*(undefined4 *)((int)this + 0x178) = 0;   // vel_z ← **VERIFIED**

Verified by Ball_ApplyForceWithMultipliers (0x00402650)

// Accumulates directional force into velocity:
*(float *)((int)this + 0x170) = param_1 * param_4 + *(float *)((int)this + 0x170);
*(float *)((int)this + 0x174) = param_2 * param_4 + *(float *)((int)this + 0x174);
*(float *)((int)this + 0x178) = param_3 * param_4 + *(float *)((int)this + 0x178);

Verified by Ball_ApplyForceV2 (0x004016F0)

Same pattern — writes to +0x170/174/178 with identical guards.

Layout check (no gaps, no overlaps)

0x164  float  pos_x
0x168  float  pos_y
0x16C  float  pos_z
0x170  float  vel_x  ← confirmed
0x174  float  vel_y  ← confirmed
0x178  float  vel_z  ← confirmed
0x17C  float  accel_x
0x180  float  accel_y
0x184  float  accel_z
0x188  float  max_speed

✅ The velocity offsets are definitively correct.


Ball-Related Functions

Core Lifecycle

Function Address Purpose
Ball_ctor 0x0040AFE0 Full constructor (calls Ball_ctor2, sets vtable to 0x004CF3A0)
Ball_ctor2 0x004039E0 Base init — all fields zeroed / defaulted
Ball_dtor 0x004027F0 Destructor wrapper (calls Ball_dtor2, then _free)
Ball_dtor2 0x00401CC0 Actual destructor — resets matrix, calls GameObject_dtor
Ball_Update 0x00405E00 Main 23-phase physics tick (param_1 = Ball*)
Ball_FallUpdate 0x00408830 Death-fall physics (out-of-bounds, water, pits)

Physics & Movement

Function Address What It Does
Ball_ApplyForceWithMultipliers 0x00402650 Add force to velocity with impact/speed/ice/dizzy multipliers
Ball_ApplyForceV2 0x004016F0 Alt force app with gravity-plane awareness
CollisionMesh_SetSpeed 0x004029C0 DEAD CODE. Writes +0xC64 (roll_friction) and +0xC98/C9C/CA0 (unused), but physics loop immediately overwrites them. Does NOT control ball speed.
Ball_SetVec3AtOffset 0x00402A20 Write Vec3 to arbitrary offset (modding helper)
Ball_SetTargetPos 0x00402030 Network sync position (lerp toward target)
Ball_SetTrajectory 0x00403850 Launch-pad trajectory setup (stores params at +0x2AC to +0x2B8)
Ball_ApplyTrajectory 0x00403750 Execute launch — applies physics_body trajectory, plays sound, sets impact block
Ball_SetTiltedGravity 0x00403100 Set gravity plane to tilted (normal 0, -1, 0)
Ball_SetFlatGravity 0x00403150 Set gravity plane to flat (normal 0, 0, 1)
Ball_CheckProximity 0x00402150 Distance check — sets +0x744 if within threshold
Ball_FindMeshCollision 0x00403980 Raycast vs level mesh (delegates to Mesh_FindClosestCollision)
Ball_FindClosestRespawnPoint 0x00405190 Full respawn logic — reset collision, free display string, find nearest checkpoint
Ball_TestPlaneIntersection 0x00402810 Frustum culling helper for render shadow
Ball_AdvancePositionOrCollision 0x004564C0 Multiplayer sync position advance

State Changes

Function Address What It Does
Ball_Shrink 0x00402200 odd race E:SHRINK: radius=13.0, max_speed=2.5, play shrink sound
Ball_Grow 0x00402270 odd race E:GROW: radius=26.0, max_speed=5.0 (restores from shrunk)
Ball_ResetCollisionMesh 0x004030B0 Reset physics body orientation, zero velocity, reset timer
Ball_Shatter 0x00408D70 Arena: split ball into 3 AI balls (called from FollowBall_Update, NOT E:JUMP)
Ball_DizzyImmunity 0x00402400 Grant dizzy immunity/score at +0x2F4

Rendering

Function Address What It Does
Ball_Render 0x00402DE0 Full render pass — shadow, particles, sprite quad
Ball_RenderShadow 0x00401920 Render ground shadow decal
Ball_SetupCollisionRender 0x004015B0 Setup collision sounds, render visibility flags
Ball_InitRenderState 0x00402860 One-time D3D render state init (cull mode, texture stages)
Ball_RenderWithMaterial 0x0045D8F0 Render with custom material override
Ball_GetTransform 0x0040AF90 Extract transform matrix for rendering

Input

Function Address What It Does
Ball_GetInputForce 0x0046EC30 Read input device, output 2D force vector (keyboard DIK codes at InputDevice+0x50C/510/514/518)

Misc

Function Address What It Does
Ball_SetName 0x00401660 Allocate & copy display name string to +0xC28
Ball_CreateTrailParticles 0x00401DD0 Spawn sparkle trail effect at ball position

What You Can Do With the Ball Object

1. Read/Write Velocity (Instant Boosts)

// Direct velocity manipulation — bypasses all engine multipliers
float* ball = (float*)ball_ptr;
ball[0x170/4] = 5000.0f;  // X velocity
ball[0x174/4] = 0.0f;     // Y velocity
ball[0x178/4] = 0.0f;     // Z velocity

Verified source: Ball_ApplyForceWithMultipliers @ 0x00402650 reads/writes these exact offsets.

2. Read/Write Position (Teleport)

ball[0x164/4] = 100.0f;   // X position
ball[0x168/4] = 50.0f;    // Y position
ball[0x16c/4] = 200.0f;   // Z position

Note: Also set teleport field at +0xC3C to prevent physics from overriding it on the next frame.

3. Change Ball Radius

// Ball_Shrink sets radius = 13.0f (0x41500000)
// Ball_Grow sets radius = 26.0f (0x41D00000)
// Normal radius = 27.0f (0x41D80000) — set in Ball_ctor2
*(float*)(ball + 0x284) = 50.0f;  // Giant ball

4. Change Max Speed

// Ball_ctor2 default: 0x459C4000 (~5000.0f)
// Ball_Shrink: 0x40200000 (2.5f)
// Ball_Grow: 0x40A00000 (5.0f)
*(float*)(ball + 0x188) = 10000.0f;  // Super speed

5. Force Death-Fall State

Ball_Shrink(ball_ptr);  // @ 0x00402200
// Sets: +0xC4C = 1 (is_shrunk), radius=13.0, max_speed=2.5, plays sound

6. Force Split Power-Up

Ball_Shatter(ball_ptr, some_param);  // @ 0x00408D70
// Guard: checks *(char*)(ball + 0x324) == 0 (not already split)

7. Apply Launch Pad Boost

// First set trajectory vector on physics body:
*(float*)(*(int*)(ball + 0x1A4) + 0xCA4) = dir_x;
*(float*)(*(int*)(ball + 0x1A4) + 0xCA8) = dir_y;
*(float*)(*(int*)(ball + 0x1A4) + 0xCAC) = dir_z;
Ball_ApplyTrajectory(ball);  // @ 0x00403750
// Damps Y by 0.7x, normalizes, scales by 0.01, sets impact block = 100 frames

8. Change Gravity Plane

Ball_SetTiltedGravity(ball);  // Normal = (0, -1, 0)  @ 0x00403100
Ball_SetFlatGravity(ball);    // Normal = (0, 0, 1)     @ 0x00403150
Ball_ResetCollisionMesh(ball); // Reset orientation        @ 0x004030B0

9. Check If Ball Is Falling

char is_shrunk = *(char*)(ball + 0xC4C);
// Set by Ball_Shrink (→1), cleared by Ball_Grow (→0)

10. Read Ball Speed Scale

float speed_scale = *(float*)(ball + 0x18C);  // default 1.0f
// Affects all velocity calculations in Ball_Update

11. Read Player Index

int player_idx = *(int*)(ball + 0x18);
// -1 = no player (AI / demo), 0-3 = human player index

Modding Recipes

Recipe 1: Permanent Super Speed

Hook Ball_ctor2 (0x004039E0), after the line:

*(undefined4 *)((int)this + 0x188) = 0x459c4000;  // max_speed = 5000

Change 0x459c4000 to 0x461C4000 (10000.0f).

Recipe 2: Giant Ball Mode

In your mod DLL, every frame after Ball_Update returns:

*(float*)(ball + 0x284) = 100.0f;  // radius

The collision mesh (at ball + 0x1A4) must also be scaled — call CollisionMesh_SetRadius or patch the mesh directly.

Recipe 3: Zero-Gravity Mode

Patch Ball_Update to skip gravity accumulation. The gravity vector is at +0x1A8 (default 0, 1.0, 0).

*(float*)(ball + 0x1A8) = 0.0f;  // gravity_x
*(float*)(ball + 0x1AC) = 0.0f;  // gravity_y
*(float*)(ball + 0x1B0) = 0.0f;  // gravity_z

Recipe 4: Invincibility (No Death-Fall)

Hook Ball_Shrink (0x00402200) to return immediately:

xor eax, eax
retn 4

Recipe 5: Always-Split Power-Up

Hook the guard check in Ball_Shatter (0x00408D70):

; NOP out the "if (*(char*)(this+0x324) == 0)" check
nop
nop
nop
nop
nop
nop

Verified Memory Layout (Key Fields)

Extracted directly from Ball_ctor2 decompilation @ 0x004039E0:

Offset Type Initial Value Description
+0x00 vtable 0x004CF314 GameObject vtable (ctor2 sets base vtable)
+0x04 byte[2] 0, 0 collision_result flags
+0x0C int 200 string_timer (ms)
+0x10 void* param_1+0x878 App pointer (scene's app ref)
+0x14 void* param_1 Scene pointer
+0x18 int -1 player_index (-1 = none, 0-3 = player)
+0x1C UITimer Timer object (size 0xEC)
+0x108 Timer Main timer
+0x150 int 0 accumulated_time
+0x154 int RNG random seed
+0x158 Vec3 0,0,0 prev_pos
+0x164 float 0 pos_x
+0x168 float 0 pos_y
+0x16C float 0 pos_z
+0x170 float 0 vel_x ← verified
+0x174 float 0 vel_y ← verified
+0x178 float 0 vel_z ← verified
+0x17C float 0 accel_x
+0x180 float 0 accel_y
+0x184 float 0 accel_z
+0x188 float 0x459C4000 max_speed (~5000.0f)
+0x18C float 0x3F800000 speed_scale (1.0f)
+0x190 float facing_angle (computed)
+0x194 float 0xBF800000 (-1.0f)
+0x198 float spin_angle
+0x1A0 float 0x3F800000 (1.0f)
+0x1A4 void* new CollisionMesh Physics body pointer
+0x1A8 Vec3 0, 1.0, 0 gravity vector
+0x1B8 RenderContext Primary render context
+0x208 RenderContext Secondary render context
+0x260 byte 0 boost_active
+0x264 ArenaBoard Rumble timer (0x14 bytes)
+0x278 float 0x3DCCCCCD (0.1f)
+0x27C int 0
+0x281 byte 1 unused_init_flag (DEAD: set by ctor, never read by any function)
+0x284 float 0x41D80000 radius (27.0f)
+0x290 int 0 spin_timer
+0x2A4 float 0x40A00000 (5.0f)
+0x2A8 Vec3 0,0,0 accel vector
+0x2B8 Vec3 0,0,0 another vector (trajectory?)
+0x2C0 Vec3 0,0,0 checkpoint position
+0x2CC byte 0 block_input
+0x2D4 byte 0
+0x2D5 byte 0
+0x2D8 int 0
+0x2DC int 0 checkpoint index
+0x2E8 byte 0 event_flag
+0x2E9 byte 0 ⚠ impact_shatter (NOT on_ramp! Sticky limit/trajectory, never cleared in Ball_Update)
+0x2EC int 0 current_score
+0x2F0 int 0 impact_counter (frames force is blocked)
+0x2F4 int 0 best_score
+0x2F8 byte 1 alive_flag (set 0 on death)
+0x2F9 byte 0 disabled_flag
+0x300 int 0
+0x310 byte 1 can_collide
+0x314 int 0
+0x318 int 0
+0x31C byte 0
+0x324 byte 0 is_split (split power-up active)
+0x328 int -1
+0x32C AthenaList Linked list #1
+0x744 int 0 proximity_counter
+0x748 int 0 gravity_plane_type (0=tilted, 1=flat, 2=?)
+0x764 float 1.0f
+0x768 byte 1 visible
+0x769 byte 0
+0x76A byte 0 has_target_pos (network sync)
+0x76C Vec3 0,0,0 target_pos (network)
+0x808 int 0 freeze_timer
+0xC28 char* 0 display_name string
+0xC3C byte 0 teleport_flag
+0xC4C byte 0 is_shrunk (runtime)
+0xC50 float 0 fall_depth
+0xC54 int 0 sound_channel
+0xC58 byte 0
+0xC5C int 0 dizzy_flag
+0xC60 float 1.0f scale_factor
+0xC64 float 0 roll_friction (overwritten every frame by physics loop)
+0xC74 int 0 render_alpha
+0xC80 byte 0 has_viewport_clip
+0xC88 int[4] viewport clip rect
+0xC98 End of struct

How to Use This Document

  1. Attach a debugger (Cheat Engine, x64dbg) to Hamsterball.exe.
  2. Find the Ball pointer: In Ball_Update (0x00405E00), the first argument in ECX is this.
  3. Read/write fields using the offsets above.
  4. Hook functions by patching the vtable at 0x004CF3A0 or placing JMP hooks at the function addresses.

Sources

All data verified via live GhidraMCP headless decompilation (http://127.0.0.1:8089/decompile_function) on Hamsterball.exe:

  • Ball_ctor2 @ 0x004039E0 — struct layout, initial values
  • Ball_ApplyForceWithMultipliers @ 0x00402650 — velocity write pattern
  • Ball_ApplyForceV2 @ 0x004016F0 — velocity write pattern (alt)
  • Ball_Shrink @ 0x002200odd race shrink state (E:SHRINK)
  • Ball_Grow @ 0x00402270odd race grow recovery (E:GROW)
  • Ball_Update @ 0x00405E00 — main physics tick overview
  • Ball_Shatter @ 0x00408D70 — arena 8-ball split mechanic (called from FollowBall_Update)
  • Ball_GetInputForce @ 0x0046EC30input system integration
  • Ball_SetTrajectory / Ball_ApplyTrajectory — launch pad mechanics

Document generated: 2026-06-05
Method: Ghidra decompilation + cross-reference with ball_struct.h
Confidence: High — all offsets verified against primary source code


🔗 Related Documents

[[39687043557968|audio system]]

types : audio
keywords :

📂 View source on GitHub


Audio System — Deep Documentation

Architecture Overview

Hamsterball uses a dual-API audio system:

  1. BASS library (un4seen.com) — music playback (.mo3/.xm/.it tracker modules)
  2. DirectSound8 — sound effects with 3D positional audio

Both systems are managed through the SoundDevice object stored at App+0x178 (offset 0x84C from SoundDevice's App pointer).

SoundDevice Structure

The SoundDevice (vtable at 0x4D911C) is the central audio manager:

Offset Size Type Description
+0x000 4 vtable* SoundDevice vtable
+0x004 4 App* Back-pointer to App
+0x008 4 int Sound entry list count (AthenaList)
+0x00C 4 int Sound entry list capacity
+0x010 4 int Next channel index (circular allocator)
+0x414 4 int** Sound entry array pointer
+0x838 4 float Global sound volume (default 1.0, registry "Sound Volume")
+0x83C 1 bool Sound enabled flag
+0x84C 4 IDirectSound8* DirectSound8 COM interface
+0x850 4 int Listener count
+0x854 varies Vec3[] Listener position array (12 bytes each: X, Y, Z)
+0x914 4 float Min rolloff distance
+0x918 4 float Max rolloff distance
+0x107*4 AthenaList Secondary sound buffer list

Sound Loading Pipeline

Entry Point: Sound_LoadOggOrWav (0x459660)

Sound_LoadOggOrWav(filename):
  1. Try "%s.ogg" — check file access
  2. If .ogg exists → Sound_LoadOgg()
  3. Else try "%s.wav" — check file access
  4. If .wav exists → SoundList_LoadWAV()

The game prefers OGG Vorbis over WAV, always trying .ogg first.

SoundList_LoadWAV (0x458F40)

Manual WAV file parser — does NOT use DirectSound helpers:

  1. Open file, read entire contents into memory
  2. Validate "RIFF" + "WAVE" headers (4-char string compare)
  3. Parse WAV format chunks: sample rate, channels, bits per sample
  4. Scan for "data" chunk by iterating through file
  5. Create IDirectSound8 buffer (WAVEFORMATEX + DSBUFFERDESC):
    • DSBUFFERDESC.dwSize = 0x24
    • DSBUFFERDESC.dwFlags = 0x82 (DSBCAPS_STATIC | DSBCAPS_CTRLVOLUME)
    • DSBUFFERDESC.dwBufferBytes = data_chunk_size
  6. Lock buffer (DSLock), copy PCM data, unlock
  7. Create SoundEntry (12 bytes: vtable + App* + buffer_handle) per DirectSound buffer
  8. Append to SoundList's AthenaList
  9. For multi-buffer sounds (streaming): create additional buffers with duplicate entries

Sound_LoadOgg (0x459310)

  1. Open .ogg file with CRT_fsopen("rb")
  2. Parse OGG Vorbis headers (channels, sample rate, bits)
  3. Compute buffer size: channels × bits/8 × sample_count
  4. Create IDirectSound8 buffer with computed format
  5. Decode OGG stream into DirectSound buffer (read loop)
  6. Create SoundEntry entries, append to AthenaList
  7. For multi-buffer sounds: duplicate entries for streaming segments

SoundEntry Structure (12 bytes)

Offset Size Type Description
+0x000 4 vtable* SoundEntry vtable (scalar dtor at 0x4D8E78)
+0x004 4 App* Back-pointer to App
+0x008 4 handle DirectSound buffer handle

Sound Playback Pipeline

Channel Management

Sound_GetNextChannel (0x459810) — Circular allocator:

Sound_GetNextChannel(SoundList):
  index = list.next_index
  if index < list.count:
    entry = list.entries[index]
    list.next_index = index + 1
    if entry != NULL: return entry
  // Wrap around
  list.next_index = 0
  if list.count > 0:
    return list.entries[0]
  return 0

3D Positional Audio

Sound_CalculateDistanceAttenuation (0x466750):

Sound_CalculateDistanceAttenuation(this, x, y, z) → float (0.0-1.0):
  // Find nearest listener
  best_dist = FLT_MAX
  for each listener in this.listeners:
    dist = sqrt((x-lx)² + (y-ly)² + (z-lz)²)
    if dist < best_dist: best_dist = dist
  
  // Linear rolloff between min and max distances
  if best_dist <= this.min_rolloff:
    return 1.0  // Full volume
  elif best_dist >= this.max_rolloff:
    return 0.0  // Silent
  else:
    range = max_rolloff - min_rolloff
    if range == 0.0: range = 1.0  // Avoid divide-by-zero
    return 1.0 - (best_dist - min_rolloff) / range

Key constants:

  • _DAT_004d9120 = FLT_MAX (initial distance for comparison)
  • _DAT_004cf368 = 0.0 (zero/silence)
  • _DAT_004cf310 = 1.0 (full volume)

Sound_Play3D (0x459860):

Sound_Play3D(this, x, y, z):
  Sound_CalculateDistanceAttenuation(this->device, x, y, z)
  Sound_PlayChannel(this)

Sound_PlayChannel (0x4597B0):

Sound_PlayChannel(SoundList):
  if !app || !device || volume == 0.0: return
  // Get next channel from circular list
  channel = next_entry_from_list
  if channel: Sound_StartSample(channel)
  else: wrap to beginning, try first entry

Sound_StartSample (0x4595B0):

Sound_StartSample(entry):
  if entry->app->sound_enabled:
    Reset sample via vtable(+0x48)       // IDirectSoundBuffer::SetCurrentPosition(0)
    Set volume via vtable(+0x3C)         // IDirectSoundBuffer::SetVolume(clamped)
    Play via vtable(+0x30, 0, 0, 0)     // IDirectSoundBuffer::Play(0, 0, 0)

Sound_InitChannels (0x434580)

Used during game object initialization (e.g. sawblades):

Sound_InitChannels(this, randomize_pitch):
  this.sound_flag = 0
  this.channel_1 = Sound_GetNextChannel(this->app->sound_ids_0x4AC)
  this.channel_2 = Sound_GetNextChannel(this->app->sound_ids_0x4B0)
  if channel_1: Sound_StartSample(channel_1)
  this.pitch = 0x140 (320 decimal = base pitch/frequency)
  if channel_2: Sound_Play3DAtPosition(channel_2)
  if channel_1: Sound_Play3DAtPosition(channel_1)
  if randomize_pitch:
    this.float_pitch = RNG_Rand(0, 25)  // Random 0-25
  this.sound_playing = 0

Music System (BASS)

MusicPlayer Structure

Offset Size Type Description
+0x000 4 vtable* MusicPlayer vtable
+0x004 varies Internal state
+0x008 4 HMUSIC BASS music handle
+0x424 varies char[] Path buffer (filepath copy)

LoadMusicFile (0x46A020)

LoadMusicFile(this, filepath):
  strcpy(this+0x424, filepath)
  handle = BASS_MusicLoad(0, filepath, 0, 0, BASS_MUSIC_PRESCAN, 0)
  this->handle = handle
  if handle != 0:
    return (1 << 24) | handle  // Success
  else:
    BASS_ErrorGetCode()
    Window_Notify("Music Initialization Error: %s")
    return 0

BASS_MusicLoad flags: 4 = BASS_MUSIC_PRESCAN (pre-scan for accurate length/sync)

LoadJukebox (0x46A4D0)

Parses jukebox.xml for song definitions:

LoadJukebox(filename):
  FileHandle_Open(filename)
  while tag = MWParser_ReadTag():
    if tag.name == "SONG":
      name = ""
      hex_id = 0
      while inner_tag = MWParser_ReadTag():
        if inner_tag.name == "NAME": name = inner_tag.value
        if inner_tag.name == "HEX": hex_id = sscanf(value, "%x")
      RegKeyList_AppendStr(name, hex_id)

Example jukebox.xml:

<SONG>
  <NAME>music_mo3</NAME>
  <HEX>1A</HEX>
</SONG>

Audio_StopChannel (0x46A0D0)

Audio_StopChannel(music_player):
  BASS_ChannelStop(music_player->handle)

Music Channels (App Offsets)

Offset Description
App+0x534 MusicPlayer* (BASS music handle + path at +0x424)
App+0x538 music_channel_2 (BASS channel handle)
App+0x53C music_channel_1 (BASS channel handle)

Sound Volume Management

Level_ReadSoundVolume (0x466570)

Level_ReadSoundVolume(SoundDevice):
  RegKey_Open(app->registry)
  if RegKey_ReadString("Sound Volume") exists:
    volume = Registry_ReadFloat("Sound Volume")
    device->volume = volume
  else:
    device->volume = 1.0  // 0x3F800000
  RegKey_Close()

SoundDevice_dtor (0x4668A0)

On destruction:

  1. Save volume to registry: RegKey_WriteDWORD("Sound Volume", device->volume)
  2. Iterate and delete all SoundEntry objects (AthenaList at +0x4)
  3. Iterate and free secondary buffer list (AthenaList at +0x41C)
  4. Release IDirectSound8 interface (COM Release at vtable+8)
  5. Free listener position arrays (Vec3List_Free)

3D Listener System

The 3D listener array enables per-ball positional audio in multiplayer:

  • Listener count at SoundDevice+0x850
  • Each listener is a Vec3 (12 bytes: X, Y, Z) starting at SoundDevice+0x854
  • Updated per-ball in Ball_Update Phase 17 (multiplayer sync)
  • Ball positions are the listener positions for distance calculations

Sound Events During Gameplay

Ball_Update Phases

Phase Sound Action
0 Play ambient sound if ambient_sound_id != 0
12 Ball-ball collision: Sound_Play3D with collision sound
15 Calculate distance attenuation; replay ambient if active

Collision Event Sounds

Event Location Description
E:JUMP DispatchCollisionEvents (0x40C5D0) Jump 3D sound at collision point
N:TARPIT DispatchCollisionEvents Tarpit 3D sound + physics modify
N:MOUSETRAP DispatchCollisionEvents Mousetrap 3D sound
PIPEBONK DispatchCollisionEvents Random from 3 impact sounds
E:CATAPULTBOTTOM TowerCollisionEvents (0x40DCD0) Catapult launch sound
E:OPENSESAME TowerCollisionEvents Door opening sound
N:TRAPDOOR TowerCollisionEvents 3D trapdoor + timer activation
E:MACETRIGGER TowerCollisionEvents Mace activation 3D sound

Key Address Map

Address Function Description
0x458F40 SoundList_LoadWAV Parse WAV → IDirectSound8 buffer
0x459310 Sound_LoadOgg Decode OGG Vorbis → IDirectSound8 buffer
0x4595B0 Sound_StartSample Start IDirectSound8 sample playback
0x459660 Sound_LoadOggOrWav Try .ogg first, fallback .wav
0x4597B0 Sound_PlayChannel Dispatch sound to channel pool
0x459810 Sound_GetNextChannel Circular channel allocator
0x459860 Sound_Play3D 3D positioned sound (attenuation + play)
0x434580 Sound_InitChannels Init game object sound channels
0x466570 Level_ReadSoundVolume Read volume from registry
0x466750 Sound_CalculateDistanceAttenuation Linear 3D rolloff
0x4668A0 SoundDevice_dtor Release buffers, save volume
0x46A020 LoadMusicFile BASS_MusicLoad wrapper
0x46A0D0 Audio_StopChannel BASS_ChannelStop
0x46A4D0 LoadJukebox Parse jukebox.xml song list

Reimplementation Notes (SDL2/SDL_mixer or OpenAL)

Replacement Strategy

  • BASS → SDL_mixer: Load .mo3/.xm/.it via SDL_mixer with mikmod/timidity
  • DirectSound8 → SDL_mixer: Use SDL_mixer channels (Mix_Chunk/Mix_Music)
  • Alternative: OpenAL for true 3D positional audio with proper distance model

Key Differences

  1. Channel allocation: SDL_mixer has built-in channel management (Mix_GroupChannel), similar to circular allocator pattern
  2. 3D audio: SDL_mixer lacks native 3D — approximate with pan/volume or use OpenAL
  3. OGG/WAV loading: Both natively supported by SDL_mixer (Mix_LoadWAV_RW)
  4. Volume control: SDL_mixer uses 0-128 scale; DirectSound uses dB attenuation
  5. Music format: .mo3 tracker modules need mikmod/timidity — SDL_mixer supports these natively

Recommended Architecture

// Audio system (SDL_mixer + OpenAL for 3D)
class SoundDevice {
    std::vector<SoundEntry> entries;
    float volume;           // 0.0-1.0
    float min_rolloff;      // 3D min distance
    float max_rolloff;      // 3D max distance
    std::vector<Vec3> listeners;  // Per-ball positions
    
    SoundEntry* LoadOggOrWav(const char* filename);
    void Play3D(SoundEntry* entry, Vec3 position);
    float CalculateDistanceAttenuation(Vec3 position);
};

class MusicPlayer {
    Mix_Music* handle;
    std::string filepath;
    
    uint32_t LoadMusicFile(const char* filepath);
    void Stop();
};

🔗 Related Documents

[[40451446322523|half_size_all]]

types : mods
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/mods/half_size_all/README.md)


half_size_all

Shrinks the player's ball to half size by inlining Ball_Shrink's physics fields

How It Works

Hooks Scene_SpawnBallsAndObjects at the point where the player ball has just been created and registered. A code cave checks if the ball's player index (ball+0x18) is 0, and if so, writes the same three fields that Ball_Shrink (0x00402200) sets — but without calling the function, so no sound effect plays.

Fields written (identical to Ball_Shrink):

Ball Offset Field Value Effect
+0x284 radius 13.0 (0x41500000) Half visual + collision size
+0x188 physics_scale 2.5 (0x40200000) Half max speed
+0xC4C is_shrunk 1 Shrunk physics state

Only player index 0 is affected. AI balls, split balls, follow balls, and board-init balls remain normal size.

Hook Details

Hook Point Address Original Instruction Catches
Scene_SpawnBallsAndObjects 0x0041C8D7 MOV byte [ESI+0x281], 0 (7 bytes) Player balls (loop, gated on index 0)

The code cave:

  1. CMP dword [ESI+0x18], 0 — is this player 0?
  2. If no → skip to step 4
  3. If yes → write radius=13.0, physics_scale=2.5, unused_init_flag=1 (NOTE: this flag is DEAD code — never read by any function, but we set it anyway to match original behavior)
  4. Execute original MOV byte [ESI+0x281], 0
  5. JMP back to 0x0041C8DE

Files

  • half_size_balls.c — C source code
  • bass.dll — Compiled DLL (PE32 i386)
  • half_size_balls.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll into the game folder
  3. Launch Hamsterball

A Hamsterball_half_size.log file is written next to the EXE showing whether the hook applied.


🔗 Related Documents

[[52144855544476|global variables]] and Data Cons

types : objects
keywords :

📂 View source on GitHub


Hamsterball — Global Variables and Data Constants

What This Document Is

A reverse-engineering reference for modders and researchers listing global variables and data constants found in Hamsterball.exe. These are symbols that live in the .data or .rdata sections of the PE image and are accessed by the game code through DAT_xxxxxx references, named labels, or inline constants.


Why This Matters for Modding

Many game behaviors are controlled not by function logic but by scalar constants in .data:

  • Ball physics multipliers (speed, gravity, friction)
  • Camera orbit distance and damping
  • Damage/touch force values
  • RNG range constants
  • Vtable pointers used for dynamic dispatch

If you patch these values in memory or replace them at image load time, you can drastically change game behavior without changing a single instruction.


Table of Contents

  1. Confirmed Named Globals (via Ghidra renames)
  2. Game Constants .data / .rdata (0x004C0000–0x00534...)
  3. Vtables and Jumps in the Data Section
  4. Named Data Constants with Context
  5. Float Constants (0x004CF000–0x004D0500)
  6. String Table Addresses in .rdata
  7. How to Patch Constants at Runtime
  8. Recommended Modding Recipes
  9. Address Quick Reference
  10. Verification Methodology

Confirmed Named Globals

These have been manually renamed in Ghidra and are confirmed by the renames_backup.json (restored at session start).

Address Symbol Type / Meaning Xrefs
0x004FD680 g_App App* — global singleton pointer to the App struct 5
0x005341CC g_renderIndex uint32_t — frame/batch render counter 3
0x004D2334 s_BACK char or char[] — used by the UI text system for the "Back" button / menu logic 45
0x004D9CDC MeshWorld_vtable void** — pointer to the MeshWorld vtable 2
0x004F7360 PTR_OBJ_VTABLE void** — generic object vtable base for Obj class (193 xrefs) 193

Floating-Point Constants (0x004CF000–0x004D0500)

The engine keeps many scalar constants in the .data section. They are NOT loaded as immediate values into instructions; they are loaded as memory references (_DAT_004CFxxx). This makes them trivial to patch from external code.

Core Ball Physics Constants

Address Approx. Value Function Description
0x004CF310 1.0f Ball_GetInputForce / Ball_ApplyForce Full-speed backward / max multiplier
0x004CF368 0.0f Ball_ApplyForce, Math_Atan2Angle Zero constant (force neutral, atan2 fallback)
0x004CF380 ~0.1–0.2 Ball_ApplyForce Hit recovery multiplier — reduces force after ball takes a hit (+0x2F0 != 0)
0x004CF378 ~0.5–0.8 Ball_ApplyForce Flag +0x324 multiplier (e.g., dizzy state)
0x004CF374 ~0.5–0.8 Ball_ApplyForce Flag +0xC5C (momentum-transfer state) multiplier
0x004CF3E8 1.0f Ball_ApplyForce Angular velocity scale (applied to +0xFC/0x100/0x104)
0x004CF3F0 0.95f Ball_AdvancePosition, Ball_vtable[0x14] Gravity / damping constant (per-frame friction multiplier)
0x004CF36C ~0.5–0.7 Ball_ApplyForce Flag +0xC4C (e.g., speed boost / power-down) multiplier

Modding Recipes — Physics

  1. Super Ball Mode — set 0x004CF380 (hit recovery) to 1.0f:
    The ball no longer loses momentum when hit by hazards.
  2. Ice Mode — set 0x004CF3F0 (damping) to 1.0f:
    Ball never slows down from friction/gravity. Will fly off levels.
  3. Dizzy Immunity — set 0x004CF378 to 1.0f:
    When the +0x324 dizzy flag is set, force is not reduced.
  4. Heavy Steel Ball — set 0x004CF3E8 to 0.1f:
    Angular velocity barely changes; ball rolls slowly.

Camera / Orbit Constants

Address Approx. Value Function Description
0x004CF3EC ~1.0–2.0 Scene_SetCamera Camera MIN_DIST for orbit clamping
0x004CFF78 700.0f Scene_SetCamera Camera MAX_DIST (hard cap fallback)
0x004D03A4 ~0.01–0.1 Scene_SetCamera Camera wave SCALE (Wave_Sin input)
0x004D03A0 ~0.5–1.0 Scene_SetCamera Camera wave AMPLITUDE multiplier

Modding Recipes — Camera

  1. Far Orbit — change 0x004CFF78 from 700.0f to 1200.0f:
    Camera can zoom much farther from the ball.
  2. Wave-Cam Disable — set 0x004D03A4 to 0.0f:
    Camera sine-wave bob disappears — perfectly smooth follow.

Collision / Touch Damage Constants

Address Approx. Value Function Description
0x004CF484 ~20.0–50.0 Ball_Update / 0x405190 Collision distance threshold for hazard proximity check
0x004CF370 ~1.0–1.5 CollisionEvents Direction/force multiplier on collision deflection
0x004CF508 ~0.5–1.0 Ball_vtable[0x10] (AI update) AI chase activation distance ratio
0x004D0434 ~2.0–10.0 Scene_Spawn Ball spawn vertical offset from START marker

Modding Recipes — Collision

  1. Hazard Immunity — raise 0x004CF484 to 99999.0f:
    Collision check never succeeds → hazards never touch you.
  2. Spawn Offset Fix — set 0x004D0434 to 0.0f:
    Ball spawns exactly at START.y instead of floating above.

RNG / Math Constants

Address Approx. Value Function Description
0x004CF558 360.0f Ball_vtable[0x10] (AI) Angle wrap helper (add 360 to normalize negative deg)
0x004CF554 0.0f Ball_vtable[0x10] (AI) Angle comparison lower bound
0x004CF550 ~300.0–350.0 Ball_vtable[0x10] (AI) AI flee distance threshold
0x004CF48C ~0.05f Ball_vtable[0x10] (AI) AI tick accumulator per frame
0x004D0418 ~114.59 Math_Atan2Angle 180/π — radian→degree constant
0x004D03A0 ~1.0 Scene_SetCamera Camera sine multiplier

Graphics / Rendering Constants

Address Approx. Value Function Description
0x004CF3C8 1.0f LoadBinaryMesh / mesh parser UV V-inversion constant (1.0 - v)
0x004CF454 ~0.01–0.1 MeshWorld_Parse Material shininess multiplier
0x004CF41C ~1.0 Graphics_Initialize Light range / projection scale factor
0x004CF308 ~0.001–0.01 Scene_RenderAllObjects Depth bias for shadow decal offset

Movement / Input Constants

Address Approx. Value Function Description
0x004D0250 ~0.5f Ball_GetInputForce Forward half-speed input multiplier

Vtables and Jumps

These are pointers to vtable arrays or function pointer tables stored in .data.

Address Symbol What it points to Usage
0x004D9CDC MeshWorld_vtable MeshWorld class vtable (2 xrefs) Level geometry dispatch
0x004CE400 (implicit) App vtable base App virtual method dispatch (see APP_OBJECT.md)
0x004CF3A0 (implicit) Ball vtable Ball virtual method array (set in Ball_ctor)
0x004D0260 (implicit) Scene vtable Scene virtual method array
0x004F7360 PTR_OBJ_VTABLE Generic Obj base-class vtable 193 xrefs — extremely common base pointer

String Table Addresses in .rdata

The binary embeds strings at fixed addresses in the .rdata section. Some are used as global identifiers passed to functions.

Address String / Content Used By
0x004D9E54 String-table base for strtok-style token parsing in MESHWORLD parser MeshWorld_Parse / FUN_004BC0D1
0x004D48A0 Save-file header / format string SaveTournament
0x004D9120 999999.0f or similar sentinel float Ball_Update (AI min-distance init)
0x004D2334 s_BACK — "Back" button / menu state 45 xrefs across UI code

How to Patch Constants at Runtime

Method 1 — External DLL

// In your mod DLL (injected into Hamsterball.exe process)
#define ADDR_HIT_RECOVERY      ((float*)0x004CF380)
#define ADDR_DAMPING           ((float*)0x004CF3F0)

void __declspec(dllexport) ApplyMods() {
    // Make the pages writable
    DWORD oldProtect;
    VirtualProtect(ADDR_HIT_RECOVERY, sizeof(float), PAGE_READWRITE, &oldProtect);

    // Patch values
    *ADDR_HIT_RECOVERY = 1.0f;   // Never reduce force on hit
    *ADDR_DAMPING      = 0.999f; // Very slippery ball

    VirtualProtect(ADDR_HIT_RECOVERY, sizeof(float), oldProtect, &oldProtect);
}

Method 2 — Cheat Engine

  1. Open Cheat Engine, attach to Hamsterball.exe
  2. Add Address Manually → enter 004CF3F0 as float
  3. Freeze value at 1.0f → instant zero-friction mode

Method 3 — DLL Injection at Load Time

Use detours, MinHook, or a manual IAT hook. Patch .data section before WinMain runs so the game never sees the original values.


Recommended Modding Recipes

1. God Mode (Hazard Immunity)

*(float*)0x004CF484 = 99999.0f;  // Distance threshold

2. Super Speed

*(float*)0x004CF3F0 = 1.0f;   // No friction damping
*(float*)0x004CF310 = 5.0f;   // Max backward speed

3. Dizzy Immunity

*(float*)0x004CF378 = 1.0f;   // Dizzy flag no longer reduces force

4. Smooth Camera

*(float*)0x004D03A4 = 0.0f;   // Wave_Sin scale → no bob
*(float*)0x004CFF78 = 2000.0f; // Far max orbit

5. Spawn Height Fix

*(float*)0x004D0434 = 0.0f;   // No offset above START marker

Address Quick Reference

0x004CF308    Depth bias (shadow decal)
0x004CF310    Max movement multiplier (1.0f)
0x004CF368    Zero float (0.0f)
0x004CF36C    Speed boost / power-down mult
0x004CF370    Collision deflection dir multiplier
0x004CF374    Momentum-transfer mult
0x004CF378    Dizzy state force mult
0x004CF380    Hit recovery force mult
0x004CF3C8    UV V-inversion (1.0f)
0x004CF3E8    Angular velocity scale (1.0f)
0x004CF3EC    Camera MIN_DIST
0x004CF3F0    Gravity / damping (0.95f)
0x004CF41C    Light range scale
0x004CF454    Shininess multiplier
0x004CF484   Collision dist threshold
0x004CF48C   AI tick accumulator (~0.05)
0x004CF508   AI activation distance ratio
0x004CF550   AI flee distance
0x004CF554   Angle lower bound (0.0)
0x004CF558   Angle wrap (360.0)
0x004CFF78   Camera MAX_DIST (700.0)
0x004D0250   Forward half-speed (~0.5)
0x004D03A0   Camera wave amplitude
0x004D03A4   Camera wave scale
0x004D0418   180/PI rad→deg
0x004D0434   Spawn vertical offset
0x004D2334   s_BACK (UI string)
0x004D48A0   Save header string
0x004D9120   Huge float sentinel
0x004D9CDC   MeshWorld_vtable
0x004D9E54   MESHWORLD tokenizer string table
0x004F7360   PTR_OBJ_VTABLE (193 xrefs)
0x004FD680   g_App (global App* pointer)
0x005341CC   g_renderIndex

Verification Methodology

All addresses in this document were derived from:

  1. Ghidra decompilation (analysis/ghidra/decompilations/) — raw _DAT_004xxxxx references in .c files
  2. Ghidra renames_backup.json — manually renamed global labels
  3. GhidraMCP list_globals — live project query of labeled data
  4. Cross-reference count from Ghidra xrefs — values with ≥3 xrefs are high-confidence

What was NOT verified

  • Exact float values at each address (Ghidra shows _DAT_ names but not the literal in decompilation). Where exact values are given, they are inferred from context (e.g., * 0.95f comment near 0x004CF3F0). To get ground-truth values, read 4 bytes at the VA in the running process.
  • Values may change between game versions. This document targets the version analyzed (Hamsterball.exe MD5 from docs/APP_OBJECT.md).

Document Revision

  • Compiled from: Ghidra project, renames_backup.json, raw decompilations
  • Coverage: 24 unique DAT_004xxxxx references extracted from raw .c files + 5 named globals
  • Next additions: Runtime memory dump verification (read actual float/int values at each VA), more vtable addresses, additional string table bases

For object-level offsets (App, Ball, Scene structs), see APP_OBJECT.md, BALL_OBJECT_MODDING.md, and SCENE_STRUCT.md.


🔗 Related Documents

[[53708688069228|8-ball spawn mod]]

types : mods
keywords :

📂 View source on GitHub


8-Ball Spawn Mod

Press B during gameplay to spawn an 8-ball in front of your hamster ball. The 8-ball spawns with physics and rolls/collides just like the balls in Rodent Rumble arenas.

What's New

  • Audio forwarding: All 10 BASS functions the game imports are forwarded to bass_real.dll. The game now has full audio while the mod is active.
  • Single-ball overwrite: Only one 8-ball can exist at a time. Pressing B again repositions the existing 8-ball to your current location and velocity — no new ball is created.
  • Dead AI fields documented: CHASE/HOME/SPINDISTANCE values are set but have no effect because is_8ball (ball+0xC74) is never set to 1. The ball is a physics-only debris ball.

Installation

  1. In your Hamsterball game folder, rename bass.dllbass_real.dll
  2. Copy the mod's bass.dll into the game folder
  3. (Optional) Copy 8ball_spawn.ini next to bass.dll for configuration
  4. Launch the game, enter a level or arena, press B to spawn an 8-ball

How It Works

The 8Ball mesh is preloaded by the game's resource loader into the board mesh array at board+0x268 (index 9 of the array at board+0x244). The ball mesh index field ball+0x754 controls which mesh the ball uses — setting it to 9 makes the ball render as the 8-ball.

Ball creation follows the same pattern as CreateBadBall (0x40BCA0):

  1. operator_new(0xC98) allocates ball memory
  2. Ball_ctor(mem, scene) constructs the ball
  3. vtable[1]() initializes physics defaults
  4. Position is set in front of the player's ball
  5. ball+0x754 = 9 selects the 8Ball mesh
  6. ball+0x18 = -1 sets player_index to none (debris ball)
  7. Player's exact velocity vector is copied (same direction and speed)
  8. Radius stays at Ball_InitPhysicsDefaults default (35.0)
  9. Ball is added to scene+0x29D4 (bad_balls_list) and scene+0x2DEC (all_balls_list)

Single-Ball Overwrite

If a previously spawned 8-ball is still valid (vtable check against 0x4CF3A0), pressing B again repositions it to the new spawn location with the player's current velocity. No new allocation is made — the same ball object is reused. If the old ball has been freed or corrupted, a fresh ball is allocated.

Audio Forwarding

The mod loads bass_real.dll on the first BASS function call (not in DllMain, to avoid loader lock deadlocks on real Windows). All 10 functions the game imports are forwarded with correct __stdcall calling conventions:

Function Forwarded
BASS_Init
BASS_Free
BASS_Start
BASS_Stop
BASS_SetConfig
BASS_ErrorGetCode
BASS_MusicLoad
BASS_MusicPlayEx
BASS_ChannelSetAttributes
BASS_ChannelStop

If bass_real.dll is not found, all functions return success values (1/0) — the game runs without audio but does not crash.

AI Fields (Dead Code)

The mod sets CHASE=100000, HOME=100000, SPINDISTANCE=1.0 on the spawned ball. However, these fields have no effect because:

  • ball+0xC74 (is_8ball flag) is never set to 1
  • Without that flag, Ball_Update (0x408390) skips the AI chase block entirely via if (ball[0x31d] != 0 || scene[0x237] != 0)
  • The ball behaves as a physics-only debris ball — it rolls, falls, and collides but never actively chases other balls

To enable AI behavior, ball+0xC74 would need to be set to 1. This would make the 8-ball chase the nearest player using the CHASE/HOME/SPINDISTANCE parameters.

Configuration

Edit 8ball_spawn.ini (placed next to bass.dll):

[8ball]
spawn_key=0x42       ; B key (default). See VK_* constants on MSDN
spawn_distance=40    ; Distance in front of player

Build

i686-w64-mingw32-gcc -shared -o bass.dll 8ball_spawn.c \
    -lwinmm -Wl,--enable-stdcall-fixup \
    -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Files

File Description
8ball_spawn.c Mod source code
8ball_spawn.ini Configuration file
bass.dll Compiled mod (proxy bass.dll)

Technical Notes

  • BASS_SetConfig/BASS_GetConfig are now forwarded to bass_real.dll (previously stubbed). The game calls BASS_SetConfig via SEH-protected code — forwarding with correct __stdcall convention works safely.
  • Scene/Board discovery uses brute-force App struct scan for the Scene vtable (0x4D0260), same approach as the player_clones mod.
  • The 8-ball has no AI — it spawns as a physics debris ball with player_index = -1. It rolls, falls, and collides with other balls and geometry.
  • load_real_bass() is called from spawn_thread (not DllMain) to avoid the Windows loader lock deadlock that affects LoadLibraryA calls during DLL initialization.

🔗 Related Documents

[[53892486924051|collision system]] Deep Dive

types : physics
keywords :

📂 View source on GitHub


Collision System Deep Dive

Overview

Hamsterball uses a spatial tree (octree) for broad-phase collision detection,
with AABB (Axis-Aligned Bounding Box) tests and per-triangle collision mesh
processing for narrow-phase. Ball_AdvancePositionOrCollision (0x4564C0) is the
main physics entry point that delegates to collision handlers via vtable dispatch.

Spatial Tree (Octree)

Collision_TraverseSpatialTree (0x465EF0)

Recursive traversal of the octree structure. For each node:

  1. Descend children (AthenaList at node+0x18):

    • Get first child from node+0x424
    • For each child: recurse into Collision_TraverseSpatialTree(child, aabb, result_list)
    • Increment iterator at node+0x20
  2. Process leaf data (AthenaList at node+0x2C):

    • Get collision mesh buffer from node+0x438
    • For each collision mesh buffer:
      • For each vertex group (3 vertices per face = 1 triangle):
        • AABB_ContainsPoint(param_1, vx, vy, vz)
        • If contained: AthenaList_Append(param_2, vertex_ptr)

AABB_ContainsPoint

Tests whether a point (x, y, z) is inside an axis-aligned bounding box.
Returns 0 (outside) or 1 (inside).

CollisionMesh Structure (0xCB0 bytes)

Offset Type Description
+0x00 vtable* Mesh_DeletingDtor (0x4D8E10)
+0x10 void* Scene pointer (param_1)
+0x18 AthenaList Child mesh list (octree children)
+0x430 AthenaList Face/vertex data (collision geometry)
+0x848 AthenaList Additional mesh data
+0xC74 int Flags/counter
+0xCA4 Vec3 AABB min (or position)
+0xCA8 Vec3 AABB max (or size)
+0xCAC int Collision state flag

CollisionMesh_ctor (0x456D80)

CollisionMesh_ctor(this, scene):
  this->vtable = 0x4D8E10  // Mesh_DeletingDtor
  this->scene = scene       // +0x10
  AthenaList_Init(this + 0x18)   // children
  AthenaList_Init(this + 0x430)  // faces
  AthenaList_Init(this + 0x848)  // mesh data
  this->flags = 0          // +0xC74
  this->aabb_min = {0,0,0} // +0xCA4
  this->aabb_max = {0,0,0} // +0xCA8
  this->state = 0          // +0xCAC
  Ball_InitBattleMode(this)

Collision Event Dispatch (Per-Board Handlers → Shared Base)

IMPORTANT: There is no single "Level" or "Arena" handler. Almost every board type overrides vtable[0x1D] (+0x74) with its own unique collision handler. Each processes board-specific events, then falls through to DispatchCollisionEvents (0x40C5D0) as the shared base. Arena (Rumble) boards have their own separate vtables with separate handlers.

Race Board Handlers

Board Handler Address Handler Name
WarmUp 0x0040C5D0 DispatchCollisionEvents (no override — uses base directly)
Intermediate 0x0040D340 IntermediateCollisionEvents
Dizzy 0x0040D500 DizzyCollisionEvents
Tower 0x0040DCD0 TowerCollisionEvents
Expert 0x0040E6A0 ExpertCollisionEvents
Odd 0x0040ED30 OddCollisionEvents
Wobbly 0x0040F9A0 WobblyCollisionEvents
Toob 0x00410020 ToobCollisionEvents
Sky/Neon 0x00410D00 NeonCollisionEvents
Beginner 0x004111E0 BeginnerCollisionEvents
Up 0x004119B0 UpCollisionEvents
Master 0x00412850 MasterCollisionEvents
Glass 0x00417EB0 GlassCollisionEvents
Impossible 0x00418360 ImpossibleCollisionEvents

Arena (Rumble) Board Handlers

Arena Handler Address Handler Name Events
Beginner Arena 0x00413DF0 BeginnerArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
Intermediate Arena 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM only
Dizzy Arena 0x00414350 DizzyArenaCollisionEvents N:SWIRL, DN:SINKPLATFORM
Tower Arena 0x00414570 TowerArenaCollisionEvents E:CATAPULTBOTTOM, DN:SINKPLATFORM
Up Arena 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM only
Odd Arena 0x00414DA0 OddArenaCollisionEvents E:GRAVITY, DN:SINKPLATFORM
Expert Arena 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM only
Toob Arena 0x00415010 ToobArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
Wobbly Arena 0x00415540 WobblyArenaCollisionEvents N:SQUAREWOBBLY, DN:SINKPLATFORM
Sky/Neon Arena 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM only
Warmup Arena 0x00416140 WarmupArenaCollisionEvents E:LAUNCH, DN:SINKPLATFORM
Impossible Arena 0x00418600 ImpossibleArenaCollisionEvents N:BOUNCE, DN:SINKPLATFORM
Master Arena 0x00412850 MasterCollisionEvents (shared with race board)
Board vtable[0x1D] (board-specific handler)
  ├─ Process board-specific events (if event name matches)
  │   └─ Return early (for some events) OR fall through
  └─ DispatchCollisionEvents (0x40C5D0) — shared base handler
      └─ Process universal events (N:GOAL, N:TARPIT, N:WATER, E:JUMP, etc.)

Tower Events (TowerCollisionEvents)

Event Name Description
E:CATAPULTBOTTOM Launch pad activation
N:TRAPDOOR Trapdoor opening
E:OPENSESAM Open trapdoor
E:BITE Mace/Chomper bite damage
E:MACETRIGGER Trigger mace swing
N:MACE Mace ball bounce

Expert Events (ExpertCollisionEvents)

Event Name Description
E:CALLHAMMER Hammer chase activation
E:HAMMERCHASE Hammer movement
E:ALERTSAW1/2 Saw warning
E:ACTIVATESAW1/2 Saw blade activation
E:ALERTJUDGES Reset all judges
E:SCORE Score display
E:BELL Bell + bonus time

Base Events (DispatchCollisionEvents)

Event Name Description
SECRET Secret area discovered
UNLOCKSECRET Secret unlock reward
NODIZZY No-dizzy powerup

Event strings can include <TIME> XML tags for timed events.

Ball Physics Pipeline

Ball_AdvancePositionOrCollision (0x4564C0)

6-phase physics pipeline:

  1. Free Lists: Release trail point and collision marker lists
  2. Input Velocity: Add input force to velocity, clamp to max_speed
  3. Damping: Apply damping formula: (1-dt) + (1-damping)*dt
  4. Collision: If collision flag set → vtable[0x1C] dispatch
  5. Gravity: Apply gravity with 0.95 damping constant
  6. Trail Recording: Record trail points on non-collision frames

Ball Collision Constants

Constant Value Description
_DAT_004CF310 1.0 Forward speed
_DAT_004CF368 0.0 Zero
_DAT_004CF3F0 0.95 Damping constant
_DAT_004D03A0 Wave scale Oscillation scale for path camera
_DAT_004D03A4 Wave frequency Oscillation frequency
_DAT_004CFF78 700.0 Path camera max distance
_DAT_004CF3EC min_dist Path camera minimum distance

Ball Struct Physics Offsets

Offset Type Description
+0x14 char Trail recording flag
+0x18 AthenaList Trail points (Material structs, 0x68 each)
+0x164 Vec3 Position (X)
+0x170 Vec3 Velocity
+0x188 float Max speed (5000.0 = 5.0f)
+0x18C float Speed scale
+0x1A4 void* Physics body
+0x1A8 float Gravity scale
+0x27C float Radius factor
+0x284 float Radius (26.0 default)
+0x744 int Shake flag (camera shake)
+0x758 Vec3 Camera target
+0x76C Vec3 Camera actual
+0x848 AthenaList Collision markers
+0xC604 vtable* Collision handler (vtable[0x1C])
+0xCA4 Vec3 Velocity X/Y/Z
+0xC64 float Speed scale factor
+0xC68 float Damping factor
+0xC70 float Max velocity clamp
+0xC74 float Accumulated trail distance
+0xC7C char Collision occurred flag
+0xC88 Vec3 Gravity components
+0xC94 float Gravity scale (0.5 default)

Face/Vertex Data Format

CollisionFace Structure (0x18 bytes)

Offset Type Description
+0x00 vtable* Face vtable
+0x04 uint16 Vertex index A
+0x08 uint16 Vertex index B
+0x0C uint16 Vertex index C
+0x10 uint16 Face type (0=solid, 1=trigger, etc.)
+0x14 float Distance/direction

MeshBuffer Structure (0x14 bytes)

Offset Type Description
+0x00 uint32 Vertex count
+0x04 uint32 Vertex offset
+0x08 uint32 Index count
+0x0C uint32 Index offset
+0x10 uint32 Face count

.COL Binary Format

Header: "COL\0" (4 bytes)
uint32 vertex_count
uint32 face_count
uint16 vertex_coords[vertex_count * 3]  // 16-bit fixed-point
float positions[vertex_count * 3]        // 32-bit float positions

Key Collision Functions

Address Function Description
0x4564C0 Ball_AdvancePositionOrCollision Main physics pipeline (6 phases)
0x465EF0 Collision_TraverseSpatialTree Octree traversal + AABB test
0x456D80 CollisionMesh_ctor Collision mesh constructor
0x40E6A0 ExpertCollisionEvents Expert board collision handler
0x418360 ImpossibleCollisionEvents Impossible board collision handler (N:BOUNCE, N:ONROTATOR, N:ONGEAR)
0x40DCD0 TowerCollisionEvents Tower board collision handler
0x40C5D0 DispatchCollisionEvents Base collision handler
0x46B070 FlagWaver_Render Water ripple collision (physics-based)
0x415480 CreateWobbly1 Wobbly bridge (baked vertex animation)
0x43B6F0 Rotator_AddBall Registers ball on rotator tracking list (formerly misnamed Rotator_AddBall)
0x43E600 Catapult_Update Per-frame rotation of tracked balls (applies rotation matrix to pos+vel)
0x43E9C0 Catapult_AddObjectConditional Registers ball on catapult/gear tracking list (guarded by +0x1510)
0x434290 Catapult_Launch Launch pad activation (sets catapult+0x10F0=1, +0x10F4=50 timer)

Rotator System (Gears, Swirls, Spinny Objects)

Spinning objects in Hamsterball physically carry the ball using a two-function system:

  1. Rotator_AddBall (0x43B6F0) — called on collision with N:ONROTATOR, N:SPINNY, or N:SWIRL
  2. Catapult_Update (0x43E600) — called every frame, applies rotation matrix to tracked balls

How it works

When the ball touches a spinning object's collision surface, the collision handler calls Rotator_AddBall(scene, ball). This function:

  1. Searches the rotator's AthenaList (at scene+0x10F0) for the ball pointer
  2. If found: resets the entry's tick counter to 10 (ball already tracked — keeps it on the rotator)
  3. If not found: allocates an 8-byte struct [ball_ptr, tick_counter=10] and appends it to the list

Every frame, Catapult_Update iterates the ball-tracking list:

  • Decrements each entry's tick counter
  • If counter reaches 0: frees the entry (ball released from rotator)
  • Otherwise: applies the object's rotation matrix to the ball's position (ball+0x164/+0x168/+0x16C) and velocity (ball+0xCA4/+0xCA8/+0xCAC)

10-frame grace period (not a carry limit)

The counter resets to 10 every frame the ball remains in contact with the rotator surface, because Ball_FallUpdate fires the collision event every frame. The countdown only starts ticking down after the ball leaves the rotator. This means:

  • While on the rotator: ball stays tracked indefinitely (counter keeps resetting to 10)
  • After leaving: 10 more frames of rotation before release (smooth transition)

Rotator/Catapult struct offsets

Offset Type Field
+0x436 float centerX (pivot X)
+0x437 float centerY (pivot Y)
+0x438 float centerZ (pivot Z)
+0x439 float rotSpeedZ
+0x43A float rotSpeedX
+0x43B float rotSpeedAngle
+0x43C float rotAngle (accumulated, decremented by rotSpeed each frame)
+0x43E AthenaList ballList (tracked balls with tick counters)
+0x10F0 AthenaList rotatorList (Scene-level, used by Rotator_AddBall)
+0x10F8 AthenaList catapultBallList (used by Catapult_AddObjectConditional)
+0x1510 byte active flag (Catapult_AddObjectConditional guard)

Event → collision handler mapping

Event Collision Handler Level
N:ONROTATOR ImpossibleCollisionEvents (0x418360) Impossible race (gears)
N:SPINNY ToobCollisionEvents (0x410020) Toob race
N:SWIRL DizzyArenaCollisionEvents (0x414350) Dizzy arena
N:ONGEAR ImpossibleCollisionEvents (0x418360) Impossible race (calls Catapult_AddObjectConditional)

🔗 Related Documents

00 - RE Mindset

types : playbook
keywords :

📂 View source on GitHub


00 - RE Mindset

Reverse engineering is evidence-based reconstruction.

Core Principles

  1. The binary is the ground truth. Tools and docs are hypotheses.
  2. Never trust a name without evidence. Even a pretty function name is just a label.
  3. Work from the known to the unknown. Start with strings, imports, and entry points; move toward internal logic.
  4. Cross-reference everything. A struct offset needs at least two decompilations before it is trustworthy.
  5. Write as you go. A session without notes is a session you will repeat.

When You Are Stuck

  • Look for the constructor. It tells you field sizes and default values.
  • Look for the destructor. It tells you nested objects and cleanup order.
  • Look for the update loop. It tells you which fields actually matter.
  • Look for string comparisons. They reveal state machines and object types.
  • Look for magic numbers. 0x3F800000 is 1.0f; 0x40400000 is 3.0f.

Reproducibility

Record:

  • Binary MD5/SHA256
  • Tool versions (Ghidra, plugin, OS)
  • Exact commands run
  • Failed hypotheses, not just successes

🔗 Related Documents

01 - Bootstrap a Hamsterball R

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/01-BOOTSTRAP.md)


01 - Bootstrap a Hamsterball RE Project

This guide assumes you have access to the hamsterball-re repo and the original game assets.

1. Workspace Layout

hamsterball-re/
├── originals/              # NEVER modify these
│   ├── installer/
│   └── installed/
│       └── extracted/
│           └── Hamsterball.exe
├── analysis/
│   ├── ghidra/
│   │   ├── HamsterballProject/
│   │   ├── scripts/
│   │   ├── structs/
│   │   └── renames_backup.json
│   ├── code_ref/
│   └── screenshots/
├── docs/                   # Human-readable findings
├── reimpl/                 # Reimplementation source
└── tools/                  # Custom RE tooling

2. Hash Everything First

Before analysis, record integrity baselines:

cd ~/hamsterball-re
python3 - <<'PY'
import hashlib, json, os
root = "originals/installed/extracted"
hashes = {}
for dirpath, _, files in os.walk(root):
    for f in files:
        p = os.path.join(dirpath, f)
        with open(p, "rb") as fh:
            data = fh.read()
            hashes[os.path.relpath(p, root)] = {
                "md5": hashlib.md5(data).hexdigest(),
                "sha256": hashlib.sha256(data).hexdigest(),
                "size": len(data)
            }
with open("docs/FILE_HASHES.json", "w") as out:
    json.dump(hashes, out, indent=2)
print("hashed", len(hashes), "files")
PY

Known good MD5 for Hamsterball.exe: 7d25019366b8d7f55906325bd630d7fe.

3. Initial Binary Reconnaissance

# File type and PE layout
file Hamsterball.exe
objdump -x Hamsterball.exe | head -80

# String sweeps
strings Hamsterball.exe | grep -iE '\.(dll|mesh|world|xml|wav|png|bmp|mo3)'
strings Hamsterball.exe | grep -iE 'direct3d|d3d|dinput|dsound|bass'
strings Hamsterball.exe | grep -iE 'menu|level|race|score|hamster'

Key findings to expect:

  • PE32 i386, Visual C++ 2003 era
  • Imports from D3D8.dll, DINPUT8.dll, DSOUND.dll, BASS.dll
  • Window class name AthenaWindow at ~0x4D9374
  • Engine assets under levels\, data\, fonts\

4. PE Analysis With Python

python3 - <<'PY'
import pefile
pe = pefile.PE("originals/installed/extracted/Hamsterball.exe")
print(f"Image base: 0x{pe.OPTIONAL_HEADER.ImageBase:08X}")
print(f"Entry point RVA: 0x{pe.OPTIONAL_HEADER.AddressOfEntryPoint:08X}")
print("Imports:")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(f"  {entry.dll.decode()}: {len(entry.imports)} imports")
PY

5. Asset Discovery

Common asset directories and files:

levels\          # .MESH and .MESHWORLD level files
data\            # HS.CFG save, RaceData.xml, Jukebox.xml, etc.
fonts\           # .description + .png font data
meshes\          # ball, hamster, hazards
sounds\          # .wav SFX
music\           # .mo3 tracker music (BASS)

6. First Wine Run (Optional)

Never background Wine. Instead:

cd originals/installed/extracted
WINEDEBUG=+loaddll timeout 30 wine Hamsterball.exe 2>&1 | tee /tmp/wine_first_run.log

Look for missing DLLs in the log.


🔗 Related Documents

01 - Targets and Tools

types : playbook

📂 View source on GitHub


01 - Targets and Tools

Supported Binary Formats

Format Extensions Typical Tools
PE32/PE32+ .exe, .dll pefile, Ghidra, x64dbg
ELF (none), .so readelf, Ghidra, objdump
Mach-O .app, .dylib otool, Ghidra, radare2
Raw/Firmware .bin binwalk, Ghidra, IDA

Analysis Types

Static analysis: No code execution. Safe and repeatable.

  • File headers, strings, imports/exports, disassembly, decompilation.

Dynamic analysis: Code runs under observation.

  • Debugging, hooking, memory dumps, API tracing.

Tool List

Task Tool
Disassemble/decompile Ghidra, IDA, radare2, Binary Ninja
PE inspection pefile, Detect It Easy, CFF Explorer
Strings strings
Hex/entropy 010 Editor, ImHex, binwalk
Debugger x64dbg, WinDbg, gdb, LLDB
API tracing API Monitor, Process Monitor, frida
Network Wireshark, tcpdump

🔗 Related Documents

02 - Binary Reconnaissance

types : playbook

📂 View source on GitHub


02 - Binary Reconnaissance

Initial Commands (any binary)

file target.exe
strings target.exe | head -50
strings target.exe | grep -iE '\.(dll|so|dylib)'
strings target.exe | grep -E '^[A-Za-z_][A-Za-z0-9_]*$' | sort | uniq -c | sort -rn | head
python3 - <<'PY'
import pefile, hashlib
with open('target.exe','rb') as f: data = f.read()
print('md5', hashlib.md5(data).hexdigest())
print('sha256', hashlib.sha256(data).hexdigest())
pe = pefile.PE('target.exe')
print('imagebase', hex(pe.OPTIONAL_HEADER.ImageBase))
print('entrypoint', hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))
print('imports')
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(' ', entry.dll.decode(), len(entry.imports))
PY

What to Look For

  • Import table: reveals graphics/audio/input/network libraries.
  • String table: filenames, URLs, error messages, debug paths, class names.
  • Sections: large .text, .rdata, .data; unusual names may indicate packing.
  • Entropy: packed/encrypted sections have entropy near 8.
  • Exports: DLL interfaces, COM classes, plugin APIs.

Red Flags

Sign Meaning
High entropy .text Packed or encrypted code
Missing normal imports Custom loader or syscall usage
TLS callback Anti-debug / early execution
Resources with high entropy Embedded payload

🔗 Related Documents

02 - Ghidra Setup and GhidraMC

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/02-GHIDRA-SETUP.md)


02 - Ghidra Setup and GhidraMCP Headless Server

1. Import the Binary

Use Ghidra's headless analyzer (do this once):

export GHIDRA_HOME=/opt/ghidra_12.0.4_PUBLIC
mkdir -p ~/hamsterball-re/analysis/ghidra/HamsterballProject
$GHIDRA_HOME/support/analyzeHeadless \
    ~/hamsterball-re/analysis/ghidra/HamsterballProject \
    Hamsterball \
    -import ~/hamsterball-re/originals/installed/extracted/Hamsterball.exe \
    -overwrite

Warning: -overwrite wipes any existing renames. Only do this if restoring from FUNCTION_MAP.md afterwards.

2. Install / Update GhidraMCP Extension

The project uses GhidraMCP for REST-based decompilation. Extension path:

~/.config/ghidra/ghidra_12.0.4_PUBLIC/Extensions/GhidraMCP/lib/GhidraMCP-5.12.0.jar

Update procedure:

cd /tmp && mkdir -p ghidra-mcp-update && cd ghidra-mcp-update
curl -sLO https://github.com/benethington/ghidra-mcp/releases/latest/download/GhidraMCP.zip
curl -sLO https://github.com/benethington/ghidra-mcp/releases/latest/download/bridge_mcp_ghidra.py
unzip -q GhidraMCP.zip -d ~/.config/ghidra/ghidra_12.0.4_PUBLIC/Extensions/GhidraMCP
sudo mkdir -p /opt/ghidra-mcp
sudo cp bridge_mcp_ghidra.py /opt/ghidra-mcp/

3. Start the Headless Server

Use a Hermes background terminal process (do NOT use nohup ... & in foreground):

export GHIDRA_HOME=/opt/ghidra_12.0.4_PUBLIC
MCP_JAR=/home/evan/.config/ghidra/ghidra_12.0.4_PUBLIC/Extensions/GhidraMCP/lib/GhidraMCP-5.12.0.jar
CLASSPATH="$MCP_JAR"
for jar in $GHIDRA_HOME/Ghidra/Framework/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done
for jar in $GHIDRA_HOME/Ghidra/Features/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done
for jar in $GHIDRA_HOME/Ghidra/Processors/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done

java -Xmx4g -XX:+UseG1GC \
    -Dghidra.home=$GHIDRA_HOME -Dapplication.name=GhidraMCP \
    -classpath "$CLASSPATH" \
    com.xebyte.headless.GhidraMCPHeadlessServer \
    --port 8089 --bind 127.0.0.1 \
    --project /home/evan/hamsterball-re/analysis/ghidra/HamsterballProject/Hamsterball.gpr \
    --program /Hamsterball.exe \
    > /tmp/ghidra-mcp.log 2>&1

Required flags:

  • --project must end in .gpr
  • --program must have leading slash (/Hamsterball.exe)

4. Verify the Server

# Health check
curl -s http://127.0.0.1:8089/health

# Expected output:
# {"status":"healthy","version":"5.12.0-headless","program_loaded":true,"program_name":"Hamsterball.exe"}

# Rename coverage
curl -s http://127.0.0.1:8089/compare_programs_documentation

# List functions
curl -s "http://127.0.0.1:8089/list_functions?page=1&limit=5000" | head

# Decompile a known function
curl -s "http://127.0.0.1:8089/decompile_function?address=0x004278E0"

5. Hermes MCP Bridge (Optional)

If Hermes does not auto-discover mcp_ghidra_mcp_* tools, enable the bridge in ~/.hermes/config.yaml:

mcp_servers:
  ghidra-mcp:
    command: python3
    args:
      - /opt/ghidra-mcp/bridge_mcp_ghidra.py
    env:
      GHIDRA_SERVER_URL: http://127.0.0.1:8089/
    timeout: 180
    connect_timeout: 30

Then verify with hermes tools list | grep ghidra.

6. Important Server Quirks

  • Use GET with query params, not POST with JSON. Direct POST to /decompile_function returns errors.
  • /list_functions returns plain text, not JSON.
  • Name lookups (?name=Foo) are unreliable; prefer ?address=0x....
  • run_script_inline is broken in headless mode due to OSGi BundleHost issues.

7. Stop / Restart

kill $(pgrep -f GhidraMCPHeadlessServer)
# Wait 2 seconds, then re-run the start command.

🔗 Related Documents

03 - Ghidra Setup

types : playbook

📂 View source on GitHub


03 - Ghidra Setup

Import Any Binary

/opt/ghidra/support/analyzeHeadless /path/to/project ProjectName -import /path/to/binary -overwrite

Auto-Analysis

Run all default analyzers first. For MSVC binaries, enable:

  • Windows PE x86 Propagate External Parameters
  • Apply Function Signature Data
  • Create Address Tables

Project Hygiene

  • Name the project after the binary version.
  • Keep an untampered copy of the original.
  • Export renames to git regularly.
  • Use commit messages like analysis: rename rendering functions @ 0x45xxxx.

Headless Decompilation

Useful for batch work:

/opt/ghidra/support/analyzeHeadless /path/to/project ProjectName -scriptPath ./scripts -postScript DecompileByAddress.java 0x1234 /tmp/out.c

🔗 Related Documents

03 - Restore Function Renames

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/03-RENAME-RESTORE.md)


03 - Restore Function Renames from FUNCTION_MAP.md

The Hamsterball project has 975+ hand-labeled functions. The authoritative list lives in docs/FUNCTION_MAP.md. This file explains how to apply those names to a fresh Ghidra project.

1. Prerequisite

  • GhidraMCP headless server running on port 8089
  • Program loaded (program_loaded: true on /health)

2. Run the Restore Script

cd ~/hamsterball-re
python3 analysis/ghidra/apply_renames.py

The script:

  1. Parses docs/FUNCTION_MAP.md for | 0x... | Name | rows.
  2. Skips auto-generated names (Catch, Unwind, operator_, thunk_, entry, Start).
  3. Sends batches of 50 names to http://127.0.0.1:8089/batch_create_labels.
  4. Prints progress and final documentation percentage.

3. Verify Coverage

curl -s http://127.0.0.1:8089/compare_programs_documentation | python3 -m json.tool

Expected final coverage: ~100% (3,781/3,781 functions documented).

4. Export Renames Back to JSON (After Heavy RE Session)

If you add new labels, back them up:

cd ~/hamsterball-re
python3 - <<'PY'
import json, re
renames = []
with open('docs/FUNCTION_MAP.md', 'r') as f:
    for line in f:
        m = re.match(r'\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(\S+.*?)\s*\|', line.strip())
        if m:
            addr, name = m.group(1), m.group(2).strip()
            if name not in ('Address', 'Name', '---'):
                renames.append({'address': addr, 'name': name})
with open('analysis/ghidra/renames_backup.json', 'w') as f:
    json.dump({'renames': renames, 'count': len(renames),
               'timestamp': '2026-06-17', 'source': 'FUNCTION_MAP.md'}, f, indent=2)
print(f"Backed up {len(renames)} renames")
PY

5. Maintaining FUNCTION_MAP.md

When you discover and rename a new function:

  1. Add it to docs/FUNCTION_MAP.md in the appropriate subsystem section.
  2. Run apply_renames.py to sync Ghidra.
  3. Commit both docs/FUNCTION_MAP.md and analysis/ghidra/renames_backup.json.

6. Format Rules for FUNCTION_MAP.md

## Subsystem Name

| Address | Name | Description |
|---------|------|-------------|
| 0x004XXXXX | FunctionName | What it does |
  • Address 0x prefix, 8 hex digits.
  • Name is one word (no spaces), snake_case allowed.
  • Description is concise but specific.

🔗 Related Documents

04 - Decompilation and Code-Re

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/04-DECOMP-WORKFLOW.md)


04 - Decompilation and Code-Reference Workflow

1. Single Address Decompile via REST

ADDR=0x0046BD80  # App_Run
curl -s "http://127.0.0.1:8089/decompile_function?address=$ADDR"     > analysis/code_ref/decomp_app_run.c

2. Batch Decompile via Headless Script

When the REST server is slow or unreliable, use a native Ghidra Java script. A template is provided at:

analysis/ghidra/scripts/DecompileByAddress.java

Run it from the repo root:

/opt/ghidra/support/analyzeHeadless \
  $(pwd)/analysis/ghidra/HamsterballProject Hamsterball \
  -scriptPath $(pwd)/analysis/ghidra/scripts \
  -postScript DecompileByAddress.java 0x46EC30 /tmp/decomp_46EC30.c

For multiple addresses, use DecompileMultiAddresses.java (same directory) with a comma-separated address list.

3. Cleaning Decompiled Code

When converting raw Ghidra C to readable reference code:

DO:

  • Give variables meaningful names.
  • Replace magic numbers with named constants.
  • Add section comments (// === physics integration ===).
  • Preserve original pointer arithmetic semantics (use PTR_OFF, READ_U8, WRITE_U8 macros).

DO NOT:

  • Replace flat __strnicmp chains with switch/enum class.
  • Change __thiscall free functions into C++ class methods with early returns.
  • Factor repeated pointer arithmetic into accessor methods unless the original did so.

See analysis/code_ref/DispatchCollisionEvents_clean.cpp for an example of a faithful cleanup.

4. Required Reading for New Findings

After decompiling a new function, update these canonical docs:

If you found... Update...
New function docs/FUNCTION_MAP.md
New field / struct docs/STRUCTS_AND_TYPES.md + analysis/ghidra/structs/*.h
Modding behavior Relevant docs/*_MODDING.md or create new file
Rendering behavior docs/D3D8_RENDERING_PIPELINE.md or docs/RENDERING_ITERATION_LOG.md
Input / controls docs/INPUT_SYSTEM.md

5. Naming Conventions

Construct Convention Example
Functions Subsystem_VerbNoun Ball_ApplyForce, Scene_SetCamera
Structs PascalCase Ball, Scene, CollisionMesh
Offsets UPPERCASE with prefix BALL_POS_X, SCENE_BALL_LIST
Globals g_ prefix g_app, g_graphics

6. Cross-Reference Search Patterns

# Find all references to a byte offset in raw decompilations
grep -rn '(int)this + 0xCA8)' analysis/ghidra/decompilations/
grep -rn 'param_1\[0x32a\]' analysis/ghidra/decompilations/   # 0x32a * 4 = 0xCA8

# Find xrefs to a vtable
grep -rn '0x4CF3A0' analysis/code_ref/ docs/ analysis/ghidra/structs/

🔗 Related Documents

04 - Naming Strategy

types : playbook
keywords :

📂 View source on GitHub


04 - Naming Strategy

Naming Unknown Functions

Pattern Suggested Name
Reads a known import ImportName_Wrapper
Calls string search Subsystem_FindByName
Manipulates a known struct StructName_Action
Main loop Subsystem_Update
Only sets a field Subsystem_SetField

Be Conservative

If you only know a function calls Direct3DCreate8, name it D3D8_CreateDevice_Wrapper, not Graphics_Initialize unless you prove it initializes the whole subsystem.

Anti-Pattern: Naming by Call Site

A function called from Ball-related code that actually dereferences CollisionMesh fields should be named CollisionMesh_*, not Ball_*. Check which struct offsets this accesses.


🔗 Related Documents

05 - Decompilation Without Dis

types : playbook

📂 View source on GitHub


05 - Decompilation Without Distortion

The Goal

Produce readable reference code that preserves the original control flow and memory layout.

Cleaning Rules

  • Rename variables.
  • Name constants and struct offsets.
  • Add section comments.
  • Keep the original if-chain order.
  • Keep the original calling convention.

Forbidden Cleanups

  • Replacing flat string-compare chains with switch.
  • Changing __thiscall free functions into C++ methods with early returns.
  • Factoring repeated pointer arithmetic into accessors unless the original did so.
  • Inventing types for unverified offsets.

🔗 Related Documents

05 - Struct Offset Verificatio

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/05-STRUCT-VERIFICATION.md)


05 - Struct Offset Verification Methodology

Every documented offset in this project is guilty until proven innocent by raw decompiled C. Human-written comments can be wrong.

1. Confidence Markers

Marker Meaning
Verified in raw decompiled C by 2+ distinct functions
⚠️ Found in exactly 1 decompilation or only in comments
Inferred from adjacent fields; no raw C evidence yet

2. Automated Verification Script

A reusable Python verifier is provided at:

python3 docs/agent-knowledge/scripts/verify_struct_offsets.py Scene > /tmp/scene_verify.md

It decompiles the configured list of functions, extracts all (int)this + 0xNNNN and param_1[0xNNN] references, and compares them to the claimed offsets in CLAIMED_OFFSETS.

Template lives at templates/verify_struct_offsets_template.py.

3. Manual Spot-Check Commands

# Direct byte offset in App functions
grep -rn '(int)this + 0x184)' analysis/ghidra/decompilations/app/

# Int-indexed access (multiply by 4)
grep -rn 'param_1\[0x5' analysis/ghidra/decompilations/app/decomp_app_run.c

# Cross-object references (e.g., Ball -> Scene)
grep -rn '0x5dc\|0x5DC' analysis/ghidra/decompilations/ball/

4. Common Offset Confusion Patterns

Pattern Example Fix
Int-indexed vs byte param_1[0x5C] = 0x170 Multiply index by 4
Nested object flattened CollisionMesh+0xCA8 under Ball Document pointer chain: Ball+0x1A4 → CollisionMesh+0xCA8
Wrong base object App+0x5DC = Scene* Raw C shows Scene+0x5DC player array, not App
Stale build Doc says v1.0 offset; binary v1.1 moved it Always state binary MD5 in findings

5. Creating / Extending Structs in Ghidra

The GhidraMCP create_struct API expects a JSON array of fields:

fields = [
    {"name": "vtable",       "offset": 0x000, "type": "pointer"},
    {"name": "owner_ball",   "offset": 0x010, "type": "pointer"},
    {"name": "roll_friction","offset": 0xC64, "type": "float"},
]
# send to create_struct endpoint

Important: add_struct_field works for extending existing structs. import_data_types is not implemented. create_struct requires the JSON array format shown above; key/value and comma-separated field formats are broken.

6. Plate Comments for ECX this-Param Retyping

Ghidra cannot retype register-based this parameters via the API (set_parameter_type fails on ECX). Workaround: use plate comments on functions to document intended types, or manually retype in the Ghidra GUI.


🔗 Related Documents

06 - Calling Conventions

types : playbook

📂 View source on GitHub


06 - Calling Conventions

x86 32-bit

Convention this/args Stack cleanup Typical use
__cdecl All args on stack Caller C functions, variadic
__stdcall All args on stack Callee Win32 APIs
__fastcall ECX, EDX, then stack Callee Compiler optimization
__thiscall ECX=this, args on stack Callee MSVC member functions

x64

RCX, RDX, R8, R9 for first four integer/pointer args; XMM0-3 for floats. Caller cleans.

ARM64

X0-X7 for args. Stack may be used for extras. Link register X30/LR holds return address.

Detection in Assembly

Convention Prologue tells you
__thiscall MOV ESI, ECX saves this
__stdcall RET 0x8 etc.
__cdecl Plain RET, caller pops

🔗 Related Documents

06 - Common Reverse-Engineerin

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/06-MODDING-PATTERNS.md)


06 - Common Reverse-Engineering Patterns

1. Dead Code Detection

A setter is dead code if the field it writes is never read by the main update/render loop, or if it is overwritten every frame by a computed value.

Method:

  1. Decompile the setter.
  2. Find the main update function for that object.
  3. Search all decompilations for reads of the same offset.
  4. If zero reads → dead value.

Example: Ball_SetSpeed (historical misname) writes CollisionMesh+0xC64 and +0xC98, but Ball_Update never reads them. The function is actually CollisionMesh_SetFriction and the fields are overwritten by physics.

2. Nested Object Verification

If a constructor does operator_new(size) + SubObject_ctor(...) and stores the result at this + N, that field is a pointer to a nested object.

Pointer chain template:

void* ball = ...;
void** collisionMeshPtr = (void**)((DWORD)ball + 0x1A4);
void* collisionMesh = *collisionMeshPtr;
float* trueVelY = (float*)((DWORD)collisionMesh + 0xCA8);

3. Vtable-Based Function Discovery

Constructors always assign the vtable first. Parse vtable slots as DWORDs to find virtual method addresses.

// Ball vtable at 0x4CF3A0
// [0] Ball_dtor      @ 0x4027F0
// [1] Ball_InitPhysicsDefaults @ 0x405100
// [2] probably Update  @ ...

A function reached only through vtable dispatch has no direct CALLs. Hook the function start directly or patch the vtable slot.

4. Object Spawning Level-Gating Pattern

Objects fall into two categories:

Self-loading: Constructor calls MeshWorld_ctor(..., "hardcoded_path").

  • Can be added to any level by patching app+0x23C mode gate.
  • Examples: Bonk, Bumper/Rebound.

Scene-dependent: Constructor receives a pre-loaded mesh pointer from a BoardLevel*_ctor.

  • Requires both the mode gate patch AND the level constructor to load the sub-mesh.
  • Examples: Tipper, Gluebie, BlockDawg, Catapult, BreakBridge, PopCylinder.

5. Calling Game Functions from Injected Code

Most game member functions use __thiscall:

  • this in ECX
  • Remaining args on stack
typedef void* (__thiscall *Ball_ctor_fn)(void* thisPtr, int scene);
auto BallCtor = (Ball_ctor_fn)0x40AFE0;
void* ball = BallCtor(allocatedMem, (int)scene);

Calling via __cdecl (all args on stack) will crash because ECX contains garbage.

6. The CRT Heap Trap

Never call the game's operator_new / malloc / _free from an injected DLL — the VS2003 Small Block Heap critical sections can deadlock or corrupt.

Fix: Read the private CRT heap handle and use HeapAlloc/HeapFree directly:

HANDLE hCrtHeap = *(HANDLE*)0x005369C0;  // image base 0x400000 + RVA 0x1369C0
void* mem = HeapAlloc(hCrtHeap, 0, 0xC98);

For ASLR safety, compute the runtime address: GetModuleHandle(NULL) + RVA.

7. AI as Vtable Override

AI objects reuse the same struct as the player; behavior differences come from config flags and a vtable thunk:

  1. Level loader writes AI config fields (e.g., ball[0x31D] = 1 for is_8ball).
  2. Ball_Update checks the flag and calls Ball_ApplyForce vtable slot with AI-computed input.
  3. Look for string comparisons (__stricmp) against names like BADBALL, CHASE, HOME, SIZE, SPINDISTANCE.

🔗 Related Documents

07 - Recovering Structs and Gl

types : playbook

📂 View source on GitHub


07 - Recovering Structs and Globals

Struct Recovery Steps

  1. Find constructor. Note operator_new(size) — that is the struct size.
  2. Watch vtable assignment. First field confirms object type.
  3. Watch sub-object allocations. this + N = operator_new(...) + ctor means pointer to nested object.
  4. Watch default writes. They reveal field types.
  5. Find update/render function to see which fields are read.

Globals

Location How to find
IAT entries Import table
Singletons Look for static pointer written once in init
CRT heap handle Trace operator_newHeapAlloc handle

Offset Confidence

  • ✅ Raw decompiled C, 2+ functions
  • ⚠️ One function only or comment-only
  • ❓ Inferred from size/alignment, no direct evidence

🔗 Related Documents

07 - Reimplementation Lessons

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/07-REIMPL-LESSONS.md)


07 - Reimplementation Lessons Learned

The project produced a working D3D8 reimplementation and learned a great deal about what does/does not match the original. This file captures those lessons so future work does not repeat the same mistakes.

1. Rendering is D3D8 Only

  • The original uses Direct3DCreate8, IDirect3DDevice8, DrawPrimitiveUP, CreateVertexBuffer.
  • Any OpenGL/SDL2 files in the old repo are leftovers, not the active code.
  • Cross-compile with MinGW i686-w64-mingw32-gcc -m32.

2. Wine + llvmpipe Workaround

Original EXE runs under Wine with a d3d8to9 proxy plus software llvmpipe:

# Build/copy d3d8to9 d3d8.dll next to Hamsterball.exe
Xvfb :99 -screen 0 800x600x16 &
DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 wine Hamsterball.exe

This is useful for screenshot/testing but has known rendering bugs:

  • D3DRS_LIGHTING=TRUE + D3DFVF_NORMAL ignores normals → flat surfaces.
  • Textures may not render even with correct API usage.

3. Lighting Target-Match Approach

Instead of relying on D3D8 material/lighting interpolation (which behaves incorrectly under llvmpipe), define lit/shadow target colors directly:

Surface Lit Shadow
Wall (0.72,0.90,1.0) (0.24,0.35,0.49)
Floor (0.96,0.96,1.0) (0.70,0.78,1.0)
Sky (85,120,215) -

Formula:

float lit_factor = pow(max(dot(normal, light_dir), 0.0f), 0.35f);
Vec3 color = shadow + (lit - shadow) * lit_factor;

4. Camera Sign Bug

Original Scene_SetCamera computes:

orbit_dir = (cos_a, 0.9, sin_a);  // camera-to-target direction
eye = target + normalize(orbit_dir) * orbit_dist;

A naive reimplementation placing the eye with the opposite sign (target - dir * dist) works for some levels (Level3) but breaks others (Level1, Level2). Use an adaptive heuristic or match the original sign per level.

5. Spawn / Collision Placement

  • Ball spawn Y must be derived from actual geometry (probe downward from START object), not from the START object's raw Y coordinate.
  • Level3 START1-1 is at Y=-85.4; the track surface is nearby, but blindly spawning at START.y + radius places the ball in empty space.

6. Per-Geometry vs Per-Level Colors

WarmUp (Level1) uses per-geometry pink diffuse on platform faces. Arena levels use Section 1 PLATFORM diffuse colors. Do not hardcode one color scheme globally.

7. Controls

Ball_GetInputForce reads exactly 4 directional DIK codes at InputDevice+0x50C..0x518. There is NO brake key and NO player-jump; jumping is triggered by E:JUMP collision objects only.

8. Avoid Backgrounding Wine Processes

Wine will hang the agent for 50+ minutes if backgrounded. Use timeout 10s, or launch and kill in the same command.

9. Cross-Compiling DSound8

When compiling DSound code with MinGW, include <mmeapi.h> (or <mmreg.h>) before <dsound.h> to get LPWAVEFORMATEX/LPCWAVEFORMATEX.

10. WASM BoxedWine Dead End

BoxedWine WASM cannot run D3D8/D3D9 games in the browser. The WASM port requires a native SDL2/WebGL reimplementation, not the original EXE.


🔗 Related Documents

08 - Object-Oriented Patterns

types : playbook

📂 View source on GitHub


08 - Object-Oriented Patterns

Vtables

A vtable is an array of function pointers stored in .rdata.

// Constructor assigns vtable first
this->vtable = &KnownVtable;

// Slot N = vtable + N*4 (32-bit) or N*8 (64-bit)

Use vtables to map:

  • Destructor slot 0
  • Virtual methods 1..N
  • Multiple inheritance may have multiple vtables

Constructors / Destructors

// In constructor:
this->vtable = &LiveVtable;   // most-derived
SubObject_ctor(this + N);

// In destructor:
this->vtable = &BaseVtable;    // base
SubObject_dtor(this + N);
operator_delete(this);

Multiple Inheritance

A class with multiple bases has multiple vtable pointers, often at +0, +N.
Watch for destructors that restore a different vtable than the constructor sets.


🔗 Related Documents

08 - Troubleshooting Known Fai

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/08-TROUBLESHOOTING.md)


08 - Troubleshooting Known Failure Modes

Ghidra / GhidraMCP

Server returns program_loaded: false

  1. Read /tmp/ghidra-mcp.log.
  2. Confirm --project ends in .gpr and --program starts with /.
  3. Re-import with analyzeHeadless if the program was never loaded.
  4. After re-import, restore renames from docs/FUNCTION_MAP.md.

run_script_inline fails with BundleHost error

Inline Python/Ghidra scripts are broken in headless mode. Use native Java scripts in analysis/ghidra/scripts/ instead.

MCP tools missing from Hermes

  1. Check ~/.hermes/config.yaml for the mcp_servers: ghidra-mcp: bridge block.
  2. Verify /opt/ghidra-mcp/bridge_mcp_ghidra.py exists.
  3. Fall back to direct curl GET requests.

Decompile by name returns "Address or function name is required"

Use ?address=0x... instead of ?name=.... Name lookups are unreliable.

Tools / Environment

pip vs python3 mismatch

This machine has python3=3.11.15 with no pip module; pip points to python3.12. Use uv or a venv for Python dependencies.

Web search / web extract hanging

Ensure FIRECRAWL_API_KEY and FIRECRAWL_API_URL are set in the Hermes config pointing to the Guanaco router (or disable web fallback).

Guanaco router AttributeError (UsageConfig missing fields)

The installed guanaco package is older than the repo code. Fix:

cd ~/.guanaco/repo
source ~/.guanaco/venv/bin/activate
pip install -e .

Binary / RE

Spawned BadBall crashes

  • Verify __thiscall convention.
  • Verify ECX = allocated memory.
  • Ensure the Scene pointer and CollisionLevel pointer at Scene+0x440 are valid.
  • Use HeapAlloc on the game's CRT heap, not operator_new from a DLL.

Camera geometry invisible

Try both eye = target + dir*dist and eye = target - dir*dist or sample which side has vertices in front of the camera.

Textures not rendering under Wine/llvmpipe

Programmatic textures via CreateTexture + LockRect work; loaded BMP/PNG textures may not. Use runtime-generated checker textures with D3DTOP_MODULATE for correct per-pixel modulation.

Commit Safety

Never commit

  • Original binaries (.exe, .dll)
  • Installer packages
  • User credentials or API tokens
  • Save games or registry data

Always commit

  • docs/FUNCTION_MAP.md
  • analysis/ghidra/renames_backup.json
  • analysis/ghidra/structs/*.h
  • Clean C++ reference reconstructions in analysis/code_ref/

🔗 Related Documents

09 - Dynamic Analysis

types : playbook

📂 View source on GitHub


09 - Dynamic Analysis

Debugger Uses

  • Set breakpoint on suspected function, observe args.
  • Watch memory writes to structs.
  • Capture call stack for crashes.
  • Trace API calls (CreateFile, LoadLibrary, etc.).

Hooking

Method Best For
DLL injection Long-term interception
IAT hooking Replacing imported APIs
Inline hooking Replacing internal functions
Frida Scriptable tracing

Safe Hook Checklist

  • Match calling convention exactly.
  • Preserve registers not used by your hook.
  • Do not call CRT allocators from injected threads if the target uses a private heap.
  • Log before changing state.

🔗 Related Documents

10 - Subsystem Patterns

types : playbook

📂 View source on GitHub


10 - Subsystem Patterns

Graphics

Look for Direct3DCreate8/9, CreateWindowEx, RegisterClass, wglCreateContext, SDL_CreateWindow.

Input

Look for DirectInput8Create, GetAsyncKeyState, RegisterRawInputDevices, XInput.

Audio

Look for DirectSoundCreate, BASS_Init, waveOutOpen, OpenAL.

File Formats

Binary formats often follow:

  • Magic / version
  • Count fields
  • Fixed-size records or length-prefixed strings
  • Float arrays (3 or 16 floats for 3D transforms)

Event Systems

Many older games use flat string comparison chains:

if (__stricmp(name, "E:JUMP") == 0) { ... }
if (__stricmp(name, "E:ACTION") == 0) { ... }

🔗 Related Documents

11 - Verification

types : playbook

📂 View source on GitHub


11 - Verification

Every Claim Needs Evidence

Claim Required Evidence
Function name Decompilation or direct xrefs
Struct size Constructor operator_new(size)
Field offset Raw C statement (this + N) or (this)[N*4]
Field meaning Observed values and read/write context
Calling convention Prologue/disassembly
Vtable slot Parsed vtable array at known address

Falsification

Actively look for evidence against your hypothesis:

  • Search other functions for the same offset.
  • Check if a setter's field is ever read.
  • Check if a flag is overwritten every frame.

Peer-Review Checklist

Before committing a finding:

  1. Can another agent reproduce the address from the doc?
  2. Is the binary hash stated?
  3. Are confidence markers applied to every claim?
  4. Are wrong guesses removed rather than left commented out?

🔗 Related Documents

12 - Troubleshooting Common De

types : playbook

📂 View source on GitHub


12 - Troubleshooting Common Dead Ends

Decompilation Looks Nonsensical

  • Check that the binary base is correct.
  • Check that you are analyzing the right code (thumb vs ARM, 16-bit vs 32-bit).
  • Look at raw disassembly alongside decompilation.

Function Has No Xrefs

  • It may be reached through a vtable.
  • It may be an export or callback.
  • It may be dead code.

Strings Do Not Match Behavior

  • Strings may be old or unused.
  • The same string may have multiple encoding blocks.
  • Look for the code that references the string, not the string itself.

Tool Hangs

  • Don't background long-running native tools.
  • Use timeout.
  • For servers, use Hermes background=true with health checks.

Dynamic Analysis Crashes

  • Verify calling convention.
  • Verify stack alignment.
  • Check for anti-debug (TLS callbacks, timing checks, debug registers).

🔗 Related Documents

8-Ball AI System

types : gameplay
keywords :

📂 View source on GitHub


Hamsterball 8-Ball AI System

Overview

The "8-ball" or "BADBALL" is an AI-controlled enemy ball that appears in single-player race levels. It is a fully autonomous ball object with a simple chase-and-spin behavior designed to interfere with the player. The AI is stateless — no pathfinding, no planning, just a position-seeking force vector updated every frame.

Key insight: The 8-ball is NOT a separate object type. It is a standard Ball instance with NPC flags set. The AI code lives inside the same Ball_Update function (vtable [0x10] at 0x408390 wrapping 0x405E00) that runs on every ball every frame. For player balls, the AI block is skipped because is_8ball = 0. There is no separate 8-ball update function — all balls share the same vtable and the same tick.

Critical consequence: If you set ball[0x31D] = 1 on a player ball, that player ball will execute the 8-ball AI and chase other balls automatically. The AI is a conditional block inside the universal update, not a separate object system.


BADBALL MESHWORLD Format

In MESHWORLD level data, BADBALL objects are defined with typed parameters:

BADBALL pos_x pos_y pos_z
  CHASE distance
  HOME distance
  SIZE radius
  SPINDISTANCE radius

Parameters

Tag Type Default Description
CHASE float 0 Max distance to target — AI shuts down if player is farther than this
HOME float 0 Max distance from spawn — AI shuts down if 8-ball is farther than this from its HOME position
SIZE float 27 Ball radius (overrides default)
SPINDISTANCE float 0 Radius of the circular "spin" wobble added to target position

Example from Level Data

BADBALL 120.0 50.0 200.0
  CHASE 800
  HOME 400
  SPINDISTANCE 50

This 8-ball:

  • Spawns at (120, 50, 200)
  • Only chases when player is within 800 units
  • Only chases when itself is within 400 units of spawn
  • Adds a 50-unit circular wobble to its target position

AI Activation Conditions

The 8-ball AI runs inside Ball_Update (vtable slot [0x10] at 0x408390) ONLY when either condition is true:

if (ball[0x31d] != 0 || scene[0x237] != 0) {
    // Run 8-ball AI
}
Condition Meaning When Set
ball[0x31d] != 0 Ball was spawned as a BADBALL Set by CreateBadBall() when parsing MESHWORLD
scene[0x237] != 0 Battle mode / multiplayer Set by Ball_InitBattleMode() for CPU players

Key: 0x31d is set to 1 during CreateBadBall() and cleared during level transitions. The AI block is skipped entirely for normal player balls.


AI Behavior: Two Modes

Mode 1: Direct Chase (when no BADBALL config)

If no CHASE/HOME parameters were set, the AI simply seeks the nearest valid ball:

// Find nearest valid target ball
float best_dist = 999999.0;
Ball* target = nullptr;
for (each ball in scene->ball_list) {
    if (ball->race_active &&               // [0x768] — ball is in active race
        !ball->is_falling &&               // [0x2f9] — not currently falling/respawning
        ball->player_index != -1 &&        // [0x18] — has a valid player
        !ball->is_teleporting &&           // [0x324] — not in teleport
        scene->some_mode_flag) {           // [0x3a4c]
        
        float dist = Math_FastDistance2D(8ball->pos.x, 8ball->pos.z,
                                          ball->pos.x, ball->pos.z);
        if (dist < best_dist) {
            best_dist = dist;
            target = ball;
        }
    }
}

Target validity check breakdown:

  • race_active (0x768): Ball is actively racing (not in menu, not finished)
  • !is_falling (0x2f9): Ball is not in a fall/respawn state
  • player_index != -1 (0x18): Ball has a player/controller assigned
  • !is_teleporting (0x324): Ball is not currently teleporting
  • scene[0x3a4c]: Some scene-level mode flag (possibly "race in progress")

Mode 2: BADBALL Configured Chase

When CHASE and HOME are set, additional distance checks gate the behavior:

// Only chase if:
// 1. Random roll succeeds (RNG < threshold)
// 2. Player is within CHASE distance
// 3. 8-ball is within HOME distance from its spawn

float rng = RandomFloat();
if (rng < threshold &&
    dist_to_player < ball[0x31c] &&    // CHASE parameter
    dist_from_home < ball[0x31b]) {   // HOME parameter
    
    target = player_ball;
}

Target Computation: Spin Wobble

The 8-ball's target position is NOT the player's exact position. It adds a circular wobble using SPINDISTANCE:

// Base target = spawn position (HOME position, stored at [0x318/0x319/0x31A])
Vec3 target = {
    ball[0x318],  // spawn X
    ball[0x319],  // spawn Y
    ball[0x31A]   // spawn Z
};

// Add spin wobble
float spin_angle = ball[0x31e] * spin_speed;  // [0x31e] increments each frame
target.x += sin(spin_angle) * ball[799];      // ball[799] = SPINDISTANCE
target.z += cos(spin_angle) * ball[799];

// Increment spin counter
ball[0x31e] += 0.05;  // _DAT_004cf48c ≈ 0.05

Key insight: The spin makes the 8-ball orbit around its HOME point while chasing, creating a "probing" behavior rather than a direct homing missile.


Force Application

Once the target is computed, the AI applies force toward it:

// Compute direction to target
float dx = target.x - ball->pos.x;   // [0x59]
float dy = target.y - ball->pos.z;   // [0x5B] — note: using Z as Y in this code
float dz = target.z - ball->pos.y;   // [0x5A]

// Normalize
float dist_sq = dx*dx + dy*dy + dz*dz;
float dist = sqrt(dist_sq);
if (dist > 1.0) {
    dx /= dist;
    dy /= dist;
    dz /= dist;
}

// If 8-ball is smaller than target ball, REVERSE direction (flee!)
if (8ball->radius < target->radius * 0.9) {
    dx = -dx;
    dy = -dy;
    dz = -dz;
}

// Apply force via vtable call
// vtable[0x14] = Ball_ApplyForceWithMultipliers(this, dirX, dirY, dirZ, multiplier)
ball->vtable[0x14](ball, dx, 0.0, dz);

Flee behavior: If the 8-ball's radius is smaller than the target's radius × 0.9, it runs away instead of chasing. This can happen with power-up balls or when the player has grown.


Ball Struct: AI-Relevant Fields

8-Ball Config (set by CreateBadBall())

Byte Offset Int Index Type Name Description
+0xC60 [0x318] Vec3 home_position Spawn position (X/Y/Z)
+0xC6C [0x31B] float home_distance HOME parameter — max distance from spawn
+0xC70 [0x31C] float chase_distance CHASE parameter — max distance to target
+0xC74 [0x31D] bool is_8ball Set to 1 by CreateBadBall(), cleared on level transition
+0xC78 [0x31E] float spin_counter Increments each frame, drives spin wobble angle
+0xC7C [0x31F] float spin_distance SPINDISTANCE parameter — radius of spin wobble

Physics/Collision (shared with all balls)

Byte Offset Int Index Type Name Description
+0x164 [0x59] float pos.x Current X position
+0x168 [0x5A] float pos.y Current Y position
+0x16C [0x5B] float pos.z Current Z position
+0x284 [0xA1] float radius Ball collision radius
+0x2F0 [0xBC] int impact_counter Frames since last impact (damps force)
+0x2F9 [0x7E] bool is_falling Currently in fall/respawn
+0x324 [0xC9] bool is_teleporting Currently teleporting
+0x768 [0x1DA] bool race_active Ball is in active race
+0xC28 [0x30A] char* display_name Ball name string (for score display)
+0xC5C [0x317] int battle_mode_flags Friction/speed modifiers for battle

Scene Struct: AI-Relevant Fields

Byte Offset Int Index Type Name Description
+0x237 [0x8D] bool battle_mode Set when in battle/multiplayer mode
+0x3A4C [0xE93] bool race_in_progress Some mode flag checked for target validity
+0x29D4 [0xA75] AthenaList ball_list List of all balls in the scene
+0x29D8 [0xA76] int ball_count Number of balls
+0x2DE0 [0xB78] void** ball_array Pointer array to ball objects

Key Functions

Address Name Role
0x40BCA0 CreateBadBall() Scans MESHWORLD for BADBALL objects, constructs Ball with NPC flags
0x408390 Ball_AI_Update() Vtable slot [0x10] — contains the chase AI logic
0x405E00 Ball_Update() Main physics tick, calls AI update if conditions met
0x456CD0 Ball_InitBattleMode() Sets battle parameters (speed, friction, battle flags)
0x401660 Ball_SetName() Sets display name (used for score announcements)
0x402650 Ball_ApplyForceWithMultipliers() Vtable [0x14] — applies directional force
0x458130 Math_FastDistance2D() Fast approximated 2D distance (no sqrt, used for target selection)
0x457DA0 Wave_Sin() Sine lookup for spin wobble
0x457DC0 Wave_Cos() Cosine lookup for spin wobble

RNG and "Randomness"

The AI uses the game's global RNG (FUN_004BA754) for two purposes:

  1. Target selection jitter: Random float < 0.5 gates whether the AI runs at all this frame
  2. Score event RNG: When an 8-ball hits a player, RNG determines score bonus and plays a random taunt sound
// When 8-ball hits a player ball:
float rng = RandomFloat();
if (rng < 0.5) {
    AddScore(2000);  // Bonus points
    PlayRandomTauntSound();  // "Gotcha!" etc.
    target_ball->score += rng * 100;  // Small random bonus
}

Decompilation Sources

All analysis is derived from raw Ghidra decompiled C:

  • analysis/ghidra/decompilations/ball/decomp_ball_vtable_0x408390.c — Main AI update logic
  • analysis/ghidra/decompilations/scene/decomp_createbadball.cCreateBadBall() function
  • analysis/ghidra/decompilations/ball/decomp_ball_vtable_update_physics.c — Ball struct field map
  • analysis/ghidra/decompilations/ball/decomp_ball_initbattlemode.c — Battle mode initialization

Modding Notes

To disable 8-balls in a level:

Option 1 — Remove from MESHWORLD: Delete the BADBALL object entries from the level file.

Option 2 — Patch at runtime: Set ball[0x31D] = 0 for all 8-ball instances. The AI block is skipped.

Option 3 — Patch the scene: Set scene[0x237] = 0 to disable battle mode AI entirely.

To make 8-balls more aggressive:

  • Increase CHASE distance (default is often 0 = unlimited)
  • Decrease HOME distance (makes them chase from farther spawn radius)
  • Increase SPINDISTANCE (makes them wobble more wildly)
  • Patch 0x408390 to remove the RNG gate (always chase instead of 50% chance)

To make 8-balls flee instead of chase:

Set their radius very small: ball[0xA1] = 5.0f (player radius is ~27). The flee branch triggers when 8ball_radius < target_radius * 0.9.


Verified Confidence Markers

Claim Marker Evidence
CreateBadBall scans for BADBALL string Raw C string compare in decomp_createbadball.c
CHASE/HOME/SIZE/SPINDISTANCE parsed Raw C __stricmp + atol chain in decomp_createbadball.c
ball[0x31D] gates AI execution Raw C if (param_1[0x31d] != 0) in decomp_ball_vtable_0x408390.c
ball[0x318..0x31A] = HOME position Copied from MESHWORLD spawn pos in CreateBadBall
ball[0x31E] = spin counter Incremented each frame, passed to Wave_Sin/Wave_Cos
Spin wobble uses SPINDISTANCE ball[799] (0xC7C) × sin/cos added to target
Flee when smaller radius if (8ball_radius < target_radius * 0.9) { negate direction }
Target validity checks 5-condition compound if in raw C
Force applied via vtable[0x14] (**(code **)(*param_1 + 0x14))(...) call
Math_FastDistance2D is approximate Raw C: max*0.96 + min*0.4 approximation

🔗 Related Documents

8-Ball Hit Detection Mod

types : mods
keywords :

📂 View source on GitHub


8-Ball Hit Detection Mod

Detects whenever a player ball collides with an 8-ball (NPC). Pure detection only — no gameplay changes.

How It Works

Hooks Ball_Update (0x405E00) at address 0x406FD1 — the start of the ball-ball collision scoring section. At this point in the code:

  • ESI = this ball (running Ball_Update)
  • EDI = other ball (collision partner)
  • Both balls are confirmed colliding (past the collision_type == 1 check)

The mod checks ball+0x18 (player_index) on both balls:

  • 0-3 = Player 1-4
  • -1 (0xFFFFFFFF) = NPC 8-ball

If exactly one ball is a player and the other is an 8-ball, it increments g_hit_count and appends a line to hitlog.txt in the game directory.

Uses pointer comparison (ESI < EDI) to avoid double-counting, since Ball_Update runs for both balls in a collision pair.

Log Output

The mod writes to hitlog.txt in the Hamsterball game directory. Example:

[Hit 1] Player 1 struck an 8-ball
[Hit 2] Player 1 struck an 8-ball
[Hit 3] Player 2 struck an 8-ball

Installation

  1. Rename the original bass.dllbass_real.dll in the Hamsterball directory
  2. Copy the mod's bass.dll to the same directory
  3. Launch Hamsterball.exe

Build

i686-w64-mingw32-gcc -shared -o bass.dll 8ball_hit_detect.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Technical Details

Item Value
Hook address 0x00406FD1
Original instruction fld dword [edi+0x284] (6 bytes)
Hook type JMP code cave (5-byte JMP + 1 NOP)
Activation delay 5 seconds after DLL load
Player index offset ball+0x18 (int: -1 = NPC, 0-3 = player)
Hit counter g_hit_count (volatile DWORD, readable via debugger)

Verification

Hook site bytes at 0x406FD1 (must match before patching):

D9 87 84 02 00 00

🔗 Related Documents

8-Ball Spawn Mod

types : tools
keywords :

📂 View source on GitHub


8-Ball Spawn Mod

Press B during gameplay to spawn an 8-ball in front of your hamster ball. The 8-ball spawns with physics and rolls/collides just like the balls in Rodent Rumble arenas.

What's New

  • Audio forwarding: All 10 BASS functions the game imports are forwarded to bass_real.dll. The game now has full audio while the mod is active.
  • Single-ball overwrite: Only one 8-ball can exist at a time. Pressing B again repositions the existing 8-ball to your current location and velocity — no new ball is created.
  • Dead AI fields documented: CHASE/HOME/SPINDISTANCE values are set but have no effect because is_8ball (ball+0xC74) is never set to 1. The ball is a physics-only debris ball.

Installation

  1. In your Hamsterball game folder, rename bass.dllbass_real.dll
  2. Copy the mod's bass.dll into the game folder
  3. (Optional) Copy 8ball_spawn.ini next to bass.dll for configuration
  4. Launch the game, enter a level or arena, press B to spawn an 8-ball

How It Works

The 8Ball mesh is preloaded by the game's resource loader into the board mesh array at board+0x268 (index 9 of the array at board+0x244). The ball mesh index field ball+0x754 controls which mesh the ball uses — setting it to 9 makes the ball render as the 8-ball.

Ball creation follows the same pattern as CreateBadBall (0x40BCA0):

  1. operator_new(0xC98) allocates ball memory
  2. Ball_ctor(mem, scene) constructs the ball
  3. vtable[1]() initializes physics defaults
  4. Position is set in front of the player's ball
  5. ball+0x754 = 9 selects the 8Ball mesh
  6. ball+0x18 = -1 sets player_index to none (debris ball)
  7. Player's exact velocity vector is copied (same direction and speed)
  8. Radius stays at Ball_InitPhysicsDefaults default (35.0)
  9. Ball is added to scene+0x29D4 (bad_balls_list) and scene+0x2DEC (all_balls_list)

Single-Ball Overwrite

If a previously spawned 8-ball is still valid (vtable check against 0x4CF3A0), pressing B again repositions it to the new spawn location with the player's current velocity. No new allocation is made — the same ball object is reused. If the old ball has been freed or corrupted, a fresh ball is allocated.

Audio Forwarding

The mod loads bass_real.dll on the first BASS function call (not in DllMain, to avoid loader lock deadlocks on real Windows). All 10 functions the game imports are forwarded with correct __stdcall calling conventions:

Function Forwarded
BASS_Init
BASS_Free
BASS_Start
BASS_Stop
BASS_SetConfig
BASS_ErrorGetCode
BASS_MusicLoad
BASS_MusicPlayEx
BASS_ChannelSetAttributes
BASS_ChannelStop

If bass_real.dll is not found, all functions return success values (1/0) — the game runs without audio but does not crash.

AI Fields (Dead Code)

The mod sets CHASE=100000, HOME=100000, SPINDISTANCE=1.0 on the spawned ball. However, these fields have no effect because:

  • ball+0xC74 (is_8ball flag) is never set to 1
  • Without that flag, Ball_Update (0x408390) skips the AI chase block entirely via if (ball[0x31d] != 0 || scene[0x237] != 0)
  • The ball behaves as a physics-only debris ball — it rolls, falls, and collides but never actively chases other balls

To enable AI behavior, ball+0xC74 would need to be set to 1. This would make the 8-ball chase the nearest player using the CHASE/HOME/SPINDISTANCE parameters.

Configuration

Edit 8ball_spawn.ini (placed next to bass.dll):

[8ball]
spawn_key=0x42       ; B key (default). See VK_* constants on MSDN
spawn_distance=40    ; Distance in front of player

Build

i686-w64-mingw32-gcc -shared -o bass.dll 8ball_spawn.c \
    -lwinmm -Wl,--enable-stdcall-fixup \
    -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Files

File Description
8ball_spawn.c Mod source code
8ball_spawn.ini Configuration file
bass.dll Compiled mod (proxy bass.dll)

Technical Notes

  • BASS_SetConfig/BASS_GetConfig are now forwarded to bass_real.dll (previously stubbed). The game calls BASS_SetConfig via SEH-protected code — forwarding with correct __stdcall convention works safely.
  • Scene/Board discovery uses brute-force App struct scan for the Scene vtable (0x4D0260), same approach as the player_clones mod.
  • The 8-ball has no AI — it spawns as a physics debris ball with player_index = -1. It rolls, falls, and collides with other balls and geometry.
  • load_real_bass() is called from spawn_thread (not DllMain) to avoid the Windows loader lock deadlock that affects LoadLibraryA calls during DLL initialization.

🔗 Related Documents

8ball_goal_fix

types : mods
keywords :

📂 View source on GitHub


8ball_goal_fix

Prevents crash when 8-ball (BadBall) touches an N:GOAL trigger.

Root Cause

BadBalls (8-balls) have player_index = -1 (set in Ball_ctor2 at 0x4039E0, never overwritten by CreateBadBall). When a BadBall hits an N:GOAL trigger, DispatchCollisionEvents (0x40C5D0) uses player_index as an array index:

addr = player_index * 0xA0 + App

With player_index = -1, this computes addr = App - 0xA0, then writes to three offsets: App+0x536, App+0x550, App+0x55C — corrupting App state and crashing the game.

Raptisoft already knew about this problem: the E:LIMIT handler in the same function (at 0x0040C785) has a CMP ECX,-1 / JZ skip guard. They just forgot to add the same guard to the N:GOAL and E:ACTION(SCORE) handlers.

Fix

Three code caves that replicate the E:LIMIT pattern: check player_index < 0 before per-player writes, and if so, skip to the safe code (camera repositioning, status strings — which don't use player_index).

Patch Address What it guards Skip target
1 0x0040CF64 N:GOAL "finished" flag (App+0x5D6, App+0x5FC) 0x0040CFA0 (camera)
2 0x0040D03A N:GOAL "show results" flag (App+0x5F0) 0x0040D05E (epilogue)
3 0x0040CA33 E:ACTION(SCORE) score write (App+0x5E4) 0x0040CA74 (loop continue)

Result

  • 8-ball crossing goal: goal music plays, camera repositions, board flag set ✅
  • Player crossing goal after: race finishes normally with results screen ✅
  • No crash ✅
  • Identical visible behavior to original game minus the crash ✅

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy this mod's bass.dll into the game folder
  3. Launch Hamsterball.exe

Build

i686-w64-mingw32-gcc -shared -o bass.dll 8ball_goal_fix.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Crash Test

Passed via hbtestd (35s survival, process alive). Tested on Wine/Xvfb/llvmpipe.


🔗 Related Documents

A1 - __thiscall Convention for

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/A1-THISCALL.md)


A1 - __thiscall Convention for Hamsterball

All member functions (constructors, destructors, vtable methods) in Hamsterball.exe use __thiscall.

Detection

Prologue pattern: PUSH ECX; PUSH ESI; MOV ESI, ECX saves this from ECX.

Correct Usage

typedef void* (__thiscall *Ball_ctor_fn)(void* thisPtr, int scene);
auto BallCtor = (Ball_ctor_fn)0x40AFE0;
void* ball = BallCtor(allocatedMem, (int)scene);

// vtable[1] init defaults
auto InitPhysics = (void (__thiscall *)(void*))0x405100;
InitPhysics(ball);

Constructor Chain

  • Ball_ctor (0x40AFE0) is the public constructor for badballs.
  • It calls Ball_ctor2 (0x4039E0) internally, then overwrites the vtable to the live Ball vtable (0x4CF3A0).
  • Do NOT call Ball_ctor2 manually when using Ball_ctor.

Heap Allocation

Avoid operator_new from injected code. Use:

HANDLE hCrtHeap = *(HANDLE*)0x005369C0;
void* mem = HeapAlloc(hCrtHeap, 0, 0xC98);

Compute runtime address with GetModuleHandle(NULL) + RVA for ASLR safety.


🔗 Related Documents

A2 - Object Spawning and Level

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/A2-OBJECT-SPAWNING.md)


A2 - Object Spawning and Level Gating

See also docs/LEVEL_LOCKED_OBJECTS.md.

Self-Loading Objects

Constructor calls MeshWorld_ctor(..., "levels\\level5-bonk") with a hardcoded path.

Object Constructor File
Bonk 0x438850 levels\level5-bonk
Bumper 0x40FA20 levels\level8
Bumper2 0x413CE0 levels\arena-beginner

Enable on any level by patching app+0x23C to non-zero and adding the object name to the MESHWORLD file.

Scene-Dependent Objects

Constructor takes a pre-loaded mesh pointer from the scene struct. The pointer is populated only by a specific BoardLevel*_ctor.

Object Constructor Scene Offset Loaded By
Tipper 0x437960 scene+0x4394/0x4398 BoardLevel3
Catapult 0x437E10 scene+0x5848 BoardLevel
Gluebie 0x437CB0 scene+0x607C BoardLevel3
BlockDawg 0x43C310 scene+0x5840/5844 BoardLevel
BreakBridge 0x436D70 scene+0x5410/5414 BoardLevel
PopCylinder 0x436EE0 scene+0x5420 BoardLevel

To add these to a level, both app+0x23C and the level constructor sub-mesh load must be patched.


🔗 Related Documents

A3 - [[21709218365663|camera system]] Pitfalls

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/A3-CAMERA-PITFALLS.md)


A3 - Camera System Pitfalls

Scene+0x3F1C (path_follow_mode) is not a camera smoothing toggle. It is a pointer check for a CAMERALOCUS object; if the object exists, the camera target blends along a spline. If absent, the camera target is the ball. The perceived "smooth follow" comes from the ball's display position interpolation, not this flag.

Actual camera parameters:

Offset Name Effect
0x29BC camera_orbit_angle Fixed orbit direction (cos, 0.9, sin)
0x29C0 camera_orbit_dist Distance from target to eye

Original Scene_SetCamera places the camera at:

orbit_dir = (cos_a, 0.9, sin_a);
eye = target + normalize(orbit_dir) * orbit_dist;

In reimpl, Level1/2 work with this sign; Level3 requires the opposite sign because its geometry lies mostly at Z < ball.Z. An adaptive sign heuristic or level-specific override is needed.


🔗 Related Documents

Adding New Controls to the Rem

types : modding
keywords :

📂 View source on GitHub


Hamsterball — Adding New Controls to the Remap Menu (DLL Modding Guide)

Scope: Original Hamsterball.exe (PE32, i386, Athena engine).
Method: MinHook-based DLL injection into live binary.
Last Updated: 2026-06-02

Table of Contents

  1. What This Guide Covers
  2. Architecture Summary
  3. Prerequisite: Existing Knowledge
  4. The Six Hook Points
  5. Hook Point 1 — Render Extra Control Rows
  6. Hook Point 2 — Menu Selection Bounds
  7. Hook Point 3 — Key Capture (Remap)
  8. Hook Point 4 — Per-Frame Input Polling
  9. Hook Point 5 — Action Execution
  10. Hook Point 6 — Registry Persistence
  11. Complete Example: Adding a "Brake" Key
  12. MinHook Skeleton
  13. Address Quick Reference

What This Guide Covers

The original game hardcodes exactly four directional controls (left/right/up/down) in the remap menu, the registry, the physics code, and the UI renderer. Adding a fifth control (e.g. brake, jump, camera reset) requires six coordinated hook points working together. This document gives you every address, offset, and C++ snippet you need.


Architecture Summary

The game’s input pipeline is single-threaded:

InputDevice_PollAndRelease (0x46EBD0)
    → fills 256-byte DI8 state buffer at InputDevice+0x0C

Ball_GetInputForce (0x46EC30)
    → reads 4 DIK codes from InputDevice+0x50C..0x518
    → writes (force_x, force_y) into Ball+0x170 (velocity)

OptionsMenu_RenderControls (0x42E840)
    → draws 4 rows of control icons at hard-coded Y intervals

The 4-key limit is baked into:

  • UI loop: iterates i = 0..3
  • Registry: only keys CONTROL1..CONTROL4
  • Struct: InputDevice+0x50C is the start of a fixed 4-slot array
  • Physics: Ball_GetInputForce only checks those 4 offsets

Your DLL must intercept all four layers.


Prerequisite: Existing Knowledge

Before reading this guide you should already understand:

  • InputDevice layout (size 0x91C, DIK codes at +0x50C/+0x510/+0x514/+0x518)
  • Ball layout (velocity at +0x170, acceleration at +0x2B8)
  • App+0xB28..0xB34 stores the 4 CONTROL DWORDs
  • OptionsMenu_RenderControls draws the remap UI
  • The game uses DirectInput 8 exclusively (single import at 0x47C7F0)

If any of the above is unfamiliar, read docs/INPUT_SYSTEM.md first.


The Six Hook Points

# Hook Target Address What You Change
1 OptionsMenu_RenderControls 0x42E840 Draw rows 5+ below the original 4
2 Menu selection bounds check find via xref Change max index from 3 → N-1
3 Key capture / remap writer find via xref Redirect index ≥4 to your custom storage
4 InputDevice_PollAndRelease 0x46EBD0 After original poll, scan your custom DIK codes
5 Ball_GetInputForce 0x46EC30 Apply custom action (e.g. decelerate)
6 Registry save/load 0x4284C0 Read/write extra keys alongside CONTROL1-4

Note: Hook points 2 and 3 do not have a single canonical address because they are inlined into the menu state-machine. We describe how to locate them with a pattern search.


Hook Point 1 — Render Extra Control Rows

Target

OptionsMenu_RenderControls at 0x42E840.

What the original does

The function iterates a 4-item loop. For each slot i = 0..3 it:

  1. Reads App+0xB28 + i*4 to get the bound device type
  2. Draws the action name ("LEFT", "RIGHT", "UP", "DOWN")
  3. Draws the bound key icon
  4. If two slots share the same device, colours one red

Your hook strategy

Do not patch the loop count. Instead, let the original 4 rows render completely, then in your post-hook draw rows 5+ using the same coordinate math.

The vertical spacing between rows is a constant pixel offset (read from the original assembly or infer from the decompilation). In the original, each row increments Y by roughly 32 pixels.

C++ Hook Body

typedef void (__thiscall *tOptionsMenu_RenderControls)(void* pMenu);
tOptionsMenu_RenderControls oRenderControls;

void __fastcall hkRenderControls(void* pMenu) {
    // 1. Let original draw the 4 built-in rows
    oRenderControls(pMenu);

    // 2. Append custom rows
    //    pMenu is the OptionsMenu/SimpleMenu object.
    //    The renderer uses a coordinate system where (0,0) is top-left.
    //    Original row 3 ends at Y ≈ base_y + 3*32.
    //    We start custom rows at Y ≈ base_y + 4*32.
    for (int i = 0; i < g_CustomControlCount; ++i) {
        int rowY = g_BaseControlY + (4 + i) * g_RowHeight;

        // Draw action name (e.g. "BRAKE")
        DrawMenuString(pMenu, g_CustomControls[i].name,
                       g_NameColumnX, rowY,
                       g_NormalColor);

        // Draw bound key name (e.g. "SPACE")
        const char* keyName = DIKToString(g_CustomControls[i].dik);
        DrawMenuString(pMenu, keyName,
                       g_KeyColumnX, rowY,
                       g_NormalColor);
    }
}

Where to get the draw helper

The game already has a string-rendering helper used by OptionsMenu_RenderControls. Look for the call to Font_DrawString or AthenaString_Render inside the original function and call the same address from your hook. The exact helper address varies by build; find it by reading the first few xrefs inside 0x42E840.


Hook Point 2 — Menu Selection Bounds

Target

The menu navigation code that clamps the selected row index.

How to find it

  1. In Ghidra, xref OptionsMenu_RenderControls (0x42E840) backwards.
  2. Look for the function that calls it — this is the menu update/tick function (likely named OptionsMenu_Update or simply a vtable slot on SimpleMenu).
  3. Inside that function, search for an immediate comparison against 3 or 4 near code that reads keyboard input (DIK_UP / DIK_DOWN).
  4. The pattern looks like:
    cmp  eax, 3        ; or cmp eax, 4
    jle  already_valid
    mov  eax, 3        ; clamp to max
    

Patch strategy

Option A — Inline patch (simplest)
Change the immediate 3 to N-1 (e.g. 4 if you added 1 custom control). This is a single-byte patch at the comparison instruction.

Option B — Hook the update function
Hook the entire menu update vtable slot, call original, then if the returned index is ≥4 verify it against your custom count instead of the hardcoded 4.

Warning: The menu also highlights the selected row with a flashing colour. Make sure the highlight-draw code uses the same index bounds; it is usually right after the clamp check.


Hook Point 3 — Key Capture (Remap)

Target

The code that captures a keypress during remap mode and stores the DIK code.

How to find it

When the user clicks a control row, the game enters a "waiting for key" state. During this state it polls the DI8 buffer (InputDevice+0x0C) for any key that transitions from 00x80. Once found, it stores that DIK code into InputDevice+0x50C + index*4.

Search in the same menu update function for:

  • A write to [reg+0x50C] or [reg+0x510]
  • The index register is usually eax or ecx holding 0..3
  • The value being written is read from InputDevice+0x0C + DIK

Patch strategy

Hook the write instruction. If index < 4, pass through to original. If index >= 4, write into your own array instead.

C++ Hook Body (inline detour at the writer)

// Global storage for custom bindings (max 8 extra controls)
struct CustomBinding {
    BYTE dik;        // DIK code (e.g. DIK_SPACE = 0x39)
    bool isDown;     // polled state
};
CustomBinding g_CustomControls[8];
int g_CustomControlCount = 1;  // e.g. 1 = only Brake

// Hook target: the instruction that does
//   mov [InputDevice+0x50C+index*4], newDik
void __stdcall hkWriteBinding(int pInputDevice, int index, BYTE newDik) {
    if (index < 4) {
        // Original 4 controls — write to game memory
        *(BYTE*)(pInputDevice + 0x50C + index*4) = newDik;
    } else {
        // Custom control — write to our array
        int customIdx = index - 4;
        if (customIdx < g_CustomControlCount) {
            g_CustomControls[customIdx].dik = newDik;
        }
    }
}

Duplicate-key check: The original renderer flags duplicate bindings in red. If you want the same behaviour for custom keys, also hook the duplicate-check loop (inside OptionsMenu_RenderControls) and extend it to scan your custom array.


Hook Point 4 — Per-Frame Input Polling

Target

InputDevice_PollAndRelease at 0x46EBD0.

Why hook here

This is the only place the game touches DirectInput each frame. By hooking it, your custom keys are sampled at the exact same moment as the built-in keys, with the same device-state semantics (acquire/release, cooperative level, etc.).

What the original does

void InputDevice_PollAndRelease(int self) {
    // Keyboard
    int didev = *(int*)(self + 0x434);
    if (didev) {
        IDirectInputDevice8_GetDeviceState(didev, 0x100, self + 0x0C);
    }
    // ... gamepad polling omitted
}

Your hook strategy

Call the original, then read the same 256-byte buffer (self+0x0C) for your custom DIK codes.

C++ Hook Body

typedef void (__thiscall *tPollAndRelease)(void* pDevice);
tPollAndRelease oPollAndRelease;

void __fastcall hkPollAndRelease(void* pDevice) {
    // 1. Let game poll keyboard + gamepads normally
    oPollAndRelease(pDevice);

    // 2. Read the freshly-polled DI8 buffer for our custom keys
    BYTE* dikBuffer = (BYTE*)pDevice + 0x0C;

    for (int i = 0; i < g_CustomControlCount; ++i) {
        BYTE dik = g_CustomControls[i].dik;
        g_CustomControls[i].isDown = (dikBuffer[dik] & 0x80) != 0;
    }
}

Do NOT create a separate thread. The game is single-threaded; a background GetAsyncKeyState loop would race with the physics update. Always sample input inside the game's own poll hook.


Hook Point 5 — Action Execution

Target

Ball_GetInputForce at 0x46EC30.

What the original does

Reads the 4 directional keys and builds a 2-D force vector:

// Inside Ball_GetInputForce case 1 (keyboard):
if (key_state[left_dik]  & 0x80) force_x -= 1.0f;
if (key_state[right_dik] & 0x80) force_x += 1.0f;
if (key_state[up_dik]    & 0x80) force_y -= 0.5f;
if (key_state[down_dik]  & 0x80) force_y += 1.0f;
// Then writes scale*force to Ball+0x170 (velocity)

Your hook strategy

Call the original function so the 4 built-in directions keep working, then apply your custom action by mutating the Ball struct directly.

C++ Hook Body — Brake Example

typedef void (__thiscall *tBallGetInputForce)(void* pBall, float* outVec);
tBallGetInputForce oBallGetInputForce;

void __fastcall hkBallGetInputForce(void* pBall, float* outVec) {
    // 1. Original physics force from 4 directional keys
    oBallGetInputForce(pBall, outVec);

    // 2. Apply brake if custom key is held
    if (g_CustomControls[0].isDown) {  // assuming index 0 = Brake
        float* vel = (float*)((char*)pBall + 0x170);

        // Simple friction model: scale velocity by 0.85 each frame
        vel[0] *= 0.85f;
        vel[1] *= 0.85f;
        vel[2] *= 0.85f;

        // Optional: also reduce acceleration so the ball stops trying to speed up
        float* accel = (float*)((char*)pBall + 0x2B8);
        accel[0] = 0.0f;
        accel[1] = 0.0f;
        accel[2] = 0.0f;
    }
}

Other action ideas

Action What to mutate Notes
Camera snap Ball+0xC88 matrix or Scene camera offsets Rotates view to behind-the-ball
Power-up trigger Call existing power-up function via its address Find the power-up activate function in FUNCTION_MAP.md
Jump Ball+0x170 Y component Hard — original physics assumes ground contact; you may need to also hook collision logic
Reset ball Ball+0x164 position Teleport to last checkpoint (read from Ball+0x2DC)

Important: Jump is the hardest action because the ball physics does not have an "airborne" state. Simply adding Y-velocity will make the ball float through floors unless you also disable ground-collision snapping while the jump is active. This requires a second hook in the collision resolver.


Hook Point 6 — Registry Persistence

Target

App_SaveAllConfig at 0x4284C0 (save) and the registry-read code inside InputHandler_ctor / App_Initialize_Full (load).

What the original does

Save:

RegKey_WriteDWORD(reg, "2PController1", *(int*)(app + 0xB28));
RegKey_WriteDWORD(reg, "2PController2", *(int*)(app + 0xB2C));
RegKey_WriteDWORD(reg, "2PController3", *(int*)(app + 0xB30));
RegKey_WriteDWORD(reg, "2PController4", *(int*)(app + 0xB34));

Load:
The 4 DWORDs are read during InputHandler construction and stored into the same App+0xB28..0xB34 slots.

Your hook strategy

Option A — Hook save/load directly
Hook App_SaveAllConfig: after the original 4 RegKey_WriteDWORD calls, add your own:

for (int i = 0; i < g_CustomControlCount; ++i) {
    char keyName[32];
    sprintf(keyName, "CustomControl%d", i+1);
    RegKey_WriteDWORD(reg, keyName, g_CustomControls[i].dik);
}

And mirror this on load by hooking the load function (or reading the same keys in your DllMain after the game has initialised).

Option B — External INI (recommended)
Instead of fighting the registry, save your custom bindings to an INI file next to the EXE:

// In DllMain or at first attach
WritePrivateProfileStringA("CustomControls", "Brake",
                           DIKToString(g_CustomControls[0].dik),
                           ".\hamsterball_mod.ini");

This avoids registry permission issues and makes portable installs easier.


Complete Example: Adding a "Brake" Key

Step-by-step checklist

  1. Allocate a new UI row

    • Hook OptionsMenu_RenderControls (0x42E840).
    • After the original 4 rows, draw "BRAKE" and the bound key name.
  2. Extend selection bounds

    • Patch the cmp eax, 3 in the menu update function to cmp eax, 4.
  3. Capture a key for Brake

    • Hook the DIK writer inside the menu update.
    • If index == 4, store DIK into g_CustomControls[0].dik.
  4. Poll the Brake key every frame

    • Hook InputDevice_PollAndRelease (0x46EBD0).
    • After original poll, check dikBuffer[g_CustomControls[0].dik] & 0x80.
  5. Apply braking force

    • Hook Ball_GetInputForce (0x46EC30).
    • If g_CustomControls[0].isDown, scale Ball+0x170 velocity by 0.85.
  6. Save the binding

    • On DLL detach (or in response to an in-game save event), write g_CustomControls[0].dik to hamsterball_mod.ini.

MinHook Skeleton

#include <windows.h>
#include <MinHook.h>

// ---------- global state ----------
struct CustomControl {
    const char* name;
    BYTE        dik;
    bool        isDown;
};
CustomControl g_CustomControls[8];
int g_CustomControlCount = 0;

// ---------- typedefs ----------
typedef void (__thiscall *tRenderControls)(void*);
typedef void (__thiscall *tPollAndRelease)(void*);
typedef void (__thiscall *tBallGetInputForce)(void*, float*);

tRenderControls    oRenderControls;
tPollAndRelease    oPollAndRelease;
tBallGetInputForce oBallGetInputForce;

// ---------- hook bodies ----------
void __fastcall hkRenderControls(void* pMenu) { /* ... see section 5 ... */ }
void __fastcall hkPollAndRelease(void* pDev)    { /* ... see section 7 ... */ }
void __fastcall hkBallGetInputForce(void* pBall, float* out)
                                                { /* ... see section 8 ... */ }

// ---------- dll entry ----------
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_ATTACH) {
        MH_Initialize();

        MH_CreateHook((LPVOID)0x42E840, hkRenderControls,
                      (LPVOID*)&oRenderControls);
        MH_CreateHook((LPVOID)0x46EBD0, hkPollAndRelease,
                      (LPVOID*)&oPollAndRelease);
        MH_CreateHook((LPVOID)0x46EC30, hkBallGetInputForce,
                      (LPVOID*)&oBallGetInputForce);

        // Enable all hooks
        MH_EnableHook(MH_ALL_HOOKS);
    }
    return TRUE;
}

Compile with: MSVC or MinGW, linking against minhook.lib (or libminhook.a). Inject with your preferred loader (e.g. dinput8.dll proxy, xinput1_3.dll proxy, or an external injector like Process Hacker).


Address Quick Reference

Symbol Address Notes
OptionsMenu_RenderControls 0x42E840 Draws 4 control rows; hook post-render
OptionsMenu_ctor 0x442CE0 Builds menu; useful for finding vtable
UIList_AddItem 0x4497F0 Menu item constructor
InputDevice_PollAndRelease 0x46EBD0 Polls DI8; hook for custom key sampling
InputDevice_ctor 0x466620 Sets default DIK codes
Ball_GetInputForce 0x46EC30 Physics force builder; hook for custom actions
Input_IsKeyDown 0x46E0B0 Generic key-state check
App_SaveAllConfig 0x4284C0 Registry save; hook for custom persistence
App_Initialize_Full 0x429530 Game init; creates InputHandler+devices
DirectInput8Create 0x47C7F0 Only DI8 import
InputDevice+0x0C 256-byte DI8 keyboard state buffer
InputDevice+0x50C key_left DIK code
InputDevice+0x510 key_right DIK code
InputDevice+0x514 key_up DIK code
InputDevice+0x518 key_down DIK code
Ball+0x170 velocity (Vec3)
Ball+0x2B8 acceleration (Vec3)
Ball+0x164 position (Vec3)
Ball+0x2DC last_checkpoint index
App+0xB28 CONTROL1 DWORD
App+0xB2C CONTROL2 DWORD
App+0xB30 CONTROL3 DWORD
App+0xB34 CONTROL4 DWORD

Common Pitfalls

  1. Do not resize the InputDevice struct. It is 0x91C bytes with hardcoded offsets throughout the EXE. Store custom keys in your own DLL globals.

  2. Do not use a background thread for input. The game is single-threaded; race with physics will corrupt Ball+0x170.

  3. The original remap UI does not exist for rows 5+. You must draw them yourself; copy the same font/colour the game uses for rows 1-4.

  4. Duplicate-key detection is hardcoded to 4 slots. If you want red-warning for conflicting custom keys, extend the duplicate-check logic in your OptionsMenu_RenderControls hook.

  5. Registry keys beyond CONTROL4 do not exist in vanilla. Use an external INI or add keys under a different sub-key (e.g. HKCU\Software\HamsterballMod) to avoid clobbering the game's save format.

  6. Joystick mode complicates things. If a player binds your custom action to a joystick button, you must also poll the gamepad state array (at InputHandler+0x40 gamepad array). The DI8 keyboard buffer will not contain joystick buttons.


Version History

Date Change
2026-06-02 Initial document — 6 hook points, brake example, MinHook skeleton

🔗 Related Documents

Agent Knowledge Package

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/INDEX.md)


Agent Knowledge Package - Hamsterball Reverse Engineering

This folder is a self-contained bootstrapping guide for any future agent that needs to reproduce or continue the Hamsterball RE work.

Start here and read the numbered sections in order. Each file is designed to be actionable on its own.

# File Purpose
1 01-BOOTSTRAP.md Set up workspace, acquire assets, run first binary analysis
2 02-GHIDRA-SETUP.md Import binary, start GhidraMCP headless, verify server
3 03-RENAME-RESTORE.md Import the 975+ function renames from FUNCTION_MAP.md
4 04-DECOMP-WORKFLOW.md How to decompile, clean, and verify code faithfully
5 05-STRUCT-VERIFICATION.md Methodology for confirming struct offsets
6 06-MODDING-PATTERNS.md Common game RE patterns (dead code, vtables, level gating)
7 07-REIMPL-LESSONS.md All lessons learned from reimplementation attempts
8 08-TROUBLESHOOTING.md Known failure modes and workarounds
9 scripts/ Reusable Python helpers
10 templates/ Copy-paste decompile/verify script templates

External Authorities

  • The repo's docs/FUNCTION_MAP.md is the source of truth for named functions.
  • The repo's analysis/ghidra/structs/*.h contain C struct definitions exported from Ghidra.
  • docs/STRUCTS_AND_TYPES.md is the human-facing struct reference (but verify offsets with raw decompilation before trusting).

First Command Checklist

cd ~/hamsterball-re
file originals/installed/extracted/Hamsterball.exe
python3 docs/agent-knowledge/scripts/check_server.py

🔗 Related Documents

ai_8ball_fix

types : mods

📂 View source on GitHub


ai_8ball_fix

Fixes 8-ball AI so it moves in races

Files

  • ai_8ball_full_v3.c — C source code
  • bass.dll — Compiled DLL (PE32 i386)

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

App::Initialize Function Decom

types : decompilation
keywords :

📂 View source on GitHub


App::Initialize Function Decomposition (0x00429530)

Function Signature

void App::Initialize(App* this, int hInstance, int nShowCmd);
// Called from WinMain at 0x4278EF
// Object pointer (this) in ECX = 0x4FD680 (global App instance)
// Stack args: hInstance (arg_4h), nShowCmd (arg_ch)
// Debug string stored at this+0x208 after each step

Initialization Sequence (CONFIRMED from r2 disassembly)

Step 1: App::Initialize(1) at 0x4D2B24

  • Stores debug string "[Initialize(1)]" at this+0x208
  • Calls fcn.0046BB40 (base init) with hInstance and nShowCmd
  • Gets this->graphics (this+0x174)
  • Sets this->graphics->initialized = true (this->graphics+0x7D1)

Step 3: App::Initialize(3) at 0x4D2B08

  • Loads blank cursor: LoadCursorA(NULL, "BLANKCURSOR")
  • Stores cursor handle at this+0x240

Step 4: App::Initialize(4) at 0x4D2AF8

  • Calls virtual function at this->+vtable+0x8C(140) with params 800, 600
    • This is likely Graphics::SetDisplayMode(800, 600)
    • this+0x240 = cursor handle
    • Push 0x258 (600) + Push 0x320 (800)

Step 5: App::Initialize(5) at 0x4D2AE8

  • Checks this->graphics (this+0x174)
  • If NULL: sets debug string "** No Graphics **" at 0x4D2AD4
  • Checks this->graphics->d3d_device (this->graphics+0x154)
  • If NULL: sets debug string "** No Graphics Device **" at 0x4D2AB8
  • If graphics OK: calls d3d_device->vtable+0xC8(200) with params
    • This is likely IDirect3DDevice8::SetRenderState(D3DRS_AMBIENT, ...)
    • Or IDirect3DDevice8::SetTransform(D3DTS_WORLD, ...)
    • Pass 2 or 3 for lighting mode

Step 6: App::Initialize(6) at 0x4D2A9C

  • Loads shadow texture: fcn.00455C50(this->graphics, "shadow.png", 1)
    • fcn.00455C50 is likely Graphics::LoadTexture()
    • Result stored at this+0x278

Step 7: App::Initialize(7) at 0x4D2A7C

  • Loads music: fcn.004743F0("music\music.mo3")
    • fcn.004743F0 is Music::Load("music\music.mo3")
    • Result stored at this->music (this+0x534)
    • this->music_channel1 (this+0x53C) = 0
    • this->music_channel2 (this+0x538) = 0

Step 8: App::Initialize(8) at 0x4D2A6C

  • If music loaded successfully (this->music != 0):
    • Loads Jukebox: fcn.0046A4D0(this->music, "jukebox.xml")
    • Calls fcn.0046A3C0(this->music) -> this->music_channel1 (this+0x53C)
    • Calls fcn.0046A3C0(this->music) -> this->music_channel2 (this+0x538)

Step 9: App::Initialize(9) at 0x4D2A50

  • Continues music initialization

Step 10: App::Initialize(10) at 0x4D2A40

  • Continues music initialization

Step 11: App::Initialize(11) at 0x4D2A30

  • Music setup continues

Step 12: App::Initialize(12) at 0x4D2A20

  • Calls fcn.00472EC0(this->config) — likely Config::Load()

Step 13: App::Initialize(13) at 0x4D2A10

  • Reads "PlayCount" from config: fcn.00473170(this->config, "PlayCount")
    • Check if key exists
    • If exists: read PlayCount value -> this+0x914
    • This is for shareware trial mode

Continues with more initialization...

App Object Structure (INFERRED from offsets)

struct App {
    void* vtable;              // +0x00
    HWND hwnd;                  // +0x04
    /* ... */
    void* unknown_08;           // +0x08 through +0x50
    /* ... */
    Graphics* graphics;         // +0x174
    /* ... */
    char debug_str[256];        // +0x208 (stores init step name)
    /* ... */
    HCURSOR blank_cursor;       // +0x240
    /* ... */
    int display_width;          // +0x254 (800)
    int display_height;         // +0x258 (600)
    /* ... */
    void* shadow_texture;       // +0x278
    /* ... */
    MusicPlayer* music;          // +0x534
    int music_channel1;          // +0x53C
    int music_channel2;          // +0x538
    /* ... */
    int play_count;             // +0x914
    /* ... */
    int running;                // +0x159 (set to 1 to quit)
    int target_fps;             // +0x170 (used for Sleep calcs)
    int frame_count;            // +0x164 (for FPS calculation)
    int last_tick;              // +0x168 (for delta time)
    Config* config;             // +0x54
};

Key Sub-Functions

Address Likely Name Notes
0x0046BB40 App::BaseInit() Called with hInstance, nShowCmd
0x00455C50 Graphics::LoadTexture() Loads shadow.png and textures
0x004743F0 Music::Load() Loads music.mo3
0x0046A4D0 Music::LoadJukebox() Loads jukebox.xml
0x0046A3C0 Music::CreateChannel() Creates playback channels
0x00472EC0 Config::Load() Loads configuration
0x00473170 Config::HasKey() Checks if config key exists
0x00473080 Config::GetInt() Gets integer config value
0x00455380 Graphics::Initialize() Creates D3D8 device
0x0046BD80 app::run() Game loop (PeekMessageA based)
0x0046BA10 App::Shutdown() Sets this->running = 1, calls vtable+8

🔗 Related Documents

App::Run

types : decompilation
keywords :

📂 View source on GitHub


App::Run - Game Loop Decomposition (0x0046BD80)

Function Signature

void App::Run(App* this);  // ECX = App* (0x4FD680)
// Called from WinMain at 0x4278F9

Key App Structure Offsets

// this+0x159: running flag (1 = quit, 0 = running)
// this+0x15A: active flag (1 = game active, 0 = paused/minimized)
// this+0x158: minimized flag
// this+0x164: last_tick (GetTickCount value)
// this+0x168: frame_time (1000 / this->0x170 = ms per frame)
// this+0x170: target_fps (e.g., 60)
// this+0x174: graphics pointer
// this+0x18C: frame counter (for FPS tracking)
// this+0x194: frame_count (for display)
// this+0x1AC: show_fps flag

Pseudocode

void App::Run(App* this) {
    // Initialize timing
    int frame_time = 1000 / this->target_fps;   // this+0x170 = FPS target
    HMODULE kernel32 = GetTickCount;
    int last_tick = GetTickCount();
    this->last_tick = last_tick;
    
    if (this->running) goto shutdown;  // this+0x159
    
    push edi;
    
    // === MAIN GAME LOOP ===
    while (1) {
        Sleep(0);  // Yield to other processes
        
        // Calculate frame timing
        int tick_interval = 1000 / this->fps_counter;  // this+0x16C
        this->debug_str = "Background";  // this+0x208
        this->frame_time_ms = tick_interval;  // this+0x168
        
        int current_tick = GetTickCount();
        
        // FPS counting
        if (current_tick > global_time_limit) {  // 0x5341E4
            if (this->show_fps) {  // this+0x1AC
                sprintf(this->fps_buffer, "%d", this->frame_count);  // this+0x198
            }
            this->frame_count = 0;  // this+0x194
            global_time_limit = GetTickCount() + 1000;
        }
        
        // === MESSAGE PUMP ===
        MSG msg;
        while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
            if (this->running) goto shutdown;
            TranslateMessage(&msg);
            DispatchMessageA(&msg);
        }
        
        if (this->running) goto shutdown;
        
        // === TIMING CHECK ===
        // Only update/draw if enough time has passed
        int now = GetTickCount();
        int elapsed = now - this->last_tick;
        int min_interval = this->frame_time_ms - 5;
        
        if (elapsed < min_interval) {
            // Too early - skip frame or count skipped frames
            this->skip_count++;  // var_10h
            if (this->skip_count >= 10) {
                // Too many skipped frames - force update
                goto force_update;
            }
            
            // === UPDATE FRAME ===
            this->frame_counter++;  // this+0x18C
            Graphics::BeginScene(this->graphics);  // fcn.00453B50
            this->vtable->Update(this);              // vtable+0x20
            this->last_tick += this->frame_time_ms;
            // Cap delta time at 1000ms
            if (now - this->last_tick > 1000) {
                this->last_tick = now;  // Reset if too far behind
            }
            this->rendered_frames++;
        } else {
            // === RENDER FRAME ===
            this->debug_str = "Draw";  // this+0x208
            
            if (this->graphics && !this->paused && !this->minimized) {
                this->frame_count++;  // this+0x194
                Graphics::BeginScene(this->graphics);  // fcn.00453B50
                this->vtable->Update(this);              // vtable+0x20
                this->vtable->Draw(this);                // vtable+0x24
                this->vtable->Flip(this);                 // vtable+0x28
                // Present (Graphics::Present or D3D Present)
                Graphics::Present(this->graphics, 1);    // fcn.00455A90
            }
            
            this->last_tick = GetTickCount();
        }
    }
    
shutdown:
    // Cleanup
    destructor(var_34);
    return;
}

Virtual Table Methods

The game uses a vtable at this->vtable with key methods:

  • vtable+0x08: Shutdown (from App::Shutdown at 0x46BA10)
  • vtable+0x20 (32): Update — Game logic per frame
  • vtable+0x24 (36): Draw — Rendering per frame
  • vtable+0x28 (40): Flip — Buffer swap / present
  • vtable+0x2C (44): Additional render step
  • vtable+0x8C (140): SetDisplayMode (called during init with 800, 600)

Key Sub-Functions

Address Name Description
0x453B50 Graphics::BeginScene Prepares scene for rendering
0x455A90 Graphics::Present Presents frame buffer
0x457AD0 (constructor?) Called at function start
0x457A40 (destructor?) Called at function end
0x4BAE43 sprintf Used for FPS display

Timing Model

  • Target FPS stored at this+0x170
  • Frame time = 1000 / FPS (stored at this+0x168)
  • Skip frames if elapsed < frame_time - 5ms
  • Force update after 10 consecutive skips (cap at ~10 physics updates per render)
  • Global time limit at 0x5341E4 for FPS counter reset

Frame Rate Control

  • 1000 / this->fps gives ms per frame (typically 16ms for 60fps)
  • Skip tolerance: frame_time - 5 ms (e.g., 11ms at 60fps)
  • Maximum 10 skip updates before forcing a render
  • Delta time capped at 1000ms to prevent huge physics jumps

🔗 Related Documents

Arena Hazard System (RumbleBoa

types : gameplay
keywords :

📂 View source on GitHub


Arena Hazard System (ArenaBoard Objects)

The ArenaBoard arenas contain interactive hazards created by the master factory
function CreateExpertLevelObjects (0x40E250). Despite its name, this function creates
ALL arena object types, not just sawblades.

Object Factory: CreateExpertLevelObjects (0x40E250)

Dispatches on name prefix (case-insensitive __strnicmp). Only creates objects
when App+0x23C != 0 (Frenzied difficulty check for tournament-only objects).

Object Type Table

Name Prefix Class Size Ctor Address Storage
"BONK" Bonk 0x1200 Bonk_ctor this+0x2578 list, this+0x436C ptr
"SAW" (3 chars) TowerLevel 0x1188 TowerLevel_Ctor this+0x2578 list
"SAWBLADE" Sawblade_Level 0x111C Sawblade_Level_Ctor this+0x2578 list, this+0x4370/4374
"BRIDGE" Spinner_Level 0x10FC Spinner_Level_ctor this+0x4380/4798 lists
"JUDGE" Gear_Level 0x1100 Gear_Level_ctor this+0x4BBC list
"BELL" Tipper_Level 0x10E8 Tipper_Level_Ctor this+0x2578 list, this+0x4FD4 ptr

Name Suffix Modifiers

Objects can be named with suffix modifiers that change behavior:

Suffix Object Type Effect Offset Modified
"SLOW" TowerLevel Slow speed mode obj+0x43B = 1
"SUPER" TowerLevel Super mode obj+0x10ED = 1
"UP" TowerLevel Initialize sound channels Sound_InitChannels(1)
"1" Sawblade Saw #1, break sound 1 this+0x4370, Sawblade_SetBreakSound(1)
"2" Sawblade Saw #2, break sound 2 this+0x4374, Sawblade_SetBreakSound(2)
"1" Bridge Add to bridge list 1 this+0x4380 list
"2" Bridge Add to bridge list 2 this+0x4798 list
"NEG" Bridge Reverse rotation (speed=-1.0) obj[0x43E] = 0xBF800000

Sawblade System

Sawblade_Level Structure

Offset Type Field Default
+0x10D0 int board_ptr Parent ArenaBoard
+0x10D4 Vec3 position Creation position
+0x10E0 Vec3 base_position Copy of position
+0x10EC int state 0 (inactive)
+0x10F0 float angle RNG_Rand(0x168) random start angle
+0x10F4 int tick_count 0
+0x10FC int anim_frame 0
+0x110C byte is_alert 0
+0x110D byte alert_enabled 1 (can show alert)
+0x1110 float alert_timer 500.0 (0x43FA0000)
+0x1114 byte is_active 0
+0x1118 int break_sound_id 0

Sawblade Lifecycle

  1. Alert Phase: Saw_AlertActivate (0x434770)

    • Sets alert_enabled = 0 (one-shot)
    • Plays warning sound at saw position (3D positioned)
    • Triggered by "E:ALERTSAW1"/"E:ALERTSAW2" collision events
  2. Active Phase: Saw_Activate (0x434A50)

    • Sets is_active = 1
    • Plays break/activate sound at position
    • Triggered by "E:ACTIVATESAW1"/"E:ACTIVATESAW2" collision events
  3. Render: Sawblade_Render (0x4347E0)

    • Draws the spinning sawblade mesh
  4. Sound: Sawblade_SetBreakSound (0x434AB0)

    • Sets which sound ID to play on break (1 or 2)
    • Two saw slots per arena (offset 0x4370 and 0x4374)

Sawblade Sound References

App Offset Sound Type
+0x4BC Alert/ping sound
+0x4C0 Break/activate sound

Spinner (Bridge) System

Spinner_Level (0x10FC bytes)

Rotating bridge platforms. Created with "BRIDGE" prefix.

Two independent bridge lists allow separate control:

  • Bridge list 1: this+0x4380 (triggered by one event)
  • Bridge list 2: this+0x4798 (triggered by another event)

The "NEG" suffix reverses rotation direction (speed = -1.0).

Judge (Gear) System

Gear_Level (0x1100 bytes)

Rotating gear hazards named "JUDGE" prefix. Stored in a judge list at
this+0x4BBC. Triggered by "E:ALERTJUDGES" collision event which calls
Judge_Reset for all gears.

Bell (Tipper) System

Tipper_Level (0x10E8 bytes)

Tilting/bell hazards named "BELL" prefix. Only one bell stored directly at
this+0x4FD4. Also added to the general object list at this+0x2578.

Triggered by "E:BELL" collision events — activates bell and awards 500 bonus
time if not already playing.

Bonk (Hammer) System

Bonk (0x1200 bytes)

Hammer obstacles named "BONK" prefix. Only created on Frenzied difficulty
(App+0x23C != 0). Stored at this+0x436C (single hammer) and this+0x2578 list.

Hammer_ChaseStart (0x438BB0) starts the chase sequence for tournament mode.
The collision event "E:CALLHAMMER" creates a bonk popup via CreateBonkPopup.

TowerLevel (Spinning Tower) System

TowerLevel (0x1188 bytes)

Spinning tower obstacles with "SAW" (3-char) prefix. Not the same as sawblades.
Has speed modifiers via name suffixes:

  • "SLOW": Reduced rotation speed (obj+0x43B=1)
  • "SUPER": Enhanced mode (obj+0x10ED=1)
  • "UP": Initialize additional sound channel

Object Storage Map (ArenaBoard offsets)

Offset Type Purpose
+0x2578 AthenaList All arena objects (master list)
+0x436C void* Bonk/Hammer pointer
+0x4370 void* Sawblade #1 pointer
+0x4374 void* Sawblade #2 pointer
+0x4380 AthenaList Bridge list #1
+0x4798 AthenaList Bridge list #2
+0x4BBC AthenaList Judge/Gear list
+0x4FD4 void* Bell/Tipper pointer

Collision-to-Action Dispatch

See COLLISION_SYSTEM.md for the full ExpertCollisionEvents dispatch table.
Summary of arena collision events:

Event Name Action Object
E:ALERTSAW1 Saw_AlertActivate Saw #1 warning ping
E:ALERTSAW2 Saw_AlertActivate Saw #2 warning ping
E:ACTIVATESAW1 Saw_Activate Saw #1 full activate
E:ACTIVATESAW2 Saw_Activate Saw #2 full activate
E:ALERTJUDGES Judge_Reset All gears warning
E:SCORE<n> ScoreDisplay_SetTime Set time from suffix
E:JUMP Jump pad activation +vert_vel, sound
E:BELL<suffix> Bell_Activate +500 bonus time
E:CALLHAMMER CreateBonkPopup Tournament bonk
E:HAMMERCHASE Hammer_ChaseStart Tournament chase

App Difficulty Values

From DifficultyMenu_ctor and Tournament_AdvanceRace:

Difficulty Value Time Bonus Speed
PIPSQUEAK "EASY" +1000ms Slower hazards
NORMAL "NORMAL" Standard Normal speed
FRENZIED! "HARD" +500ms Faster + BONK/JUDGE objects

Frenzied mode enables creation of BONK and JUDGE objects (App+0x23C check)
and reduces time bonus from +1000 to +500.

Related Functions

Address Name Purpose
0x40E250 CreateExpertLevelObjects Master arena object factory
0x434660 Sawblade_Level_Ctor Sawblade constructor
0x434A50 Saw_Activate Full activation
0x434770 Saw_AlertActivate Warning phase
0x4347E0 Sawblade_Render Render spinning blade
0x434640 Sawblade_SetActive Set active state
0x434AB0 Sawblade_SetBreakSound Set break sound ID
0x438BB0 Hammer_ChaseStart Start hammer chase
0x40E2A4 Bonk_ctor Hammer/bonk constructor
0x40E340 TowerLevel_Ctor Spinning tower constructor
0x40E4C0 Spinner_Level_ctor Bridge/spinner constructor
0x40E5AC Gear_Level_ctor Judge/gear constructor
0x40E61F Tipper_Level_Ctor Bell/tipper constructor

🔗 Related Documents

Arena Scoring System

types : gameplay
keywords :

📂 View source on GitHub


Hamsterball Arena Scoring System — Modder's Reference

Document version: 2026-06-13
Based on: Hamsterball.exe decompilation via Ghidra + GhidraMCP
Target audience: Modders, trainers, reverse engineers


Table of Contents

  1. Overview
  2. Data Structures
  3. Score Lifecycle
  4. Timer System
  5. Win Condition Logic
  6. How to Edit Scores
  7. Function Reference
  8. Offsets Quick Reference

Overview

Hamsterball's multiplayer Arena Mode (ArenaBoard) uses a per-player scoring system where:

  • Each player has a score value tracked inside the ArenaBoard object
  • A countdown timer limits round duration (default: 60 seconds)
  • When time expires, the player with the highest score wins
  • If multiple players tie for highest score, a tie-breaker round begins
  • Falling off the board typically awards points to surviving players

The scoring system is completely separate from the single-player race timer system. Arena scores are stored in the ArenaBoard struct, while race times are stored in the App struct.


Data Structures

ArenaBoard Scoring Fields

The ArenaBoard inherits from BoardGadgetScene. Within the ArenaBoard, these offsets control scoring:

Offset Type Field Description
+0x11ED int32 p1_score Player 1 current score
+0x11EE int32 p2_score Player 2 current score
+0x11EF int32 p3_score Player 3 current score
+0x11F0 int32 p4_score Player 4 current score
+0x47AC int32 time_limit Countdown duration in ticks (default: 6000 ≈ 60s)
+0x47B0 byte timer_started 1 = countdown is running
+0x47C4 byte tie_breaker 1 = tie-breaker active
+0x47C5 byte game_over 1 = round ended, results showing
+0x11EB int32 countdown Frames remaining until check
+0x11EC byte timer_toggle Internal timer flip-flop
+0x11F1 byte results_shown 1 = RaceResultsMenu already created

Verified in: ArenaBoard_Update (0x421FE0) and ArenaBoard_Render (0x421910)

App Player Slots

Each player's tournament/race data lives in the App struct with a stride of 0xA0:

Player Active Flag Score/Time Extra Time Race Index Level Name String
P1 +0x5D8 +0x5E8 +0x5EC +0x60C +0x610
P2 +0x678 +0x688 +0x68C +0x6CC +0x6D0
P3 +0x718 +0x728 +0x72C +0x76C +0x770
P4 +0x7B8 +0x7C8 +0x7CC +0x80C +0x810

In arena mode, the +0x5E8 slot is repurposed as a score accumulator rather than a race time. The ArenaBoard copies scores into these slots at round end.

Verified in: PlayerProfile_ctor (0x426F30), Tournament_AdvanceRace (0x427080)

ScoreObject Structure

Pop-up score notifications (like "EXTRA TIME:") use this struct:

Offset Type Field
+0x00 vtable* ScoreObject vtable = 0x4D6C70
+0x04 App* app pointer
+0x08 void* player data pointer
+0x0C App* app (duplicate)
+0x10 int32 display value
+0x14 int32 lifetime timer
+0x18 byte active flag
+0x1C float x position
+0x20 float y position
+0x24 float scale
+0x2C char* label string (e.g., "EXTRA TIME:")

Size: 0x30 bytes
Ctor: ScoreObject_ctor at 0x44BE80


Score Lifecycle

1. Score Initialization

When a ArenaBoard arena is constructed (ArenaBoard_ctor, 0x4217B0):

  • All four player scores (+0x11ED..+0x11F0) are initialized to 0
  • time_limit (+0x47AC) is set to 6000 (60 seconds)
  • timer_started (+0x47B0) is 0 initially

2. Score Increment Events

During gameplay, scores increase through these mechanisms:

A. Ball Falls Off Board

When a ball falls off the arena platform:

  1. Ball_Shrink (0x402200) triggers — sets airborne flag, plays sound
  2. Ball_FallUpdate (0x408830) runs physics until ball hits void
  3. The ArenaBoard detects the fallen player and awards +10 points to all surviving players via Rotator_AddBall (0x43B6F0)

B. Bell Collision Bonus

When a ball hits an E:BELL event plane:

  1. Bell_Activate plays the bell sound
  2. If not in a race/demo: +500 bonus time added to the player's timer (App+0x5EC)
  3. A ScoreObject popup with "EXTRA TIME:" text is created and appended to the scene score list (ArenaBoard+0x8B8)

Source: ExpertCollisionEvents (0x40E6A0), lines 90-108

C. Direct ScoreObject Creation

Any code can create a score popup:

void* scoreObj = operator_new(0x30);
ScoreObject_ctor(scoreObj, app, player_data_ptr, "MY LABEL:");
Timer_Decrement(scoreObj);
AthenaList_Append(rumbleBoard + 0x8B8, scoreObj);

3. Score Storage

Rotator_AddBall (0x43B6F0) manages a linked list of score entries:

// Each entry in the list is 8 bytes:
struct ScoreEntry {
    int32_t player_id;   // +0x00
    int32_t score;       // +0x04 (always set to 10)
};
  • If player_id already exists in the list: update its score to 10
  • If new: allocate 8 bytes and append to ScoreObject+0x10F0 list

Note: This function always sets score = 10. It does not accumulate.


Timer System

Countdown Timer

The ArenaBoard uses a tick-based countdown (not real-time seconds):

Value Meaning
time_limit = 6000 ~60 seconds at 100 ticks/sec
time_limit < 1100 Final 11 seconds — HUD flashes red
time_limit < 600 Final 6 seconds — HUD flashes blue
time_limit == 0 Round over — trigger win calculation

Timer Display

ArenaBoard_Render (0x421910) draws the timer at screen center:

  1. Background quad at (screen_center - 88, 10, 180, 105)
  2. Main time string in MM:SS format (large font)
  3. Decimal fraction ".N" (tenths of a second)
  4. Color shifts to red/blue during final seconds

Tie-Breaker Display

When tie_breaker flag (+0x47C4) is set:

  • "TIE BREAKER!" text renders at screen position (400, 40)
  • Timer continues counting down
  • First player to score wins

Win Condition Logic

Round End Trigger

ArenaBoard_Update (0x421FE0) checks every frame:

// Pseudocode from decompilation:
if (timer_started && countdown_expired) {
    // Find maximum score among active players
    max_score = MAX(p1_score, p2_score, p3_score, p4_score);
    
    // Count how many players have max_score
    tie_count = 0;
    for each active player:
        if (player_score == max_score) tie_count++;
    
    if (tie_count >= 2) {
        // TIE — enable tie-breaker mode
        tie_breaker = 1;
        timer_started = 0;  // Actually continues in tie-breaker
    } else {
        // WINNER — show results
        game_over = 0;
        winner_idx = player_with_max_score;
        CreateRaceResultsMenu(rumbleBoard, winner_idx);
        results_shown = 1;
    }
}

Important Logic Details

From the decompilation at 0x421FE0:

  1. Inactive players are skipped — checks App+0x5D7, +0x677, +0x717, +0x7B7 (hidden flags)
  2. Tie requires ≥2 players tied — if only 1 player has max, they win immediately
  3. RaceResultsMenu is created once — protected by results_shown flag (+0x11F1)
  4. Audio cue — plays "Game Over" music when results appear

How to Edit Scores

Method 1: Direct Memory Patching (Recommended for Trainers)

The simplest way to manipulate scores is writing directly to the ArenaBoard fields:

// Get App pointer from global
App* app = *(App**)0x004FD680;

// Get current ArenaBoard from active scene
// The ArenaBoard is the current Scene object in arena mode
Scene* scene = app->currentScene;  // App+0x178

// Verify we're in arena mode (ArenaBoard vtable = 0x4D1358)
if (*(uint32_t*)scene == 0x004D1358) {
    // Edit scores directly
    *(int32_t*)((char*)scene + 0x11ED) = 999;  // P1 score = 999
    *(int32_t*)((char*)scene + 0x11EE) = 0;     // P2 score = 0
    *(int32_t*)((char*)scene + 0x11EF) = 0;     // P3 score = 0
    *(int32_t*)((char*)scene + 0x11F0) = 0;     // P4 score = 0
}

Method 2: Hook Rotator_AddBall

Intercept score changes as they happen:

// Original: 0x43B6F0 __thiscall Rotator_AddBall(void* this, int player_id)
void __fastcall Hook_Rotator_AddBall(void* scoreObj, int player_id) {
    // Force P1 to always get +100 instead of +10
    if (player_id == 0) {
        // Write directly to the score entry after original processes
        Original_Rotator_AddBall(scoreObj, player_id);
        // Now patch the stored value from 10 to 100
        // (Requires walking the list at scoreObj+0x10F0)
        return;
    }
    Original_Rotator_AddBall(scoreObj, player_id);
}

Method 3: Freeze Timer

Prevent round from ending:

// In your per-frame hook (e.g., App_FrameUpdate at 0x46C170):
Scene* scene = app->currentScene;
if (*(uint32_t*)scene == 0x004D1358) {  // ArenaBoard vtable
    *(int32_t*)((char*)scene + 0x47AC) = 6000;  // Reset time_limit to max
    *(int32_t*)((char*)scene + 0x11EB) = 100;    // Reset countdown
}

Method 4: Force Win (Single Player Arena)

End the round immediately with P1 as winner:

Scene* scene = app->currentScene;
if (*(uint32_t*)scene == 0x004D1358) {
    // Set P1 to winning score, others to 0
    *(int32_t*)((char*)scene + 0x11ED) = 100;
    *(int32_t*)((char*)scene + 0x11EE) = 0;
    *(int32_t*)((char*)scene + 0x11EF) = 0;
    *(int32_t*)((char*)scene + 0x11F0) = 0;
    
    // Force timer to expire
    *(int32_t*)((char*)scene + 0x47AC) = 0;
    *(int32_t*)((char*)scene + 0x11EB) = 0;
    *(char*)((char*)scene + 0x47B0) = 1;   // timer_started = true
}

Method 5: Edit App Tournament Data

For tournament mode, scores are copied to App struct at round end:

// App player data offsets (P1 example):
// +0x5E8 = current_time (in arena: score)
// +0x5EC = extra_time (bonus accumulates here)

*(int32_t*)((char*)app + 0x5E8) = 0;      // P1 score = 0
*(int32_t*)((char*)app + 0x688) = 99999;  // P2 score = 99999 (win)

Function Reference

Core Scoring Functions

Address Name Signature Description
0x43B6F0 Rotator_AddBall __thiscall (void* this, int player_id) Set/add score entry for a player
0x44BE80 ScoreObject_ctor __thiscall (void* this, int app, int player_data, char* label) Create score popup object
0x434C80 ScoreDisplay_SetTime __thiscall (void* this, int time) Set displayed time string
0x421FE0 ArenaBoard_Update __fastcall (int* this) Per-frame update + win check
0x421910 ArenaBoard_Render __thiscall (void* this, undefined4) Draw HUD/timer
0x4217B0 ArenaBoard_ctor __thiscall (void* this, int app) Initialize arena with time_limit=6000

Ball / Death Functions

Address Name Description
0x402200 Ball_Shrink odd race E:SHRINK collision — shrink ball
0x408830 Ball_FallUpdate Physics update while falling
0x405190 Ball_FindClosestRespawnPoint Find respawn after fall

Arena Collision

Address Name Description
0x40E6A0 ExpertCollisionEvents Master collision dispatcher
0x434E20 Bell_Activate Bell hit → bonus time
0x438BB0 Hammer_ChaseStart Start hammer chase sequence
0x434A50 Saw_Activate Activate saw blade
0x434770 Saw_AlertActivate Saw warning ping

Results / Menu

Address Name Description
0x44CB10 RaceResultsMenu_ctor Create race results overlay
0x44C260 RaceResultPopup_ctor "TIME'S UP!" / "OUT OF TIME!" popup
0x44C4B0 RaceResults_Tick Animate results screen
0x451DF0 TourneyMenu_TickWithRank Tournament rank calculation

Offsets Quick Reference

ArenaBoard Scoring Offsets

+0x11ED  int32  p1_score
+0x11EE  int32  p2_score
+0x11EF  int32  p3_score
+0x11F0  int32  p4_score
+0x11EB  int32  countdown_timer
+0x11EC  byte   timer_toggle
+0x11F1  byte   results_shown
+0x47AC  int32  time_limit (default 6000)
+0x47B0  byte   timer_started
+0x47C4  byte   tie_breaker
+0x47C5  byte   game_over
+0x8B8   void*  score_object_list (AthenaList)

App Player Data Offsets

// Player 1
+0x5D8  byte   p1_active
+0x5E4  int32  p1_score_aux
+0x5E8  int32  p1_current_time
+0x5EC  int32  p1_extra_time
+0x60C  int32  p1_race_index
+0x610  char*  p1_level_name

// Player 2 (+= 0xA0)
+0x678  byte   p2_active
+0x688  int32  p2_current_time
+0x68C  int32  p2_extra_time

// Player 3 (+= 0x140)
+0x718  byte   p3_active
+0x728  int32  p3_current_time
+0x72C  int32  p3_extra_time

// Player 4 (+= 0x1E0)
+0x7B8  byte   p4_active
+0x7C8  int32  p4_current_time
+0x7CC  int32  p4_extra_time

Vtables for Type Checking

0x004D1358  ArenaBoard vtable
0x004CE400  App vtable
0x004D0260  Scene vtable
0x004CF3A0  Ball vtable

Modding Tips

Tip 1: Detect Arena Mode

Check if current scene is a ArenaBoard:

Scene* scene = *(Scene**)(app + 0x178);
uint32_t vtable = *(uint32_t*)scene;
bool is_arena = (vtable == 0x004D1358);

Tip 2: Find ArenaBoard from App

In arena mode, App->currentScene IS the ArenaBoard:

void* rumbleBoard = *(void**)(app + 0x178);

Tip 3: Score Multiplier

Hook Rotator_AddBall and multiply all scores:

void __fastcall Hook_SetScore(void* this, int player_id) {
    Original_SetScore(this, player_id);
    // Walk the list at this+0x10F0 and multiply by 10
}

Tip 4: Disable Tie-Breaker

Force instant win even on ties:

// In ArenaBoard_Update, nop out the tie_count check
// Or simply never let timer expire:
*(int32_t*)(rumbleBoard + 0x47AC) = 999999;

Tip 5: Visual Score Popups

Create custom score notifications:

void* popup = operator_new(0x30);
ScoreObject_ctor(popup, app, player_ptr, "CUSTOM BONUS:");
*(int32_t*)(popup + 0x10) = 500;  // Display value
Timer_Decrement(popup);
AthenaList_Append(rumbleBoard + 0x8B8, popup);

Files Referenced

File Description
analysis/ghidra/decompilations/collision/decomp_expert_collisionevents.c Arena collision events
analysis/ghidra/decompilations/tournament/decomp_rumbleboard_render.c HUD rendering
analysis/ghidra/decompilations/scene/decomp_scene_updateballs.c Ball update logic
analysis/ghidra/structs/rumbleboard_struct.h ArenaBoard C struct
docs/RUMBLEBOARD_SYSTEM.md Arena architecture overview
docs/ARENA_HAZARD_SYSTEM.md Hazard object documentation
docs/APP_OBJECT.md App struct full reference

Document compiled from live Ghidra decompilation. All offsets verified against Hamsterball.exe build. For questions or corrections, check the raw decompilation files in analysis/ghidra/decompilations/.


🔗 Related Documents

Asset Loading & Resource Manif

types : project
keywords :

📂 View source on GitHub


Asset Loading & Resource Manifest

Overview

TimerDisplay (0x4298C0) is actually App_ResourceLoader — it's the master asset loading
function that loads ALL game assets during the loading screen phase. It creates a
LoadingScreenGadget (0x3628 bytes) and calls its vtable methods to load resources.

LoadingScreenGadget Vtable Methods

Offset Method Purpose
+0x48 LoadMeshWithCollision Load mesh + collision data (2nd arg=collision flag)
+0x4C LoadMesh Load mesh without collision
+0x50 LoadLevel Load MESHWORLD level
+0x54 LoadLevelLinked Load level + link to another level
+0x58 LoadTexture Load texture (PNG/BMP)
+0x5C LoadFont Load bitmap font
+0x60 LoadSound Load OGG sound effect (2nd arg=channel count)

Complete Asset Manifest

Fonts (5)

App Offset Path Purpose
+0x318 fonts\showcardgothic28 Main title/heads-up font (28pt)
+0x324 fonts\arialnarrow12bold UI body text (12pt bold)
+0x31C fonts\showcardgothic14 Small labels (14pt)
+0x328 fonts\showcardgothic72 Large display (72pt)
+0x320 fonts\showcardgothic16 Medium labels (16pt)

Title Textures (3)

App Offset Path Purpose
+0x348 titletext-left.png Title screen left half
+0x34C titletext-right.png Title screen right half
+0x330 textures\hammy1.png Hamster character 1
+0x334 textures\hammy2.png Hamster character 2
+0x338 textures\hammy3.png Hamster character 3

Sign & Goal Textures (18)

App Offset Path Flag
+0x27C sign-bewarethetar.png 0 (no collision)
+0x280 sign-bewarethetar.png 1 (collision)
+0x284 sign-bewarethetar-mirrored.png 0
+0x288 arrow1.png 0
+0x28C arrow1.png 1
+0x290 arrow1-mirrored.png 0
+0x294 goal.png 0
+0x298 goal.png 1
+0x29C goal-lit.png 1
+0x2A0 goal-mirrored.png 1
+0x2A4 goal-lit-mirrored.png 1
+0x2A8 locktile.png 0
+0x2AC locktile.png 1
+0x2B0 locktile2.png 1
+0x2B4 goal-round.png 0
+0x2B8 goal-round.png 1
+0x2BC goal-round-lit.png 1
+0x2C0 goal-round-mirrored.png 1
+0x2C4 goal-round-lit-mirrored.png 1

Checker/Brick Textures (18)

Path Purpose
pinkchecker.bmp Pink checker tile
bluechecker.bmp Blue checker tile
bluebrick.png Blue brick tile
greenchecker.bmp Green checker tile
greenbrick.png Green brick tile
yelllowchecker.png Yellow checker tile (typo in original)
greyoutlinechecker.png Grey outlined checker
redchecker.bmp Red checker tile
redbrick.png Red brick tile
orangechecker.bmp Orange checker tile
orangebrick.png Orange brick tile
brightgreenchecker.bmp Bright green checker
brightgreenbrick.png Bright green brick
toobchecker.png Tube race checker
toobbrick.png Tube race brick
skychecker.png Sky race checker
purplechecker.bmp Purple checker tile
purplebrick.png Purple brick tile
brownbrick.png Brown brick tile
blackchecker.png Black checker tile

Ball/Mesh Textures (12)

Path Purpose
blueblot.png Blue blob background
blueblot2.png Blue blob variant
bluecircle.png Blue circle
chrome.png Chrome ball texture
chromeshadow.png Chrome ball shadow
weaselbox.png Weasel ranking box
settext.png Settings text
gotext.png GO! text overlay
ballborder.png Ball border frame
ballburner.png Ball burner effect
sweat.png Sweat drop sprite
raptisoftlogo.png Developer logo

Medal/Icon Textures (8)

Path Purpose
bronze-small.png Bronze medal small
silver-small.png Silver medal small
gold-small.png Gold medal small
goldenweasel.png Golden weasel award
bronze-icon.png Bronze medal icon
silver-icon.png Silver medal icon
gold-icon.png Gold medal icon
goldenweasel-icon.png Golden weasel icon

Tournament Thumbnails (15)

Path Level
tourney-beginner.png Beginner race
tourney-cascade.png Cascade race
tourney-intermediate.png Intermediate race
tourney-dizzy.png Dizzy race
tourney-Tower.png Tower race
tourney-Up.png Up race
tourney-Neon.png Neon race
tourney-Expert.png Expert race
tourney-Odd.png odd race
tourney-Toob.png Toob race
tourney-Wobbly.png Wobbly race
tourney-Glass.png Glass race
tourney-Sky.png Sky race
tourney-Master.png Master race
tourney-Impossible.png Impossible race

Menu/Overlay Textures (8)

Path Purpose
tournament.png Tournament mode splash
timetrials.png Time trial splash
partyrace.png Party race splash
rodentrumble.png Rodent Rumble splash
mirror.png Mirror mode icon
Burst.png Burst effect
Lost.png "You Lost" overlay
Winner.png "You Won" overlay
Winner2p.png "Player Won" 2P overlay
title1-4.png Title screen frames
unlock.png Unlock notification
scoreball.png Score ball sprite

Meshes (14)

App Offset Path Purpose
+0x244 Meshes\Sphere Main ball mesh
+0x248 Meshes\SphereBreak1 Ball break animation 1
+0x24C Meshes\SphereBreak2 Ball break animation 2
+0x260 Meshes\RBGlare Reflection glare mesh
+0x264 Meshes\Sphere+Tar Tar-stuck ball mesh
+0x250 Meshes\Hamster-Waiting Hamster idle pose
+0x254 Meshes\Hamster-trot1 Hamster walk frame 1
+0x258 Meshes\Hamster-trot2 Hamster walk frame 2
+0x25C Meshes\Hamster-trot3 Hamster walk frame 3
+0x268 Meshes\8Ball 8-ball mesh (tournament)
+0x26C Meshes\FunBall Fun ball mesh (arena)
+0x270 Meshes\Bell Bell mesh (tournament)
+0x274 Meshes\Dizzy Dizzy powerup mesh
+0x584 Meshes\GlassBonus Glass bonus mesh
+0x588 Meshes\GlassBonus-Smashed Glass bonus smashed
+0x5A4 Meshes\tarbubble Tar bubble mesh
+0x5A8 Meshes\fanblades Fan blades mesh
+0x5AC Meshes\fanbody Fan body mesh
+0x5B0 Meshes\sawblade Saw blade mesh
+0x5B4 Meshes\sawface Saw face mesh
+0x5B8 Meshes\sawface2 Saw face variant
+0x5BC Meshes\dawgshoe Blockdawg shoe 1
+0x5C0 Meshes\dawgshoe2 Blockdawg shoe 2
+0x5C4 Meshes\dawgshadow Blockdawg shadow
+0x578 Meshes\mousetrapshadow Mouse trap shadow

Levels (7)

App Offset Path Purpose
+0x570 Levels\MouseTrap Mouse trap level
+0x57C Levels\Secret Secret/unlock level
+0x580 Levels\Secret-Unlock Secret unlock level
+0x594 Levels\Level4-Trapdoor1 Trapdoor level 1
+0x598 Levels\Level4-Trapdoor2 Trapdoor level 2
+0x58C Levels\PopupSign Popup sign level
+0x5C8 Levels\Level6-Lifter Lifter level
+0x584 (linked to MouseTrap) Mouse trap collision
+0x59C (linked to Trapdoor1) Trapdoor 1 collision
+0x5A0 (linked to Trapdoor2) Trapdoor 2 collision
+0x590 (linked to PopupSign) Popup sign collision

Sound Effects (55)

App Offset Path Channels
+0x43C sounds\collide 10
+0x440 sounds\roll 10
+0x444 sounds\whistle 1
+0x448 sounds\bumper 10
+0x44C sounds\ballbreak 5
+0x450 sounds\ballbreaksmall 5
+0x454 sounds\thwomp 2
+0x458 sounds\snap 2
+0x45C sounds\popup 2
+0x460 sounds\dropin 2
+0x464 sounds\dropinshort 2
+0x468 sounds\popout 2
+0x46C sounds\pipebump1 10
+0x470 sounds\pipebump2 10
+0x474 sounds\pipebump3 10
+0x478 sounds\gearclank 20
+0x47C sounds\bridgeslam 2
+0x480 sounds\platformtick 5
+0x484 sounds\gluestuck 5
+0x488 sounds\bubble1 5
+0x48C sounds\bubble2 5
+0x490 sounds\wheelcreak 2
+0x494 sounds\catapult 2
+0x498 sounds\trapdoor 2
+0x49C sounds\fwing 2
+0x4A0 sounds\clink 3
+0x4A4 sounds\whoosh 3
+0x4A8 sounds\chomp 1
+0x4AC sounds\fan-start 10
+0x4B0 sounds\fan-blow 10
+0x4B4 sounds\crack 2
+0x4B8 sounds\crumble 2
+0x4BC sounds\sawstartup 2
+0x4C0 sounds\sawcut 2
+0x4C4 sounds\minipop 5
+0x4C8 sounds\bell 3
+0x4CC sounds\zip 2
+0x4D0 sounds\ting 20
+0x4D4 sounds\shrink 3
+0x4D8 sounds\grow 3
+0x4DC sounds\tweet 3
+0x4E0 sounds\creakyplatform 20
+0x4E4 sounds\wubba 5
+0x4E8 sounds\saw 2
+0x4EC sounds\sawspeedy 2
+0x4F0 sounds\dawgstep1 10
+0x4F4 sounds\dawgstep2 10
+0x4F8 sounds\dawgsmash 10
+0x4FC sounds\sizzle 2
+0x500 sounds\explode 3
+0x504 sounds\vac-o-sux 3
+0x508 sounds\speedcylinder 2
+0x50C sounds\bonuspop 5
+0x510 sounds\buzzbonus 1
+0x514 sounds\breakbridge 1
+0x518 sounds\unlock 1
+0x51C sounds\NeonRide 1
+0x520 sounds\NeonFlicker 50
+0x524 sounds\ZoopDown 2
+0x528 sounds\LightsOff 2
+0x52C sounds\GlassBonus 2

RaceGoalReached Ctor (0x44C880)

When a player finishes a race:

  • Sets title to "GOAL REACHED!"
  • Loads "textures\ranks\weasel.png" rank sprite
  • Updates best time in App+0x86C[player_index*4]
  • 800ms timer (frames) for goal animation
  • Medal thresholds hardcoded as integer values

🔗 Related Documents

Audio Modding Guide

types : modding
keywords :

📂 View source on GitHub


Hamsterball Audio Modding Guide

This guide covers how the original Hamsterball Windows executable loads, plays, and mixes sound effects and music, and how a modder can trigger existing sounds or add new custom audio from inside a DLL hook.

It is built from live Ghidra decompilation of Hamsterball.exe and the existing AUDIO_SYSTEM.md / AUDIO_SYSTEM_SFX.md docs.


1. Audio architecture

Hamsterball uses two completely separate audio APIs:

System Library Purpose file formats
Music BASS.dll (un4seen) Background music, menu stings .mo3 tracker modules
Sound effects DirectSound8 (dsound.dll) 3D positional gameplay sounds .ogg (preferred), .wav (fallback)

Both systems live under the single App object. The audio device object is at App + 0x178 (SoundDevice). Music channels are stored at App + 0x534, 0x538, 0x53C.


2. Key structs

2.1 SoundDevice — the DirectSound8 manager

Created by SoundDevice_ctor (0x00466620). It wraps the IDirectSound8 interface and owns all loaded sound buffers.

Offset Type Description
+0x000 vtable* SoundDevice vtable (0x4D911C)
+0x004 App* Back-pointer to owning App
+0x008 int Count of SoundList objects in the master list
+0x00C int Capacity of that list
+0x010 int* Array pointer for the master SoundList list
+0x414 int* Same array pointer (redundant)
+0x834 App* Cached App pointer
+0x838 float Global SFX volume (read from registry "Sound Volume")
+0x83C bool Sound enabled flag
+0x84C IDirectSound8* DirectSound8 COM interface
+0x850 int Number of 3D listeners
+0x854 Vec3[16] Listener positions for 3D attenuation
+0x914 float Min rolloff distance (default 0.0)
+0x918 float Max rolloff distance (default 6000.0f)

The global volume is read by Level_ReadSoundVolume (0x00466570) and written back by SoundDevice_dtor (0x004668A0).

2.2 SoundList — one logical sound with N hardware buffers

A SoundList is an AthenaList that owns one or more SoundEntry objects. Each SoundEntry wraps one DirectSound buffer handle. Multiple entries allow the same sound to be played polyphonically.

Offset Type Description
+0x000 vtable* SoundList vtable (0x4D8E7C)
+0x004 App* Back-pointer
+0x008 AthenaList List of SoundEntry* buffers
+0x00C int Entry count
+0x010 int Circular "next" index for playback
+0x414 int* Entry array pointer

Object size is 0x420 bytes.

2.3 SoundEntry — one DirectSound buffer

Offset Type Description
+0x000 vtable* SoundEntry scalar dtor (0x4D8E78)
+0x004 App* Back-pointer
+0x008 IDirectSoundBuffer* DirectSound buffer handle

2.4 MusicChannel / MusicPlayer — BASS music

Offset Type Description
+0x000 vtable* MusicChannel dtor
+0x004 DWORD BASS device handle
+0x008 HMUSIC BASS music handle
+0x00C AthenaList Track list (name → handle)
+0x418 int* Track array
+0x528 float Current music volume
+0x530 byte Muted flag
+0x531 byte Paused flag

MusicPlayer (App + 0x534) also stores the loaded file path at +0x424.


3. How the game loads SFX at startup

App_ResourceLoader (misnamed TimerDisplay, 0x0042A8C0) is the master loader. It creates a LoadingScreenGadget and calls its vtable methods to queue every asset. Slot +0x60 queues a sound load.

// LoadingScreenGadget vtable
+0x48  LoadTexture(dest, filename, alpha_flag)
+0x4C  LoadMesh  (dest, filename)
+0x50  LoadLevel (dest, filename)
+0x54  LoadCollision(dest, level_src)
+0x58  LoadSprite(dest, filename)
+0x5C  LoadFont  (dest, font_path)
+0x60  LoadSound (dest, sound_path, max_channels)

Example queue call from the loader:

LoadingScreenGadget* loader = *(LoadingScreenGadget**)(app + 0x22C);
(**(code**)(*(int*)loader + 0x60))(loader, app + 0x460, "sounds\\dropin", 2);

The dest argument is an App offset, not a pointer. The loader writes the resulting SoundList* into App + dest.

3.1 Full startup sound → App offset map

App offset Sound name Max channels Notes
+0x43C sounds\\collide 10 Ball/ball and ball/wall impacts
+0x440 sounds\\roll 10 Rolling loop
+0x444 sounds\\whistle 1 Spin/whistle
+0x448 sounds\\bumper 10 Bumper impacts
+0x44C sounds\\ballbreak 5 Large break
+0x450 sounds\\ballbreaksmall 5 Small break
+0x454 sounds\\thwomp 2 Thwomp hazard
+0x458 sounds\\snap 2 Snap hazard
+0x45C sounds\\popup 2 Popup sign
+0x460 sounds\\dropin 2 DROPIN event
+0x464 sounds\\dropinshort 2 Short drop
+0x468 sounds\\popout 2 POPOUT event
+0x46C sounds\\pipebump1 10 PIPEBONK random set
+0x470 sounds\\pipebump2 10 PIPEBONK random set
+0x474 sounds\\pipebump3 10 PIPEBONK random set
+0x478 sounds\\gearclank 20 Gear mechanisms
+0x47C sounds\\bridgeslam 2 Bridge slam
+0x480 sounds\\platformtick 5 Ticking platform
+0x484 sounds\\gluestuck 5 Tarpit / glue
+0x488 sounds\\bubble1 5 Bubble sound 1
+0x48C sounds\\bubble2 5 Bubble sound 2
+0x490 sounds\\wheelcreak 2 Creaking wheel
+0x494 sounds\\catapult 2 Catapult launch
+0x498 sounds\\trapdoor 2 Trapdoor
+0x49C sounds\\fwing 2 Fwing hazard
+0x4A0 sounds\\clink 3 Metal clink
+0x4A4 sounds\\whoosh 3 Whoosh
+0x4A8 sounds\\chomp 1 Chomper
+0x4AC sounds\\fan-start 10 Fan startup
+0x4B0 sounds\\fan-blow 10 Fan loop
+0x4B4 sounds\\crack 2 Crack
+0x4B8 sounds\\crumble 2 Crumble
+0x4BC sounds\\sawstartup 2 Saw startup
+0x4C0 sounds\\sawcut 2 Saw cutting
+0x4C4 sounds\\minipop 5 Mini pop
+0x4C8 sounds\\bell 3 Bell
+0x4CC sounds\\zip 2 Zip (mouse-trap / fast move)
+0x4D0 sounds\\ting 20 Ting
+0x4D4 sounds\\shrink 3 Shrink powerup
+0x4D8 sounds\\grow 3 Grow powerup
+0x4DC sounds\\tweet 3 Tweet
+0x4E0 sounds\\creakyplatform 20 Creaky platform
+0x4E4 sounds\\wubba 5 Wubba
+0x4E8 sounds\\saw 2 Saw loop
+0x4EC sounds\\sawspeedy 2 Speedy saw
+0x4F0 sounds\\dawgstep1 10 Dawg step 1
+0x4F4 sounds\\dawgstep2 10 Dawg step 2
+0x4F8 sounds\\dawgsmash 10 Dawg smash
+0x4FC sounds\\sizzle 2 Sizzle
+0x500 sounds\\explode 3 Explosion
+0x504 sounds\\vac-o-sux 3 Vacuum
+0x508 sounds\\speedcylinder 2 Speed cylinder
+0x50C sounds\\bonuspop 5 Bonus pop
+0x510 sounds\\buzzbonus 1 Buzz bonus
+0x514 sounds\\breakbridge 1 Break bridge
+0x518 sounds\\unlock 1 Unlock
+0x51C sounds\\NeonRide 1 Neon ride
+0x520 sounds\\NeonFlicker 50 Neon flicker
+0x524 sounds\\ZoopDown 2 Zoop down
+0x528 sounds\\LightsOff 2 Lights off
+0x52C sounds\\GlassBonus 2 Glass bonus

All of these are stored as SoundList* pointers inside App.


4. Core SFX engine functions

Address Name Notes
0x00458F20 SoundList_Ctor __thiscall (SoundList* this, App* app)
0x00466620 SoundDevice_ctor Creates DirectSound8 device
0x00458F40 SoundList_LoadWAV Manual WAV parser → DirectSound buffer
0x00459310 Sound_LoadOgg OGG Vorbis decoder → DirectSound buffer
0x00459660 Sound_LoadOggOrWav Tries name.ogg, falls back to name.wav
0x00466500 Sound_LoadAndAppend Allocates SoundList, constructs it, loads file, appends to master list
0x00459810 Sound_GetNextChannel Circular allocator from a SoundList
0x004597B0 Sound_PlayChannel Plays the next available buffer in a list
0x00459860 Sound_Play3D Computes distance attenuation, then plays
0x00458EE0 Sound_Play3DAtPosition Applies the attenuated volume to a channel
0x00466570 Level_ReadSoundVolume Reads registry volume into SoundDevice+0x838
0x00466750 Sound_CalculateDistanceAttenuation Linear 3D rolloff from nearest listener

4.1 Sound_LoadOggOrWav behavior

void __thiscall Sound_LoadOggOrWav(SoundList* this, const char* baseName);

It builds the filenames "baseName.ogg" and "baseName.wav" and checks file access. OGG is tried first. If neither file exists, nothing is loaded and the list stays empty.

4.2 Sound_PlayChannel behavior

void __fastcall Sound_PlayChannel(SoundList* soundList);
  • If sound is disabled or global volume is 0, returns.
  • Grabs the next SoundEntry* from the circular list.
  • Calls DirectSound StopSetVolumePlay.
  • The volume passed to DirectSound comes from Audio_ClampPanValue() and the global volume.

4.3 Sound_Play3D behavior

void __thiscall Sound_Play3D(SoundList* soundList, float x, float y, float z);
  1. Calls Sound_CalculateDistanceAttenuation using all listeners.
  2. Calls Sound_PlayChannel.

For menu/UI sounds where you do not want 3D attenuation, call Sound_PlayChannel directly on the list.


5. How to trigger an existing sound effect

Because every SFX is already loaded into an App offset, the easiest trigger is:

// Get App base. In a hook you usually already have this.
App* app = GetAppSomehow();

// Example: play the "dropin" sound at full volume (no 3D)
SoundList* dropin = *(SoundList**)((char*)app + 0x460);
if (dropin) {
    typedef void (__fastcall *Sound_PlayChannel_t)(SoundList*);
    static auto fn = (Sound_PlayChannel_t)0x004597B0;
    fn(dropin);
}

// Example: play "fwing" at a 3D position
SoundList* fwing = *(SoundList**)((char*)app + 0x49C);
if (fwing) {
    typedef void (__thiscall *Sound_Play3D_t)(SoundList*, float, float, float);
    static auto fn = (Sound_Play3D_t)0x00459860;
    fn(fwing, x, y, z);
}

5.1 Triggering sounds from collision events

The engine already wires many level tags to sounds in DispatchCollisionEvents (a.k.a. DispatchCollisionEvents, 0x0040C5D0) and TowerCollisionEvents (0x0040DCD0):

Tag Sound source Function called
E:JUMP App + 0x49C (sounds\\fwing) Sound_Play3D
N:TARPIT App + 0x484 (sounds\\gluestuck) Sound_Play3D
DROPIN App + 0x460 (sounds\\dropin) Sound_PlayChannel
PIPEBONK random App + 0x46C/0x470/0x474 Sound_Play3D
POPOUT App + 0x468 (sounds\\popout) Sound_PlayChannel
N:GOAL Audio_PlayMusic(App+0x53C, "Goal!") BASS
E:CATAPULTBOTTOM App + 0x464 (sounds\\dropinshort) Sound_PlayChannel

So adding an object with one of these tags in a MESHWORLD level already produces the corresponding sound. For custom tags, hook DispatchCollisionEvents and call the sound yourself.


6. How to add a completely custom sound

There are two practical approaches.

6.1 Option A: load during app initialization via the loader queue

If your DLL is loaded early enough (before App_ResourceLoader finishes), you can queue a sound load exactly like the game does:

void QueueCustomSound(App* app, const char* path, int maxChannels) {
    // App+0x22C = LoadingScreenGadget* loader
    void* loader = *(void**)((char*)app + 0x22C);
    if (!loader) return;

    // Pick an unused App offset. The block +0x530..+0x52C is the last used SFX region,
    // so +0x5B0, +0x5B4, etc. are safe unused slots.
    int destOffset = 0x5B0;

    void** vtable = *(void***)loader;
    ((void(__thiscall*)(void*, int, const char*, int))(vtable[0x60 / 4]))
        (loader, destOffset, path, maxChannels);
}

After the loader merges its lists, App + 0x5B0 will hold a SoundList* you can play with Sound_PlayChannel.

6.2 Option B: load a sound manually at runtime (recommended)

This works any time after SoundDevice_ctor has run.

// Operator new from the original CRT
void* operator_new(size_t);

typedef void* (__thiscall *SoundList_Ctor_t)(void* this_ptr, App* app);
typedef void  (__thiscall *Sound_LoadOggOrWav_t)(void* this_ptr, const char* baseName);
typedef void  (__fastcall *Sound_PlayChannel_t)(void* soundList);

void* LoadCustomSound(App* app, const char* baseName) {
    static auto op_new      = (void*(*)(size_t))0x004BA57B;
    static auto ctor        = (SoundList_Ctor_t)0x00458F20;
    static auto load        = (Sound_LoadOggOrWav_t)0x00459660;

    void* soundList = op_new(0x420);
    if (!soundList) return nullptr;

    ctor(soundList, app);
    load(soundList, baseName);   // tries baseName.ogg then baseName.wav
    return soundList;
}

void PlayCustomSound(void* soundList) {
    static auto play = (Sound_PlayChannel_t)0x004597B0;
    if (soundList) play(soundList);
}

void PlayCustomSound3D(void* soundList, float x, float y, float z) {
    typedef void (__thiscall *fn_t)(void*, float, float, float);
    static auto play3d = (fn_t)0x00459860;
    if (soundList) play3d(soundList, x, y, z);
}

Place MySound.ogg (or .wav) in the game root or under Sounds/ and call:

void* mySound = LoadCustomSound(app, "sounds\\MySound");  // or just "MySound"
PlayCustomSound(mySound);

The file path is passed to _check_file_access, so relative paths work from the game's working directory.

6.3 Notes on custom sounds

  • Format: OGG Vorbis is preferred. WAV is parsed manually and must be standard PCM RIFF/WAVE.
  • Channels / polyphony: maxChannels in the loader queue controls how many duplicate DirectSound buffers are created. For manual loads, the game only creates one buffer per file; rapid re-triggers will cut off the previous instance. If you need polyphony, load the same file into multiple SoundList objects and rotate between them, or use the loader queue.
  • 3D attenuation: Sound_Play3D uses the listener array at SoundDevice+0x854. In single-player, listener 0 is the camera/ball position. In multiplayer, each ball is a listener.
  • Global volume: Sound_PlayChannel already multiplies by SoundDevice+0x838, so custom sounds respect the user's volume setting.

7. Music system (BASS)

7.1 BASS imports

Address Function Typical call
0x00487E40 BASS_SetConfig Configuration
0x00487E4C BASS_Init BASS_Init(-1, 44100, 0, 0, NULL)
0x00487E46 BASS_Start Resume
0x00487E58 BASS_Stop Pause all
0x00487E52 BASS_Free Shutdown
0x004794B0 BASS_ErrorGetCode Last error
0x004794B6 BASS_MusicLoad Load .mo3/.xm/.it
0x004794BC BASS_ChannelStop Stop channel
0x004794C2 BASS_ChannelSetAttributes Freq/volume/ramp
0x004794C8 BASS_MusicPlayEx Play module

7.2 Music channels

App offset Type Use
+0x534 MusicPlayer* Main music player, file path at +0x424
+0x538 HMUSIC Music channel 2
+0x53C HMUSIC Music channel 1 (used by Audio_PlayMusic)

7.3 Playing a music track by name

typedef void (__thiscall *Audio_PlayMusic_t)(void* musicChannel, const char* trackName);
static auto playMusic = (Audio_PlayMusic_t)0x0046A310;

void* channel = *(void**)((char*)app + 0x53C);
playMusic(channel, "Goal!");   // plays the track named "Goal!" from the loaded MO3

Audio_PlayMusic (0x0046A310) searches the track list linearly by name, sets volume to 1.0, and calls BASS_MusicPlayEx.

7.4 Tempo control

typedef void (__thiscall *Audio_PlayMusicAtSpeed_t)(void* musicChannel, const char* trackName, float speed);
static auto playSpeed = (Audio_PlayMusicAtSpeed_t)0x0046A440;

playSpeed(channel, "Intro", 4.0f);  // 4x speed
playSpeed(channel, "Race",  2.0f);  // normal-ish

7.5 Stopping music

typedef void (__fastcall *Audio_StopChannel_t)(void* musicChannel);
static auto stopMusic = (Audio_StopChannel_t)0x0046A0D0;

stopMusic(channel);

7.6 Jukebox / track list

jukebox.xml defines named tracks:

<jukebox>
  <SONG>
    <NAME>Goal!</NAME>
    <HEX>1A</HEX>
  </SONG>
</jukebox>

LoadJukebox (0x0046A4D0) parses this and stores name→handle mappings. If you add a custom music track, you must either:

  • Edit jukebox.xml and the .mo3 file, or
  • Use BASS_MusicLoad directly and call BASS_MusicPlayEx yourself.

7.7 Custom music with BASS directly

typedef DWORD (__stdcall *BASS_MusicLoad_t)(BOOL mem, void* file, DWORD offset, DWORD length, DWORD flags, DWORD freq);
typedef BOOL  (__stdcall *BASS_MusicPlayEx_t)(DWORD handle, DWORD pos, BOOL restart, DWORD flags);
typedef BOOL  (__stdcall *BASS_ChannelStop_t)(DWORD handle);

static auto BASS_MusicLoad   = (BASS_MusicLoad_t)0x004794B6;
static auto BASS_MusicPlayEx = (BASS_MusicPlayEx_t)0x004794C8;
static auto BASS_ChannelStop = (BASS_ChannelStop_t)0x004794BC;

DWORD h = BASS_MusicLoad(0, "music\\custom.mo3", 0, 0, 4, 0);
if (h) BASS_MusicPlayEx(h, 0, TRUE, 0);

Flag 4 is BASS_MUSIC_PRESCAN.


8. Volume and 3D math

8.1 Reading/changing SFX volume

float* volume = (float*)((char*)soundDevice + 0x838);
*volume = 0.75f;  // 75% SFX volume

The registry key "Sound Volume" is read at startup and saved on shutdown.

8.2 3D attenuation formula

Sound_CalculateDistanceAttenuation (0x00466750) does:

  1. Find nearest listener in SoundDevice+0x854.
  2. dist = distance(soundPos, nearestListener).
  3. If dist <= minRolloff → return 1.0.
  4. If dist >= maxRolloff → return 0.0.
  5. Else linear: 1.0 - (dist - minRolloff) / (maxRolloff - minRolloff).

Default maxRolloff is 6000.0f (0x45BB8000).


9. Code example: a complete custom sound hook

#include <windows.h>

// Type defs
using SoundList_Ctor_t      = void* (__thiscall*)(void*, void*);
using Sound_LoadOggOrWav_t  = void  (__thiscall*)(void*, const char*);
using Sound_PlayChannel_t   = void  (__fastcall*)(void*);
using Sound_Play3D_t        = void  (__thiscall*)(void*, float, float, float);
using operator_new_t        = void* (*)(size_t);

static SoundList_Ctor_t     SoundList_Ctor     = (SoundList_Ctor_t)    0x00458F20;
static Sound_LoadOggOrWav_t Sound_LoadOggOrWav = (Sound_LoadOggOrWav_t)0x00459660;
static Sound_PlayChannel_t  Sound_PlayChannel   = (Sound_PlayChannel_t) 0x004597B0;
static Sound_Play3D_t       Sound_Play3D        = (Sound_Play3D_t)      0x00459860;
static operator_new_t       operator_new        = (operator_new_t)      0x004BA57B;

void* g_CustomSound = nullptr;

void InitCustomSound(void* app) {
    if (g_CustomSound) return;
    void* list = operator_new(0x420);
    if (!list) return;
    SoundList_Ctor(list, app);
    Sound_LoadOggOrWav(list, "sounds\\MyCustom");
    g_CustomSound = list;
}

void PlayCustomSFX(void* app, float x, float y, float z) {
    InitCustomSound(app);
    if (!g_CustomSound) return;
    Sound_Play3D(g_CustomSound, x, y, z);
}

void PlayCustomUI(void* app) {
    InitCustomSound(app);
    if (!g_CustomSound) return;
    Sound_PlayChannel(g_CustomSound);
}

Drop MyCustom.ogg into Sounds/MyCustom.ogg (or the game root) and call PlayCustomUI(app) from any hook.


10. Reimplementation / replacement notes

If you are rebuilding the audio layer for a port:

  • BASS music: replace with SDL_mixer, OpenAL, or miniaudio + libopenmpt / libxmp for .mo3/.xm/.it.
  • DirectSound8 SFX: replace with SDL_mixer Mix_Chunk channels or OpenAL sources. For true 3D, OpenAL gives you the listener/distance model for free.
  • Channel pool: the original uses a circular allocator across duplicated buffers. A modern replacement can just allocate dynamic channels.
  • OGG/WAV: both are trivial with stb_vorbis / dr_wav or SDL_mixer.
  • Volume scale: DirectSound uses dB attenuation (-10000 to 0). SDL_mixer uses 0..128. Convert linear 0..1 → mixer volume.

11. Quick reference: call these addresses

What you want Call
Play existing SFX at full volume Sound_PlayChannel(App+offset) (0x004597B0)
Play existing SFX at a 3D position Sound_Play3D(App+offset, x, y, z) (0x00459860)
Load custom SFX at runtime SoundList_CtorSound_LoadOggOrWav (0x00458F20, 0x00459660)
Play a BASS music track Audio_PlayMusic(App+0x53C, name) (0x0046A310)
Stop BASS music Audio_StopChannel(channel) (0x0046A0D0)
Load BASS file directly BASS_MusicLoad (0x004794B6)
Read global SFX volume *(float*)(SoundDevice + 0x838)

Generated from GhidraMCP decompilation of Hamsterball.exe at commit time. Addresses assume the default image base 0x00400000. If you are hooking a relocated process, convert to RVA first: RVA = addr - 0x400000, then real = GetModuleHandle(NULL) + RVA.


🔗 Related Documents

Audio System

types : audio
keywords :

📂 View source on GitHub


Audio System

Overview

Hamsterball uses a dual audio system:

  1. BASS.dll (shipped alongside exe) for music playback (MO3 tracker format)
  2. DirectSound (dsound.dll) for 3D positional sound effects (OGG)

BASS Music System

API Calls

Address Function Parameters
0x487E40 BASS_SetConfig Config options
0x487E4C BASS_Init device=-1, freq=44100, flags=0, win=NULL
0x487E46 BASS_Start Resume playback
0x487E58 BASS_Stop Pause all playback
0x487E52 BASS_Free Shutdown
0x4794B6 BASS_MusicLoad Load .MO3 file
0x4794C8 BASS_MusicPlayEx Play with flags
0x4794C2 BASS_ChannelSetAttributes freq, volume, ramp
0x4794BC BASS_ChannelStop Stop channel
0x4794B0 BASS_ErrorGetCode Last error

MusicChannel Struct (0x534 bytes)

Offset Type Field
+0x00 vtable* MusicChannel_DeletingDtor
+0x04 DWORD BASS device handle
+0x0C AthenaList Track list
+0x10 int Track count
+0x418 AthenaList Track data array
+0x528 float Current volume (1.0 default)
+0x530 byte Muted flag
+0x531 byte Paused flag

Music Playback

App_Initialize_Full step 7:
  music_channel = MusicChannel_LoadAndAppend(App+0x17C, "music\\music.mo3")
  
Audio_PlayMusic(channel, track_name):
  Linear search track list (this+0x418) for matching name
  Set volume = 1.0
  BASS_ChannelSetAttributes(handle, -1, 100, fade_ramp)
  BASS_MusicPlayEx(handle, track_handle, restart, flags)

Audio_PlayMusicAtSpeed(channel, track_name, speed):
  Same but with tempo control
  speed=4.0 for intro, speed=2.0 for normal

MusicPlayer_ctor (0x426030):
  Creates jukebox player for XML-driven music sequences

MusicPlayer_SetTempoScale (0x46A140):
  Adjusts playback speed (2.0=normal, 4.0=fast forward)

MusicDevice_FadeAll (0x474780):
  Fade all music channels simultaneously

Jukebox XML Format

jukebox.xml contains song sequences:
  <jukebox>
    <song name="TrackName" file="music\track.mo3"/>
    ...
  </jukebox>

LoadJukebox("jukebox.xml") loads this at startup

DirectSound SFX System

SoundDevice Struct

Created at 0x466620 (SoundDevice_ctor).
Manages DirectSound buffer pool for 3D positional audio.

Sound List

Address Function Description
0x458F20 SoundList_Ctor Sound list constructor
0x59700 SoundList_DtorInner Cleanup inner data
0x59570 SoundEntry_ScalarDtor Individual sound destructor
0x466320 SoundChannel_Ctor DirectSound channel (0x80 bytes)
0x466C90 SoundBuffer_ScalarDtor DSound buffer destructor
0x466A10 SoundDevice_UpdateChannels Per-frame mix/update
0x466B80 SoundDevice_Play3DAll 3D positional sound playback
0x4ABAFAF DSound_SetVolume Volume control

3D Sound Effects

Each sound effect is played at a 3D position:

  • SceneObject_SpawnWithSound (0x439A30) - Play sound at object position
  • Audio position comes from Scene+0x29C0 (camera distance)
  • Sound attenuation: 3D falloff based on distance

Level-Specific Sound Functions

Address Function Description
0x435B00 CollisionLevel_PlayBreakSound Collision break sound
0x437300 Lifter_PlaySound Lifter mechanism sound
0x436B70 Pendulum_PlayCollisionSound Pendulum impact sound
0x434030 Rotator_PlayCollisionSound Rotator impact sound
0x4367E0 Rotator_StartSound Rotator running sound
0x436CF0 Rotator_TriggerSound Rotator trigger effect
0x434AB0 Sawblade_SetBreakSound Sawblade break sound
0x401090 Scene_SetSoundMode Toggle sound mode

Menu Sound

Address Function Description
0x475430 Menu_AddSound Play menu click/transition sound

Sound Volume

Address Function Description
0x466570 Level_ReadSoundVolume Read volume from registry
0x474480 MusicDevice_SetVolume Set master music volume
0x4744B0 MusicDevice_ReadVolume Read current volume
0x474510 MusicDevice_MuteToggle Toggle mute

App Audio Initialization

App_CreateAudioDevice (0x46C0B0):
Creates DirectSound device for SFX
Allocates 16 channels for sound mixing

App_Initialize_Full step 7-11:

  • Load "music\music.mo3" via BASS
  • Load "jukebox.xml" for track sequencing
  • RegKeyList_CopyFromSibling for music channel linking
  • Two music channels: primary (App+0x534) and secondary (App+0x53C/0x538)

Level_InitScene (0x40B090):

  • Create SoundChannel (0x80 bytes) for level SFX
  • Set volume to -50.0 dB initially
  • Play level music at speed 2.0 (normal) or 4.0 (intro skip)

🔗 Related Documents

BadBall (8-Ball) AI Circling S

types : skill
keywords :

📂 View source on GitHub


BadBall (8-Ball) AI Circling System

Function: Ball_AI_ChaseNearest (vtable[4], 0x408390)

Created via GhidraMCP create_function (was not in function table). Body size: 1184 bytes.

Activation Gate

if ((param_1[0x31d] != 0) || (*(char *)(param_1[4] + 0x237) != '\0')) {
    // Run AI
}

Two paths to AI activation:

  1. ball+0xC74 (C-index 0x31D) = is_8ball flag ≠ 0 — set by Ball_InitBattleMode
  2. App+0x237 ≠ 0 — scene is in tournament/demo mode (App is at ball+0x10 via param_1[4])

AI Pipeline (in order)

  1. Save previous frame's is_active state, call Ball_Update(param_1) (0x405E00 — full 23-phase physics tick)

  2. Score proximity check — if a ball just became inactive (transition from active→inactive), scan all balls within _DAT_004cf554 (3000.0 units) and award score via Difficulty_GetTimeModifier × 2000.0

  3. Compute distance from homefVar1 = 2D distance from ball pos to home position (param_1[0x318..0x31A])

  4. Find nearest player ball — iterate board+0x29D4 AthenaList, filter by:

    • ball+0x768 ≠ 0 (is active/alive)
    • ball+0x2F9 == 0 (NOT falling)
    • ball+0x300 == 0 (not in some special state)
    • ball+0x324 == 0 (not flagged)
    • scene+0x3A4C ≠ 0 (countdown finished / race active)
    • ball+0x18 ≠ -1 (has a player index)

    Track nearest by Math_FastDistance2D.

  5. Orbit computation (if distance from home < 220.0):

    // _DAT_004cf550 = 220.0 (orbit threshold)
    if (dist_from_home < 220.0) {
        target_x = sin(spin_angle) × SPINDISTANCE + home_x;
        target_z = cos(spin_angle) × SPINDISTANCE + home_z;
    }
    
    • spin_angle = param_1[0x31e] = ball+0xC78 (float, radians)
    • SPINDISTANCE = param_1[799] = ball+0xC7C (float, orbit radius)
    • Uses Wave_Sin and Wave_Cos (not standard C math)
  6. Angle increment (always, every frame):

    param_1[0x31e] += _DAT_004cf48c;  // += 2.0 radians per frame
    

    _DAT_004cf48c = 2.0 (0x40000000). This is CONSTANT — the angular speed of the orbit is always the same.

  7. Chase/flee override (if player ball found within CHASE+HOME):

    if (nearest_ball != 0 && dist_from_home < HOME && nearest_dist < CHASE) {
        target_x = nearest_ball->pos_x;  // +0x164
        target_z = nearest_ball->pos_z;  // +0x16C
        
        // FLEE if player is bigger
        if (ball+0x284 (own_radius) < nearest_ball+0x284 × _DAT_004cf508) {
            target_x = -target_x;
            target_z = -target_z;
        }
    }
    

    _DAT_004cf508 = 0.6666667 (2/3) — flee if own radius < player radius × 0.667

  8. Apply force toward target — normalize direction vector, call vtable[5] (0x14 offset):

    dx = target_x - ball_pos_x;
    dz = target_z - ball_pos_z;
    dist = sqrt(dx² + dz²);
    if (dist > 0) { inv = 1.0 / dist; dx *= inv; dz *= inv; }
    (*vtable[5])(dx, 0, dz);  // Ball_ApplyForceWithMultipliers
    

Key Constants (verified from .data section)

Address Hex Float Purpose
0x4CF550 00005C43 220.0 Orbit threshold: if dist_from_home < this, do orbit
0x4CF48C 00000040 2.0 Spin angle increment per frame (radians)
0x4CF508 66666666 0.667 Flee ratio: flee if own_radius < player_radius × 0.667
0x4CF554 00803B45 3000.0 Score award distance (when ball deactivates near player)
0x4CF558 0000804F 250000.0(?) FastDistance2D correction constant
0x4CF368 00000000 0.0 Zero constant (used as floor/clamp)
0x4CF310 0000803F 1.0 One constant (used for normalization)

MESHWORLD Tag Values Per Level

Level File Race CHASE HOME SPINDISTANCE Notes
LevelCascade Beginner 300 375 NOT SPECIFIED (default 50.0) No SPINDISTANCE tag — uses InitPhysics default
Level2 Intermediate (BADBALL not present in this level)
Level3 Dizzy 200 300 45 Explicit orbit radius

SPINDISTANCE Initialization — VERIFIED (June 2026)

VERIFIED by decompilation of Ball_InitPhysicsDefaults (0x00405100):

void __fastcall Ball_InitPhysicsDefaults(void *param_1) {
    Ball_SetupCollisionRender((int)param_1);
    Vec3_Init(&stack, 0, 0, 0);
    Ball_SetTrajectory(param_1, ...);
    *(int*)(param_1 + 0x18) = -1;           // player_index = none
    *(float*)(param_1 + 0x278) = 0.5;       // gravity_scale
    *(float*)(param_1 + 0x27c) = 0.2;       // unknown
    *(float*)(param_1 + 0x284) = 35.0;      // radius
    *(float*)(param_1 + 0x1a0) = 0.2;       // speed_scale
    *(float*)(param_1 + 0x188) = 6.0;       // max_speed
    *(float*)(param_1 + 0xC78) = 0.0;       // spin_angle = 0 (radians)
    *(float*)(param_1 + 0xC7C) = 50.0;      // SPINDISTANCE = 50.0 (orbit radius)
    *(float*)(param_1 + 0xC6C) = 600.0;     // CHASE default = 600.0
    *(float*)(param_1 + 0xC70) = 1200.0;    // HOME default = 1200.0
}

SPINDISTANCE IS initialized to 50.0 — it is NOT uninitialized heap garbage. The orbit radius on Beginner (where no SPINDISTANCE MESHWORLD tag is present) is deterministically 50.0 units.

This means behavioral variance on Beginner comes from:

  1. Timing of player entry — spin_angle increments 2.0 rad/frame. At 25fps that's ~50 rad/s ≈ 8 full revolutions/s. The orbit position when the player enters determines initial approach direction.
  2. Orbit vs chase transition — entering CHASE range (300 on Beginner) overrides orbit with direct chase. The transition point relative to the orbit cycle makes it look more or less aggressive.
  3. Player radius vs 8-ball radius — if player is bigger, 8-Ball flees instead of chases (× 0.667 ratio check).
  4. Ball_Update phase interactions — Ball_AI_ChaseNearest calls Ball_Update every frame; physics state (velocity, position on slopes) at the moment of chase transition affects how aggressively the ball curves.

Ball Field Layout (AI-relevant offsets)

Byte Offset C-Index Type Field Set By
0xC60 0x318 float home_x CreateBadBall (from MW coords)
0xC64 0x319 float home_y CreateBadBall
0xC68 0x31A float home_z CreateBadBall
0xC6C 0x31B float CHASE distance CreateBadBall (from MW tag)
0xC70 0x31C float HOME distance CreateBadBall (from MW tag)
0xC74 0x31D byte is_8ball flag Ball_InitBattleMode
0xC78 0x31E float spin_angle (radians) Ball_ctor2 (byte 0 only? or InitPhysics?)
0xC7C 0x31F (799) float SPINDISTANCE (orbit radius) CreateBadBall (from MW tag, or InitPhysics default?)
0x164 0x59 float pos_x Ball_Update (physics)
0x168 0x5A float pos_y Ball_Update
0x16C 0x5B float pos_z Ball_Update
0x284 0xA1 float radius Ball_InitPhysicsDefaults (35.0) or SIZE tag

GhidraMCP Notes

  • Ball_AI_ChaseNearest at 0x408390 was NOT in Ghidra's function table — had to create_function first, then decompile_function worked.
  • The decompiled output uses param_1[N] (int* indexing, ×4 for byte offset) and param_1[799] (decimal index, ×4 = 0xC7C).
  • __ftol2() calls in the decompilation are FPU register spills from float→int conversions used by Math_FastDistance2D — not actual function calls.
  • unaff_EBX, unaff_EBP, unaff_ESI, unaff_EDI are register variables that Ghidra couldn't fully recover — they carry ball position coordinates through the distance calculations.

See Also

  • references/object-spawning-createbadball.md — CreateBadBall spawning analysis, Ball_InitPhysicsDefaults defaults
  • references/ball-vtable-player-vs-ai-path.md (in hamsterball-dll-modding skill) — vtable[4] dispatch for AI vs player balls

🔗 Related Documents

Ball [[53892486924051|collision system]] Analysis

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/collision-system-analysis.md)


Ball Collision System Analysis

Verdict: Collisions Are ASYMMETRIC Per-Event (Definitively Proven via Decompilation)

Each collision event generates a collision entry in only ONE ball's collision
list. The detecting ball processes the collision (push-apart, velocity exchange)
and writes velocity changes to BOTH balls' physics objects — but only the
detecting ball gets a collision entry. The other ball does not receive an entry
for this event.

Both balls independently detect the same contact across separate Ball_Update
calls (which run sequentially within Scene_UpdateBallsAndState), but each
detection is a separate event at a different tick. This is why runtime dumps
show entries at different timestamps with unequal counts (e.g., 4 player
entries vs 7 eight-ball entries).


How the Collision System Works (Full Call Chain)

1. Scene_UpdateBallsAndState (0x41B540)

Iterates the ball list at Scene+0x29D4 and calls vtable[4] (Ball_Update)
for each ball sequentially:

while (ball = AthenaList_Next(scene+0x29D4)) {
    Scene_SetCamera(scene, ball, 1);
    (*ball->vtable[4])();  // Ball_Update
}

No ball-ball collision detection happens here. It just calls each ball's
update function. Collision detection happens INSIDE each ball's Ball_Update.

2. Ball_Update (0x405E00) — Collision Setup

Each ball's Ball_Update creates its own collision detection objects:

Phase A: Create SpatialTree node (for wall/floor collisions)

00406898: PUSH 0x20              ; allocate 32 bytes
0040689a: CALL operator_new
004068bc: CALL SpatialTree_ctor   ; 0x463330 — vtable = 0x4D9038
004068c5: MOV [EAX+0x10], 0xa    ; type = 10 (spatial)
004068cc: MOV EDX, [ESI+0x278]   ; ball+0x278 = geometry data
004068d2: MOV [EAX+0xc], EDX     ; node+0xc = geometry
004068db: MOV [ESP+0x1c], EAX    ; save SpatialTree ptr → [ESP+0x1c]

If ball+0x80c > 0, the SpatialTree is added to the collision mesh:

004068e9: MOV ECX, [ESI+0x1a4]   ; ECX = collision_mesh
004068f1: PUSH EAX               ; push SpatialTree
004068f2: CALL [EDX+0x14]        ; vtable[5] = CollisionMesh_AddTriangle

CollisionMesh_AddTriangle (0x456120) appends to collision_mesh+0x430:

void CollisionMesh_AddTriangle(void *this, int node) {
    AthenaList_Append(this + 0x430, node);
    *(void **)(node + 8) = this;  // back-pointer to mesh
}

Phase B: Create CollisionNode (for ball-ball collisions)

004068f5: PUSH 0x14              ; allocate 20 bytes
004068f7: CALL operator_new
0040690f: MOV ECX, [ESI+0x14]   ; ECX = Scene (ball+0x14)
00406912: ADD ECX, 0x29d4        ; ECX = Scene+0x29D4 (ball list)
00406918: PUSH ECX
0040691b: CALL CollisionNode_ctor ; 0x466CF0
0040692c: MOV [ESP+0x18], EAX    ; save CollisionNode ptr → [ESP+0x18]

CollisionNode_ctor stores the ball list reference:

void CollisionNode_ctor(void *this, undefined4 param_1) {
    CollisionNode_BaseInit(this, param_1);
    // vtable = 0x4D9128
    *(this + 0x0c) = 0x3dcccccd;  // radius scale
    *(this + 0x10) = 0x3dcccccd;
}

void CollisionNode_BaseInit(void *this, undefined4 param_1) {
    *this = &vtable_0x4DA65C;  // base vtable
    *(this + 4) = param_1;       // ← CollisionNode+0x04 = Scene+0x29D4 (ball list!)
}

If ball+0x324 == 0 (not in special state), the CollisionNode is also added
to the collision mesh via vtable[5].

Phase C: Run collision detection (vtable[1])

004069d5: MOV ECX, [ESI+0x1a4]   ; ECX = collision_mesh (this)
004069e4: MOV EDX, [ECX]         ; EDX = vtable
004069e7: CALL [EDX+0x4]         ; vtable[1] = Ball_AdvancePositionOrCollision

Ball_AdvancePositionOrCollision (0x4564C0) calls vtable[7] internally,
which does the actual collision detection.

3. CollisionMesh vtable[7] (0x456890) — Collision Detection Engine

This function iterates all nodes in collision_mesh+0x430 (the SpatialTree
and CollisionNode added above) and tests each against the ball's movement:

void CollisionMesh_Detect(void *this, float *out_pos, float *in_pos,
                          float *in_dir, float *in_scale, ...) {
    do {
        Material_Init(&local_material);  // zero 0x68-byte struct
        
        // Iterate all nodes in +0x430 list
        for each node in AthenaList(this+0x430):
            node->vtable[2](this, aabb, &local_material);  // collision test
        
        if (local_material.type_field == 0) break;  // no collision
        
        *param_7 = 1;  // collision flag
        
        // Call resolution function
        node->vtable[3](this, &result_pos, this, &in_dir, &local_material);
        
        // Create entry and append to collision list
        entry = operator_new(0x68);
        Material_Init(entry);
        Material_Copy(entry, &local_material);  // copy all fields
        AthenaList_Append(this + 0x18, entry);  // ← ONLY to THIS mesh's list!
        
    } while (iteration_count < this+0xc60);
}

Critical: AthenaList_Append(this + 0x18, entry) appends to only this
ball's
collision list. There is no second AthenaList_Append call to the
other ball's list.

4. CollisionNode vtable[2] (0x467030) — Ball-Ball Collision Test

This is the function that tests THIS ball against ALL other balls:

void CollisionNode_Test(int *this, int collision_mesh, float *aabb, int material) {
    int ball_count = AthenaList_GetSize(this[1]);  // this+0x04 = ball list
    
    for (int i = 0; i < ball_count; i++) {
        int *other_ball = AthenaList_Get(this[1], i);
        
        // Self-collision skip: compare mesh IDs
        if (*(collision_mesh + 0x10) != *(other_ball->collision_mesh + 0x10)) {
            // Get other ball position and radius
            float ox = other_ball[0x164];  // pos X
            float oy = other_ball[0x168];  // pos Y
            float oz = other_ball[0x16c];  // pos Z
            float r  = other_ball[0x284];  // radius
            
            // AABB overlap test
            if (aabb_overlap(aabb, sphere(ox, oy, oz, r))) {
                // Detailed collision test
                char hit = this->vtable[4](aabb, ..., &collision_t);
                
                if (hit && (material->type == 0 || collision_t < material->t)) {
                    material->t = collision_t;
                    material->subtype = 4;          // +0x04 = 4
                    material->source_ref = this;     // +0x64 = CollisionNode ptr
                    material->other_ball = other_ball; // +0x0C = other ball ptr
                    material->material_ptr = 0;       // +0x10 = NULL
                    // +0x14 = contact point (XYZ)
                    // +0x20 = normal (XYZ, negated)
                    // +0x58 = previous point
                }
            }
        }
    }
}

Key: The +0x64 field (source_ref) is set to this — the CollisionNode
pointer. This CollisionNode was created by THIS ball's Ball_Update, so only
this ball will match the +0x64 check in the collision processing loop.

5. CollisionNode vtable[3] (0x467360) — Ball-Ball Collision Resolution

This function is called AFTER vtable[2] detects a collision. It finalizes the
entry and applies velocity changes to BOTH balls:

void CollisionNode_Resolve(int this, float *out_pos, int collision_mesh,
                          int dir, undefined4 *material) {
    // Set entry type to BALL-BALL
    *material = 1;  // +0x00 = 1 (ball-ball type!)
    
    // Compute reflection/deflection
    float angle = asin(...);
    material[0x0b] = 1.0 - 2.0 * angle * angle;  // +0x2C
    
    // Copy relative velocity into entry
    material[0x0c] = current_vel.x;  // +0x30 = rel_vel X
    material[0x0d] = current_vel.y;  // +0x34 = rel_vel Y
    material[0x0e] = current_vel.z;  // +0x38 = rel_vel Z
    
    // Get other ball's collision mesh
    int other_ball = material[3];              // +0x0C = other_ball
    int other_mesh = *(other_ball + 0x1a4);    // other_ball+0x1A4
    
    // Write velocity to THIS ball's mesh
    *(collision_mesh + 0xca4) = push_vel.x * factor_a;
    *(collision_mesh + 0xca8) = push_vel.y * factor_a;
    *(collision_mesh + 0xcac) = push_vel.z * factor_a;
    
    // Write velocity to OTHER ball's mesh
    *(other_mesh + 0xca4) = push_vel2.x * factor_b;
    *(other_mesh + 0xca8) = push_vel2.y * factor_b;
    *(other_mesh + 0xcac) = push_vel2.z * factor_b;
}

This is the critical finding: The resolution function writes velocity changes
to BOTH balls' collision meshes (+0xCA4/+0xCA8/+0xCAC), but only ONE entry
is created (in the detecting ball's list).

6. Ball_Update Collision Processing Loop

After vtable[1] returns, Ball_Update iterates the collision entries:

// Clear old entries from +0x430 list
collision_mesh->vtable[6]();  // AthenaList_Free(+0x430)

// Iterate entries in +0x18 list (populated by vtable[1])
for each entry in AthenaList(collision_mesh + 0x18):
    
    // Phase 1: Wall collision (type == 2)
    if (entry->type == 2 && entry->source_ref == spatialtree_node) {
        // Update camera focus point
    }
    
    // Phase 2: Ball-ball trajectory (type == 1, source_ref == spatialtree)
    // DEAD CODE: ball-ball entries have source_ref == CollisionNode, 
    //            never == SpatialTree. This never fires for ball-ball.
    if (entry->type == 1 && entry->source_ref == spatialtree_node) {
        Ball_ApplyTrajectory(ball);
        // camera/sound stuff
    }
    
    // Phase 3: Ball-ball physics (type == 1, source_ref == collisionnode)
    if (entry->type == 1 && entry->source_ref == collisionnode) {
        other_ball = entry->other_ball;  // +0x0C
        // Push-apart: compute distance, apply separation
        // Velocity exchange: read both balls' positions
        // Sound: play collision sound if rel_vel > threshold
        // Camera: call vtable[8] for camera focus
    }

Disasm Verification of +0x64 Check

The decompiler incorrectly mapped unaff_EBP and unaff_ESI to the collision
checks. The actual disassembly shows:

; EBP = current collision entry (piVar16)
; ESI = ball pointer (set at 00405e20: MOV ESI,ECX)
; [ESP+0x1c] = SpatialTree node (set at 004068db)
; [ESP+0x18] = CollisionNode (set at 0040692c)

; Phase 1 (type == 2, wall):
00406b80: CMP [EBP], 0x2              ; entry->type == 2?
00406b8a: CMP [EBP+0x64], EDX         ; entry+0x64 == [ESP+0x1c] (SpatialTree)?

; Phase 2 (type == 1, trajectory — DEAD CODE for ball-ball):
00406bd3: CMP [EBP], 0x1              ; entry->type == 1?
00406be1: CMP [EBP+0x64], EDX         ; entry+0x64 == [ESP+0x1c] (SpatialTree)?

; Phase 3 (type == 1, physics):
00406c79: CMP [EBP], 0x1              ; entry->type == 1?
00406c87: CMP [EBP+0x64], ECX         ; entry+0x64 == [ESP+0x18] (CollisionNode)?
00406c90: MOV EDI, [EBP+0xc]          ; other_ball = entry+0x0C

Ghidra decompiler error: The decompiler mapped [ESP+0x18] (loaded into
ECX at 0x00406c83) to unaff_ESI. This is wrong — ESI holds the ball pointer
(set at MOV ESI,ECX in the prologue), but ECX at 0x00406c83 is loaded from
[ESP+0x18] (the CollisionNode). The actual comparison is entry+0x64 == CollisionNode, not entry+0x64 == ball.


Collision Entry Struct (0x68 bytes)

Offset Type Name Set By Evidence
+0x00 int type vtable[3] (0x467360): *material = 1 0x00000001 for ball-ball, 0x00000005 for floor (set in Ball_AdvancePositionOrCollision: *puVar7 = 5)
+0x04 int subtype vtable[2] (0x467030): *(param_4+4) = 4 Always 4 for ball-ball, 1 for wall, 0 for floor
+0x08 float collision_t vtable[2]: *(param_4+8) = fStack_8c Distance ratio along movement ray
+0x0C ptr other_ball vtable[2]: *(param_4+0xc) = iVar3 (ball from list) Verified in dump: points to other ball's address
+0x10 ptr material_ptr vtable[2]: *(param_4+0x10) = 0 NULL for ball-ball, set for wall entries
+0x14 float[3] contact_point vtable[2]: Vec3_CopyUnlessEqual(param_4+0x14, ...) World-space XYZ of contact
+0x20 float[3] normal vtable[2]: Vec3_CopyUnlessEqual(param_4+0x20, ...) Contact normal, negated (points away from other ball)
+0x2C float deflection vtable[3]: material[0x0b] = fVar11 Angle-based deflection factor
+0x30 float[3] rel_vel vtable[3]: material[0xc..0xe] Relative velocity at contact point
+0x3C–0x50 unused Material_Init zeroes Always zero for ball-ball
+0x54 float penetration Not set for ball-ball Only used for type==5 (floor)
+0x58 float[3] prev_point vtable[2]: Vec3_CopyUnlessEqual(param_4+0x58, ...) Previous contact point
+0x64 ptr source_ref vtable[2]: *(param_4+100) = param_1 (CollisionNode) Ownership token: which CollisionNode created this entry
+0x68–0x7C trailing data Not part of Material struct (0x68 bytes) Appears in dump as adjacent heap data

Note: The entry struct is 0x68 bytes (allocated by operator_new(0x68)),
NOT 0x80 bytes. The dump shows 0x80 bytes because the logging code reads
past the allocation boundary. Fields +0x68–0x7C are adjacent heap data, not
part of the entry struct.


CollisionMesh vtable

Vtable at 0x4D8E10 (set by CollisionMesh_ctor at 0x456D80):

Slot Offset Address Function
0 0x00 0x456870 Mesh dtor
1 0x04 0x4564C0 Ball_AdvancePositionOrCollision — move + detect collisions
2 0x08 0x456280 Unknown
3 0x0C 0x456CD0 Ball_InitBattleMode — battle mode setup
4 0x10 0x456110 Unknown
5 0x14 0x456120 CollisionMesh_AddTriangle — add node to +0x430 list
6 0x18 0x456140 AthenaList_Free(+0x430) — clear node list
7 0x1C 0x456890 Collision detection engine — test nodes, create entries
8 0x20 0x457A20 Unknown

CollisionNode vtable

Vtable at 0x4D9128 (set by CollisionNode_ctor at 0x466CF0):

Slot Offset Address Function
0 0x00 0x466D50 dtor
1 0x04 0x466CD0 Unknown
2 0x08 0x467030 Ball-ball collision test — iterate ball list, AABB test
3 0x0C 0x467360 Ball-ball collision resolve — set type=1, exchange velocities
4 0x10 0x466D70 Sphere-ray intersection test
5 0x14 0x466F50 Unknown

SpatialTree vtable

Vtable at 0x4D9038 (set by SpatialTree_ctor at 0x463330):

Slot Offset Address Function
2 0x08 0x463880 SpatialTree_ForEach — iterate children, call their vtable[8]
6 0x18 0x463500 Unknown (called at 0x00406895 via vtable)

Collision List Location

Offset from Ball Offset from CollisionMesh Field
Ball + 0x1A4 collision_mesh_ptr (pointer to CollisionMesh)
collision_mesh + 0x10 mesh_id (unique ID for self-collision skip)
collision_mesh + 0x18 entry_list (AthenaList of collision entries)
collision_mesh + 0x1C entry_count (AthenaList count)
collision_mesh + 0x424 entry_arr (AthenaList data pointer)
collision_mesh + 0x430 node_list (AthenaList of SpatialTree/CollisionNode)
collision_mesh + 0xC60 max_iterations (collision detection loop limit)
collision_mesh + 0xC74 collision_time (accumulated collision time)
collision_mesh + 0xCA4 velocity (XYZ float, written by resolution)
collision_mesh + 0xCA8 velocity_y
collision_mesh + 0xCAC velocity_z

Note: In the decompiler, param_1[0x69] means *(int*)(ball + 0x69*4) =
*(int*)(ball + 0x1A4). The memory note "Ball + 0x69 (DWORD)" was using a
different indexing convention. The correct offset is ball + 0x1A4 (byte offset).


Stale Entry Problem

The AthenaList at collision_mesh+0x18 is NOT cleared between frames for
entries that persist. Old collision entries remain with stale +0x0C pointers.
These stale entries have invalid playerID values at other_ball + 0x18.

Fix

int other_id = *(int*)((char*)other_ball + 0x18);
if (other_id != 0 && other_id != -1) continue;  // skip stale

Modding Implications

1. Hook ALL Balls, Not Just the Player

Since each ball creates entries only in its own list, hooking only the player's
Ball_Update misses ~64% of collision events (based on the 4:7 ratio observed
in the dump). Hook 0x405E00 and process entries for every ball:

void __fastcall Hooked_BallUpdate(Ball* ball, void* edx) {
    Original_BallUpdate(ball, edx);

    CollisionMesh* mesh = *(CollisionMesh**)((char*)ball + 0x1A4);
    int count = *(int*)((char*)mesh + 0x1C);
    if (count <= 0) return;

    void** entries = *(void***)((char*)mesh + 0x424);
    if (!entries || !*entries) return;

    for (int i = 0; i < count; i++) {
        DWORD* e = (DWORD*)entries[0][i];
        if (!e || IsBadReadPtr(e, 0x68)) continue;
        if (e[0] != 1) continue;  // ball-ball only (type at +0x00)

        DWORD other_ptr = e[3];  // +0x0C
        if (other_ptr <= 0x10000) continue;
        if (IsBadReadPtr((void*)other_ptr, 0x20)) continue;

        int other_id = *(int*)((char*)other_ptr + 0x18);
        if (other_id != 0 && other_id != -1) continue;

        mod->onBallBump(ball, (Ball*)other_ptr);
    }
}

2. Fire onBallBump for Both Participants

Since only the detecting ball has the entry, the other ball won't see this
collision event. For full coverage (e.g., if you need both balls to react),
fire the callback for both:

mod->onBallBump(ball, (Ball*)other_ptr);      // detecting ball
mod->onBallBump((Ball*)other_ptr, ball);       // other ball (no entry, but was hit)

3. Contact Deduplication

The collision list persists entries across frames while balls remain in contact.
Use a per-ball touching set to fire onBallBump only on new contacts:

static DWORD touching[MAX_BALLS][16] = {0};

4. Velocity Fields Are Shared

The resolution function writes to BOTH balls' velocity fields
(collision_mesh+0xCA4/+0xCA8/+0xCAC). If you modify one ball's velocity
after collision, the other ball may have already been affected. Read velocities
AFTER Original_BallUpdate returns to see post-collision values.

5. Phase 2 (Ball_ApplyTrajectory) Is Dead Code

The if (type==1 && source_ref==SpatialTree) check at 0x00406be1 never fires
for ball-ball entries because they have source_ref == CollisionNode. If you
need trajectory effects on ball-ball collisions, you must add them yourself
in the hook — the game's built-in trajectory code only runs for wall collisions.

6. Asymmetry Affects Detection Frequency

The 8-ball receives ~1.8x more collision entries than the player because:

  • The 8-ball's Ball_Update runs after the player's in the iteration order
  • By the time the 8-ball processes, the player may have already pushed it
  • The 8-ball then detects the ongoing contact and creates its own entry
  • The player, on its next update, may find the balls have separated

This means collision events are biased toward whichever ball runs second in
the Scene_UpdateBallsAndState iteration.


Summary of Decompilation Chain

Scene_UpdateBallsAndState (0x41B540)
  └→ Ball_Update (0x405E00) [for each ball]
       ├→ Create SpatialTree node → [ESP+0x1c]
       ├→ Create CollisionNode (with Scene+0x29D4 ball list) → [ESP+0x18]
       ├→ CollisionMesh_AddTriangle (vtable[5]) — add both nodes to +0x430
       ├→ Ball_AdvancePositionOrCollision (vtable[1] = 0x4564C0)
       │    └→ CollisionMesh_Detect (vtable[7] = 0x456890)
       │         ├→ For each node in +0x430:
       │         │    ├→ SpatialTree_ForEach (vtable[2] = 0x463880) — wall/floor test
       │         │    │    └→ Recursively test child nodes (CollisionFace)
       │         │    └→ CollisionNode_Test (vtable[2] = 0x467030) — ball-ball test
       │         │         └→ For each ball in Scene+0x29D4:
       │         │              ├→ Skip self (compare mesh+0x10 IDs)
       │         │              ├→ AABB overlap test
       │         │              └→ On hit: set entry fields (+0x04, +0x08, +0x0C, +0x64)
       │         ├→ If collision detected:
       │         │    └→ CollisionNode_Resolve (vtable[3] = 0x467360)
       │         │         ├→ Set entry type = 1 (+0x00)
       │         │         ├→ Set rel_vel (+0x30)
       │         │         ├→ Write velocity to THIS mesh (+0xCA4/+0xCA8/+0xCAC)
       │         │         └→ Write velocity to OTHER mesh (+0xCA4/+0xCA8/+0xCAC)
       │         └→ Create 0x68-byte entry, Material_Copy, append to +0x18
       │              ⚠ ONLY to THIS ball's list — NOT the other ball's list
       ├→ AthenaList_Free(+0x430) (vtable[6]) — clear node list
       └→ Iterate entries in +0x18:
            ├→ Phase 1: type==2 && +0x64==SpatialTree → wall (camera focus)
            ├→ Phase 2: type==1 && +0x64==SpatialTree → DEAD CODE for ball-ball
            └→ Phase 3: type==1 && +0x64==CollisionNode → push-apart physics

Document created from decompiled source analysis of Hamsterball.exe via GhidraMCP.
All function addresses, vtable layouts, and field offsets verified through
disassembly cross-referencing. The Ghidra decompiler's unaff_EBP/unaff_ESI
mapping was corrected against raw x86 disassembly.


🔗 Related Documents

Ball Break Mod

types : mods
keywords :

📂 View source on GitHub


Ball Break Mod

Press X to shatter your ball and respawn at the nearest checkpoint.

How It Works

  1. A background thread polls DIK_X (0x2D) every 16ms
  2. On rising-edge keypress, checks gates:
    • Countdown finished (scene+0x3A4C == 1)
    • Race not ended (App+0x5D6 == 0)
    • Player flag2 clear (App+0x5D5 == 0)
    • Ball not already shattered (ball+0x2E8 == 0)
  3. Calls Ball_Shatter (0x408D70) — the game's own "ball breaks into 3 pieces" function
  4. Sets ball+0x2E8=1 (shattered flag)
  5. Next frame, Scene_UpdateBallsAndState (0x41B540) detects the shattered flag and calls Ball_FindClosestRespawnPoint (0x405190) → teleports ball to nearest respawn point, clears velocity, starts fall animation

Reverse Engineering Details

Function Address Convention Purpose
Ball_Shatter 0x408D70 __thiscall(ball, Vec3List*) Breaks ball into 3 debris pieces, plays sound
Ball_Shrink 0x402200 __fastcall(ball) odd race E:SHRINK: ball+0xC4C=1, radius to 13.0
Ball_Grow 0x402270 __fastcall(ball) Sets ball+0xC4C=0, radius to 26.0 (normal)
Ball_FindClosestRespawnPoint 0x405190 __fastcall(ball) Finds nearest respawn, teleports, clears velocity
Scene_UpdateBallsAndState 0x41B540 __fastcall(scene) Per-frame: checks ball+0x2E8, triggers respawn

Ball Struct Offsets

Offset Type Field
0x014 DWORD Board pointer
0x018 int Player index (0=P1, -1=AI)
0x164 float Position X
0x168 float Position Y
0x16C float Position Z
0x2E8 byte Is shattered (set by Shatter)
0x2F9 byte Is falling (set by FindClosestRespawnPoint)
0x300 DWORD Respawn timer (set to 150 = 0x96)
0x324 byte Is invincible (if 1, destroyed instead of respawned)

Installation

  1. Back up your original bass.dll (rename to bass_real.dll)
  2. Copy this mod's bass.dll into the Hamsterball game folder
  3. Launch the game
  4. Press X during a race to break and respawn

Build

i686-w64-mingw32-gcc -shared -o bass.dll ball_break.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Crash-tested: process survives 40s on Wine/Xvfb (DLL loads cleanly, no stack corruption).


🔗 Related Documents

Ball Color Cycle Mod

types : mods

📂 View source on GitHub


Ball Color Cycle Mod

Info

  • File: bass.dll (proxy)
  • Controls: Press F2 in-game to cycle through 10 colors
  • Android-safe: No IAT hooks, no code caves, no D3D API calls from thread

Colors (10 total)

White → Orange → Blue → Green → Pink → Yellow → Purple → Cyan → Red → Dark Orange → (loops back)

What it does

Tints player 1's hamster ball to a selected color. Sets the ball's material
diffuse/ambient/emissive RGBA values and activates the game's material override
system (gfx+0x7C0) so the 3D sphere mesh uses the custom color.

v3 Crash Fix

v2 crashed after finishing a race because gfx+0x7C0 was set to ball+0x208
but never cleared. When the ball was destroyed at race end, the pointer became
dangling — the render thread read freed memory → crash.

Fix: When no ball is found (scene transition/race end), gfx+0x7C0 is set
to 0 (NULL), making the game use its default material. The pointer is re-set
to the new ball's render context on the next frame when the ball is found again.

Build

i686-w64-mingw32-gcc -shared -o bass.dll ball_color_cycle.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy mod bass.dll to game folder
  3. On Android/Wine: set DLL override to native for bass.dll

🔗 Related Documents

Ball Ground Detection

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/ball-ground-detection.md)


Ball Ground Detection — Verified Offsets

TL;DR

Offset Name Reliable for ground check? Notes
Ball+0x281 unused_init_flag ❌ NO DEAD: set by ctor, NEVER read by any function
Ball+0xC4C is_shrunk ⚠ Partial Only set by odd race E:SHRINK/E:GROW, NOT ground contact
Ball+0x2E9 impact_shatter BROKEN Sticky flag, never cleared in Ball_Update. DO NOT USE.
Ball+0x260 is_airborne ⚠ Partial Set by speed/friction thresholds, not true ground contact

For jump mods: Use a cooldown timer (60 frames). No ground flag needed.
For mods needing true ground contact: Use the engine's raycast (Mesh_FindClosestCollision) from a background thread.


⚠ CRITICAL: Ball+0x2E9 is NOT a ground-contact flag

Previous versions of multiple docs (BALL_OBJECT.md, BALL_UPDATE_DECOMP.md, etc.) labeled Ball+0x2E9 as:

  • on_ramp (slope/ramp flag)
  • on_surface (ground-contact flag)
  • flag2 (reset to 0 each frame)
  • is_teleporting

All of these labels are WRONG.

What 0x2E9 actually is

Ball+0x2E9 is a limit/trajectory flag:

  • Set to 1 by:
    • E:LIMIT arena events (finish line reached)
    • Type-5 floor collision with deep penetration (piVar16[0x15] > _DAT_004cf310)
  • Cleared to 0 by Ball_FindClosestRespawnPoint (at 0x00405262: MOV byte [ESI+0x2E9],0) and Ball_ctor2

⚠ CORRECTION: Previous "sticky flag" claim was wrong

Previous versions of this doc claimed Ball_FindClosestRespawnPoint's clear was int* arithmetic writing to +0xBA4 instead of +0x2E9, making the flag "sticky." This was wrong. The actual disassembly at 0x00405262 is:

00405262: C6 86 E9 02 00 00 00    MOV byte [ESI+0x2E9], 0

This is a direct byte write to offset 0x2E9 via the ModRM 9E encoding ([ESI+disp32]). The Ghidra decompilation's param_1 + 0x2e9 uses param_1 declared as int (not int*), so the arithmetic IS byte offset 0x2E9. The flag IS properly cleared on respawn.

However, the flag is still not cleared per-frame within Ball_Update — it persists between frames within a single life. It's only cleared on respawn/teleport, not every physics tick.

Real-world bug caused by this

The jump_mod used Ball+0x2E9 as a ground check, reading it at the end of Ball_Update. Symptoms:

  • Can't jump on flat ground (flag hasn't been set yet from a deep collision)
  • CAN jump midair (flag is stuck at 1 from a previous frame's deep collision)

Fix: replaced with a 60-frame cooldown timer after each jump.


What DOES work for ground detection

Option 1: Cooldown timer (simplest, for jump mods)

After applying jump velocity, set a 60-frame counter. While >0, block jumping.

  • Jump velocity 500, gravity ~15/frame
  • Apex at ~33 frames, landing at ~66 frames
  • 60-frame cooldown covers most of the arc
  • Pros: Simple, no external calls, works on ramps/slopes
  • Cons: Ball can technically double-jump in the last ~6 frames before landing (frame 60-66). Can increase to 80 to be safe.

Option 2: Engine raycast (for true ground contact)

Call Mesh_FindClosestCollision from a background thread (NOT from a code cave):

// Ball_FindMeshCollision @ 0x00403980
// thiscall: this=Ball*, param_1=Vec3* out
// Reads ball's CollisionMesh (ball+0x1A4), uses ball position + gravity
// direction (CollisionMesh+0xC8C), writes closest hit point to out.

typedef void (__attribute__((thiscall)) *BallFindMeshCollision_t)(
    void* ball, Vec3* out);

// Call from background thread:
Vec3 out = {0};
find_collision(ball_ptr, &out);
// Compare out to ball position — if close, ball is grounded

⚠ DO NOT call from a code cave — calling C functions from hand-assembled code caves inside Ball_Update corrupts stack/FPU/SEH state and crashes the game. Use Pattern 4 (volatile flag + polling thread) from the modding skill.

Key offsets for raycast

Offset Name Type Purpose
Ball+0x164 position Vec3 Current ball position (X/Y/Z)
Ball+0x1A4 collision_mesh ptr CollisionMesh pointer
Ball+0x284 radius float Ball radius (~35.0)
CollisionMesh+0xC8C gravity_dir Vec3 Gravity direction (normalized)
Scene+0x8B0 collision_level ptr Level CollisionMesh (for Mesh_FindClosestCollision)

⚠ unused_init_flag (Ball+0x281) is DEAD CODE

  • Documented as unused_init_flag (formerly is_falling) in BALL_OBJECT.md
  • NOT read during Ball_Update (0x405E00) physics tick
  • Legacy/init flag only
  • Setting it to 0 in water mods works as a side effect (clears a stale state), but it does NOT reflect current ground contact

is_shrunk (Ball+0xC4C) is Odd Race shrink mechanic only

  • Set by Ball_Shrink (0x402200) during odd race E:SHRINK collision event
  • Cleared by Ball_Grow (0x402270) during odd race E:GROW collision event
  • Does NOT track per-frame ground contact — ball can be midair (from a jump) with is_shrunk=0
  • NOT related to falling off edges — Ball_Shatter (0x408D70) handles falling and never writes to 0xC4C

🔗 Related Documents

Ball Object

types : objects
keywords :

📂 View source on GitHub


Ball Object — Modding Reference

Parameter of Ball_Update (0x405E00)
Total size: 0xC98 bytes (3224 bytes)
Vtable: 0x4CF3A0 (9 method pointers)
Constructor: Ball_ctor2 (0x4039E0)
Verified via: live GhidraMCP decompilation of 22 Ball functions


1. Quick Anatomy

The Ball is the player (or AI) physics object. Every frame GameLoopScene_UpdateBall_Update ticks it through a 23-phase pipeline (see §3). Modders patch mid-pipeline to alter physics, rendering, or input without breaking collision coherence.

Canonical entry points

  • Scene+0x21EAppApp+0x5DC → current Scene → iterate balls
  • Ball+0x14 → back-pointer to owning Scene (all balls in a scene share it)
  • Ball+0x1A4CollisionMesh (separate 0xCB0-byte physics body)

2. Verified Field Layout

Offsets below are byte addresses (not int[0xNN] array indices). All types are confirmed by Ball_ctor2 writes and Ball_Update reads.

Offset Type Name Verified By Modder Notes
0x000 uint32_t vtable ctor2 GameObject vtable; 9 methods. Do NOT overwrite.
0x004 uint8_t[2] field_04 ctor2 Reserved
0x008 int32_t collision_result ctor2 Bitflags from collision system
0x00C int32_t string_timer ctor2, Update Countdown; frees display_string at 0
0x010 App* app ctor2 Back-pointer to global App
0x014 Scene* scene ctor2 Back-pointer to owning Scene
0x018 int32_t player_index ctor2 -1 = AI, 0 = Player 1, 1 = Player 2
0x01C uint32_t render_callback ctor2 Vtable for render/audio callbacks
0x020 uint8_t[0xEC] UITimer ctor2 UITimer sub-object (236 bytes)
0x108 Timer timer ctor2 Timer_Init target
0x150 float accumulated_time Update Delta-time accumulator for physics sub-steps
0x154 int32_t rng_seed ctor2 RNG_Rand seed
0x158 float prev_pos_x Update Position previous frame
0x15C float prev_pos_y Update "
0x160 float prev_pos_z Update "
0x164 float pos_x ctor2, Update Current position — primary mod target
0x168 float pos_y ctor2, Update "
0x16C float pos_z ctor2, Update "
0x170 float vel_x ctor2, Update Current velocity — write AFTER collision phase
0x174 float vel_y ctor2, Update "
0x178 float vel_z ctor2, Update "
0x17C float display_vel_x Update Display-interpolated velocity
0x180 float display_vel_y Update "
0x184 float display_vel_z Update "
0x188 float max_speed ctor2, Update Default 5000.0f. Mod for speed limits.
0x18C float speed_scale ctor2, Update Default 1.0f. Multiplier on input force.
0x190 uint8_t[12] pad_190 ctor2
0x19C uint8_t field_19c ctor2 Set 0
0x1A0 float max_speed_cap ctor2 1.0f hard cap
0x1A4 CollisionMesh* physics_body ctor2 Separate physics object — see COLLISIONMESH_OBJECT_MODDING.md
0x1A8 float[3] gravity ctor2, Update Default (0, 1.0, 0). Flip Y sign for inverted gravity.
0x1B4 uint8_t[4] pad_1b4
0x1B8 RenderContext render_ctx_1 ctor2 First render context
0x1C8 float render_alpha ctor2 0.75f
0x1CC uint8_t[0x3C] pad_1cc
0x208 RenderContext render_ctx_2 ctor2 Second render context
0x20C float color_a ctor2 RGBA alpha
0x210 float color_r ctor2 "
0x214 float color_g ctor2 "
0x218 float color_b ctor2 "
0x21C uint8_t[0x20] pad_21c
0x23C float tint_x ctor2 1.0f
0x240 float tint_y ctor2 1.0f
0x244 float tint_z ctor2 1.0f
0x248 float tint_w ctor2 1.0f
0x24C uint8_t[8] pad_24c
0x254 bool uses_alpha ctor2 color_a != 1.0f
0x255 uint8_t[0xB] pad_255
0x260 uint8_t boost_flag ctor2 Set 0
0x261 uint8_t[3] pad_261
0x264 ArenaBoard rumble_timer1 ctor2 ToggleTimer_Init target (20 bytes)
0x278 float gravity_scale ctor2, Update Default 0.1f. Scale gravity strength.
0x27C uint32_t field_27c ctor2 0
0x280 uint8_t field_280 ctor2 0
0x281 bool unused_init_flag ctor2, Update 1 in ctor; DEAD: never read by any function
0x282 uint8_t[2] pad_282
0x284 float radius ctor2, Update Default 27.0f. Hit-box size.
0x288 uint32_t field_288 ctor2 0
0x28C uint8_t field_28c ctor2 0
0x28D uint8_t[3] pad_28d
0x290 ArenaBoard rumble_timer2 ctor2 Second ArenaBoard timer
0x2A4 float field_2a4 ctor2 5.0f
0x2A8 Vec3 speed_modifier ctor2 Vec3_Init target
0x2B4 uint8_t[4] pad_2b4
0x2B8 float accel_x Update Frame-cleared acceleration
0x2BC float accel_y Update "
0x2C0 float accel_z Update "
0x2C4 uint8_t field_2c4 Update
0x2C8 uint8_t[4] pad_2c8
0x2CC uint8_t field_2cc ctor2 0
0x2CD uint8_t[7] pad_2cd
0x2D4 uint8_t field_2d4 ctor2 0
0x2D5 uint8_t field_2d5 ctor2, Update 0
0x2D6 uint8_t[2] pad_2d6
0x2D8 uint32_t field_2d8 ctor2 0
0x2DC float lgp_x Update Last Grounded Position (LGP) — snapshot at last type-2 ground collision
0x2E0 float lgp_y Update "
0x2E4 float lgp_z Update "
0x2E8 bool event_flag ctor2, Update Checkpoint hit event
0x2E9 bool impact_shatter ctor2 ⚠ NOT on_ramp! Sticky limit/trajectory flag (E:LIMIT + type-5 collision). Never cleared within Ball_Update.
0x2EC uint32_t field_2ec ctor2 Collision counter
0x2F0 uint32_t field_2f0 ctor2 0
0x2F4 uint32_t field_2f4 ctor2 0
0x2F8 uint8_t field_2f8 Update
0x2F9 uint8_t field_2f9 ctor2, Update
0x2FC float timer_bf ctor2, Update Default 1.0f
0x300 uint32_t field_300 Update Set 0
0x304 float saved_pos_x Update Saved position copy
0x308 float saved_pos_y Update "
0x30C float saved_pos_z Update "
0x310 uint8_t field_310 ctor2 1
0x311 uint8_t[3] pad_311
0x314 uint32_t field_314 Update Decayed each frame
0x318 uint8_t[4] pad_318
0x31C bool field_31c ctor2 0
0x31D uint8_t field_31d ctor2 0
0x31E bool field_31e ctor2 0
0x31F uint8_t field_31f ctor2
0x320 uint8_t[8] pad_320
0x328 int32_t field_328 ctor2 -1
0x32C AthenaList collision_list ctor2 AthenaList_Init target
0x744 float field_744 ctor2 0
0x748 float field_748 ctor2 0
0x74C float cam_offset_1 Update Camera offset
0x750 float cam_offset_2 Update Camera offset
0x754 uint8_t[0x10] pad_754
0x764 float cam_follow_factor ctor2 Camera lerp factor 1.0f
0x768 bool cam_active ctor2, Update 1 = camera follow on
0x769 uint8_t pad_769 ctor2 0
0x76A uint8_t[0x5E] pad_76a
0x7C8 float[16] matrix_1 ctor2, Update 4×4 transform matrix
0x808 float[16] matrix_2 ctor2, Update 4×4 transform matrix
0x848 uint8_t[0x380] pad_848
0xC28 char* display_string ctor2, Update Allocated string; freed when timer expires
0xC2C uint8_t pad_c2c ctor2
0xC2D uint8_t[0xB] pad_c2d
0xC38 int32_t field_c38 ctor2 -1
0xC3C bool teleport_active ctor2, Update Teleport pending flag
0xC3D uint8_t[3] pad_c3d
0xC40 float teleport_x Update Teleport destination
0xC44 float teleport_y Update "
0xC48 float teleport_z Update "
0xC4C uint8_t field_c4c ctor2, Update 0
0xC4D uint8_t[3] pad_c4d
0xC50 float field_c50 Update Decayed each frame (*= 0.998 approx)
0xC54 uint32_t field_c54 Update
0xC58 uint8_t field_c58 Update 0
0xC59 uint8_t[3] pad_c59
0xC5C uint32_t field_c5c Update Decayed each frame

3. Ball_Update 23-Phase Execution Order

The function at 0x405E00 runs these phases every tick. Hook at the right phase:

Phase Offset Range What Happens Safe to Patch?
1 0xC50, 0xC5C Decay timers (multiplicative fade) ⚠️ Post-decay OK
2 0x2F0, 0x2F4 Decay collision counters ⚠️ Post-decay OK
3 0x314 field_314 decay ⚠️ Post-decay OK
4 0x2FC timer_bf countdown ⚠️ Post-decay OK
5 0xC54 Check flag, call vtable+0x18 ❌ Skip if event logic
6 0x150 accumulated_time += deltaTime ❌ Pre-physics
7 0x154 rng_seed = RNG_Rand() ❌ Random state
8 0x158-0x16C Save prev_pos = pos ❌ Copy state
9 0x300 field_300 = 0 (reset) ❌ Pre-collision
10 0x2A0 speed_modifier decay ⚠️ Post OK
11 0x29C field_29C = 1.0f (reset) ❌ Resets your changes
12 0xC0 piVar16 = param_1 + 0xC0 (trail setup) ❌ Trail state
13 0xC28 display_string check & free ❌ String lifetime
14 0x14C field_14C = 0 (reset)
15 0x768 Camera follow check Hook here for camera hacks
16 0x310 field_310 check
17 0x1A4 CollisionMesh gravity reflect ❌ Physics internals
18 0x1D6 Teleport apply (teleport_activepos) Use teleport_* fields instead
19 0x164-0x16C Velocity integration → position ⚠️ Best hook for pos overrides
20 0x170-0x178 Velocity clamping & friction ⚠️ Best hook for speed hacks
21 0x188 Max speed enforcement ❌ Overwritten
22 0x2B8-0x2C0 Acceleration clear ❌ Resets to 0
23 0x2DC-0x2E4 Checkpoint update

Best hook phases for common mods:

  • Teleport / position override: Phase 19 (after integration, before friction)
  • Speed boost: Phase 20 (after velocity integration, before clamping)
  • Invincibility / no-clipping: Phase 17 (skip collision by zeroing collision results)
  • Camera control: Phase 15 (before camera logic reads pos)
  • Gravity hack: Phase 17 (overwrite gravity at +0x1A8 before reflect)

4. Callable Functions (VTable + Standalone)

Vtable Methods (offset 0x000 → 0x4CF3A0)

VTable Offset Function Called From
+0x00 GameObject_sub2_dtor Destructor chain
+0x04 UITimer_Ctor Ball_ctor2
+0x08 ?
+0x0C ?
+0x10 ?
+0x14 ?
+0x18 Ball_Render (indirect) Scene_Render via callback
+0x1C ?
+0x20 Ball_Update trigger Called by scene tick

Standalone Ball Functions

Address Name Signature Modder Use
0x4015B0 Ball_SetupCollisionRender (Ball*, float, float, float, float) Custom collision visualization
0x401660 Ball_SetName (Ball*, char*) Set display name
0x4016F0 Ball_ApplyForceV2 (Ball*, float, float, float) Apply impulse vector
0x401920 Ball_RenderShadow (Ball*) Manual shadow render
0x401CC0 Ball_dtor2 (Ball*) Full destructor
0x401DD0 Ball_CreateTrailParticles (Ball*, int, float, float, float) Spawn trail effect
0x402030 Ball_SetTargetPos (Ball*, float, float, float) Set target position
0x402200 Ball_Shrink (Ball*) Enter odd race shrink state
0x402270 Ball_Grow (Ball*) Exit odd race shrink state
0x402400 Ball_DizzyImmunity (Ball*) Grant dizzy immunity
0x402650 Ball_ApplyForceWithMultipliers (Ball*, float, float, float, float, float) Force with scale factors
0x4027F0 Ball_dtor (Ball*) Light destructor
0x402810 Ball_TestPlaneIntersection (Ball*, float, float, float, float) Test plane collision
0x402860 Ball_InitRenderState (Ball*) Reset render contexts
0x4029C0 Ball_SetSpeed (Ball*, float) ⚠️ DEAD CODE — does nothing lasting
0x402A20 Ball_SetVec3AtOffset (Ball*, int offset, float, float, float) Generic Vec3 write
0x402DE0 Ball_Render (Ball*) Full render (shadow + sprite + material)
0x4030B0 Ball_ResetCollisionMesh (Ball*) Reset physics body
0x403100 Ball_SetTiltedGravity (Ball*, float, float, float) Set custom gravity direction
0x4039E0 Ball_ctor2 (void* mem, Scene*) Constructor
0x405E00 Ball_Update (Ball*) Main physics tick
0x45D8F0 Ball_RenderWithMaterial (Ball*, Material*) Render with custom material
0x46EC30 Ball_GetInputForce (InputHandler*, float[2]) Read input as 2D force vector

5. Modding Recipes

Recipe A: Instant Teleport

// Write destination, set flag — Ball_Update phase 18 handles the rest
float* ball = (float*)0xDEADBEEF;  // your ball pointer
*(bool*)(ball + 0xC3C/4) = true;   // teleport_active
ball[0xC40/4] = targetX;          // teleport_x
ball[0xC44/4] = targetY;          // teleport_y
ball[0xC48/4] = targetZ;          // teleport_z

Recipe B: Super Speed

// Phase 20 hook: override velocity after integration, before clamping
float* vel = &ball[0x170/4];
float speed = sqrtf(vel[0]*vel[0] + vel[1]*vel[1] + vel[2]*vel[2]);
if (speed > 0) {
    float scale = 3.0f;  // 3× speed
    vel[0] *= scale; vel[1] *= scale; vel[2] *= scale;
}

Recipe C: Disable Gravity

// Phase 17 hook: zero gravity vector
float* gravity = &ball[0x1A8/4];
gravity[0] = 0; gravity[1] = 0; gravity[2] = 0;
// Also zero gravity_scale
ball[0x278/4] = 0.0f;

Recipe D: Low Friction (Ice Mode)

// Phase 20 hook: reduce velocity decay
// Ball_Update applies friction 3×; skip or reduce it
// Hook at the spin-friction loop and multiply retention

Recipe E: Giant Ball

// Modify radius — affects collision and render scale
ball[0x284/4] = 100.0f;  // 27.0f → 100.0f

Recipe F: Position Override (Noclip)

// Phase 19 hook: directly overwrite position
ball[0x164/4] = desiredX;
ball[0x168/4] = desiredY;
ball[0x16C/4] = desiredZ;
// Zero velocity to prevent fight-back
ball[0x170/4] = ball[0x174/4] = ball[0x178/4] = 0;

Recipe G: Input Force Scaling

// Ball_GetInputForce reads +0x50C-0x518 from InputHandler
// Scale the output vector: output[0] *= 2.0f; output[1] *= 2.0f;

Recipe H: Disable Fall State

// Keep unused_init_flag = false (NOTE: this flag is DEAD code, never read by any function)
*(bool*)(ball + 0x281) = false; // dead flag (never read) // dead flag (never read)

6. Cross-Reference Map

Related Document What It Covers
COLLISIONMESH_OBJECT_MODDING.md The Ball+0x1A4 physics body (velocity, mass, collision tree)
SCENE_OBJECT_MODDING.md The Ball+0x14 Scene (level geometry, camera, lighting)
APP_OBJECT.md The Ball+0x10 App (global state, window, input handler)
INPUT_SYSTEM.md How Ball_GetInputForce reads DIK codes and joystick axes

7. Verification Notes

  • 99 unique offsets extracted from Ball_Update decompilation (43,655 bytes)
  • Field names verified against Ball_ctor2 writes (7,400 bytes)
  • Function names verified against list_functions REST endpoint (3,801 total functions, 3,782 documented = 99.5%)
  • Render offsets verified against Ball_Render (3,900 bytes)
  • Input offsets verified against Ball_GetInputForce (2,000 bytes)
  • All decompilations performed live via GhidraMCP headless server (v5.2.0, port 8089)

Document: docs/BALL_UPDATE_OBJECT_MODDING.md
Generated: 2025-06-05
Verified against: Hamsterball.exe, GhidraMCP v5.2.0


🔗 Related Documents

Ball Physics Decompilation

types : physics
keywords :

📂 View source on GitHub


Hamsterball - Ball Physics Decompilation

Overview

The ball (hamsterball) is the player-controlled object. It derives from GameObject and has a rich vtable with physics, collision, and rendering methods.

Ball Vtable (0x4CF3A0)

Offset Address Name Description
+0x00 0x4027F0 Ball_dtor Destructor (calls Ball_Cleanup)
+0x04 0x405100 (thunk) Update method (jumps to Ball_Update at 0x405190)
+0x08 0x402DE0 Ball_CollisionCheck Per-frame collision check against mesh
+0x0C 0x402A70 (not defined) Possibly render setup
+0x10 0x408390 (not defined) Unknown method
+0x14 0x401590 (not defined) Unknown method
+0x18 0x402650 Ball_ApplyForce Apply directional force (x,y,z,magnitude)
+0x1C 0x402C10 (not defined) Unknown method
+0x20 0x409480 (not defined) Unknown method

Base class vtable: GameObject at 0x4CF314

Ball Object Layout (0xC98 bytes)

Offset Type Description
0x00 void** Vtable pointer
0x04 int level reference (param_1 in ctor)
0x08 int Parent object reference
0x0C float[6] Collision planes (4 component each: aX + bY + cZ + d)
0x42 Timer Per-object timer
0x59 float Ball X position
0x5A float Ball Y position
0x5B float Ball Z position
0x62 float Ball size (default 0x40400000 = 3.0f)
0x69 void* Sound object
0x82 unknown Sound data
0x96 int Sound handle
0x99 unknown More sound data
0xAA unknown String data
0xC8 float Position component?
0x154 IDirect3DDevice8* D3D device pointer (set in render)
0x164 float X position (authoritative)
0x168 float Y position (authoritative)
0x16C float Z position (authoritative)
0x170 float X velocity (zeroed on collision)
0x174 float Y velocity (zeroed on collision)
0x178 float Z velocity (zeroed on collision)
0x17C float Acceleration X (zeroed on collision)
0x180 float Acceleration Y (zeroed on collision)
0x184 float Acceleration Z (zeroed on collision)
0x1A4 int Level state reference
0x1C void** Physics vtable (for collision mesh interaction)
0x284 float Ball radius / height offset
0x2CC byte Force disable flag
0x2DC float X position (secondary)
0x2E0 float Y position (secondary)
0x2E4 float Z position (secondary)
0x2F0 int Frame counter (affects force scaling)
0x2F8 byte Active flag
0x2F9 byte Collision occurred flag
0x2FC int Physics parameter
0x300 int Value 0x96 (150) - possibly mass or timer constant
0x310 byte State flag (1 = active)
0x314 float Home X position
0x318 float Home Y position
0x31C byte Home valid flag
0x31D byte Home valid flag 2
0x324 byte Special state (shrunk/slow -> affects force multiplier)
0x440 void* Allocated physics data
0x6F8 int D3D device sub-count
0x6FC int Initialized flag for rendering
0x700 byte Render mode flag (affects lighting: on/off)
0x708 int Render parameter
0x70D byte Render state initialized flag
0x734 byte Sub-render mode
0x748 int Collision mesh pointer (0 = use default)
0x76A byte Flag (zeroed on collision)
0x7C8 int Render call counter
0x808 int Freeze counter (skip force if > 0)
0x810 AthenaList Force application list
0x894 AthenaList Object tracking list
0xA1 float Ball radius (from SIZE parameter)
0x9F int Counter (0 = start)
0xC28 void* Allocated buffer
0xC2C char[] Collision target name buffer
0xC5C int Alternate physics state flag (-> different force multiplier)
0xC74 int Collision counter (incremented on collision)
0xC80 byte Flag (0)
0xC84 Quaternion Object rotation (identity = 0,0,0,1)
0xC88 float[4] Rotation matrix/quaternion data

Key Physics Constants (Data Section)

Address Value Description
0x4CF368 float Collision radius threshold
0x4CF374 float Force multiplier when C5C flag set
0x4CF378 float Force multiplier when 324 flag set (shrunk)
0x4CF380 float Force scaling for frame 0 (first frame)
0x4CF3E8 float Force direction multiplier
0x4CF48C float Gravity/height offset constant

Ball_Update (0x405190) - Main Physics Loop

18,499 chars of decompiled code. Key operations:

  1. Reset frame state (flags, counters, collision mesh)
  2. If collision mesh flag set, call Ball_ResetCollisionMesh (0x4030B0)
  3. Iterate through level object list, checking for nearby objects
  4. For each object, check distance (sqrt(dx²+dy²+dz²))
  5. Find closest collision point using Mesh_FindClosestCollision (0x465D90)
  6. On collision:
    • Set ball position to collision point + radius offset
    • Zero out velocity and acceleration
    • Set collision flag (+0x2F9 = 1)
    • Reset physics parameters

Ball_ApplyForce (0x402650) - Force Application

Takes (float x, float y, float z, float magnitude).

  • Skipped if collision flag (+0x2F9) or disable flag (+0x2CC) set
  • Skipped if freeze counter (+0x808) > 0
  • Frame 0 applies different force scaling (_DAT_004CF380)
  • Shrunk state (+0x324) applies multiplier _DAT_004CF378
  • Alternate state (+0xC5C) applies multiplier _DAT_004CF374
  • Force direction modified by _DAT_004CF3E8
  • Updates velocity components at +0xFC, +0x100

Ball_CheckCollisionPlanes (0x402810) - Plane Collision

Tests ball position against 6 collision planes stored at object+0xC.
Each plane has 4 float components (a,b,c,d) for plane equation: ax + by + cz + d = 0.
Returns byte result: 0 = no collision, 1 = collision detected.

Ball_CollisionCheck (0x402DE0) - Frame Collision

Called from vtable+8. Performs:

  1. Graphics_BeginFrame
  2. Ball_CheckCollisionPlanes with ball position + radius 200.0
  3. If collision: increment counter, call collision response vtable methods
  4. If no collision: decrement counter (clamped to 0)

Ball_Render (0x402860) - D3D8 Rendering

Sets up D3D8 render states:

  • SetRenderState(D3DRS_ZENABLE, 1)
  • SetRenderState(D3DRS_CULLMODE, 1)
  • SetRenderState(D3DRS_SPECULARENABLE, 1)
  • SetTexture(0x112, ...)
  • SetRenderState(D3DRS_LIGHTING, flag) based on +0x700 byte
  • DrawPrimitiveUP with vertex format depending on +0x700
  • Calls FUN_00453970 (sub-object render based on +0x734 mode)

Collision System Summary

Two-tier collision:

  1. Ball_CheckCollisionPlanes (0x402810): Simple plane test for basic boundaries
  2. Mesh_FindClosestCollision (0x465D90): Full mesh ray-trace for accurate terrain collision
    • Uses AthenaList to build collision planes
    • Calls FUN_004564C0 for actual intersection math
    • Returns closest hit point with 0.01 precision threshold

🔗 Related Documents

Ball Tint Mod

types : mods
keywords :

📂 View source on GitHub


Ball Tint Mod

Tints player 1's ball to any hex color, read from a text file at runtime.

How It Works

The mod creates ball_tint.txt next to bass.dll on first launch. Edit the file with any hex color (e.g. FF6B35 for orange) and the ball recolors within ~60ms — no restart needed.

Mechanism: Writes RGBA floats into the ball's render context material (ball+0x20C diffuse, +0x21C ambient, +0x23C emissive), then sets the Graphics material override (gfx+0x7C0 = ball+0x208) so the game uses our material instead of the mesh's default white material. This tints both the 3D sphere body and the sprite overlays (border, hamster).

Files

  • bass.dll — the mod (replace game's bass.dll)
  • ball_tint.txt — auto-created config file
  • ball_tint.c — source code

Usage

  1. Backup your original bass.dll
  2. Copy bass.dll to the game folder
  3. Make sure bass_real.dll (the original renamed) is in the same folder
  4. Launch the game — ball_tint.txt is created automatically
  5. Edit ball_tint.txt with a hex color (e.g. 4A90D9 for blue)
  6. Save the file — ball recolors instantly

Config Format

FFFFFF
# Ball Tint Color (hex RGB, no alpha)
# Examples: FF6B35 (orange), 4A90D9 (blue), 2ECC71 (green)
# Lines starting with # are ignored

Supported formats: FF6B35, #FF6B35, 0xFF6B35 (case-insensitive).

Build

i686-w64-mingw32-gcc -shared -o bass.dll ball_tint.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Tested

  • ✅ Crash test: 35s on Wine/Xvfb, process alive
  • ⚠️ Visual testing on real Windows required (Wine/llvmpipe renders black)

🔗 Related Documents

Ball_Update Decomposition (0x4

types : physics
keywords :

📂 View source on GitHub


Ball_Update Decomposition (0x405190)

Function Signature

void __fastcall Ball_Update(int param_1);  // param_1 = Ball*

Overview

Ball_Update is the main per-frame physics update for the ball. It handles:

  1. Reset per-frame state
  2. Find the closest collision surface (floor/wall) based on gravity plane
  3. Snap ball to surface and zero velocity when surface found
  4. If no surface found, ball falls freely

New Ball Structure Offsets Discovered

Offset Type Field Description
+0x150 uint32 ??? Reset to 0 each frame
+0x18 int player_index 0 or 1 (player), -1 = AI/none
+0x1A4 int level_ptr Pointer to Level object
+0x2DC float ??? Used as X/Z position for collision
+0x2E0 float ??? Used as Y/height for collision
+0x2E4 float ??? Used as Z position for collision
+0x2E8 char flag1 Reset to 0 each frame
+0x2E9 char impact_shatter NOT "reset to 0 each frame"! Sticky flag, only cleared by Ball_ctor2 (full respawn). The param_1 + 0x2e9 = 0 in decompiled code uses int* arithmetic (= byte 0xBA4), NOT byte 0x2E9.
+0x2EC uint32 ??? Reset to 0 each frame
+0x2F0 uint32 force_count Number of forces this frame
+0x2F8 char update_in_progress Set to 1 during update
+0x2F9 char frozen Ball is stuck/frozen on surface
+0x2FC uint32 freeze_timer Countdown when frozen
+0x29C float scale Reset to 1.0 (0x3F800000)
+0x300 uint32 ??? Set to 150 (0x96) on surface snap
+0x310 char ??? Set to 1 each frame
+0x314 float ang_vel_y Set to 0 on snap
+0x318 float ang_vel_x Set to 0 on snap
+0x31C char ??? Set to 0 on snap
+0x31D char ??? Set to 0 on snap
+0x324 char in_tube If true, skip entire update
+0x76A char ??? Set to 0 on snap
+0x768 char ??? Set to 1 each frame
+0x810 AthenaList path_list Ball path tracking
+0xC28 void* free_ptr Freed and nulled each frame
+0xC2C char[] section_filter Current collision section name
+0xC74 uint32 collision_count Reset to 0 each frame
+0xC80 char ??? Has special collision flag
+0xC88 float[4] special_col Special collision data

Collision Surface Finding Algorithm

The function has THREE code paths based on gravity_plane (this+0x748):

Gravity Plane 0 (XY - Top-down / standard)

  • Iterates through collision surface list at level+0x1518
  • For each collision entry (AthenaList iterator):
    • Check section filter: if this+0xC2C is set, only surfaces matching (<filter> prefix
    • Check [Z] and [X] tags: skip surfaces tagged [Z] or [X]
    • Compute distance from ball to surface using 3D distance (SQRT)
    • Ball position is compared against (this+0x2E0 - radius - _DAT_004CF48C) for Y threshold
    • Keep the CLOSEST surface (minimum distance)

Gravity Plane 1 (Y-tilted)

  • Similar iteration but checks for [X] tag (NOT [Z])
  • Distance computed with opponent ball comparison for 2-player mode
  • Snaps ball differently: pos.x = radius + surface.x, pos.y = surface.y, pos.z = surface.z

Gravity Plane 2 (XZ)

  • Similar iteration but checks for [Z] tag (NOT [X])
  • Snaps ball: pos.x = surface.x, pos.y = surface.y, pos.z = surface.z - radius

Surface Snap (when closest surface found)

When a valid surface is found (local_84 != NULL):

// Gravity 0: snap Y to surface + radius
ball->y = surface[2] + ball->radius;

// Gravity 1: snap X to radius + surface
ball->x = ball->radius + surface[1];
ball->y = surface[2];
ball->z = surface[3];

// Gravity 2: snap Z to surface - radius
ball->z = surface[3] - ball->radius;

// ALL gravity planes: zero velocity + angular velocity
ball->vx = ball->vy = ball->vz = 0;
ball->ang_vel_x = ball->ang_vel_y = 0;
ball->frozen = 1;
ball->freeze_timer = 150;

Ball-to-Ball Collision (2-Player Mode)

When level+0x234 is set (2-player mode) and ball->player_index != -1:

  • Gets the OTHER ball: level + 0x5DC + (1 - player_index) * 0xA0
  • Compares ball position to other ball position
  • If distance < ball->radius, skip this collision surface
  • This prevents one ball from detecting surfaces near the other ball

Mesh_FindClosestCollision

Called when gravity_plane == 0 and level has active collision mesh:

Mesh_FindClosestCollision(level+0x8B0, &result);
// result is offset from surface position
// Checks if distance > _DAT_004CF484 (some threshold)

Key Global Constants

Address Value Usage
0x4CF48C 2.0 Y offset threshold (radius + epsilon)
0x4CF484 40.0 Collision mesh distance threshold
0x4CF368 0.0 Epsilon for float comparison
0x4CF380 0.25 Force multiplier when force_count > 0
0x4CF378 0.0 Force multiplier when in_tube (no force!)
0x4CF374 0.2 Force multiplier when on_ice
0x4CF36C 0.75 Force multiplier when is_dizzy
0x4CF3E8 6.0 Ice friction factor

Level Structure Offsets (from Ball_Update)

Offset Type Field Description
+0x234 char is_2player Two player mode
+0x237 char ??? Active flag for collision
+0x878 int ??? Sub-object reference
+0x8B0 void* collision_mesh Mesh_FindClosestCollision target
+0x1518 AthenaList collision_list List of collision surfaces
+0x151C int collision_count Number of collision entries
+0x1520 int* collision_iter Per-thread iterator indices
+0x1924 void** collision_array Pointers to collision data
+0x29D4 AthenaList ball_list List of other ball objects
+0x29D8 int ball_count Number of ball entries
+0x2DE0 void** ball_array Pointers to ball data
+0x5DC Ball[2] balls Two player ball slots (0xA0 each)

Collision Entry Structure

Each collision entry in the AthenaList has:

[0]  char*  name         // Surface name (e.g. "PLATFORM(X)", "Walls[Y]")
[1]  float  x            // Surface position X (or Y for gravity 1)
[2]  float  y            // Surface position Y (or Z for gravity 1)
[3]  float  z            // Surface position Z (or height)

Tags in Collision Names

  • [X] — Surface only applies to gravity plane 1 (Y-tilted)
  • [Z] — Surface only applies to gravity plane 0 (standard)
  • (section) — Section filter prefix for zone-based collision

Decompiled from Ghidra output, 477 lines of decompiled C
Date: April 12, 2026


🔗 Related Documents

Batch Decompilation

types : decompilation
keywords :

📂 View source on GitHub


Batch Decompilation: 10 New Functions (Session 2928)

Date: 2026-06-20
Tool: GhidraMCP v5.12.0-headless, Hamsterball.exe
Selection criteria: Named, undocumented functions spanning diverse game systems (arena hazards, physics, camera, UI, input, rendering).


1. Catapult_Update @ 0x0043E600

Category: Arena Hazard Physics (Catapult + Rotator objects — shared update)
Called via: vtable DATA ref at 0x004D5AFC
Signature: __fastcall Catapult_Update(int *this)

Summary

Updates the Catapult/Rotator arena hazard — a rotating platform that carries balls and launches them. The catapult has a rotation timer at this+0x43C that decreases by this+0x43D (rotation speed) each frame. It applies the rotation matrix to all attached balls (via AthenaList at this+0x43E), transforming both their positions (ball+0x164/0x168/0x16C = X/Y/Z) and their velocity vectors (CollisionMesh+0xCA4/CA8/CAC).

This function serves double duty:

  1. Catapult launch system — triggered by E:CATAPULTBOTTOM, launches ball via Catapult_Launch (0x434290)
  2. Rotator/gear system — triggered by N:ONROTATOR/N:SPINNY/N:SWIRL, attaches ball via Rotator_AddBall (0x43B6F0) or Catapult_AddObjectConditional (0x43E9C0)

In both cases, the 8-byte ball tracking entry [ball_ptr, tick_counter] is decremented each frame. The tick counter starts at 10 and resets to 10 on every frame of continued collision contact (grace period, not carry limit). When it reaches 0 after the ball leaves the surface, the entry is freed.

Key Struct Offsets

Offset Type Description
this+0x436..0x438 float[3] Catapult pivot position (X,Y,Z)
this+0x439 float Z-axis rotation amount
this+0x43A float X-axis rotation amount
this+0x43B float Y-axis rotation amount
this+0x43C float Current rotation angle
this+0x43D float Rotation speed (delta per frame)
this+0x43E AthenaList Attached balls list header
this+0x43F int Attached balls count
this+0x541 void** Ball pointer array
ball+0x164/168/16C float[3] Ball position (X,Y,Z)
ball+0x1A4 int* CollisionMesh pointer
CollisionMesh+0xCA4/CA8/CAC float[3] Ball velocity vector

Mechanism

  1. Decrements rotation angle: this->rotation -= this->rotation_speed
  2. Builds a rotation matrix from X/Y/Z scale components using Gfx_ScaleX/Y/Z and Timer_Init
  3. For each attached ball: computes delta from catapult pivot, applies rotation matrix, updates ball position and velocity
  4. Frees expired ball attachments (reference count drops to 0)
  5. Calls vtable methods +0x58 and +0x54 (likely render and post-update hooks)

2. BounceBall_Update @ 0x00440840

Category: Arena Hazard Physics (BounceBall spawner)
Called via: vtable DATA ref at 0x004D5734
Signature: __fastcall BounceBall_Update(int *this)

Summary

A bounce-pad hazard that launches balls into the air on a timer. It has two states:

  • State 0 (this+0x43A == 0): Countdown mode — multiplies this+0x439 (timer) by 1.5 each frame. When timer exceeds 120.0, it spawns a FollowBall entity (a ball that follows a predefined path), resets the timer to 120.0, and flips to state 1.
  • State 1 (this+0x43A != 0): Cooldown mode — multiplies timer by 0.95 each frame. When timer drops below 1.0, resets to 0 and returns to state 0.

Key Struct Offsets

Offset Type Description
this+0x434 int* Scene pointer
this+0x435..0x437 float[3] Bounce pad position
this+0x438 float Scale X (render)
this+0x439 float Countdown timer
this+0x43A int State flag (0=counting up, 1=counting down)
this+0x43B byte Dirty flag (position changed)

Spawn Mechanism

When the timer exceeds 120.0:

  1. Calls operator_new(0xC68) — allocates a Ball struct (3144 bytes)
  2. Calls FollowBall_Ctor(ptr, scene, Level_FindObjectByName(scene, "BallPath"))
  3. Looks up "FOLLOWBALLSPOT" in the level's AthenaHashTable → gets spawn position
  4. Sets ball position from hash table lookup
  5. Sets ball velocity: (-3.0, 10.0, 0.0) (upward launch)
  6. Sets ball+0x80C = 0x0F (flag/mode = 15)
  7. Appends ball to scene's ball list at scene+0x29D4
  8. Plays 3D sound at spawn position

Constants

Address Value Description
_DAT_004cf458 1.5 (f64) Countdown multiplier (state 0)
_DAT_004d5dbc 120.0 (f32) Spawn threshold
_DAT_004d5d98 0.95 (f64) Cooldown multiplier (state 1)
_DAT_004cf310 1.0 (f32) Cooldown minimum

3. Lifter_Update @ 0x0043B330

Category: Arena Hazard Physics (Lifter platform)
Called via: vtable DATA ref at 0x004D5544
Signature: __fastcall Lifter_Update(int *this)

Summary

An oscillating elevator platform that moves up and down on the Z-axis, carrying attached balls. The lifter bounces between position bounds: when it exceeds +2.0, it reverses direction; when it drops below -2.0, it reverses again. It applies its vertical displacement to all balls on the platform.

Key Struct Offsets

Offset Type Description
this+0x436..0x438 float[3] Lifter pivot position
this+0x439 float Accumulated vertical offset
this+0x43A float Current oscillation position
this+0x43B float Direction (-0.5 or +1.0)
this+0x43C AthenaList Attached balls list
this+0x43D int Attached balls count
this+0x53F void** Ball pointer array

Oscillation Logic

position += direction * 0.004   // _DAT_004d5c88 = 0.004
if position > 2.0: direction = -0.5   // _DAT_004cf48c = 2.0
if position < -2.0: direction = +1.0   // _DAT_004d5c84 = -2.0

Ball Transform

Same matrix transform pattern as Catapult_Update — applies oscillation displacement to each attached ball's position and velocity through Gfx_ScaleX, Matrix_TransformVec3, and Timer_Init/Cleanup.

Constants

Address Value Description
_DAT_004d5c88 0.004 (f32) Oscillation speed
_DAT_004cf48c 2.0 (f32) Upper position bound
_DAT_004d5c84 -2.0 (f32) Lower position bound

4. ArenaSceneObj_Tick @ 0x0042B660

Category: arena scoring system (ArenaBoard timer)
Called via: vtable DATA ref at 0x004D3A5C
Signature: __fastcall ArenaSceneObj_Tick(int param_1)

Summary

Per-frame tick for ArenaBoard (arena mode) decorative timers. Manages two oscillating timer fields used for visual effects on the arena board (likely animated score displays or arena hazard timing):

  1. Timer A (this+0x880): Increments by 0.01 each frame. When it exceeds 0.75, resets to 0.75 (clamps at max).
  2. Timer B (this+0x884): Decrements by 20.0 each frame. When it drops below 0.0, resets to 0.0 (clamps at min).
  3. Calls ToggleTimer_Tick on two sub-timer fields at this+0x888 and this+0x89C.

Key Struct Offsets

Offset Type Description
this+0x880 float Timer A (ramps up to 0.75)
this+0x884 float Timer B (ramps down to 0.0)
this+0x888 Timer Sub-timer 1
this+0x89C Timer Sub-timer 2

Constants

Address Value Description
_DAT_004cf524 0.01 (f32) Timer A increment
_DAT_004cf438 0.75 (f64) Timer A max
_DAT_004cf370 20.0 (f32) Timer B decrement
_DAT_004cf368 0.0 (f32) Timer B min

5. Scene_UpdateArenaPhysics @ 0x00440390

Category: Arena Core Loop (Wave physics)
Called via: vtable DATA ref at 0x004D5484
Signature: __fastcall Scene_UpdateArenaPhysics(int *this)

Summary

The main per-frame physics update for arena levels. Runs every 3rd frame (frame counter at this+0x43E, triggers when counter > 2, then resets to 0). This implements the arena's wave/surface physics:

  1. Frame gate: this->frame_counter++; if (counter <= 2) return; — runs at 20fps on a 60fps game
  2. Wave computation: For each of this->num_waves (at this+0x43B) wave segments:
    • Computes wave phase: (frame * 360.0) * (i / num_waves) + base_offset
    • Applies Wave_Sin() to get displacement amplitude × this+0x43D (wave height)
    • Updates vertex Z-offsets in mesh data arrays (two separate lists: piVar5+0x102 and piVar5+0x342E)
  3. Ball physics: Iterates arena balls (list at this+0x6A99):
    • For each ball, finds closest collision mesh point via Mesh_FindClosestCollision
    • If ball Y-position is within this+0x43D (wave height) of the surface, snaps ball Y to surface + ball radius (ball+0x284)
    • Ball radius stored at CollisionMesh+0xC98/0xC9C/0xCA0
  4. Vertex buffer upload: Copies updated vertex data from this+0x43F to render buffer, calls vtable methods +0x38 and +0x3C (likely UnlockVertexBuffer and DrawPrimitive)
  5. Wave sound: Every 360th frame (frame % 360 == 0), plays a 3D wave sound at the arena center

Key Struct Offsets

Offset Type Description
this+0x434 int* App/scene context pointer
this+0x435 void* Collision mesh
this+0x43B int Number of wave segments
this+0x43C int Frame counter (×360 for wave phase)
this+0x43D float Wave height amplitude
this+0x43E int Frame gate counter (mod 3)
this+0x43F void* Vertex buffer source
this+0x440 int Vertex count
this+0x6A99 AthenaList Arena ball list
this+0x6A9A int Arena ball count
this+0x6B9C void** Arena ball pointer array
this+0x120 int* Graphics/device context
ball+0x164/168/16C float[3] Ball position
ball+0x284 float Ball radius
ball+0x1A4 int* CollisionMesh

Constants

Address Value Description
_DAT_004cf454 100.0 (f32) Ball Y surface offset
_DAT_004cfecc 25.0 (f32) Wave displacement offset
_DAT_004d0418 180.0 (f32) Wave frequency (degrees)
_DAT_004d03a0 90.0 (f32) Wave phase offset (degrees)

6. FollowBall_Update @ 0x0043ECC0

Category: Entity AI (FollowBall path follower)
Called via: vtable DATA ref at 0x004D4F64
Signature: __fastcall FollowBall_Update(int *this)

Summary

Updates a FollowBall — a ball that follows a predefined path through the arena (spawned by BounceBall_Update). The FollowBall has two movement modes controlled by this+0x441 (a boolean flag):

  • Mode 0 (this+0x441 == 0): Normal speed — moves at 0.45 multiplier (or 0.09 if this+0x43F > 0)
  • Mode 1 (this+0x441 != 0): Fast speed — decays spawn velocity by 0.0, clamps at 50.0 max

The FollowBall also manages a spawning cycle:

  1. When the cycle timer (this+0x43E) reaches its threshold, it spawns a RegisterDialog entity (a score popup or registration marker) at a random position near the ball
  2. The spawn position uses RNG_Rand(0, 10) for X/Z offset, with Z offset of 60.0
  3. Spawns into the scene's dialog list at scene+0x3B00

Key Struct Offsets

Offset Type Description
this+0x434 int* Scene pointer
this+0x435..0x437 float[3] Ball position
this+0x438..0x43A float[3] Ball position (mirror/offset)
this+0x43B float Animation timer (increments by 8.0)
this+0x43C float Current speed
this+0x43D float Target speed
this+0x43E float Spawn cycle timer
this+0x43F float Cycle threshold
this+0x440 float Cycle reset offset
this+0x441 byte Mode flag (0=normal, 1=fast)
this+0x442 float Spawn velocity decay

Constants

Address Value Description
_DAT_004cf380 0.25 (f32) Base position offset
_DAT_004cf4dc 8.0 (f32) Animation timer increment
_DAT_004cf4d0 0.0 (f32) Spawn decay (mode 1)
_DAT_004cf3ec 50.0 (f32) Max spawn velocity
_DAT_004cf3c8 0.0 (f32) Cycle threshold
_DAT_004cf3e0 0.0 (f32) Lower cycle bound
_DAT_004d5d78 1.12 (f64) Position base
_DAT_004d5d68 0.45 (f64) Normal speed multiplier
_DAT_004d5d70 0.09 (f64) Fast speed multiplier
_DAT_004d5d60 0.9 (f64) Spawn decay (mode 0)
_DAT_004d5d58 1.75 (f64) Cycle max
_DAT_004d5d50 0.0065 (f64) Spawn velocity
_DAT_004d5d48 0.005 (f64) Spawn velocity alt
_DAT_004d0930 60.0 (f32) Z offset
_DAT_004d039c 15.0 (f32) Scale modifier

7. Scene_HandleRaceEnd_ClampZoom @ 0x0041F7E0

Category: Race Camera (Finish-line zoom)
Called via: vtable DATA ref at 0x004D0EC4
Signature: __fastcall Scene_HandleRaceEnd_ClampZoom(int this)

Summary

Called when a race ends. First calls Scene_HandleRaceEnd(this) to handle the actual race-finish logic, then clamps 8 zoom-related float values at this+0x644C through this+0x6468 (every 4 bytes = 8 floats). Each value has 0.05 subtracted and is clamped to a minimum of 0.0.

These 8 floats likely represent camera zoom parameters (field-of-view multipliers or distance values for up to 4 players in split-screen). The zoom-out animation at race end reduces each one by 0.05 per frame until they reach 0.0.

Key Struct Offsets

Offset Type Description
this+0x644C float Zoom param 1
this+0x6450 float Zoom param 2
this+0x6454 float Zoom param 3
this+0x6458 float Zoom param 4
this+0x645C float Zoom param 5
this+0x6460 float Zoom param 6
this+0x6464 float Zoom param 7
this+0x6468 float Zoom param 8

Constants

Address Value Description
_DAT_004cf428 0.05 (f64) Zoom decrement per frame
_DAT_004cf6a8 0.0 (f32) Minimum zoom clamp

8. App_UpdateCullMode @ 0x00429040

Category: Graphics/Rendering (Cull mode toggle)
Called via: vtable DATA ref at 0x004D26D8
Signature: __fastcall App_UpdateCullMode(int this)

Summary

A tiny function that syncs the D3D cull mode (backface culling) with the current graphics settings. Reads a cull-mode byte from App+0x236 (a settings field — likely the "rendering quality" or "wireframe" toggle) and writes it to the graphics device's cull-mode field at device+0x7D2, then calls Gfx_SetCullMode(device) to apply it.

Key Struct Offsets

Offset Type Description
App+0x174 int* Graphics device pointer
App+0x236 byte Desired cull mode (from settings)
device+0x7D2 byte Current cull mode field

Mechanism

device = App->graphics_device;
device->cull_mode = App->cull_setting;
Gfx_SetCullMode(device);

9. CreditsScreen_Render @ 0x00425AC0

Category: UI/Screens (Credits display)
Called via: vtable DATA ref at 0x004D2548
Signature: __thiscall CreditsScreen_Render(void *this, void *param_1)

Summary

Renders the end-game credits screen. Draws a scrolling list of credit text entries with special formatting markers:

  • '-' prefix (0x2D): Renders in a smaller, dimmer font (scale 0.5/0.5/1.0, color tint 0.75 alpha). Used for sub-credits (secondary roles).
  • '*' prefix (0x2A): Renders in a different color/style (scale 1.0/1.0/0.5, color tint 0.5 alpha). Used for section headers. If the entry matches this+0x864 (currently selected/highlighted entry), applies additional emphasis (scale 0.5/1.0/0.5).
  • No prefix: Renders at default scale (1.0/1.0/1.0) with full color.

Each entry is drawn with UI_DrawTextCenteredAbsolute at X=400 (screen center), Y computed from __ftol2 (entry index × line height). After rendering all entries, calls UIList_Render for the standard list scrollbar/border.

Key Struct Offsets

Offset Type Description
this+0x44C AthenaList Credit entries list
this+0x450 int Entry count
this+0x858 void** Entry pointer array
this+0x864 int Highlighted entry index
this+0xCDc int* UI/font context
this+0xCE0 float Scroll animation value

Color/Scale Constants

Hex Value Float Meaning
0x3F800000 1.0 Full scale/alpha
0x3F400000 0.75 Reduced scale/alpha
0x425BA4 String pointer (default color)

10. KeyRemapMenu_WaitForKey @ 0x00443430

Category: input system (Key rebinding)
Called via: vtable DATA ref at 0x004D5F54
Signature: __fastcall KeyRemapMenu_WaitForKey(int *this)

Summary

Handles the "Press a key..." state in the key rebinding options menu. Has three code paths:

  1. Not waiting (this+0x338 == 0): Calls UIList_ScrollUpdate to handle normal menu scrolling. Returns immediately.
  2. Debounce mode (this+0x33A != 0): Clears the debounce flag, then scans the DirectInput keyboard state buffer (256 bytes) for any key with the high bit set (& 0x80). If any key is still pressed, re-sets the debounce flag and returns. This ensures the user releases the old key before pressing a new one.
  3. Capture mode (this+0x338 != 0, this+0x33A == 0): Scans the 256-byte DirectInput keyboard state buffer. When it finds a key with high bit set (& 0x80), it:
    • Stores the DIK (DirectInput Key) scan code at *this+0x339 (the key binding slot)
    • Clears the waiting flag (this+0x338 = 0)
    • Calls KeyRemapMenu_UpdateKeyLabels to refresh the UI
    • Sets a timer at scene+0x560 to 0x32 (50) — likely a UI cooldown

Key Struct Offsets

Offset Type Description
this+0x21E int* App/scene context
this+0x338 byte Waiting-for-key flag (1=capturing)
this+0x339 int* Key binding storage slot pointer
this+0x33A byte Debounce flag (1=waiting for key release)
App+0x180 int* Input device context
InputDevice+0x434 int* DirectInput keyboard state buffer
InputDevice+0x434+0xC byte[256] DIK key state array (high bit = pressed)
scene+0x560 int UI cooldown timer

DirectInput Scanning

The 256-byte keyboard state buffer is the standard DirectInput IDirectInputDevice8::GetDeviceData format. Each byte represents one DIK code; bit 7 (0x80) is the "pressed" flag. The function scans all 256 keys linearly.


Summary

# Function Address System Lines
1 Catapult_Update 0x0043E600 Arena Hazard 141
2 BounceBall_Update 0x00440840 Arena Hazard (spawner) 99
3 Lifter_Update 0x0043B330 Arena Hazard (elevator) 149
4 ArenaSceneObj_Tick 0x0042B660 Arena Scoring 22
5 Scene_UpdateArenaPhysics 0x00440390 Arena Wave Physics 182
6 FollowBall_Update 0x0043ECC0 Entity AI 125
7 Scene_HandleRaceEnd_ClampZoom 0x0041F7E0 Race Camera 50
8 App_UpdateCullMode 0x00429040 Graphics 11
9 CreditsScreen_Render 0x00425AC0 UI/Credits 126
10 KeyRemapMenu_WaitForKey 0x00443430 input system 38

Total: 943 lines of decompiled C documented.

Architecture Notes

All 10 functions are called exclusively via vtable DATA references (virtual dispatch). None are called directly. This confirms the engine's C++-style polymorphism: each game object type (Catapult, BounceBall, Lifter, FollowBall, etc.) has its own vtable with Update, Render, and Tick virtual methods. The Scene's main loop iterates object lists and calls the appropriate vtable slot.

Common Patterns

  • Timer_Init/Timer_Cleanup: Structured exception handling (SEH) frame setup/teardown used as scoped profiling/timing wrappers.
  • AthenaList_NextIndex: Iterator pattern for the engine's custom linked-list/array hybrid container.
  • Gfx_ScaleX/Y/Z: These are NOT just scale operations — they build transformation matrix rows used for rotation/translation of attached objects.
  • _DAT_ globals: Mix of f32 and f64 constants. Ghidra doesn't distinguish — must read 8 bytes and try both interpretations.

🔗 Related Documents

Binary [[21334133727843|meshworld format]]

types : meshworld
keywords :

📂 View source on GitHub


Binary MESHWORLD Format — Complete Decompilation

Based on Ghidra decompilation of FUN_004629E0 (Level::BinaryLoader)

File Structure (Sequential reads)

=== SECTION 1: Materials (0x7C structure each) ===
[uint32] material_count

For each material:
  [uint32] name_length
  [name_length bytes] name  (allocated, stored at material+0x00)
  [uint32] field1 -> material[1]  (position X or face_start, float value for objects)
  [uint32] field2 -> material[2]  (position Y)
  [uint32] field3 -> material[3]  (position Z)
  [uint32] field4 -> material[4]  (often 0)
  [uint32] field5 -> material[5]  (often 0)
  [uint32] field6 -> material[6]  (often 0x80000000 = -0.0f)
  [4 bytes]  extended_flag    -> material[10] (checked as char, rest padding)
  
  If extended_flag != 0:
    [4 × float] ambient_rgba    -> material[0x10..0x13]
    [4 × float] diffuse_rgba    -> material[0x0C..0x0F]
    [4 × float] specular_rgba   -> material[0x14..0x17]
    [4 × float] emission_rgba   -> material[0x18..0x1B]   *** WAS MISSING ***
    [float]     shine           -> material[0x1C]
    [uint32]    has_reflective  -> stored as bool at material+0x79
    [uint32]    has_texture     (checked with == 1, not != 0)
    
    If has_texture == 1:
      [uint32] texture_name_length
      [texture_name_length bytes] texture_filename
      (loaded via Graphics_LoadTexture)

NOTE: Section 1 items include BOTH materials AND game objects!
In WarmUp (16 items): START2-2, START2-1, CAMERALOOKAT, 
PLATFORM(x5, ext=1), SAFESPOT(x5), STANDS, START2-4, START2-3
The 6 u32 "fields" are actually float[3] position + 3 padding values.

=== SECTION 2: Mesh Buffers (0x54 structure each) ===
[uint32] mesh_buffer_count

For each mesh buffer:
  [uint32] data_length
  [data_length bytes] raw_data (stored at buffer+0x50)
  [uint32] face_count
  For each face:
    [3 × uint32] face_data (12 bytes per face)
    FUN_004685e0(buffer) processes vertex data
  FUN_00469090(buffer, 0) finalizes buffer

=== SECTION 3: Game Objects (0xD4 structure each) ===
[uint32] object_count

For each object:
  [uint32] object_type        (0 = straight/ramp, non-zero = special)
  
  If object_type == 0:
    [3 × float] position       (3 reads via vtable+4)
    [3 × float] euler_rotation (3 reads, 2 via vtable+8, skip pattern)
    [3 × float] scale/transform
    [D3D color/material init]
  
  (All objects added to level+0x478 list)

=== SECTION 4: Bounding Box ===
[float] min_x -> level+0x454
[float] min_y -> level+0x458
[float] min_z -> level+0x45C
[float] max_x -> level+0x468
[float] max_y -> level+0x46C
[float] max_z -> level+0x470

Note: In the Level STRUCT, there's a 12-byte gap between +0x45C and +0x468
(+0x460, +0x464, +0x468). But from the FILE it's 6 sequential reads (24 bytes).

**IMPORTANT**: For many Arena levels, the bbox values in the file are INVALID
(min > max for some axes). They appear to be placeholder/garbage values.
The game likely computes the real bbox from vertex data at runtime.
Our reimpl recomputes bbox from vertex positions if invalid or all-zeros.

Example invalid bbox from Arena-WarmUp: (0.98, 0.46, 1.0, 0.56, 0.56, 0.56)
Example zero bbox from Arena-SpawnPlatform: (0, 0, 0, 0, 0, 0)

=== SECTION 5: Vertex Array ===
[uint32] vertex_count -> level+0x438
(level+0x43C = 0, buffer allocated = vertex_count * 32 bytes at level+0x440)
[vertex_count * 32 bytes] vertex data

Vertex format (32 bytes each):
  [3 × float] position (x, y, z)     // 12 bytes
  [3 × float] normal (nx, ny, nz)    // 12 bytes
  [2 × float] texcoord (u, v)        // 8 bytes

**RENDERING NOTE**: Vertex data is in TRIANGLE-LIST order.
Every 3 consecutive vertices form a single triangle.
Confirmed with SpawnPlatform: V0-V5 = top face (2 tris), V6-V11 = front face,
V12-V17 = right face, V18-V23 = back face, V24-V29 = left face (10 tris total).
Use D3DPT_TRIANGLELIST with DrawPrimitive, prim_count = vertex_count / 3.

=== SECTION 6: Post-load ===
level+0x43C = 0 (vertex_data_ready flag)
level->self_ptr = this              (level+0x47C)
level+0x431 = 1 (loaded flag)
Timer_Init for level+0x10D
(**vtable+0x34)(file_handle, this) // callback
__close(file_handle)
FUN_004601A0(this) // finalize load

=== Trailing data (after vertex array in some files) ===
After the vertex data, some files contain:
- Material color overrides (ambient/diffuse/specular RGBA, shine, etc.)
- Texture name strings (e.g. "PinkChecker.bmp\0")
- Face index arrays (pairs of: vertex_index + triangle_group_flag)

Level Structure Offsets (from MeshWorld object)

Offset Size Field Description
+0x120 ptr mesh_data Pointer to mesh data master struct
+0x438 uint32 vertex_count Number of vertices
+0x43C uint32 zero_flag Set to 0 after vertex allocation
+0x440 ptr vertex_array Vertex data (32 bytes each)
+0x454 float min_x Bounding box min X
+0x458 float min_y Bounding box min Y
+0x45C float min_z Bounding box min Z
+0x460 ??? ??? (gap in struct layout)
+0x468 float max_x Bounding box max X
+0x46C float max_y Bounding box max Y
+0x470 float max_z Bounding box max Z
+0x478 list object_list Game objects (AthenaList)
+0x47C ptr self_ptr Pointer back to this object
+0x894 list material_list Materials (AthenaList)
+0xCAC list mesh_buffer_list Mesh buffers (AthenaList)
+0x10D ptr timer Timer object

Material Structure (0x7C bytes)

Offset Size Field Description
+0x00 ptr name Material name string
+0x04 uint32 field1 Position X (float) or face_start
+0x08 uint32 field2 Position Y (float) or face_count
+0x0C float[4] diffuse Diffuse color RGBA
+0x10 float[4] ambient Ambient color RGBA
+0x14 float[4] specular Specular color RGBA
+0x18 float[4] emission Emission color RGBA
+0x1C float shine Shininess
+0x28 byte extended Extended flag (byte from u32 read)
+0x74 bool has_reflective Reflective material flag
+0x75 bool has_texture Texture present flag
+0x78 ptr texture_ptr Loaded texture pointer
+0x79 bool is_transparent Alpha != -0.0

Global Constants

Address Float Value Usage
0x4CF3C8 -0.0 (0x80000000) Transparency comparison value

Verified File Data

Arena-SpawnPlatform.MESHWORLD (1,476 bytes)

  • material_count=0, meshbuf_count=0, obj_count=0
  • bbox at offset 12: all zeros (placeholder)
  • vertex_count=34 at offset 36
  • 34 vertices at offset 40 (34 × 32 = 1088 bytes)
  • Real bbox at offset 1128: (-160, 2, -149) to (172, 13, 145)
  • Trailing data: face indices + material data

Arena-WarmUp.MESHWORLD (83,254 bytes)

  • material_count=16 (includes START, PLATFORM, SAFESPOT, STANDS items)
  • 5 PLATFORM items have ext=1 with ambient/diffuse data
  • Items 0-2,8-16 have no extended section
  • meshbuf_count=0 after materials (at offset 1042)
  • obj_count=1 at offset 1046
  • Object type=0 at offset 1050, position=(634.2, 286.6, -375.3)
  • Bbox+vertices location: TBD (need to parse object section correctly)
  • File ends with "PinkChecker.bmp" material data and face indices

🔗 Related Documents

Browser D3D8 Shim

types : rendering

📂 View source on GitHub


Hamsterball Browser D3D8 Shim — Phase 1: API Verification

Binary

  • Path: originals/installed/extracted/Hamsterball.exe
  • Size: 1,404,928 bytes
  • MD5: 7d25019366b8d7f55906325bd630d7fe
  • Type: PE32 i386 executable

Import Verification

DirectX Graphics API

  • Imports d3d8.dll — confirmed via objdump -p
  • Only one D3D8 import: Direct3DCreate8
  • All other D3D8 calls go through COM interface vtables (IDirect3D8, IDirect3DDevice8, etc.)
  • No d3d9.dll, ddraw.dll, or dxgi.dll imports

Dead Code Strings

  • d3d9.dll, d3d8d.dll, D3D9 — present as strings but NOT imported
  • These are dead code in the Athena engine (optional API path compiled out)

Other Graphics Dependencies

  • GDI32.dll — standard Win32 GDI (window, font, bitmap)
  • USER32.dll — window management, input messages
  • SHELL32.dll — shell operations

Input

  • DINPUT8.dll — DirectInput 8 keyboard input

Audio

  • BASS.dll — BASS audio library (music/sound)
  • DSOUND.dll — DirectSound for sound effects

Networking

  • WS2_32.dll — Winsock (eSellerate DRM/registration)
  • eSellerateEngine.dll — DRM/licensing

Conclusion

Hamsterball is a fixed-function Direct3D 8 game. It uses:

  • Direct3DCreate8 → COM interfaces
  • No shaders (D3D8 fixed-function pipeline)
  • DrawPrimitiveUP for rendering
  • DirectInput for input
  • BASS + DirectSound for audio

No D3D9 path exists at runtime. The shim must implement D3D8 COM interfaces.


🔗 Related Documents

Camera System

types : rendering
keywords :

📂 View source on GitHub


Camera System

Overview

Hamsterball uses a 5-mode camera system controlled by Scene_SetCamera (0x419FA0).
The camera follows the ball with spring-damped interpolation, and supports path-based
rails, shake effects, snap transitions, and orbital rotation.

Camera Modes

1. Default Follow (no flags)

Camera target = ball position + offset (Scene+0x434C, Scene+0x4350, Scene+0x4354).
Camera smoothly follows ball with spring damping (hardcoded 0.9 factor).

2. Path Rail Mode (Scene+0x3F1C != 0)

When a path is active, the camera rides along a spline path:

  1. Path_GetPosition(path, &pos, path_t) gets current path position
  2. Direction from path to ball is computed
  3. If distance < threshold → camera offset = 0 (ball on rail)
  4. If distance > threshold → offset scales linearly with sine wobble
  5. Max camera offset capped at 700.0 units
  6. Ball_SetTargetPos sets ball's camera target position

Path rail adds a spring-like oscillation using Wave_Sin modulation,
creating a rubber-band effect when the ball deviates from the path.

3. Camera Shake (Ball+0x744 != 0)

When shake flag is set, random offsets of ±50 units are added to camera position:

shake_x = RNG_Rand(-50, 50)
shake_y = RNG_Rand(-50, 50)  
shake_z = RNG_Rand(-50, 50)
camera_pos += (shake_x, shake_y, shake_z)

4. Camera Snap (Scene+0x3F2C != 0, countdown frames)

When snap countdown > 0:

  • Set ball target pos directly to ball's forward vector (Ball+0x60..0x68)
  • Set ball actual cam pos to ball's forward vector
  • Decrement countdown each frame
    Used for race start countdowns and scene transitions.

5. Orbital Rotation (always active)

Every frame, regardless of mode:

cos_angle = Wave_Cos(Scene+0x29BC)  // orbit angle
sin_angle = Wave_Sin(Scene+0x29BC)  // orbit angle
orbit_dir = (cos_angle, 0.9, sin_angle)  // slightly above horizontal
Camera_SetView(orbit_dir, Scene+0x29C0)  // distance
Graphics_Refresh()  // commit camera

Scene_SetCamera (0x419FA0)

void Scene_SetCamera(Scene* this, Ball* ball, char use_path) {
    // Start with ball's target cam position
    Vec3 cam_target = ball->cam_target;  // +0x758
    Vec3 scene_offset = this->cam_offset; // +0x434C
    Vec3 desired = cam_target + scene_offset;
    
    // Mode 2: Path rail (if path active and use_path enabled)
    if (this->path_active && use_path) {
        Vec3 path_pos = Path_GetPosition(this->path, this->path_t);
        Vec3 delta = desired - path_pos;
        float dist = Vec3_Length(delta);
        
        if (dist < threshold) {
            // On rail - no offset
            offset = 0.0f;
        } else {
            if (dist > 700.0f) dist = 700.0f;
            dist -= MIN_DIST;  // _DAT_004cf3ec
            float wave = Wave_Sin(dist * WAVE_SCALE);  // oscillation
            offset = dist - dist * DAMPING * wave;  // spring
        }
        Vec3 offset_vec = Vec3_NormalizeAndScale(delta, offset);
        desired = path_pos + offset_vec;
        Ball_SetTargetPos(ball, desired.x, desired.y, desired.z);
    }
    
    // Mode 3: Camera shake
    if (ball->shake_flag) {  // +0x744
        desired.x += RNG_Rand(-50, 50);
        desired.y += RNG_Rand(-50, 50);
        desired.z += RNG_Rand(-50, 50);
    }
    
    // Mode 4: Camera snap (countdown)
    if (this->snap_countdown > 0) {  // +0x3F2C
        this->snap_countdown--;
        desired = ball->forward;  // +0x60..0x68
        ball->cam_actual = desired;  // +0x76C..0x774
        ball->cam_target = desired;  // +0x758..0x760
    }
    
    // Mode 5: Orbital rotation (always)
    float cos_a = Wave_Cos(this->orbit_angle);  // +0x29BC
    float sin_a = Wave_Sin(this->orbit_angle);
    Vec3 orbit_dir = {cos_a, 0.9f, sin_a};
    Camera_SetView(orbit_dir, this->orbit_distance);  // +0x29C0
    Graphics_Refresh();
}

CameraLookAt (Arena Camera) (0x413280)

Arena initialization sets up a fixed overhead camera:

1. Load "levels\arena-spawnplatform" mesh (MeshWorld_ctor, 0x10D0 bytes)
2. Load "levels\arena-stands" mesh (MeshWorld_ctor, 0x10D0 bytes)
3. Level_InitScene (build scene graph from loaded meshes)
4. Find "CAMERALOOKAT" object in hash table
5. Store target position: Scene+0x43BC..0x43C4 = CAMERALOOKAT pos
6. Store initial position: Scene+0x43AC..0x43B4 = CAMERALOOKAT pos
7. Set camera distance: Scene+0x29BC = 45.0f  (0x42340000)
8. Set camera height:  Scene+0x29C0 = 800.0f  (0x44480000)
9. Set max height:     Scene+0x43C8  = 800.0f  (0x44480000)
10. Set camera mode:   Scene+0x43CC = 1 (orbits)
11. Call vtable[0x54]() to apply camera settings

Level_SelectCameraProfile (0x40ACA0)

Selects camera profile based on level type:

  • Different profiles for: Warm Up, Beginner, Intermediate, etc.
  • Adjusts orbit distance, height, damping factor

Key Offsets

Offset Type Description
Ball+0x60..0x68 Vec3 Ball forward vector
Ball+0x164..0x16C Vec3 Ball position
Ball+0x758..0x760 Vec3 Camera target position
Ball+0x76C..0x774 Vec3 Camera actual position
Ball+0x744 int Camera shake flag
Scene+0x29BC float Camera orbit angle
Scene+0x29C0 float Camera orbit distance
Scene+0x3F1C int Path rail active flag
Scene+0x3F20 Path* Path object pointer
Scene+0x3F24 float Path parameter (t)
Scene+0x3F2C int Camera snap countdown
Scene+0x434C..0x4354 Vec3 Camera offset from ball
Scene+0x87C Camera* Camera object pointer

🔗 Related Documents

Case Study

types : playbook
keywords :

📂 View source on GitHub


Case Study: Hamsterball.exe

This case study shows how the generic RE playbook was applied to a real target: Hamsterball (2006, Raptisoft), PE32 i386, MSVC 2003.

1. Target Acquisition

  • Windows installer and installed directory preserved in originals/.
  • MD5: 7d25019366b8d7f55906325bd630d7fe.
  • Imports: D3D8.dll, DINPUT8.dll, DSOUND.dll, BASS.dll, USER32.dll, KERNEL32.dll.

2. Static Recon

file Hamsterball.exe               # PE32 executable (GUI) Intel 80386
strings Hamsterball.exe | grep -iE 'direct3d|d3d|dinput|dsound|bass'
pefile summary script               # Image base 0x400000, entry 0xBB4C8

Strings revealed:

  • Window class AthenaWindow
  • Level paths levels\\level1 through levels\\level8, arena variants
  • Music format .mo3
  • Font and asset names

3. Ghidra Import and Naming

  • Imported with default analyzers.
  • Started GhidraMCP headless server.
  • Applied 975+ renames from docs/FUNCTION_MAP.md.
  • Final documentation coverage: 100%.

4. Subsystem Discovery

Suspected Evidence Confirmed
D3D8 renderer Direct3DCreate8 import, D3D8_RENDERING_PIPELINE.md Yes
DirectInput8 DirectInput8Create import, INPUT_SYSTEM.md Yes
BASS audio BASS_Init, BASS_MusicLoad, .mo3 strings Yes
MESHWORLD levels Multiple .meshworld files, loader strings Yes
Vtable OOP Constructor vtable assignments, destructors restoring base vtables Yes

5. Struct Recovery

Key structs recovered using the playbook:

Struct Evidence Size
Ball Ball_ctor2 allocates 0xC98, vtable 0x4CF3A0 0xC98
Scene Scene_dtor iterates nested lists 0x47AC+
CollisionMesh Allocated via operator_new(0xCB0), owner back-pointer +0x10 0xCB0
App App_Initialize_Full constructs singleton 0x918+

6. Verification Story: App+0x5DC

Initial claim: App+0x5DC = Scene* currentScene

Verification:

  • Zero raw-C references to (int)this + 0x5DC in App functions.
  • Raw C in Ball_Update showed Scene+0x5DC used as a player-data array.
  • Correct conclusion: App→Scene path remained unverified; only Ball+0x14 → Scene was proven.

This demonstrates the rule: every offset is guilty until proven innocent by raw decompilation.

7. Dynamic Analysis

  • Ran original under Wine + Xvfb + d3d8to9 + llvmpipe.
  • Observed camera sign discrepancy across levels.
  • Confirmed texture behavior differences between loaded and programmatic textures.

8. Outputs

  • docs/FUNCTION_MAP.md — 3,781 named functions.
  • analysis/ghidra/structs/*.h — C struct definitions.
  • docs/*SYSTEM.md — subsystem documentation.
  • Working D3D8 reimplementation in reimpl/.

🔗 Related Documents

Catapult System

types : docs
keywords :

📂 View source on GitHub


Hamsterball Catapult System — Complete Reverse Engineering

Overview

Catapults are level objects that launch the ball when it touches a specific collision surface. They appear in Level4 (Tower Race) and the Dizzy Arena. The catapult mesh is loaded from Levels/Level4-Catapult.MESHWORLD, which contains a single entity name: E:CATAPULTBOTTOM.

Why your global-spawn catapults are "hollow": Catapults are NOT standalone objects — they require a CollisionLevel child object with a spatial tree, the mesh must be registered with the scene's collision system, the catapult must be added to TWO AthenaLists on the scene (general + catapult-specific), and the launch is triggered by the collision dispatcher (TowerCollisionEvents / HandleArenaCollisionEvents) matching the collision entry's entity name. If you just operator_new + Catapult_ctor without going through CreateLevelObjects, you get a visual shell with no collision registration and no event-name association.


Class Hierarchy

SpriteAnim (base)
  └─ Level (Level_ctor @ 0x00461740, size ~0x10D0)
       └─ Stands (Stands_ctor @ 0x00462850)
            └─ Catapult (Catapult_ctor @ 0x00437E10, alloc size = 0x1108 bytes)

Vtable

The Catapult vtable is at 0x004D5AD0 (referenced as PTR_MeshNode_Level_DeleteDtor3 in Ghidra). Key slots:

Vtable Offset Address Function
+0x00 0x0043E5E0 Gear_Vec3List_DeletingDtor (dtor)
+0x58 SceneObj_CallUpdate (inherited, updates render matrix)
+0x54 SceneObj_CallRender (inherited, renders mesh)
+0x2C (slot 11) 0x0043E600 Catapult_Update
+0x30 (slot 12) 0x0045DE30 LoadMeshWorld (inherited)
+0x58 (slot 22) 0x0043EA50 Catapult_Vec3List_DeletingDtor
+0x5C (slot 23) 0x0043EA70 Catapult_Render

Note: The vtable at 0x004D4F98 (set in Catapult_ctor) is the construction vtable used during C++ construction. The real runtime vtable at 0x004D5AD0 is set later by the Stands_CtorCollision/CollisionLevel_ctorWithLevel chain. The data refs at 0x004D5AFC (Catapult_Update) and 0x004D5B5C (Catapult_Render) confirm the runtime vtable.


Catapult Struct Layout (size 0x1108 bytes)

All offsets are byte offsets from the catapult object pointer.

Offset Type Field Description
+0x000 void** vtable Points to Catapult vtable
+0x008 void* mesh_ptr MeshWorld/SceneObject mesh data (set by Level_ctor → SceneObject_BaseInit)
+0x00D byte has_mesh_flag Set to 0 by Level_ctor
+0x018 AthenaList spatial_list SpatialTree clone list (from Stands_ctor)
+0x430 byte flag_430 Collision flag (copied from parent)
+0x431 byte flag_431 Set to 1 in Stands_ctor (marks as "stands" type)
+0x434 void* timer_obj Timer object (allocated in Stands_ctor)
+0x47C void* parent_level Pointer back to the parent level/scene (set in Stands_ctor)
+0x488 AthenaList list_488 Initialized in Level_ctor
+0x8A0 AthenaList list_8A0 Initialized in Level_ctor
+0xCB8 AthenaList list_CB8 Initialized in Level_ctor
+0x10D0 void* scene_ptr Pointer to the Board/Scene (passed as param_1 to Catapult_ctor)
+0x10D4 void* collision_level CollisionLevel child object (allocated 0x10D0 bytes, CollisionLevel_ctorWithLevel)
+0x10D8 float[3] position XYZ position (set from param_4 in CreateLevelObjects)
+0x10E4 int pad Set to 0
+0x10E8 float launch_dir_y Initial value = -1.0 (0xBF800000) — negative Y = downward initial direction
+0x10EC void* launch_ball_ptr Ball to launch (set by TowerCollisionEvents when E:CATAPULTBOTTOM fires)
+0x10F0 float launch_timer Launch countdown timer (set to 0 initially, set to 50.0 by Catapult_Launch, decremented each frame)
+0x10F4 float launch_decrement Set to 50.0 (0x42480000) by Catapult_Launch — wait, that's wrong.
+0x10F8 AthenaList ball_list List of balls currently on the catapult (AthenaList, initialized in ctor). Each entry is 8 bytes: [ball_ptr, tick_counter]. Used by Catapult_AddObjectConditional (N:ONGEAR). Tick counter starts at 10, decremented each frame by Catapult_Update, entry freed when it reaches 0. Counter resets to 10 on every frame of continued contact (grace period, not carry limit).
+0x10FC int ball_list_count Count of balls in ball_list
+0x1100 byte active_flag Set to 1 in CreateLevelObjects (marks catapult as active)
+0x1104 float launch_force Initial value = 17.0 (0x41880000) — upward launch velocity
+0x1108 AthenaList object_list Object list (used by Catapult_AddObjectConditional)
+0x110C int object_list_count Count for object_list
+0x1510 byte conditional_flag Checked by Catapult_AddObjectConditional (must be non-zero)

Important: The 0x1104 value of 17.0 is set in Catapult_ctor at *(float*)(this + 0x1104) = 0x41880000. This is the launch velocity applied to the ball.

Rotator system note: Catapult_Update (0x43E600) serves double duty — it handles both catapult launch arcs AND rotator/gear ball carrying. When used by the rotator system (via Rotator_AddBall at 0x43B6F0), the tick counter at entry+4 counts down from 10 and the rotation matrix is applied to the ball's position and velocity each frame. The counter resets to 10 on every frame of continued collision contact, so the ball stays on the rotator indefinitely while touching it, with a 10-frame grace period after leaving. See docs/physics/COLLISION_SYSTEM_DEEP.md → "Rotator System" for full details.


Creation Process

Step 1: Level Loading

When a level is loaded (e.g., Scene_LoadLevel4 @ 0x0040D6D0):

  1. MeshWorld_ctor loads the main level geometry from "levels\\level4" → stored at Scene+0x22B (= scene+0x8AC offset, param_1[0x22b])
  2. CollisionLevel_ctorWithLevel creates a collision level from the MeshWorld → stored at Scene+0x22C
  3. Level_InitScene sets up the scene
  4. The level's .MESHWORLD file is loaded by Level_LoadMeshes (@ 0x00465860)

Step 2: Level_LoadMeshes — Entity Registration

Level_LoadMeshes (@ 0x00465860) iterates all objects in the MeshWorld:

  1. For each object with an entity name (stored at obj+0x864):

    • Creates a MeshBuffer (0x874 bytes) via CreateMeshBuffer
    • If name starts with "N:" → sets meshbuffer+0x85D = 1 (name flag)
    • If name starts with "E:" → sets meshbuffer+0x85D = 1 AND meshbuffer+0x863 = 1 (event flag)
    • Copies the entity name string into meshbuffer+0x219*4 (= +0x864 offset)
    • Creates triangle collision data and appends to the meshbuffer's list
  2. This is how E:CATAPULTBOTTOM gets registered — the MeshWorld file Level4-Catapult.MESHWORLD contains the string E:CATAPULTBOTTOM at offset 13580, and Level_LoadMeshes creates a MeshBuffer with that entity name.

Step 3: CreateLevelObjects — Catapult Factory

CreateLevelObjects (@ 0x00412711) is the factory dispatcher. When a mesh name matches "CATAPULT" (via __strnicmp):

// From CreateLevelObjects @ 0x00412711, CATAPULT section:
if (__strnicmp(param_1, "CATAPULT", 8) == 0) {
    // 1. Allocate 0x1108 bytes for the Catapult object
    pvVar5 = operator_new(0x1108);
    
    // 2. Call Catapult_ctor(this, scene_ptr, mesh_source)
    pvVar5 = Catapult_ctor(pvVar5, this, *(int*)((int)this + 0x5848));
    
    // 3. Mark as active
    *(byte*)((int)pvVar5 + 0x1100) = 1;
    
    // 4. Set position from param_4 (the mesh transform data)
    *(float*)((int)pvVar5 + 0x10D8) = param_4[1];  // X
    *(float*)((int)pvVar5 + 0x10DC) = param_4[2];  // Y  
    *(float*)((int)pvVar5 + 0x10E0) = param_4[3];  // Z
    
    // 5. CRITICAL: Add to scene's general object list
    AthenaList_Append((void*)((int)this + 0x2578), (int)pvVar5);
    
    // 6. CRITICAL: Add to scene's catapult-specific list
    AthenaList_Append((void*)((int)this + 0x584C), (int)pvVar5);
}

This is the key step you're missing. Two AthenaList_Append calls:

  • Scene+0x2578 — the general level objects list (for rendering/updating)
  • Scene+0x584C — the catapult-specific list (used by the collision dispatcher to find catapults)

Step 4: Catapult_ctor Internals

Catapult_ctor (@ 0x00437E10, __thiscall):

void* Catapult_ctor(void* this, void* scene_ptr, int mesh_source) {
    // 1. Call parent: Stands_ctor(this, mesh_source)
    //    - Calls SpriteAnim_Ctor (base)
    //    - Calls Level_ctor → sets up AthenaLists, SceneObject
    //    - Clones SpatialTree nodes from mesh_source
    Stands_ctor(this, mesh_source);
    
    // 2. Set Catapult vtable
    *(void***)this = &PTR_MeshNode_Level_DeleteDtor3_004D4F98;
    
    // 3. Store scene pointer
    *(void**)((int)this + 0x10D0) = scene_ptr;
    
    // 4. Initialize position fields (will be overwritten by CreateLevelObjects)
    *(float*)((int)this + 0x10D8) = 0;  // X
    *(float*)((int)this + 0x10DC) = 0;  // Y
    *(float*)((int)this + 0x10E0) = 0;  // Z
    
    // 5. Clear pad and launch direction
    *(int*)((int)this + 0x10E4) = 0;
    *(float*)((int)this + 0x10E8) = -1.0f;  // Initial launch direction Y = -1
    
    // 6. CRITICAL: Create CollisionLevel child object
    void* col = operator_new(0x10D0);
    col = CollisionLevel_ctorWithLevel(col, (int)this);
    *(void**)((int)this + 0x10D4) = col;
    
    // 7. Copy timer from parent and clear collision flag
    *(void**)((int)col + 0x434) = *(void**)((int)this + 0x434);
    *(byte*)(*(int*)((int)this + 0x10D4) + 0x431) = 0;
    
    // 8. Clear launch flags
    *(byte*)((int)this + 0x10F0) = 0;  // launch_timer = 0 (not active)
    *(byte*)((int)this + 0x10F8) = 0;  // ball_list empty
    *(byte*)((int)this + 0x1100) = 0;  // active_flag = 0 (set to 1 later by CreateLevelObjects)
    
    // 9. Set launch force
    *(float*)((int)this + 0x1104) = 17.0f;  // 0x41880000
}

Step 5: CollisionLevel_ctorWithLevel

CollisionLevel_ctorWithLevel (@ 0x00465080, __thiscall):

void* CollisionLevel_ctorWithLevel(void* this, int parent_level) {
    // 1. Call Level_ctor with parent's mesh data
    Level_ctor(this, *(void**)(parent_level + 4));  // mesh source
    
    // 2. Set CollisionLevel vtable
    *(void***)this = &PTR_Level_DeletingDtor2_004D9068;
    
    // 3. CRITICAL: Load meshes (creates MeshBuffers with entity names)
    Level_LoadMeshes(this, parent_level);
    
    // This is where E:CATAPULTBOTTOM gets its collision triangles created
}

Collision Detection & Launch Trigger

How the Ball Hits the Catapult

The game's collision system (SpatialTree) detects when the ball intersects a collision triangle. When a collision entry is created, its entity name (at collision_obj+0x864) is checked by the collision dispatcher.

Race Mode: TowerCollisionEvents

TowerCollisionEvents (@ 0x0040DCD0, __thiscall):

void TowerCollisionEvents(void* this, int* ball, int* coll_obj) {
    // Check entity name on the collision object
    if (__stricmp(*(char**)(coll_obj[1] + 0x864), "E:CATAPULTBOTTOM") == 0) {
        // Cooldown check: ball+0x202 prevents re-triggering
        if (ball[0x202] < 1) {
            ball[0x202] = 1000;  // Set cooldown (frames)
            
            // Iterate catapult list (Scene+0x43B8 in race mode)
            int idx = AthenaList_NextIndex((int)this + 0x43B8);
            for (each catapult in Scene+0x47C4 list) {
                // Match collision mesh to catapult
                if (catapult->collision_level (catapult+0x10D4) == *coll_obj) {
                    catapult->launch_ball_ptr (catapult+0x10EC) = ball;
                    Catapult_Launch(catapult);  // 0x00434290
                    Sound_PlayChannel(*(scene+0x878)->sounds[0x464]);  // catapult sound
                }
            }
        }
    }
    // ... other collision handlers (OPENSESAME, TRAPDOOR, BITE, MACETRIGGER, etc.)
    DispatchCollisionEvents(this, ball, coll_obj);
}

Arena Mode: HandleArenaCollisionEvents

HandleArenaCollisionEvents (@ 0x00412D57) handles the same E:CATAPULTBOTTOM collision but uses different scene offsets for the catapult list:

// In HandleArenaCollisionEvents, E:CATAPULTBOTTOM handler:
if (__stricmp(*(char**)(coll_obj[1] + 0x864), "E:CATAPULTBOTTOM") == 0) {
    if (ball[0x202] < 1) {
        ball[0x202] = 1000;
        
        // ARENA catapult list: Scene+0x584C (instead of Scene+0x43B8)
        // ARENA catapult items: Scene+0x5C58 (instead of Scene+0x47C4)
        int idx = AthenaList_NextIndex((int)this + 0x584C);
        for (each catapult in Scene+0x5C58 list) {
            if (catapult+0x10D4 == *coll_obj) {
                catapult+0x10EC = ball;
                Catapult_Launch(catapult);
                Sound_PlayChannel(*(scene+0x878)->sounds[0x464]);
            }
        }
    }
}

Catapult_Launch

Catapult_Launch (@ 0x00434290, __fastcall):

void Catapult_Launch(int catapult) {
    *(byte*)(catapult + 0x10F0) = 1;      // Set launch active flag
    *(float*)(catapult + 0x10F4) = 50.0f;  // Set launch timer (0x32 = 50 as int, 
                                            // but stored as float 0x42480000 = 50.0)
}

This is extremely simple — it just sets two fields:

  • +0x10F0 = 1 — marks the catapult as "launching"
  • +0x10F4 = 50.0 — countdown timer (decremented each frame by Catapult_Update)

Catapult_Update — The Launch Physics

Catapult_Update (@ 0x0043E600, __fastcall) is called every frame via the vtable. It:

1. Timer Countdown

// FLD [EBX+0x10F0]  (load launch_timer)
// FSUB [EBX+0x10F4]  (subtract decrement)
// FSTP [EBX+0x10F0]  (store result)
catapult->launch_timer -= catapult->launch_decrement;

This counts down the launch timer. When launch_timer reaches 0 or below, the launch is complete.

2. Ball Position Transformation

Catapult_Update iterates through the ball list (catapult+0x10F8 AthenaList) and applies a matrix transformation to each ball:

// For each ball in the catapult's ball_list:
//   1. Calculate ball position relative to catapult (ball_pos - catapult_pos)
//   2. Apply rotation matrix (built from launch_timer value)
//   3. Apply scaling (Gfx_ScaleY, Gfx_ScaleX, Gfx_ScaleZ)
//   4. Transform ball position using the matrix
//   5. Update ball position (ball+0x164/X, ball+0x168/Y, ball+0x16C/Z)
//   6. Transform ball velocity (ball+0x1A4 → physics_obj+0xCA4/CAC)

The key physics operations:

  • Ball position is at ball+0x164 (X), ball+0x168 (Y), ball+0x16C (Z)
  • Ball velocity is at ball->physics_obj (ball+0x1A4) + 0xCA4 (velX), +0xCA8 (velY), +0xCAC (velZ)
  • The matrix is built using Timer_Init + Gfx_Scale* calls, which construct a transformation matrix
  • The launch_force at +0x1104 (17.0) determines the upward velocity

3. Object Aging (Time-Out)

Catapult_Update also manages objects in the ball_list with a countdown:

// For each entry in the list:
//   entry[1] -= 1;  (decrement timer)
//   if (timer < 1) { remove entry; free memory; }

This means balls stay on the catapult list for a limited number of frames (initial value = 10, set by Catapult_AddObjectConditional).


Catapult_AddObjectConditional

Catapult_AddObjectConditional (@ 0x0043E9C0, __thiscall):

void Catapult_AddObjectConditional(void* this, int ball_ptr) {
    // Only proceed if catapult is active (flag at +0x1510)
    if (*(char*)((int)this + 0x1510) != '\0') {
        // Check if ball is already in the list
        for (each entry in ball_list (this+0x10F8)) {
            if (entry->ball == ball_ptr) {
                entry->timer = 10;  // Reset timer
                return;
            }
        }
        // Add new entry
        int* entry = operator_new(8);
        entry[0] = ball_ptr;   // ball pointer
        entry[1] = 10;         // frames to stay on list
        AthenaList_Append(this+0x10F8, entry);
    }
}

This is called from an unnamed function at ~0x004184D9 (likely a per-frame collision check that adds balls that are near/on the catapult to the ball list).


Catapult_Render

Catapult_Render (@ 0x0043EA70, __fastcall):

// 1. Calculate oscillation using Wave_Sin
float wave = Wave_Sin(PTR_PTR_004F7188, catapult->render_time (+0x53F));
Gfx_ScaleZ(wave * _DAT_004D5D20);

// 2. Set position
Gfx_SetPosition(catapult_pos_x, catapult_pos_y, catapult_pos_z);

// 3. Update render time
catapult->render_time (+0x53F) += catapult->render_speed (+0x540);

// 4. Call inherited render (vtable+0x58 = CallUpdate, vtable+0x54 = CallRender)
(**(code**)(*catapult + 0x58))();  // SceneObj_CallUpdate
(**(code**)(*catapult + 0x54))();  // SceneObj_CallRender

// 5. Age and free expired objects from the render list

The Wave_Sin call creates the visual oscillation/animation of the catapult platform.


MESHWORLD File: Level4-Catapult.MESHWORLD

  • File: Levels/Level4-Catapult.MESHWORLD (14,370 bytes)
  • Entity name: E:CATAPULTBOTTOM (at file offset 13,580)
  • This is the ONLY entity name in the file — no N: names, no other E: events
  • The mesh name "CATAPULT" is what CreateLevelObjects matches with __strnicmp

The catapult's collision geometry (the "bottom" surface the ball touches) is tagged with E:CATAPULTBOTTOM. When the SpatialTree detects a collision between the ball and any triangle belonging to this mesh, the collision entry's +0x864 field points to the string "E:CATAPULTBOTTOM", which the collision dispatcher matches.


Scene List Offsets Summary

Context General Objects List Catapult List Catapult Items Array
Race (TowerCollisionEvents) Scene+0x2578 Scene+0x43B8 Scene+0x47C4
Arena (HandleArenaCollisionEvents) Scene+0x2578 Scene+0x584C Scene+0x5C58

Note: In race mode, Scene+0x43B8 is the catapult AthenaList and Scene+0x47C4 is its internal items array. In arena mode, Scene+0x584C and Scene+0x5C58 serve the same purpose. The CreateLevelObjects factory adds to Scene+0x584C for both modes — the arena offsets appear to be the primary ones, and TowerCollisionEvents uses different offsets that may be set up during race initialization.


Why Your Global Spawns Are Hollow — And How to Fix Them

What you're probably doing:

// WRONG: Just creating the object
void* cat = operator_new(0x1108);
Catapult_ctor(cat, scene, mesh_source);
// ... set position ...
// Catapult renders but has NO collision and NO launch behavior

What you need to do:

// CORRECT: Full creation pipeline

// 1. Allocate and construct
void* cat = operator_new(0x1108);
Catapult_ctor(cat, scene, mesh_source);

// 2. Mark as active
*(byte*)((int)cat + 0x1100) = 1;

// 3. Set position
*(float*)((int)cat + 0x10D8) = x;
*(float*)((int)cat + 0x10DC) = y;
*(float*)((int)cat + 0x10E0) = z;

// 4. CRITICAL: Add to scene's general object list (for rendering/updating)
AthenaList_Append((void*)((int)scene + 0x2578), (int)cat);

// 5. CRITICAL: Add to scene's catapult-specific list (for collision dispatch)
//    Use Arena offsets (0x584C / 0x5C58) — these are what CreateLevelObjects uses
AthenaList_Append((void*)((int)scene + 0x584C), (int)cat);

// 6. The CollisionLevel child (cat+0x10D4) was created by Catapult_ctor
//    and Level_LoadMeshes registered the E:CATAPULTBOTTOM entity name.
//    BUT: the collision mesh must be registered with the scene's SpatialTree
//    for the ball to actually collide with it.

The Missing Piece: Collision Registration

The CollisionLevel at cat+0x10D4 has its own SpatialTree (cloned from the parent in Stands_ctor). However, for the ball's collision system to detect intersections with the catapult's mesh, the catapult's collision triangles must be registered in the scene's main SpatialTree.

In the normal level loading flow, this happens because:

  1. Level_LoadMeshes creates MeshBuffers with entity names and collision triangles
  2. The SpatialTree in the CollisionLevel stores these triangles
  3. When TowerCollisionEvents / HandleArenaCollisionEvents checks catapult+0x10D4 == *coll_obj, it's matching the collision object that was created from the CollisionLevel's mesh data

For a global spawn, you need to ensure:

  1. The CollisionLevel's mesh is loaded (Catapult_ctor does this via CollisionLevel_ctorWithLevel)
  2. The collision triangles are registered with the scene's SpatialTree
  3. The catapult is added to the correct scene list so the collision dispatcher can find it

The simplest approach: call CreateLevelObjects with a mesh named "CATAPULT" — this does everything correctly. If you want to spawn at runtime, replicate all the steps that CreateLevelObjects does (allocation, construction, position set, both AthenaList_Append calls).


Related Functions Reference

Address Name Description
0x00437E10 Catapult_ctor Constructor (thiscall: this, scene, mesh_source)
0x0043E600 Catapult_Update Per-frame update: timer countdown + ball physics
0x0043EA70 Catapult_Render Per-frame render: oscillation + mesh render
0x00434290 Catapult_Launch Sets launch flag + timer (called on E:CATAPULTBOTTOM)
0x0043E9C0 Catapult_AddObjectConditional Adds ball to catapult's ball_list
0x0043EA50 Catapult_Vec3List_DeletingDtor Destructor
0x00437820 Catapult_Vec3List_Level_Dtor Level destructor
0x00412711 CreateLevelObjects Factory: matches "CATAPULT" mesh name
0x0040DCD0 TowerCollisionEvents Race collision dispatcher (E:CATAPULTBOTTOM)
0x00412D57 HandleArenaCollisionEvents Arena collision dispatcher (E:CATAPULTBOTTOM)
0x00462850 Stands_ctor Parent constructor
0x00465080 CollisionLevel_ctorWithLevel Creates CollisionLevel + loads meshes
0x00461740 Level_ctor Grandparent constructor
0x00465860 Level_LoadMeshes Loads meshes + registers entity names
0x00461460 SceneObject_BaseInit Initializes SceneObject mesh data
0x0045DF80 SceneObject_CallUpdate Inherited vtable: update render matrix
0x0045DF90 SceneObject_CallRender Inherited vtable: render mesh

String References

Address String Usage
0x004CF99C "CATAPULT" Mesh name matched by CreateLevelObjects
0x004CF9A4 "E:CATAPULTBOTTOM" Collision event name (stricmp in dispatchers)
0x004D09C6 "Levels\\Level4-Catapult" MESHWORLD file path
0x004D2DEF "sounds\\catapult" Sound file path

🔗 Related Documents

Collision Dumper

types : mods
keywords :

📂 View source on GitHub


Collision Dumper

Dumps all fields of every collision entry from the game's PhysicsObject to a text file, for reverse-engineering unknown struct fields.

What It Does

  • Hooks Ball_Update (0x405E00) with a 5-byte JMP detour
  • After each Ball_Update completes, walks the PhysicsObject's collision entry list
  • Dumps 0x80 bytes (32 DWORDs) per entry as hex + float, with known-field annotations
  • Throttled to ~1 dump per second to avoid flooding
  • Output: collision_dump.txt in the game directory

Entry Fields (Known)

Offset Type Field
+0x00 int32 type (1=ball-ball, 2=wall, 5=floor)
+0x0C void* other_ball (type==1 only)
+0x20 float normal_x
+0x24 float normal_y
+0x28 float normal_z
+0x2C float collision_pt
+0x30 float normal2_x
+0x34 float normal2_y
+0x38 float normal2_z
+0x64 int32 collision_id

All other offsets are UNKNOWN — that's what this dumper is designed to test.

Build

i686-w64-mingw32-gcc -shared -o bass.dll collision_dumper.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Install

  1. Rename original bass.dllbass_real.dll in the Hamsterball directory
  2. Copy this compiled bass.dll to the same directory
  3. Launch Hamsterball.exe
  4. A MessageBox confirms the hook is installed
  5. Play the game — collide with walls and balls
  6. Check collision_dump.txt for output

🔗 Related Documents

Collision Event System

types : physics
keywords :

📂 View source on GitHub


Hamsterball Collision Event System

Architecture

The collision event system uses a 2-tier dispatcher chain. The scene's vtable determines which top-level handler runs:

Ball Update (ball->vtable[0x10], called from Scene_UpdateBallsAndState 0x41B540)
  └─ Geometric collision detected by Ball_AdvancePositionOrCollision (0x4564C0)
       └─ Event dispatch based on object name string at collObj+0x864
            ├─ ExpertCollisionEvents (0x40E6A0)  ← Rumble arenas (vtable-driven)
            │    └─ DispatchCollisionEvents (0x40C5D0)  ← Shared base (ALL events)
            └─ TowerCollisionEvents (0x40DCD0)  ← Race levels (vtable-driven)
                 └─ DispatchCollisionEvents (0x40C5D0)  ← Shared base (ALL events)

Arena and Level handlers are parallel, not chained — they never call each other. Both delegate to DispatchCollisionEvents for universal events. Ball_AdvancePositionOrCollision handles only geometric collision detection (velocity integration, mesh intersection) and does NOT dispatch events itself.

Event Dispatch Table

Tier 1: ExpertCollisionEvents (0x40E6A0) — Arena Events

Domain: Rumble arena multiplayer events. Most events gated behind App+0x23C (multiplayer flag).

Event Effect Gated
E:CALLHAMMER Spawn bonk popup Yes
E:HAMMERCHASE Start hammer chase Yes
E:ALERTSAW1 Saw blade 1 alert mode Yes
E:ALERTSAW2 Saw blade 2 alert mode Yes
E:ACTIVATESAW1 Saw blade 1 full active Yes
E:ACTIVATESAW2 Saw blade 2 full active Yes
E:ALERTJUDGES Reset all judge objects No
E:SCORE(N) Set score display to N No
E:JUMP Ball bounce + sound + 200pts No
E:BELL Extra time (+500) + popup No

After processing: delegates to DispatchCollisionEvents (not to TowerCollisionEvents — Arena and Level are parallel).

Tier 2: TowerCollisionEvents (0x40DCD0) — Level Events

Domain: Race level mechanical objects.

Event Effect Details
E:CATAPULTBOTTOM Launch catapult Ball freeze=1000, catapult stores ball ref
E:OPENSESAME Open all trapdoors Opens first trapdoor in list
N:TRAPDOOR Activate matching trapdoor Matches by object ID or secondary ID
E:BITE Set damage Timer=25.0 (0x41C80000), clear counter
E:MACETRIGGER Activate all maces Sets each mace active flag at +0x10F0
N:MACE Ball bounces off mace Only if mace swinging (not 0x42A00000) and not already hit

After processing: delegates to DispatchCollisionEvents.

Base Tier: DispatchCollisionEvents (0x40C5D0) — Shared Base Handler

Domain: All game events (base handler, processes everything).

Event Effect Key Details
N:SECRET Mark rotator triggered Secret found marker
N:UNLOCKSECRET Check arena unlock May unlock new arena
E:NODIZZY Anti-dizzy zone Parses <TIME>N</TIME> tags, grants dizzy immunity
E:SAFESWITCH Copy switch data Data in parentheses → ball+0xC2C
E:LIMIT Arena boundary Tracks completions per player (0-3)
E:BREAK Ball bounce Calls ball vtable[0x20]
E:JUMP Ball jump 3D sound, impact=10, force=0.025, +200pts
E:ACTION Score award <ONCE>TRUE</ONCE> prevents repeat, <SCORE>N</SCORE> with difficulty mod
E:TRAJECTORY Set trajectory Parses <X>, <Y>, <Z> XML tags
N:NOCONTROL Disable input 10 frame freeze
N:WATER Water effect Sets in-water flag + 10 frame timer
N:TARPIT Tar slowdown 3D sound, marks tar state, clears velocity
N:GOAL Finish race Plays "Goal!" music, sets finish flags, records time
N:MOUSETRAP Ball deflect Normalizes direction × speed (0x4CF370), finds rotator
DROPIN Pipe drop-in Sound + +200pts if speed > threshold
PIPEBONK Pipe bonk Random sound from 3, +100pts, 10 frame cooldown
POPOUT Pipe pop-out Sound + +100pts, 50 frame cooldown

Event Name Format

Event names are stored in collision objects at object+0x864 (string pointer).
Two prefixes:

  • N: = Named/physical object (something the ball interacts with physically)
  • E: = Event/trigger (something that activates on ball contact)

Some events have XML-style tag parameters:

E:NODIZZY<TIME>500</TIME>
E:ACTION<ONCE>TRUE</ONCE><SCORE>500</SCORE>
E:TRAJECTORY<X>1.0</X><Y>0.5</Y><Z>-1.0</Z>
E:SCORE250
E:SAFESWITCH(my_data)

Tags are parsed by MWParser_ReadTag which extracts name-value pairs.

Ball State Offsets Used by Events

Ball Offset Type Used By
+0x059/5A/5B float Position X/Y/Z (for 3D sound)
+0x0A7/0A8 float Jump force direction/mode
+0x0B3 byte In-tar flag
+0x0B4 float Tar entry Y position
+0x0B6 int Water timer (10 frames)
+0x0CB AthenaList ONCE event tracker (prevents re-trigger)
+0x1DA byte Active flag (cleared on LIMIT/TARPIT)
+0x1F2 int DROPIN cooldown
+0x1F5 int Unknown cooldown (50 frames)
+0x1F7 int Impact counter (jump/bounce cooldown)
+0x202 int Freeze counter (input disabled while > 0)
+0x2D5 byte In-water flag
+0x2E9 byte LIMIT hit flag (⚠ sticky — never cleared within Ball_Update, NOT a ground flag)
+0x30B byte SAFESWITCH state
+0xC2C char[] SAFESWITCH data buffer
player_index*0xA0 per-player Score, timer, finish state offsets

App Offsets Used by Events

App Offset Type Description
+0x0210 char* Activity log string ("Reach Goal 1"..."Update")
+0x0220 ptr Player profile (checks +0x10/11 for multiplayer)
+0x0234 byte Some flag
+0x023C int Multiplayer flag (gates arena events)
+0x0294-2C4 float* Race timer positions (4 players × mirror/normal)
+0x0460 int DROPIN sound handle
+0x0464 int Catapult sound handle
+0x0468 int POPOUT sound handle
+0x046C+ int[3] PIPEBONK sound handles (3 random choices)
+0x0484 int TARPIT sound handle
+0x049C int JUMP sound handle
+0x04CC int Unknown sound handle
+0x053C void* Music player handle
+0x05CC+ per-player Score timer position
+0x05D6+ per-player Finish flag (bool)
+0x05E4+ per-player Accumulated score (float)
+0x05EC+ per-player Extra time (bell, 500 units)
+0x05F0+ per-player Another finish flag
+0x05FC+ per-player Race finished flag
+0x05E8 int Tournament time record
+0x090C int Tournament scene manager (optional)

Per-Player Offset Calculation

Many app offsets use the pattern: app + playerIndex * 0xA0 + baseOffset

Ball player index is at ball[6] (param_1[6]).

Examples:

  • Score: app + playerIdx*0xA0 + 0x5E4
  • Timer: app + playerIdx*0xA0 + 0x5CC
  • Finish: app + playerIdx*0xA0 + 0x5D6
  • Extra time: app + playerIdx*0xA0 + 0x5EC

Sound System Integration

3D positioned sounds use Sound_Play3D(handle, x, y, z) where position comes
from the ball. Non-positioned sounds use Sound_PlayChannel(handle).

Each sound event has a cooldown timer (impact/freeze counter) to prevent
re-triggering every frame while the ball is inside the trigger volume.

Arena Object Storage

Arena-specific objects are stored in the Board/Level:

Level Offset List Objects
+0x436C BonkList Hammer/bonk objects
+0x4370 SawList1 Saw blade 1
+0x4374 SawList2 Saw blade 2
+0x43B8 CatapultList Catapults (count at +0x43BC, array at +0x47C4)
+0x47D0 TrapdoorList Trapdoors (count at +0x47D4, array at +0x4BDC)
+0x4BBC JudgeList Judges (count at +0x4BC0, array at +0x4FC8)
+0x4FD4 BellList Bell objects
+0x5000 MaceList Maces (count at +0x5004, array at +0x540C)

Each object in these lists has its ID at +0x10D4 which is compared against
the collision object's ID to find the matching game object.


🔗 Related Documents

Collision Handler Complete Map

types : physics
keywords :

📂 View source on GitHub


Collision Handler Complete Mapping (Verified June 2026)

CORRECTED: All 30 board vtable[0x1D] entries verified from binary via GhidraMCP read_memory.
Previous docs combined Sky/Neon and missed Glass Race, Neon Race, Neon Arena, and Glass Arena handlers.
The REF_LOADING_SYSTEM.md had Race/Arena constructor tables SWAPPED.

Summary

  • 15 race boards + 15 arena boards = 30 total boards
  • 26 unique handler functions (some shared)
  • 4 handlers were newly discovered in this pass: NeonRace, NeonArena, GlassRace, GlassArena
  • 1 handler was mislabelled: 0x410D00 was called "NeonCollisionEvents" but is actually SkyCollisionEvents
  • Previous docs combined Sky+Neon and Glass+Impossible into single entries

Race Board Handlers (15)

# Race Level Constructor Vtable vtable[0x1D] Handler Name Events
1 WarmUp 0x41CA40 0x4D04A8 0x40C5D0 DispatchCollisionEvents (base) (base only — no override)
2 Beginner 0x4200E0 0x4D1098 0x4111E0 BeginnerCollisionEvents N:BUMPER
3 Intermediate 0x41CB20 0x4D05A0 0x40D340 IntermediateCollisionEvents N:BRIDGE
4 Dizzy 0x41D060 0x4D0890 0x40D500 DizzyCollisionEvents N:WATERWHEEL, N:WHEELEMBED, N:SWIRL
5 Tower 0x41E340 0x4D0A08 0x40DCD0 TowerCollisionEvents E:CATAPULTBOTTOM, E:OPENSESAME, N:TRAPDOOR, E:BITE, E:MACETRIGGER, N:MACE
6 Up 0x420390 0x4D11A0 0x4119B0 UpCollisionEvents E:HELPINERTIA, E:UNHELPINERTIA, E:VACPOPOUT, N:SPEEDCYLINDER, N:EXTRATIME
7 Neon 0x424440 0x4D1DF0 0x416CA0 NeonRaceCollisionEvents (NEW) N:NEONPLATFORM, E:ZOOP, E:LIGHTSOFF, E:LIGHTSON
8 Expert 0x41EA40 0x4D0B00 0x40E6A0 ExpertCollisionEvents E:CALLHAMMER, E:HAMMERCHASE, E:ALERTSAW1/2, E:ACTIVATESAW1/2, E:ALERTJUDGES, E:SCORE, E:JUMP, E:BELL
9 Odd 0x41ED80 0x4D0BC0 0x40ED30 OddCollisionEvents E:GRAVITY, N:JUMPFIRST, N:JUMPSECOND, E:SHRINK, E:GROWSOUND, E:GROW, E:DROPLIFT, E:PIPERANDOM, E:LIMIT, E:LIMITX, E:LIMITZ, E:LIMITPIPE1, E:LIMITPIPE2, E:SWALLOW
10 Toob 0x41F4B0 0x4D0E78 0x410020 ToobCollisionEvents E:ALERTSAW2, E:BRANCH(A/B), N:SPINNY, N:SAWTEETH, N:BUMPER
11 Wobbly 0x41F110 0x4D0D38 0x40F9A0 WobblyCollisionEvents N:SQUAREWOBBLY, N:WAVY
12 Glass 0x424A90 0x4D1F90 0x417760 GlassRaceCollisionEvents (NEW) N:GLASS, N:TENBONUS1, N:TENBONUS2
13 Sky 0x41F930 0x4D0FC8 0x410D00 SkyCollisionEvents (renamed from "NeonCollisionEvents") E:PEGS, E:TRAPPOP, E:NOPEGS, E:HEATON, E:HEATOFF, E:LIMIT
14 Master 0x4206D0 0x4D12B0 0x412850 MasterCollisionEvents N:SPINNER, N:BUMPER, E:LAUNCH, E:CALLHAMMER, E:HAMMERCHASE, E:CATAPULTBOTTOM
15 Impossible 0x424C20 0x4D21C0 0x418360 ImpossibleCollisionEvents N:BOUNCE, N:ONROTATOR, N:ONGEAR, E:HELPINERTIA, E:UNHELPINERTIA

Arena (Rumble) Board Handlers (15)

# Arena Level Constructor Vtable vtable[0x1D] Handler Name Events
1 Warmup Arena 0x4224A0 0x4D1428 0x413BD0 SinkPlatformArenaCollisionEvents (shared) DN:SINKPLATFORM only
2 Beginner Arena 0x422550 0x4D14F0 0x413DF0 BeginnerArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
3 Intermediate Arena 0x4226E0 0x4D15C0 0x413BD0 SinkPlatformArenaCollisionEvents (shared) DN:SINKPLATFORM only
4 Dizzy Arena 0x422790 0x4D1680 0x414350 DizzyArenaCollisionEvents N:SWIRL, DN:SINKPLATFORM
5 Tower Arena 0x4228C0 0x4D1740 0x414570 TowerArenaCollisionEvents E:CATAPULTBOTTOM, DN:SINKPLATFORM
6 Up Arena 0x422B10 0x4D17F8 0x413BD0 SinkPlatformArenaCollisionEvents (shared) DN:SINKPLATFORM only
7 Neon Arena 0x424860 0x4D1EC8 0x417490 NeonArenaCollisionEvents (NEW) N:BUMP, DN:SINKPLATFORM
8 Expert Arena 0x423060 0x4D18C8 0x413BD0 SinkPlatformArenaCollisionEvents (shared) DN:SINKPLATFORM only
9 Odd Arena 0x423220 0x4D1980 0x414DA0 OddArenaCollisionEvents E:GRAVITY(TYPE), DN:SINKPLATFORM
10 Toob Arena 0x4234E0 0x4D1A40 0x415010 ToobArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
11 Wobbly Arena 0x423690 0x4D1B18 0x415540 WobblyArenaCollisionEvents N:SQUAREWOBBLY, DN:SINKPLATFORM
12 Glass Arena 0x424B60 0x4D2048 0x417EB0 GlassArenaCollisionEvents (NEW) N:GLASS, DN:SINKPLATFORM
13 Sky Arena 0x423BF0 0x4D1BD8 0x413BD0 SinkPlatformArenaCollisionEvents (shared) DN:SINKPLATFORM only
14 Master Arena 0x424380 0x4D1C80 0x416140 WarmupArenaCollisionEvents E:LAUNCH, DN:SINKPLATFORM
15 Impossible Arena 0x424EC0 0x4D2298 0x418600 ImpossibleArenaCollisionEvents N:BOUNCE, DN:SINKPLATFORM

Corrections from Previous Documentation

1. Sky vs Neon were combined into one entry

WRONG: Old docs had "Sky/Neon | 0x4D0FC8 | NeonCollisionEvents" as a single entry.
CORRECT: Sky Race (0x4D0FC8) and Neon Race (0x4D1DF0) are separate boards with separate handlers:

  • Sky Race → 0x410D00 (SkyCollisionEvents, was mislabelled "NeonCollisionEvents")
  • Neon Race → 0x416CA0 (NeonRaceCollisionEvents, was COMPLETELY MISSING)

2. Neon Arena handler was missing

WRONG: Old docs had "Sky/Neon Arena | 0x4D1BD8 | SinkPlatformArenaCollisionEvents" combining both.
CORRECT: Sky Arena (0x4D1BD8) uses the shared handler, but Neon Arena (0x4D1EC8) has its OWN handler:

  • Sky Arena → 0x413BD0 (SinkPlatformArenaCollisionEvents, shared) ✓ (was correct)
  • Neon Arena → 0x417490 (NeonArenaCollisionEvents, was COMPLETELY MISSING)

3. Glass Race handler was missing

WRONG: Old docs only listed "Glass | 0x4D2048 | GlassCollisionEvents" for the Glass Arena vtable.
CORRECT: Glass Race and Glass Arena are separate boards:

  • Glass Race → vtable 0x4D1F90 → handler 0x417760 (GlassRaceCollisionEvents, was MISSING)
  • Glass Arena → vtable 0x4D2048 → handler 0x417EB0 (GlassArenaCollisionEvents, was listed but as "GlassCollisionEvents")

4. 0x410D00 was mislabelled

WRONG: Called "NeonCollisionEvents" in old docs.
CORRECT: It is the Sky Race handler (LevelBoard_Sky_ctor at 0x41F930 sets vtable 0x4D0FC8, whose vtable[0x1D] = 0x410D00). Renamed to SkyCollisionEvents.

5. Impossible Race/Arena were correct (no change needed)

Old docs correctly had:

  • Impossible Race → 0x418360 ✓
  • Impossible Arena → 0x418600 ✓

6. REF_LOADING_SYSTEM.md constructor tables were swapped

WRONG: The doc labelled 0x422xxx constructors as "Race Board System" and 0x41Cxxx as "Arena Board System".
CORRECT: It's the OPPOSITE:

  • 0x41Cxxx constructors (0x41CA40–0x41F930) create RACE boards ("Board (X)" + "X RACE")
  • 0x422xxx constructors (0x4224A0–0x424EC0) create ARENA boards ("ArenaBoard (X)" + "X ARENA")
    Verified by decompiling all 30 constructors and reading the board name strings.

4 Newly Discovered Handlers — Event Details

NeonRaceCollisionEvents @ 0x416CA0

__thiscall void(void *board, int *ball, int *collPair) — Neon Race board only.

Event Action
N:NEONPLATFORM Lifter_PlaySound on colliding object
E:ZOOP Sound_Play3D + cooldown (ball+0x7F0=100)
E:LIGHTSOFF Sound + vtable4 on neon platform + Scene_RegisterObject + AthenaList_Append to board+0x2578 (update list) + increment light-off counter at board+0x4390
E:LIGHTSON Sound + vtable4 on neon platform + Scene_RegisterObject + decrement light-off counter; when counter reaches 0, set state=2 at mesh+0x10DC

Falls through to: DispatchCollisionEvents (master collision dispatcher)

NeonArenaCollisionEvents @ 0x417490

__thiscall void(void *board, int *ball, int *collPair) — Neon Arena board only.

Event Action
N:BUMP Bumper kick: reads velocity from physics_obj+0xCA4, scales by _DAT_004CF55C. If too slow: normalize to 5.0. If fast enough: normalize to 8.0 (different from Master's 12.0!). Writes back to physics velocity.
DN:SINKPLATFORM Scene_StartCountdown (arena sink)

Falls through to: DispatchCollisionEvents (master collision dispatcher)

GlassRaceCollisionEvents @ 0x417760

__thiscall void(void *board, int *ball, int *collPair) — Glass Race board only.

Event Action
N:GLASS Sets ball+0xCDC = 15 (glass break effect counter, byte offset 0xCDC = DWORD index 0x317)
N:TENBONUS1 Speed-gated time bonus. If ball speed > threshold (_DAT_004D0178) and not already triggered (board+0x438C==0): +1000 score (App+0x5EC + player_idx*0xA0), "EXTRA TIME:" ScoreObject popup, Sound_Play3D. Uses board+0x436C/4370/4374 for sound position.
N:TENBONUS2 Same as TENBONUS1 but uses board+0x4378/437C/4380 for position and board+0x438D for triggered flag

Falls through to: DispatchCollisionEvents (master collision dispatcher)

GlassArenaCollisionEvents @ 0x417EB0

__thiscall void(void *board, int *ball, int *collPair) — Glass Arena board only.

Event Action
N:GLASS Sets ball+0xCDC = 15 (glass break effect counter)
DN:SINKPLATFORM Scene_StartCountdown (arena sink)

Falls through to: DispatchCollisionEvents (master collision dispatcher)

Shared Handler Summary

Handler Address Shared By
DispatchCollisionEvents 0x40C5D0 WarmUp Race (base, no override)
SinkPlatformArenaCollisionEvents 0x413BD0 Warmup Arena, Intermediate Arena, Up Arena, Expert Arena, Sky Arena
MasterCollisionEvents 0x412850 Master Race only (NOT shared with Master Arena — arena uses WarmupArenaCollisionEvents)

Note: Master Arena (0x4D1C80) uses WarmupArenaCollisionEvents (0x416140), NOT MasterCollisionEvents. Previous docs incorrectly claimed "Master Arena reuses the exact same handler as the Master race board." This was WRONG.

New Event Names Discovered

These event names were NOT in previous documentation:

Event Handler Description
N:NEONPLATFORM NeonRaceCollisionEvents Neon platform interaction sound
E:ZOOP NeonRaceCollisionEvents 3D sound effect with cooldown
E:LIGHTSOFF NeonRaceCollisionEvents Turn off neon platform lights
E:LIGHTSON NeonRaceCollisionEvents Turn on neon platform lights
N:BUMP NeonArenaCollisionEvents Arena bumper (scale 5.0/8.0, different from N:BUMPER)
N:TENBONUS1 GlassRaceCollisionEvents Speed-gated +1000 time bonus (position 1)
N:TENBONUS2 GlassRaceCollisionEvents Speed-gated +1000 time bonus (position 2)

DispatchCollisionEvents — Common Fallthrough

All 4 new handlers fall through to DispatchCollisionEvents (the master dispatcher at 0x40C5D0).
DispatchCollisionEvents is the master collision event dispatcher — it parses
<TIME>value</TIME> XML tags and calls Ball_DizzyImmunity. This is a subset of
DispatchCollisionEvents that only handles the E:NODIZZY event.

Implication: The 4 new handlers do NOT call DispatchCollisionEvents as fallthrough.
They only call DispatchCollisionEvents. This means universal events (N:GOAL, N:TARPIT, E:JUMP,
etc.) do NOT fire on Neon Race, Neon Arena, Glass Race, or Glass Arena boards unless
DispatchCollisionEvents is also called. However, since DispatchCollisionEvents is a separate
function that only handles E:NODIZZY, these boards may handle universal events
elsewhere in their update pipeline, or they may simply not support them.

Wait — correction: Looking more carefully at the decompilation, the function name
The function was previously mislabeled "DispatchCollisionEvents" in Ghidra. It is actually DispatchCollisionEvents
(0x40C5D0) — the decompiler resolved the call target to a thunk/mislabel. The function
at the call target IS DispatchCollisionEvents. So all 4 handlers DO fall through to
DispatchCollisionEvents as expected.

Verification Methodology

  1. Decompiled all 30 board constructors (15 race + 15 arena) to extract vtable addresses
  2. Read board name strings ("Board (X)" / "ArenaBoard (X)" + "X RACE" / "X ARENA") to verify race vs arena
  3. Read vtable[0x1D] (offset +0x74) for all 30 vtables via GhidraMCP read_memory
  4. Created functions for 4 unknown addresses via create_function API
  5. Decompiled all 4 new handlers via GhidraMCP
  6. Renamed all 5 functions (4 new + 1 corrected) via rename_function_by_address
  7. Verified event names via __strnicmp calls in decompiled code
  8. Cross-referenced with MESHWORLD event string extraction

All data verified June 2026 from Hamsterball.exe V3.6.c (md5=7d25019366b8d7f55906325bd630d7fe).


🔗 Related Documents

Collision Hook

types : tools
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/tools/collision_hook/README.md)


Hamsterball Collision Hook

What This Does

Hooks three collision dispatch functions in Hamsterball.exe to log all collision events:

  1. DispatchCollisionEvents (0x0040C5D0) — shared base handler, processes ALL event types
  2. TowerCollisionEvents (0x0040DCD0) — Tower board events (catapults, trapdoors, maces)
  3. ExpertCollisionEvents (0x0040E6A0) — Expert board events (hammers, saws, judges, bells)

Files

File Description
<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>.dll The hook DLL — inject into Hamsterball.exe
injector.exe Standalone DLL injector (finds Hamsterball.exe automatically)
<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>.cfg Config file — enable/disable hooks, set filters
collision_log.csv Output log (created automatically when game runs)

Quick Start

  1. Copy all files into your Hamsterball game directory (next to Hamsterball.exe)
  2. Launch Hamsterball.exe normally
  3. Run injector.exe (double-click or command line)
  4. Play the game — collision events are logged to collision_log.csv
  5. Check the CSV in any spreadsheet editor or text editor

Config File (collision_hook.cfg)

# Enable/disable individual hooks (1=on, 0=off)
hook_DispatchCollisionEvents=1
hook_Level=1
hook_Arena=1

# Log options
log_event_names=1
log_ball_pos=1
log_timestamps=1

# Filter: only log events matching this substring (empty = log all)
# Examples: filter=E:JUMP  or  filter=N:GOAL  or  filter= (empty = all)
filter=

CSV Output Format

timestamp_ms,handler,scene_ptr,ball_ptr,collobj_ptr,event_name,x,y,z
  • timestamp_ms — milliseconds since Windows boot (GetTickCount)
  • handler — which function was called (DispatchCollisionEvents/TowerCollisionEvents/ExpertCollisionEvents)
  • scene_ptr — pointer to the Scene/BoardLevel object (ECX/this)
  • ball_ptr — pointer to the [[39040799821588|ball object]]
  • collobj_ptr — pointer to the collision pair array
  • event_name — the event string from the collision object (e.g. "E:JUMP", "N:GOAL")
  • x,y,z — ball position at time of collision

How It Works

The DLL installs inline hooks (jmp detours) at the entry point of each target
function. When the game calls a collision handler, our hook intercepts the call,
logs the event details, then forwards to the original function via a trampoline.

Hamsterball.exe calls DispatchCollisionEvents(this, ball, collObj)
  → jmp to hook_DispatchCollisionEvents
    → log event to CSV
    → call original via trampoline
      → execute relocated original bytes
      → jmp back to DispatchCollisionEvents+5

Technical Details

  • Target addresses (image base 0x400000):
    • DispatchCollisionEvents: VA 0x0040C5D0
    • TowerCollisionEvents: VA 0x0040DCD0
    • ExpertCollisionEvents: VA 0x0040E6A0
  • Calling convention: __thiscall (ECX = this/Scene pointer, stack = ball, collObj)
  • ASLR: The DLL computes the actual address by adding the module base offset
  • Trampoline: 5-byte original prologue + jmp back to original+5

Safety

  • Hooks are read-only — they only log events, never modify game behavior
  • Hooks are cleanly removed on DLL unload (DLL_PROCESS_DETACH)
  • All pointer reads use SEH (__try/__except) to prevent crashes from bad pointers
  • The injector uses standard CreateRemoteThread + LoadLibrary technique

Building from Source

# DLL
i686-w64-mingw32-gcc -shared -o collision_hook.dll collision_hook.c \
    -Wl,--enable-stdcall-fixup -Wl,--out-implib,collision_hook.lib

# Injector
i686-w64-mingw32-gcc -o injector.exe injector.c

Limitations

  • Does not modify game behavior (logging only). For actual modding, extend
    my_DispatchCollisionEvents_handler etc. to modify parameters or return values.
  • Inline hook assumes first 5 bytes of target function are complete instructions
    (no instruction boundary split at byte 5). Verified for Hamsterball.exe.
  • Requires Windows (32-bit process). Won't work under Wine (needs SEH + inline hooks).

🔗 Related Documents

Collision System

types : physics
keywords :

📂 View source on GitHub


Hamsterball Collision System

Overview

The collision system uses a spatial tree (octree) for broad-phase culling and
AABB-triangle intersection tests for narrow-phase detection. It is the core of
ball physics — every frame, the ball's position is advanced and tested against
mesh triangles via Ball_AdvancePositionOrCollision.

Architecture

Ball_Update
  └─ Ball_CollisionCheck (0x402DE0) — per-frame entry
       └─ Mesh_FindClosestCollision (0x465D90) — find closest hit point
            ├─ CollisionMesh_ctor — build collision mesh
            ├─ SpatialTree_ctor — build octree
            ├─ CollisionMesh_AddTriangle — populate tree with triangles
            └─ Ball_AdvancePositionOrCollision (0x4564C0) — advance + collide
                 ├─ Collision_TraverseSpatialTree (0x465EF0) — tree walk
                 │    └─ AABB_ContainsPoint (0x4580D0) — point-in-box test
                 └─ vtable[0x1C] callback — gravity-aware collision response

CollisionMesh (0xCB4 bytes, vtable 0x4D8E10)

Constructor (0x456D80)

CollisionMesh_ctor(this, param_1) {
    this->vtable = &Mesh_DeletingDtor;    // +0x00
    this->ref_count = param_1;            // +0x10
    AthenaList_Init(&this->triangles, 0); // +0x18
    AthenaList_Init(&this->materials, 0); // +0x430
    AthenaList_Init(&this->vertices, 0); // +0x848
    this->accumulated_distance = 0;       // +0xC74
    this->position = {0,0,0};             // +0xCA4/CA8/CAC
    Ball_InitBattleMode(this);            // +0x00 (vtable call)
}

Key Offsets

Offset Type Description
+0x00 void** Vtable (0x4D8E10)
+0x10 int Reference count / parent
+0x14 byte Has trail data flag
+0x18 AthenaList Triangle list
+0x1C int Triangle count
+0x430 AthenaList Material list
+0x424 void** Triangle data array
+0x848 AthenaList Vertex normal list
+0xC54 void** Vertex data array
+0xC64 float Scale factor (friction)
+0xC68 float Secondary scale
+0xC6C float Gravity callback scale
+0xC70 float Max distance threshold
+0xC74 float Accumulated distance (reset on collision)
+0xC7C byte Use gravity callback flag
+0xC8C float Gravity direction X
+0xC90 float Gravity direction Y
+0xC94 float Gravity direction Z
+0xCA4 Vec3 Position (X, Y, Z) — accumulated from velocity

SpatialTree (Octree, vtable 0x4D9038)

Constructor (0x463330)

SpatialTree_ctor(this, mesh_list) {
    CollisionNode_BaseInit(this, mesh_list);
    this->vtable = &SpatialTree_DeletingDtor; // +0x00
    this->scale = 0.1f;           // +0x0C (3DCCCCCDh)
    this->max_depth = 6;          // +0x10
    this->min_extent = 0.9f;      // +0x14 (3F666666h)
    this->flags = 0;              // +0x18/19
    this->enable_x = 1;           // +0x1A
    this->enable_y = 1;           // +0x1B
    this->enable_z = 1;           // +0x1C
    this->enable_sub1 = 1;        // +0x1D
    this->enable_sub2 = 1;        // +0x1E
}

Key Parameters

Offset Value Description
+0x0C 0.1f Octree node scale
+0x10 6 Maximum tree depth
+0x14 0.9f Minimum node extent

Spatial Tree Traversal (0x465EF0)

Collision_TraverseSpatialTree(this, point, result_list) does a 2-level traversal:

  1. Level 1: Recurse through child nodes of the octree

    • Iterate this->children[0..count-1] at this+0x424
    • Uses AthenaList_NextIndex(this+0x18) for iteration tracking
  2. Level 2: For each leaf, iterate mesh buffers

    • Access this->mesh_data at this+0x08
    • Iterate mesh buffer entries at mesh_data+0x438
  3. Per-triangle test: For each triangle vertex group (3 vertices × 8 floats)

    • Call AABB_ContainsPoint(point, vx, vy, vz) per vertex
    • If contained, AthenaList_Append(result_list, vertex_ptr)
    • Groups processed 4 at a time (iVar5 = 3, counting down)

This means: walk octree → for each leaf → for each mesh buffer → for each
vertex group → test 3 vertices against AABB → collect matching triangles.

AABB_ContainsPoint (0x4580D0)

Tests if point (x, y, z) is inside axis-aligned bounding box. AABB layout:

+0x00: float min_x
+0x04: float min_y
+0x08: float min_z
+0x0C: float max_x
+0x10: float max_y
+0x14: float max_z

Returns:

  • Low byte = 1 if point is inside all 6 planes
  • Bit 8 = set if a test failed (point < min for any axis)
  • Bit 10 = set if NaN detected
  • Bit 14 = set if point == boundary (exact edge hit)

Collision_InitDefaultAABB (0x458000)

Creates a massive bounding box for initial broad-phase test:

// 0x4B18967F ≈ 10,000,000.0f
// 0xCB18967F ≈ -10,000,000.0f
AABB = { min_x=1e7, min_y=1e7, min_z=1e7,
         max_x=-1e7, max_y=-1e7, max_z=-1e7 }

AABB_TriangleIntersect2 (0x4583F0)

Double test — calls AABB_TriangleTest6Edges twice. The second call may test
with modified parameters (separating axis theorem — test both AABB edges and
triangle edges as potential separating axes).

Ball_AdvancePositionOrCollision (0x4564C0) — Core Physics

Signature: Ball_AdvancePositionOrCollision(this, out_pos, start_pos, direction, gravity_accel, dt)

Algorithm

  1. Clear trail data: If this+0x14 != 0, free all entries in this+0x18 list
  2. Clear vertex normals: Free all entries in this+0x848 list
  3. Apply direction to position: If direction != (0,0,0):
    • Compute current and new position magnitude
    • If new distance > max_distance (this+0xC70): normalize and clamp
  4. Apply friction: position *= (1.0 - dt) + (1.0 - this+0xC68) * dt
  5. Collision callback or direct offset:
    • If this+0xC7C == 0: out = position + start_pos (simple translation)
    • If this+0xC7C != 0: call vtable[0x1C](out, start, dir, accel, dt, &hit) — gravity-aware
  6. Apply gravity: accumulate gravity vector scaled by dt × gravity_scale
  7. Update distance counter:
    • On no hit: this+0xC74 += dt and optionally log trail entry
    • On hit: this+0xC74 = 0 (collision resets accumulated distance)
  8. Write output position

Key Physics Constants

Address Value Description
0x4CF310 1.0 Identity friction base
0x4CF368 0.0 Zero comparison threshold
0x4CF3F0 (varies) Gravity scaling factor

Mesh_FindClosestCollision (0x465D90)

High-level collision query. Creates a temporary collision mesh, builds a
spatial tree, then traces a ray from start position in a direction to find
the closest triangle intersection.

1. AthenaList_Init(mesh_list, 0)
2. AthenaList_Append(mesh_list, this)      // add self
3. CollisionMesh_ctor(collision_mesh, 0)
4. SpatialTree_ctor(spatial_tree, mesh_list)
5. CollisionMesh_AddTriangle(collision_mesh, spatial_tree)
6. Vec3_NormalizeAndScale(direction, 99999.0)  // extend ray very far
7. Ball_AdvancePositionOrCollision(collision_mesh, out, start, dir, gravity, 0.01)
8. SpatialTree_Free(spatial_tree)
9. Mesh_Clear(collision_mesh)
10. Vec3List_Free(mesh_list)
11. return out_position

The time step is hardcoded to 0.01 and the ray extends to 99999.0 units,
making this effectively a raycast from start→direction finding the nearest
triangle surface.

Collision_GradientEval_Stub (0x458190)

Empty stub — return;. Called from Ball_Update and Gear_AdvanceAlongPath
as a no-op placeholder for gradient evaluation (unused feature).

Integration with Ball Physics

The collision system is called from Ball_Update (0x405190) every frame:

  1. Ball computes desired new position from velocity + forces
  2. Ball_CollisionCheck tests the mesh via Mesh_FindClosestCollision
  3. Ball_AdvancePositionOrCollision updates position, detects collision
  4. On collision: velocity zeroed, accumulated distance reset, material tracked
  5. On no collision: advance freely, accumulate trail data

Collision Event Dispatch System

The collision system dispatches game events when the ball hits specific object types.
There are two dispatchers: TowerCollisionEvents (for level objects) and ExpertCollisionEvents (for ArenaBoard arena objects). Both are vtable-driven — the scene's vtable determines which handler runs. Both delegates to DispatchCollisionEvents (0x40C5D0) as a shared base handler for universal events (E:JUMP, N:GOAL, E:BREAK, etc.).

Note: Ball_AdvancePositionOrCollision (0x4564C0) handles only geometric collision detection (velocity integration, mesh intersection via CollisionLevel->vtable[0x1C], max-speed clamping). It does NOT dispatch event-name-based collision events. The event dispatch is triggered separately from the ball update chain (ball->vtable[0x10], called by Scene_UpdateBallsAndState).

TowerCollisionEvents (0x40DCD0) — Level Objects

String-based dispatch on collider type name (collider+0x864):

Type Name Behavior
"E:CATAPULTBOTTOM" Catapult_Launch if ball cooldown < 1, play catapult sound
"E:OPENSESAME" Trapdoor_Open on first door in list
"N:TRAPDOOR" Trapdoor_Activate on doors matching collider ID
"E:BITE" Set damage=25.0 (damage_amount+0x43A0, damage_timer+0x43A8)
"E:MACETRIGGER" Set active=1 for all maces in mace list
"N:MACE" Ball bounce callback (vtable[0x20]) if mace is moving

ExpertCollisionEvents (0x40E6A0) — ArenaBoard Arena Objects

Type Name Behavior
"E:CALLHAMMER" CreateBonkPopup (tournament only)
"E:HAMMERCHASE" Hammer_ChaseStart (tournament only)
"E:ALERTSAW1"/"ALERTSAW2" Saw_AlertActivate (warning phase)
"E:ACTIVATESAW1"/"ACTIVATESAW2" Saw_Activate (full activation)
"E:ALERTJUDGES" Judge_Reset for all judges
"E:SCORE<time>" ScoreDisplay_SetTime parsed from suffix
"E:JUMP" Jump pad: cooldown=10, vert_vel=0.008, vert_vel_on=1, play sound
"E:BELL<suffix>" Bell_Activate, add 500 bonus time if not playing

Level_LoadCollision (0x65260) — Collision File Format

Binary .COL file format loaded at level startup:

Header: 24 bytes into MeshWorld+0x45C (transform/flags)
int32: sublevel_count

If sublevel_count < 1 (single-mesh mode):
  int32: object_count
  For each object:
    CreateMeshBuffer(0x874 bytes)
    int32: name_length
    char[name_length]: name string ("N:"=interactive, "E:"=event)
    int32: face_count
    For each face (0x60 bytes each):
      9 floats: v0, v1, v2 positions (3 floats each)
      Compute normal via cross product, normalize via SIMD
      Same normal stored for all 3 verts (flat shading)

If sublevel_count >= 1 (multi-level mode):
  scene->is_sprite_mode = 1
  For each sublevel: Level_ctor + vtable[0x60](file) recursive load

CollisionFace Structure (0x60 bytes)

Offset Field Description
+0x00 v0.x Vertex 0 X
+0x04 v0.y Vertex 0 Y
+0x08 v0.z Vertex 0 Z
+0x0C n0.x Face normal X (flat, same for all verts)
+0x10 n0.y Face normal Y
+0x14 n0.z Face normal Z
+0x18 v1.x Vertex 1 X
+0x1C v1.y Vertex 1 Y
+0x20 v1.z Vertex 1 Z
+0x24 n1.x Face normal X (same as n0)
+0x28 n1.y Face normal Y
+0x2C n1.z Face normal Z
+0x30 v2.x Vertex 2 X
+0x34 v2.y Vertex 2 Y
+0x38 v2.z Vertex 2 Z
+0x3C n2.x Face normal X (same as n0)
+0x40 n2.y Face normal Y
+0x44 n2.z Face normal Z

MeshBuffer Structure (0x874 bytes)

Offset Field Description
+0x000 vtable CollisionMesh vtable
+0x00C AthenaList Face list
+0x217 byte render_flag (=0)
+0x85D byte interactive_flag (1 for N:/E: prefixes)
+0x863 byte no_render_flag (1 for E: prefix only)
+0x864 char* Type name string ("N:WALL", "E:CATAPULTBOTTOM", etc.)
  1. Vtable[0x1C] callback handles gravity-plane-aware collision (tilted/flat modes)

🔗 Related Documents

collision_hook

types : mods

📂 View source on GitHub


collision_hook

Hooks collision detection for debugging

Files

  • collision_hook.c — C source code
  • collision_hook.dll — Compiled DLL (PE32 i386)

Proxy Type

Standalone DLL (not a bass proxy). Use an DLL injector or launcher tool.


🔗 Related Documents

CollisionMesh (Physics Object)

types : physics
keywords :

📂 View source on GitHub


CollisionMesh (Physics Object) — Complete Modder's Reference

Parameter of Ball_Update @ 0x00405E00 (accessed via Ball + 0x1A4).
All offsets below verified via live Ghidra decompilation of Hamsterball.exe.
Confidence markers: ✅ = Verified in raw decompiled C (2+ functions), ⚠️ = Verified in 1 function, ❓ = Inferred from struct layout.


What Is the CollisionMesh?

The CollisionMesh is NOT the Ball itself. It is a separate nested physics object allocated during Ball_ctor2 and pointed to by the Ball at offset +0x1A4:

Ball + 0x1A4 → CollisionMesh* (heap object, 0xCB0 bytes)

Ball_Update (0x405E00) is the function that reads this pointer and integrates all physics state. If you want to modify how the ball moves in the world — speed, gravity, friction, jumping — this is the object you modify, not the ephemeral Ball + 0x170 velocity accumulator (which gets zeroed every frame).

Getting the Pointer

// Method 1: From any Ball pointer
int* ball = (int*)0x...;  // your Ball* hook parameter
int* phys = *(int**)((char*)ball + 0x1A4);

// Method 2: From the Ball_Update hook (0x405E00)
// param_1 IS the Ball*, so:
int* phys = *(int**)((char*)param_1 + 0x1A4);

Full CollisionMesh Struct Layout

The collisionmesh object is 0xCB0 bytes (3248). It inherits from SceneObject (base vtable + refcount), then adds physics fields.

Offset Type Field Default Verified By Modding Use
+0x000 void** vtable 0x4D8E10 CollisionMesh_ctor (0x405680) Call virtual methods
+0x004 int ref_count 1 CollisionMesh_ctor Reference counting
+0x010 Ball* owner_ball this (Ball*) CollisionMesh_ctor Back-pointer to parent
+0x014 (SceneObject base) ✅ Inheritance
+0xC60 int battle_mode 3 Ball_InitBattleMode Arena mode flag
+0xC64 float roll_friction computed Ball_Update Phase 14 ⚠️ OVERWRITTEN EVERY FRAME
+0xC68 float friction_damping ~0.56 Ball_InitBattleMode Ground friction multiplier
+0xC6C float field_c6c 1.0f Ball_InitBattleMode Unknown scaler
+0xC70 float max_speed_limit 1000.0f Ball_InitBattleMode Hard speed cap
+0xC74 int field_c74 0 CollisionMesh_ctor Unused?
+0xC78 float gravity_strength 25.0f Ball_InitBattleMode Gravity pull strength
+0xC7C byte use_gravity 1 Ball_InitBattleMode 0 = no gravity
+0xC80 (padding) ❓ Alignment
+0xC8C float dir_x 0 Ball_InitBattleMode Physics up-vector X
+0xC90 float dir_y -1.0f Ball_InitBattleMode Physics up-vector Y
+0xC94 float dir_z 0 Ball_InitBattleMode Physics up-vector Z
+0xC98 float scaled_dir_x CollisionMesh_SetSpeed NEVER READ by engine
+0xC9C float scaled_dir_y CollisionMesh_SetSpeed NEVER READ by engine
+0xCA0 float scaled_dir_z CollisionMesh_SetSpeed NEVER READ by engine
+0xCA4 float vel_x 0 Ball_AdvancePositionOrCollision Persistent physics X velocity
+0xCA8 float vel_y 0 Ball_AdvancePositionOrCollision Persistent physics Y velocity — JUMP MOD TARGET
+0xCAC float vel_z 0 Ball_AdvancePositionOrCollision Persistent physics Z velocity

Total size: 0xCB0 bytes (3248).
Constructor: CollisionMesh_ctor @ 0x405680 allocates via operator_new(0xCB0).
Vtable: 0x4D8E10 (inherits SceneObject methods + physics overrides).


Two Velocity Systems (CRITICAL)

There are two separate velocity systems in Hamsterball. Writing to the wrong one has no lasting effect.

System Address Type Persistence Who Clears It
Ball input velocity Ball + 0x170/174/178 float[3] Ephemeral — cleared every frame Ball_Update Phase 1 zeros these after reading
CollisionMesh physics velocity CollisionMesh + 0xCA4/CA8/CAC float[3] Persistent — survives frames Integrated by Ball_AdvancePositionOrCollision (0x4564C0)

Modding rule: If you write to Ball + 0x174 (vel_y), it gets overwritten to 0 within one frame. If you write to *(Ball+0x1A4) + 0xCA8 (CollisionMesh.vel_y), the physics integrator uses it on the next tick and the ball actually moves.


Verified Functions

CollisionMesh Lifecycle

Address Name Role Verified
0x405680 CollisionMesh_ctor Allocates 0xCB0 bytes, stores Ball* at +0x10, sets vtable to 0x4D8E10
0x4056D0 Ball_InitBattleMode Sets default physics params (gravity=25.0f, speed limit=1000.0f)
0x4030B0 Ball_ResetCollisionMesh Resets direction to -1.0f Y, clears impact state

Physics Integration

Address Name Role Verified
0x405E00 Ball_Update Main physics tick — reads CollisionMesh, integrates, writes back. Phase 14 overwrites roll_friction every frame.
0x4564C0 Ball_AdvancePositionOrCollision Reads vel_x/y/z (+0xCA4/CA8/CAC), applies gravity, does collision broad phase, writes updated position back to Ball + 0x164/168/16C
0x402650 Ball_ApplyForce Accumulates force into Ball's ephemeral velocity (+0x170), NOT CollisionMesh

Direct Field Access

Address Name Role Verified
0x402A20 Ball_SetVec3AtOffset Direct overwrite of CollisionMesh + 0xCA4/CA8/CAC (velocity)
0x4029C0 CollisionMesh_SetSpeed ⚠️ DEAD CODE — writes +0xC64 (roll_friction, overwritten next frame) and +0xC98/C9C/CA0 (never read). No lasting effect.

Modding Recipes

Recipe 1: The Jump Mod (Verified Working)

This is how the community jump mod works. It injects upward velocity directly into the CollisionMesh, bypassing the ephemeral Ball velocity:

void InjectJump(void* ball, float jump_power) {
    // Get the CollisionMesh pointer from the Ball
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    // Write directly to persistent physics velocity
    *(float*)((char*)phys + 0xCA8) = jump_power;  // vel_y = upward impulse

    // Optional: also zero X/Z velocity for clean vertical jump
    // *(float*)((char*)phys + 0xCA4) = 0.0f;
    // *(float*)((char*)phys + 0xCAC) = 0.0f;
}

Why this works: Ball_AdvancePositionOrCollision reads +0xCA8 on the next tick, adds gravity (gravity_strength * dt), runs collision against the level mesh, and updates Ball->pos accordingly. The velocity persists until the ball hits a surface or Ball_ResetCollisionMesh is called.

Recipe 2: Super Speed (Modify Speed Limit)

void SetSuperSpeed(void* ball, float new_limit) {
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    // Raise the hard speed cap
    *(float*)((char*)phys + 0xC70) = new_limit;  // max_speed_limit
}

Note: The engine clamps speed every frame. Raising max_speed_limit allows higher speeds without the clamp kicking in. You still need to inject velocity (Recipe 1 or ApplyForce) to actually go fast.

Recipe 3: Disable Gravity

void DisableGravity(void* ball) {
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    *(char*)((char*)phys + 0xC7C) = 0;  // use_gravity = false
}

Effect: Ball_AdvancePositionOrCollision skips gravity application. The ball stops falling and maintains whatever velocity you injected. Combine with a small constant upward velocity for hover/fly mode.

Recipe 4: Low Friction / Ice Mode

void SetLowFriction(void* ball, float friction) {
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    *(float*)((char*)phys + 0xC68) = friction;  // friction_damping
}

Default: ~0.56. Set to 0.01 for ice physics (ball slides forever). Set to 2.0 for sticky mud.

⚠️ WARNING: Ball_Update Phase 14 also computes and writes roll_friction (+0xC64) every single frame:

roll_friction = (radius * 0.98 * friction_damping * speed_scale) / 0.96

So modifying +0xC64 directly is futile — it gets overwritten. Always modify friction_damping (+0xC68) instead.

Recipe 5: Reverse Gravity

void ReverseGravity(void* ball) {
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    *(float*)((char*)phys + 0xC90) = 1.0f;  // dir_y = +1.0 (up is down)
}

Effect: The physics integrator uses dir as the "up" vector for gravity application. -1.0f = normal gravity (pulls toward -Y). +1.0f = reverse gravity (pulls toward +Y). You can set any normalized vector for weird directional gravity.

Recipe 6: Instant Stop

void InstantStop(void* ball) {
    int* phys = *(int**)((char*)ball + 0x1A4);
    if (!phys) return;

    *(float*)((char*)phys + 0xCA4) = 0.0f;  // vel_x = 0
    *(float*)((char*)phys + 0xCA8) = 0.0f;  // vel_y = 0
    *(float*)((char*)phys + 0xCAC) = 0.0f;  // vel_z = 0
}

Effect: Zeroes all persistent physics velocity. The ball stops moving instantly (no inertia). Useful for teleportation or checkpoint resets.


⚠️ DEAD CODE WARNING: CollisionMesh_SetSpeed

Address: 0x4029C0
What it does:

void CollisionMesh_SetSpeed(void* this, float speed) {
    *(float*)((char*)this + 0xC64) = speed;              // roll_friction
    *(float*)((char*)this + 0xC98) = speed * dir_x;      // scaled_dir_x
    *(float*)((char*)this + 0xC9C) = speed * dir_y;      // scaled_dir_y
    *(float*)((char*)this + 0xCA0) = speed * dir_z;      // scaled_dir_z
}

Why it does nothing:

  1. +0xC64 (roll_friction) is recomputed and overwritten every frame by Ball_Update Phase 14.
  2. +0xC98/C9C/CA0 (scaled_dir) are never read by any engine function. They are sinkholes.

Historical naming note: This was originally called Ball_SetSpeed in early RE because it was found through Ball cross-references. The decompilation reveals it accesses CollisionMesh offsets on this, proving it's a CollisionMesh method, not a Ball method. Do not call this function expecting speed changes.


Ball_Update Execution Order (0x405E00)

Understanding the tick order helps you know WHEN to inject modifications:

Phase 1:  Decay timers (ice effect, lerp factors)
Phase 2:  Read ephemeral Ball velocity (+0x170) into locals, then ZERO it
Phase 3:  Read input force from keyboard/joystick
Phase 4:  Apply force multipliers (tilted gravity, speed scale)
Phase 5:  Build spatial collision tree
Phase 6:  Apply gravity to CollisionMesh velocity (+0xCA8)
Phase 7:  Integrate position from CollisionMesh velocity
Phase 8:  Collision detection (broad phase → narrow phase)
Phase 9:  Collision response (bounce, friction, normal reflection)
Phase 10: Write updated position back to Ball->pos (+0x164)
Phase 11: Write updated CollisionMesh velocity back (+0xCA4/CA8/CAC)
Phase 12: Trail particle spawn
Phase 13: Audio state update
Phase 14: Compute roll_friction = (radius * 0.98 * friction_damping * speed_scale) / 0.96
          → WRITE to CollisionMesh + 0xC64 (overwrites any external value)
Phase 15: Check out-of-bounds, respawn if needed

Injection timing:

  • Before Phase 6: Your velocity gets gravity applied → good for jump
  • After Phase 11: Your velocity overwrites the integrated result → good for instant stop
  • Phase 14: roll_friction is always overwritten — never write to +0xC64 directly

Cross-Reference: Ball ↔ CollisionMesh

What you want Ball offset CollisionMesh offset
Position (read/write) +0x164/168/16C N/A (position is in Ball)
Ephemeral input velocity +0x170/174/178 N/A (zeroed every frame)
Persistent physics velocity +0x1A4 → ptr → +0xCA4/CA8/CAC +0xCA4/CA8/CAC
Speed limit +0x1A4 → ptr → +0xC70 +0xC70
Gravity toggle +0x1A4 → ptr → +0xC7C +0xC7C
Gravity strength +0x1A4 → ptr → +0xC78 +0xC78
Friction damping +0x1A4 → ptr → +0xC68 +0xC68
Physics up-vector +0x1A4 → ptr → +0xC8C/C90/C94 +0xC8C/C90/C94
Roll friction (computed) +0x1A4 → ptr → +0xC64 +0xC64

Sources

All data verified via live GhidraMCP headless decompilation on Hamsterball.exe:

  • Ball_Update @ 0x405E00 — main physics tick (raw C, 15 phases)
  • Ball_AdvancePositionOrCollision @ 0x4564C0 — position integration + collision
  • CollisionMesh_ctor @ 0x405680 — constructor, field initialization
  • Ball_InitBattleMode @ 0x4056D0 — default physics params
  • Ball_ApplyForce @ 0x402650 — ephemeral force accumulator
  • Ball_SetVec3AtOffset @ 0x402A20 — direct velocity overwrite
  • CollisionMesh_SetSpeed @ 0x4029C0 — dead code analysis
  • BALL_OBJECT_MODDING.md — parent Ball struct for cross-reference

Document generated: 2026-06-06
Method: Ghidra decompilation + cross-reference with existing ball docs + dead code analysis
Confidence: High for ✅ verified offsets, Medium for ⚠️ single-source offsets


🔗 Related Documents

CollisionMesh Object

types : physics
keywords :

📂 View source on GitHub


CollisionMesh Object — Complete Modder's Reference

Analysis of the playerObject + 0x1A4 pointer chain used by the jump mod and other physics hacks.
All offsets verified via live Ghidra MCP decompilation of Hamsterball.exe.
This document describes the CollisionMesh object — a nested physics body that lives inside the Ball.


Quick Stats

Property Value
Primary update function Ball_AdvancePositionOrCollision @ 0x00405640
Constructor CollisionMesh_ctor @ 0x00405680
Total struct size 0x0CB0 bytes (3,248 bytes)
Parent backref Ball* stored at +0x0010
VTable 0x004D8E10 (Mesh_DeletingDtor)
Pointer in Ball Ball + 0x1A4 stores CollisionMesh*

The Pointer Chain Explained

// The jump mod follows this exact chain:
// 1. Ball + 0x1A4  →  CollisionMesh* (pointer field inside Ball)
// 2. *(Ball + 0x1A4) + 0xCA8  →  physics velocity Y (float)

DWORD* physicsObjPtr = (DWORD*)((DWORD)playerObject + 0x1a4);  // CollisionMesh**
DWORD physicsObj = *physicsObjPtr;                              // CollisionMesh*
float* trueVelY = (float*)(physicsObj + 0xca8);                 // &collisionMesh->vel_y

The Ball struct ends at ~0xC98. The CollisionMesh object it points to extends beyond the Ball's own size with its own fields up to +0xCA8 and beyond. These are NOT Ball fields — they belong to the nested object.


How CollisionMesh is Created

From Ball_ctor2 (0x004039E0) decompilation:

pvVar2 = operator_new(0xcb0);               // Allocate 3,248 bytes
pvVar2 = CollisionMesh_ctor(pvVar2, this);    // Initialize, pass Ball as owner
*(void **)((int)this + 0x1a4) = pvVar2;      // Store pointer in Ball struct

CollisionMesh is a completely separate object with its own vtable (0x4D8E10), its own constructor (0x405680), and its own destructor. It lives on the heap and is pointed to by Ball + 0x1A4.


Why Modders Target CollisionMesh Instead of Ball Velocity

System A: Ball Input Velocity (NOT the target)

Field Address Type Behaviour
vel_x Ball + 0x0170 float Input force accumulator
vel_y Ball + 0x0174 float Input force accumulator
vel_z Ball + 0x0178 float Input force accumulator
  • Written by Ball_ApplyForceWithMultipliers (0x402650)
  • Read by Ball_Update (0x405E00) then cleared to zero every frame
  • Writing here does nothing — values are erased before physics integration

System B: CollisionMesh Physics Velocity (THE target)

Field Address Type Behaviour
vel_x CollisionMesh + 0x0CA4 float Persistent physics state
vel_y CollisionMesh + 0x0CA8 float Persistent physics state
vel_z CollisionMesh + 0x0CAC float Persistent physics state
  • Read/written by Ball_Update and Ball_AdvancePositionOrCollision
  • Survives across frames — integrated by the physics engine
  • The jump mod writes 20.0f to +0xCA8 to inject an upward impulse

Complete CollisionMesh Field Map

Identity / Header

Offset Hex Type Name Initial Notes
+0x0000 0x0000 uint32_t vtable 0x4D8E10 Mesh_DeletingDtor
+0x0004 0x0004 uint32_t field_04 0
+0x0008 0x0008 uint32_t field_08 0
+0x000C 0x000C uint32_t field_0C 0
+0x0010 0x0010 Ball* owner_ball this Back-pointer to parent Ball

AthenaList #1 — Collision face list

Offset Hex Type Name Notes
+0x0018 0x0018 uint32_t list1_count 0 (from AthenaList_Init)
+0x001C 0x001C uint32_t list1_capacity 0
+0x0020 0x0020 void** list1_data Array of collision face pointers
+0x0424 0x0424 void** list1_end_ptr End of list data
  • Total span: 0x0018 to ~0x042F (~1,044 bytes)
  • Populated during collision detection with level geometry

AthenaList #2 — Secondary collision data

Offset Hex Type Name Notes
+0x0430 0x0430 uint32_t list2_count 0
+0x0434 0x0434 uint32_t list2_capacity 0
+0x0438 0x0438 void** list2_data
+0x083C 0x083C void** list2_end_ptr
  • Total span: 0x0430 to ~0x0847 (~1,047 bytes)

AthenaList #3 — Tertiary collision data

Offset Hex Type Name Notes
+0x0848 0x0848 uint32_t list3_count 0
+0x084C 0x084C uint32_t list3_capacity 0
+0x0850 0x0850 void** list3_data
+0x0C54 0x0C54 void** list3_end_ptr
  • Total span: 0x0848 to ~0x0C57 (~1,047 bytes)
  • Freed and re-allocated during Ball_AdvancePositionOrCollision

Physics Parameters

Offset Hex Type Name Initial Value Source Modding Use
+0x0C60 0x0C60 int32_t battle_mode 3 Ball_InitBattleMode Change game mode physics
+0x0C64 0x0C64 float roll_friction ~0.56f Ball_Update Phase 14
+0x0C68 0x0C68 float friction_damping ~0.56f Ball_InitBattleMode Lower = more slippery
+0x0C6C 0x0C6C float field_c6c 1.0f Ball_InitBattleMode Unknown physics param
+0x0C70 0x0C70 float max_speed_limit 1000.0f Ball_InitBattleMode Speed cap (raise for turbo)
+0x0C74 0x0C74 uint32_t field_c74 0 CollisionMesh_ctor
+0x0C78 0x0C78 float gravity_strength 25.0f Ball_InitBattleMode Lower = moon gravity
+0x0C7C 0x0C7C uint8_t use_gravity 1 Ball_InitBattleMode Set to 0 = zero gravity
+0x0C7D 0x0C7D uint8_t field_c7d Padding/unknown
+0x0C7E 0x0C7E uint16_t field_c7e Padding/unknown

Direction Vector (Normalized Heading)

Offset Hex Type Name Initial Value Notes
+0x0C80 0x0C80 float field_c80 0 Unused / unknown
+0x0C84 0x0C84 float field_c84 0 Unused / unknown
+0x0C88 0x0C88 float field_c88 0 Unused / unknown
+0x0C8C 0x0C8C float dir_x 0 Normalized heading X
+0x0C90 0x0C90 float dir_y -1.0f Default = straight down
+0x0C94 0x0C94 float dir_z 0 Normalized heading Z
  • Direction is reset to (0, -1, 0) by Ball_ResetCollisionMesh (0x4030B0)
  • CollisionMesh_SetSpeed multiplies speed_scalar by dir to produce scaled_dir

Scaled Direction (UNUSED — Written by CollisionMesh_SetSpeed but NEVER READ)

|| Offset | Hex | Type | Name | Initial Value | Notes |
|--------|-----|------|------|---------------|-------|
| +0x0C98 | 0x0C98 | float | scaled_dir_x | 0 | UNUSED — written by CollisionMesh_SetSpeed (0x4029C0), never read by physics engine |
| +0x0C9C | 0x0C9C | float | scaled_dir_y | 0 | UNUSED — same as above |
| +0x0CA0 | 0x0CA0 | float | scaled_dir_z | 0 | UNUSED — same as above |

  • These fields are dead values. CollisionMesh_SetSpeed writes them, but nothing in the game loop ever dereferences them.
  • Writing to +0xC64 (via CollisionMesh_SetSpeed or direct poke) has no lasting effect — Ball_Update Phase 14 computes roll_friction and overwrites it every frame.

Physics Velocity (THE JUMP MOD TARGET)

Offset Hex Type Name Initial Value Used By
+0x0CA4 0x0CA4 float vel_x 0 Ball_AdvancePositionOrCollision, Ball_Update
+0x0CA8 0x0CA8 float vel_y 0 Jump mod writes here
+0x0CAC 0x0CAC float vel_z 0 Ball_AdvancePositionOrCollision, Ball_Update
  • These are the actual physics velocities used for movement integration
  • Not cleared each frame — they persist and accumulate gravity/collisions
  • Ball_Update reads these, copies to local variables, runs physics, then writes back

Post-Velocity Fields

Offset Hex Type Name Notes
+0x0CB0 0x0CB0 struct_end End of CollisionMesh struct

Address Cheat Sheet

For quick reference when writing mods:

// === Accessing the CollisionMesh from a Ball pointer ===
void* ball = playerObject;                          // Ball* (player object)
void** cmPtr = (void**)((DWORD)ball + 0x1A4);       // CollisionMesh** (pointer field)
void* cm = *cmPtr;                                   // CollisionMesh* (dereferenced)

// === Identity ===
uint32_t* cmVtable     = (uint32_t*)((DWORD)cm + 0x0000);
Ball**    cmOwner      = (Ball**)((DWORD)cm + 0x0010);

// === Physics Parameters ===
float* cmSpeedScalar   = (float*)((DWORD)cm + 0x0C64);
float* cmFriction      = (float*)((DWORD)cm + 0x0C68);
float* cmMaxSpeed      = (float*)((DWORD)cm + 0x0C70);
float* cmGravity       = (float*)((DWORD)cm + 0x0C78);
uint8_t* cmUseGravity  = (uint8_t*)((DWORD)cm + 0x0C7C);

// === Direction ===
float* cmDirX          = (float*)((DWORD)cm + 0x0C8C);
float* cmDirY          = (float*)((DWORD)cm + 0x0C90);
float* cmDirZ          = (float*)((DWORD)cm + 0x0C94);

// === Scaled Direction ===
float* cmScaledDirX    = (float*)((DWORD)cm + 0x0C98);
float* cmScaledDirY    = (float*)((DWORD)cm + 0x0C9C);
float* cmScaledDirZ    = (float*)((DWORD)cm + 0x0CA0);

// === PHYSICS VELOCITY (persistent) ===
float* cmVelX          = (float*)((DWORD)cm + 0x0CA4);
float* cmVelY          = (float*)((DWORD)cm + 0x0CA8);  // ← Jump mod target
float* cmVelZ          = (float*)((DWORD)cm + 0x0CAC);

Key Functions

CollisionMesh Lifecycle

Function Address Description
CollisionMesh_ctor 0x00405680 Allocates 0xCB0 bytes, initializes lists, zeros velocity
Mesh_DeletingDtor 0x004D8E10 (vtable) Destructor — frees collision data

Ball Functions That Touch CollisionMesh

Function Address What it does to CollisionMesh
Ball_ctor2 0x004039E0 Calls operator_new(0xCB0)CollisionMesh_ctor, stores ptr at +0x1A4
Ball_Update 0x00405E00 Reads cm.vel into locals, integrates physics, writes back
Ball_AdvancePositionOrCollision 0x00405640 Main physics integrator — adds external velocity to cm.vel, applies gravity, collision response, friction
Ball_InitBattleMode 0x004056D0 Sets cm.gravity_strength, cm.max_speed_limit, cm.friction, cm.battle_mode, resets cm.dir to down
CollisionMesh_SetSpeed 0x004029C0 Writes cm[0xC64]=speed, cm[0xC98/C9C/CA0]=speed*dir. DEAD CODE — effects immediately overwritten by Ball_Update Phase 14. Does NOT control ball speed in any meaningful way.
Ball_SetVec3AtOffset 0x00402A20 Directly overwrites cm.vel_x/y/z with a Vec3
Ball_ResetCollisionMesh 0x004030B0 Resets cm.dir to (0, -1, 0), clears impact counter
Ball_FindMeshCollision 0x00403980 Passes this (CollisionMesh*) to Mesh_FindClosestCollision

The Ground Check Explained

float tolerance = 0.5f;
if (*trueVelY > -tolerance && *trueVelY < tolerance) {
    *trueVelY = 20.0f;
}

The mod checks if CollisionMesh.vel_y is between -0.5 and +0.5. This is a grounded check:

  • When resting on a surface: Collision response from Ball_AdvancePositionOrCollision exactly cancels gravity → vel_y ≈ 0
  • When airborne: vel_y is typically negative (falling due to gravity) or has significant magnitude
  • The tolerance 0.5f accounts for minor numerical jitter when the ball is "settled"

When the condition is met, the mod injects 20.0f into cm.vel_y, giving the ball an immediate upward velocity that the physics integrator will process over subsequent frames.


Modding Recipes

1. Super Jump

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0CA8) = 50.0f;  // Double the original mod's force

2. Horizontal Launch (dash)

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0CA4) = 100.0f;  // Blast forward in X
*(float*)((DWORD)cm + 0x0CAC) = 0.0f;   // No Z velocity

3. Velocity Freeze (stop instantly)

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0CA4) = 0.0f;
*(float*)((DWORD)cm + 0x0CA8) = 0.0f;
*(float*)((DWORD)cm + 0x0CAC) = 0.0f;

4. Moon Gravity

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0C78) = 5.0f;   // Default is 25.0f

5. Disable Gravity Entirely

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(uint8_t*)((DWORD)cm + 0x0C7C) = 0;   // Set use_gravity = false

6. Turbo Speed (raise speed cap)

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0C70) = 5000.0f;   // Default is 1000.0f

7. Slippery Mode (reduce friction)

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0C68) = 0.1f;   // Default is ~0.56

8. Zero Gravity

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(uint8_t*)((DWORD)cm + 0x0C7C) = 0;  // Disable gravity

9. Direction Override (face a specific way)

Note: +0xC8C/C90/C94 (dir) is read by Ball_Update and used to compute roll direction. Changing it persists until Ball_ResetCollisionMesh resets it to (0, -1, 0).

void* ball = playerObject;
void* cm = *(void**)((DWORD)ball + 0x1A4);
*(float*)((DWORD)cm + 0x0C8C) = 0.0f;   // dir_x = 0
*(float*)((DWORD)cm + 0x0C90) = 1.0f;   // dir_y = up
*(float*)((DWORD)cm + 0x0C94) = 0.0f;   // dir_z = 0

The Two Velocity Systems Side by Side

Property Ball Input Velocity CollisionMesh Physics Velocity
Address Ball + 0x170/174/178 CollisionMesh + 0xCA4/CA8/CAC
Type float accumulator float persistent state
Written by Ball_ApplyForceWithMultipliers Ball_Update, Ball_AdvancePositionOrCollision
Read by Ball_Update (each frame) Ball_Update, Ball_AdvancePositionOrCollision
Cleared? YES — set to 0 after reading NO — persists across frames
Use case Input forces from keys/gamepad Final movement + gravity + collisions
Mod effect Nothing (erased) Actual movement change

Verified via Ghidra MCP

All offsets cross-referenced against live decompiled code:

  • Ball_ctor2 (0x4039E0): operator_new(0xCB0)CollisionMesh_ctor → stored at this + 0x1A4
  • Ball_ApplyForceWithMultipliers (0x402650): Writes to Ball + 0x170/174/178
  • Ball_Update (0x405E00): Reads Ball.vel at param_1[0x5c/5d/5e] then clears to zero. Reads/writes CollisionMesh + 0xCA4/CA8/CAC via param_1[0x69].
  • Ball_AdvancePositionOrCollision (0x405640): Integrates CollisionMesh + 0xCA4/CA8/CAC with gravity, friction, collision response.
  • CollisionMesh_ctor (0x405680): Initializes +0xCA4/CA8/CAC to 0,0,0. Sets +0x10 = owner Ball.
  • Ball_InitBattleMode (0x4056D0): Initializes +0xC60 through +0xC94.
  • Ball_SetVec3AtOffset (0x402A20): Directly overwrites +0xCA4/CA8/CAC.
  • CollisionMesh_SetSpeed (0x4029C0): DEAD CODE — writes +0xC64 and +0xC98/C9C/CA0, but Ball_Update Phase 14 immediately overwrites +0xC64 with computed roll_friction. Nothing ever reads +0xC98/C9C/CA0. Do NOT use for modding.
  • Ball_ResetCollisionMesh (0x4030B0): Resets +0xC8C/C90/C94 to (0, -1, 0).

NOTE on CollisionMesh_SetSpeed: We previously documented this as a "speed setter." Decompilation of Ball_Update (0x405E00) Phase 14 and Ball_AdvancePositionOrCollision (0x4564C0) confirm +0xC64 is roll_friction, overwritten every frame. The scaled_dir fields (+0xC98/C9C/CA0) are written but never read by the engine. Calling this function appears to do nothing because its effects are immediately clobbered.


Summary

The jump mod is correct and sophisticated. The author understood the engine's architecture:

  1. Ball + 0x1A4 is a pointer to a nested CollisionMesh object (size 0xCB0)
  2. CollisionMesh + 0xCA8 is the persistent physics velocity Y, not the ephemeral input accumulator
  3. A near-zero cm.vel_y means the ball is grounded (collision response canceling gravity)
  4. Injecting 20.0f creates an immediate upward impulse that the physics integrator processes
  5. Writing to Ball + 0x174 would do nothing because that field is cleared every frame

The mod bypasses the normal Ball_ApplyForceWithMultipliersBall_Update flow and writes directly to the physics state. This is the correct low-level approach for instant-response cheats.


🔗 Related Documents

Container & Utility Systems

types : objects
keywords :

📂 View source on GitHub


Container & Utility Systems

AthenaList — Dynamic Array / Linked Index Array

AthenaList is the primary container used throughout Hamsterball. It combines a
dynamic array with a 256-entry free index system for O(1) iteration.

Structure Layout (0x418 bytes)

Offset Type Description
+0x00 vtable* IndexList_Dtor (0x4D875C)
+0x04 int Count (number of elements)
+0x08 int[256] Free index table (0x400 bytes, 256 entries)
+0x408 int Next free index (iterator, wraps at 0xFF)
+0x40C void** Data pointer (malloc'd array of void*, count*4 bytes)
+0x410 int Capacity hint (0 on init)
+0x414 int Sorted mode flag (1=sorted, 0=unsorted)

Key Functions

Address Function Xrefs Description
0x453210 AthenaList_Init 112 Initialize list with sorted flag
0x453280 AthenaList_Free 68 Free data array, reset count
0x4532B0 AthenaList_NextIndex 256 Get next free index (wraps at 255)
0x4532E0 AthenaList_SortedInsert 2 Insert in sorted order (when +0x414=1)
0x453610 AthenaList_ContainsValue 10 Check if value exists
0x453640 AthenaList_FindByValue 4 Find value, return index
0x4536A0 AthenaList_GetSize 60 Return count at +0x04
0x4536B0 AthenaList_InsertAt 7 Insert at specific index
0x453780 AthenaList_Append 22 Append raw (no sorted check)
0x453810 AthenaList_Append 301 Append with sorted-mode dispatch
0x453820 AthenaList_MergeSorted 10 Merge two sorted lists
0x467E40 AthenaList_Ctor 4 Constructor
0x46B1A0 AthenaListObj_ctor 7 Object list constructor
0x475730 AthenaList_WriteToFile 1 Serialize list to file
0x489280 AthenaList_FreeAllChunks 3 Free all chunks
0x489540 AthenaList_SplitChunk 3 Split a chunk
0x488710 AthenaList_IterateNext 15 Get next item in iteration
0x4897A0 AthenaList_WriteDword 3 Write DWORD to list file

Initialization (AthenaList_Init)

AthenaList_Init(this, sorted_flag):
    this->sorted = sorted_flag;    // +0x414
    this->count = 0;               // +0x04
    this->next_index = 0;          // +0x08..+0x408 (256 entries zeroed)
    this->vtable = &IndexList_Dtor;
    for (i = 0; i < 256; i++)      // Clear free index table
        this->free_table[i] = 0;
    this->iter_idx = 0;            // +0x408

Append (AthenaList_Append, 301 xrefs)

AthenaList_Append(this, value):
    if (this->sorted != 0)         // +0x414
        return AthenaList_SortedInsert(this, value);
    
    if (this->count == 0) {        // +0x04
        this->count = 1;
        this->data = malloc(4);    // +0x40C
        // Clear free index table
        for (i = 0; i < 256; i++)
            this->free_table[i] = 0;
        this->data[0] = value;
    } else {
        this->count++;
        this->data = realloc(this->data, this->count * 4);
        this->data[this->count - 1] = value;
    }

Free (AthenaList_Free)

AthenaList_Free(this):
    if (this->data != NULL)        // +0x40C
        free(this->data);
    this->data = NULL;
    this->count = 0;               // +0x04

NextIndex (AthenaList_NextIndex, 256 xrefs)

AthenaList_NextIndex(this):
    this->iter_idx++;               // +0x408
    if (this->iter_idx > 0xFF)     // Wraps at 255
        this->iter_idx = 1;
    return this->iter_idx;

Iteration Pattern (from decompilations)

// Standard iteration pattern used throughout the codebase:
int idx = AthenaList_NextIndex(&list);
void *item = (list.count < 1) ? NULL : list.data[0];
while (item != NULL) {
    // Process item
    idx = list.free_table[idx];
    if (list.count <= idx) break;
    item = list.data[idx];
    list.free_table[idx]++;
}
AthenaList_Free(&list);

Texture System

Texture Structure (0x74 bytes)

Created by Graphics_FindOrCreateTexture (0x455C50):

Offset Type Description
+0x00 vtable* Texture vtable
+0x08 char* Texture name/path (strdup'd)
+0x0C void** D3D texture pointer array (from +0x6F0 in list)
+0x10 int Reference count (incremented on cache hit)
+0x14-0x70 ... D3D texture data, mip levels, format info

Graphics_FindOrCreateTexture (0x455C50)

7 xrefs — texture loading with caching:

Texture* Graphics_FindOrCreateTexture(GfxEngine* this, char* path, char use_cache) {
    if (use_cache == 1) {
        // Linear search through texture list at this+0x2E4
        for each texture in this->texture_list:
            if (stricmp(texture->name, path) == 0) {
                texture->refcount++;  // +0x10
                return texture;       // Cache hit
            }
    }
    
    // Cache miss: load new texture
    Texture* tex = operator_new(0x74);
    Texture_LoadWithMips(tex, this, path);
    AthenaList_Append(this + 0x2E4, tex);  // Add to cache
    return tex;
}

Texture Cache Location

  • GfxEngine+0x2E4: AthenaList texture cache (all loaded textures)
  • GfxEngine+0x6F0: Texture data array
  • Lookup: case-insensitive strcmp (stricmp)
  • Key: texture file path (e.g., "textures\hammy1.png")

Texture Loading

  • PNG/BMP files loaded via D3DXCreateTextureFromFile
  • Alpha channel enabled for PNG
  • Mipmaps generated via Texture_LoadWithMips
  • 7 xrefs to FindOrCreateTexture

AthenaString — Dynamic String

Address Function Xrefs Description
0x473500 AthenaString_AssignCStr 75 Assign from C string
0x4736B0 AthenaString_dtor 85 Destructor
0x473A50 AthenaString_AssignCRLF 21 Assign with CRLF handling
0x4BAE43 AthenaString_SprintfToBuffer 71 sprintf to internal buffer
0x4BBDFD AthenaString_Sprintf Format string
0x466C70 AthenaString_Format Format with args
0x4737F0 AthenaString_Assign Copy assignment
0x473940 FontFormatString_Parse Parse printf-style formats

AthenaString Structure (estimated 0x1C bytes)

Offset Type Description
+0x00 vtable* AthenaString vtable
+0x04 char* String data (malloc'd)
+0x08 int Length
+0x0C int Capacity
+0x10 int Ref count (if shared)

Hash Table (MESHWORLD Lookup)

The game uses a hash table for level object lookup (Scene+0x8AC). Objects
are found by name strings like "START0-0", "SAFESPOT", "CAMERALOCUS".

Key hash table functions:

  • Scene_FindObjectByName: resolves mesh name to object
  • Used by Scene_SpawnBallsAndObjects for start positions
  • Used by Level_InitScene for camera targets (CAMERALOCUS, CAMERALOOKAT)

Graphics Utility Functions

Address Function Xrefs Description
0x455D60 Graphics_DrawScreenRect 63 Draw 2D rectangle on screen
0x455110 Graphics_ApplyMaterialAndDraw Set material + draw mesh
0x454AB0 Graphics_SetProjection 31 Set perspective projection
0x454F10 Graphics_SetViewport 8 Set viewport dimensions
0x453B50 Graphics_BeginFrame Start frame, clear buffers
0x455A90 Graphics_PresentOrEnd Present frame, swap buffers
0x453900 Graphics_ClearViewport Clear color/depth
0x453970 Graphics_SetCullMode2 Set cull mode
0x4539A0 Graphics_SetViewportZ Set depth range

CRT / Standard Library Usage

The game uses the following CRT patterns:

  • malloc/realloc/free for dynamic arrays (AthenaList)
  • operator_new/operator_delete for object allocation
  • fopen/fread/fclose for file I/O (font.description, .mesh, .meshworld)
  • _open with O_RDONLY (0x8000) for font loading
  • stricmp for case-insensitive string comparison (texture cache, menu items)
  • sprintf for string formatting (AthenaString_Sprintf)
  • GetTickCount for timing (game loop)
  • PeekMessageA/TranslateMessage/DispatchMessageA for Windows message loop
  • Sleep(0) for yielding between frames

🔗 Related Documents

Critical Function Deep Analysi

types : analysis
keywords :

📂 View source on GitHub


Critical Function Deep Analysis

Two functions were flagged as CRITICAL misnomers during the full decompilation pass.
Each was analyzed three times to verify accuracy. This document records the verified
findings.


1. TimerDisplay (0x004298C0) — Actually: App_ResourceLoader

Misnomer Summary

Field Value
Current name TimerDisplay
Correct name App_ResourceLoader (or LoadingScreenGadget_Factory)
Severity CRITICAL — name is completely unrelated to function behavior
Evidence Zero timer logic. Loads 195 game resources via vtable dispatch.

Calling Convention

void __fastcall TimerDisplay(int param_1);
  • param_1 (ECX, __fastcall): Pointer to the App struct (the global
    application object, ~2328 bytes). Not a timer, not a display widget.
  • Returns: void. All output is written through the App struct.

The function pointer 0x004298C0 is stored in a dispatch table at VA 0x004D2700
in the .rdata section, alongside other App initialization functions:
App_FrameTick (0x0046C9E0), App_SetTitleString (0x0046CB70), etc.

What It Actually Does

Step 1 — Allocate LoadingScreenGadget (lines 20-28):

this = operator_new(0x3628);          // Allocate 13,864 bytes
piVar1 = LoadingScreenGadget_Ctor(this, param_1);  // Construct
*(int**)(param_1 + 0x22c) = piVar1;   // Store gadget pointer at App+0x22C
*(undefined4*)(param_1 + 0x31c) = 0;  // Clear font slot

Creates a LoadingScreenGadget object (0x3628 = 13,864 bytes) and stores it at
App+0x22C. This gadget is the master resource manager that owns all game assets.

Step 2 — Load 195 resources via vtable dispatch:

The gadget's vtable (*piVar1) has 7 resource-loading methods at different offsets.
Each call writes a resource handle into a specific slot in the App struct.

vtable Offset Method Type Count App Slots Used Resources
+0x58 Texture loader (simple) 59 +0x318–0x438 titletext, hammy1-3, blueblot, goal.png, timerblot.png, star.png, dust.png, chrome.png, medals, tourney-*.png, etc.
+0x48 Texture loader (with flags) 37 +0x27C–0x314 sign-bewarethetar, goal.png, goal-lit.png, goal-mirrored.png, locktile.png, arrow1.png, checker textures (pink/blue/green/red/orange), brick textures, etc.
+0x5C Font loader 3 +0x318–0x328 showcardgothic28, arialnarrow12bold, showcardgothic72, showcardgothic14, showcardgothic16
+0x4C Mesh loader 24 +0x244–0x5C4 Sphere, SphereBreak1-2, Hamster-Waiting, Hamster-trot1-3, 8Ball, FunBall, Bell, Dizzy, RBGlare, Sphere+Tar, tarbubble, fanblades, fanbody, sawblade, sawface1-2, dawgshoe1-2, dawgshadow, GlassBonus, GlassBonus-Smashed, mousetrapshadow
+0x50 Level loader 7 +0x570–0x5C8 MouseTrap, Secret, Secret-Unlock, Level4-Trapdoor1-2, PopupSign, Level6-Lifter
+0x54 level reference copier 4 +0x574–0x5A0 Copies level handles from one App slot to another (aliases)
+0x60 Sound loader 61 +0x43C–0x52C collide(10ch), roll(10ch), whistle(1ch), bumper(10ch), ballbreak(5ch), ballbreaksmall(5ch), thwomp(2ch), snap(2ch), popup(2ch), dropin(2ch), dropinshort(2ch), popout(2ch), pipebump1-3(10ch each), gearclank(20ch), bridgeslam(2ch), platformtick(5ch), gluestuck(5ch), bubble1-2(5ch), wheelcreak(2ch), catapult(2ch), trapdoor(2ch), fwing(2ch), clink(3ch), whoosh(3ch), chomp(1ch), fan-start(10ch), fan-blow(10ch), crack(2ch), crumble(2ch), sawstartup(2ch), sawcut(2ch), minipop(5ch), bell(3ch), zip(2ch), ting(20ch), shrink(3ch), grow(3ch), tweet(3ch), creakyplatform(20ch), wubba(5ch), saw(2ch), sawspeedy(2ch), dawgstep1-2(10ch), dawgsmash(10ch), sizzle(2ch), explode(3ch), vac-o-sux(3ch), speedcylinder(2ch), bonuspop(5ch), buzzbonus(1ch), breakbridge(1ch), unlock(1ch), NeonRide(1ch), NeonFlicker(50ch), ZoopDown(2ch), LightsOff(2ch), GlassBonus(2ch)

Step 3 — Merge and register (lines 246-249):

Menu_MergeAllLists(*(int*)(param_1 + 0x22c));    // Merge all resource lists
Scene_AddObject(*(void**)(param_1 + 0x184),      // Add to scene at App+0x184
                *(int**)(param_1 + 0x22c));

Merges all loaded resource lists into the gadget and adds the gadget as a scene
object so it participates in the render/tick loop.

Summary

This function is the master game asset preloader. It creates the
LoadingScreenGadget that owns every font, texture, mesh, level, and sound
effect in the game. The name TimerDisplay is completely wrong — the only
timer-related string is "timerblot.png" (a texture used for timer display,
not the function itself).

Parameters

Parameter Type Direction Meaning
param_1 int (App*) IN Pointer to the global App struct
(no output params) Results stored via App struct fields

2. DispatchCollisionEvents (0x0040C5D0) — Actually: LevelCollisionEventHandler

Misnomer Summary

Field Value
Current name DispatchCollisionEvents
Correct name LevelCollisionEventHandler (or HandleCollisionEvent)
Severity CRITICAL — name describes 1 of 18 event types handled
Evidence 28 call sites, 18 event branches, 0 allocations (doesn't "create" anything)

Calling Convention

void __thiscall DispatchCollisionEvents(void *this, int *param_1, int *param_2);

Verified by decompiling TowerCollisionEvents (0x0040DCD0), which calls
DispatchCollisionEvents with the same parameters it receives:

// In TowerCollisionEvents(this, ball, collObj):
//   ... handles E:CATAPULTBOTTOM, E:OPENSESAME, N:TRAPDOOR, E:BITE, E:MACETRIGGER, N:MACE ...
//   ... then falls through to:
DispatchCollisionEvents(this, ball, collObj);

Parameters

Parameter Register Type Direction Meaning
this ECX void* (Scene/Board) IN The containing scene or board object
param_1 int* (Ball*) IN/OUT The ball that triggered the collision. Read for position, player index, cooldowns, CollisionMesh. Written to for velocity, flags, timers, scores.
param_2 int* (CollisionEvent*) IN The collision object data. param_2[1]+0x864 = event string (the N:/E: tag). *param_2+0x47C = rotator ID.
(returns) EAX void No return value

Ball Struct Offsets Accessed (param_1)

Offset Type Field Name Access Used By
+0x059 (param_1[0x59]) float ball.pos_x READ E:JUMP, N:TARPIT, PIPEBONK (3D sound position)
+0x05A (param_1[0x5A]) float ball.pos_y READ E:JUMP, N:TARPIT, PIPEBONK
+0x05B (param_1[0x5B]) float ball.pos_z READ E:JUMP, N:TARPIT, PIPEBONK
+0x0B3 (param_1[0xB3]) byte ball.in_tar READ/WRITE N:TARPIT (set=1)
+0x0B4 (param_1[0xB4]) float ball.tar_entry_y WRITE N:TARPIT (stores entry Y)
+0x0B6 (param_1[0xB6]) int ball.water_timer WRITE N:WATER (=10)
+0x0A7 (param_1[0xA7]) float ball.vel_x WRITE E:JUMP (=0.002)
+0x0A8 (param_1[0xA8]) float ball.vel_y WRITE E:JUMP (=1.0)
+0x0CB (param_1+0xCB) AthenaList ball.action_list READ/WRITE E:ACTION (ONCE tracking)
+0x018 (param_1[6]) int ball.player_index READ E:LIMIT, E:ACTION, N:GOAL (0-3)
+0x014 (param_1[5]) int* ball.board_ptr READ E:LIMIT (ArenaBoard)
+0x028 (param_1[0x69>>2]) int* ball.collision_mesh READ E:TRAJECTORY, DROPIN, N:MOUSETRAP
+0x053 (param_1[0x53]) byte ball.goal_reached READ N:GOAL (gate)
+0x1DA (param_1+0x1DA) byte ball.enabled/active READ/WRITE N:GOAL (gate), E:LIMIT, N:TARPIT (=0)
+0x1F2 (param_1[0x1F2]) int ball.dropin_cooldown READ/WRITE DROPIN (=50)
+0x1F5 (param_1[0x1F5]) int ball.zip_cooldown READ/WRITE ZIP event (=50)
+0x1F7 (param_1[0x1F7]) int ball.jump_cooldown READ/WRITE E:JUMP (=10)
+0x1F8 (param_1[500]) int ball.pipebonk_cooldown READ/WRITE PIPEBONK (=10)
+0x1F4 (param_1[499]) int ball.popout_cooldown READ/WRITE POPOUT (=50)
+0x202 (param_1[0x202]) int ball.impact_timer WRITE E:JUMP, N:NOCONTROL (=10)
+0x2D5 (param_1+0x2D5) byte ball.in_water WRITE N:WATER (=1)
+0x2E9 (param_1+0x2E9) byte ball.impact_shatter WRITE E:LIMIT (=1)
+0x30B (param_1+0x30B) byte ball.safe_switch_flag WRITE E:SAFESWITCH (=0 if no paren)
+0xC2C (param_1+0xC2C) char[] ball.switch_data WRITE E:SAFESWITCH (copies parenthesized data)

CollisionMesh Offsets (param_1[0x69])

Offset Type Field Used By
+0xCA4 float velocity_x E:TRAJECTORY (write), DROPIN (read), N:MOUSETRAP (read/write)
+0xCA8 float velocity_y E:TRAJECTORY (write), DROPIN (read), N:MOUSETRAP (write=15.0)
+0xCAC float velocity_z E:TRAJECTORY (write), DROPIN (read), N:MOUSETRAP (read/write)

The 18 Event Branches

The function reads the event string from param_2[1]+0x864 and dispatches
based on its content. All comparisons are case-insensitive.

N: prefix events (Named collision zones)

# Event String Comparison Condition Action Ball Fields Written Points
1 N:SECRET strnicmp(8) Calls Rotator_MarkTriggered(rotator_id) none 0
2 N:UNLOCKSECRET strnicmp(14) Calls CheckArenaUnlock(this) none 0
3 E:NODIZZY strnicmp(9) Parses <TIME>N</TIME> XML. Calls Ball_DizzyImmunity(ball, N). Ball_DizzyImmunity 0 (records time)
4 E:SAFESWITCH strnicmp(12) Finds ( in string. Copies parenthesized data to ball+0xC2C. If no (, clears ball+0x30B=0. +0xC2C, +0x30B 0
5 E:LIMIT stricmp Clears ball+0x1DA=0, sets ball+0x2E9=1. Tracks arena knockoff counts for all 4 players at ArenaBoard+0x47B4–0x47C0. Checks each player's ball completion flags at App+0x5D7/0x677/0x717/0x7B7 and ball validity at App+0x5DC/0x67C/0x71C/0x7BC. +0x1DA, +0x2E9, ArenaBoard counters 0
6 E:BREAK stricmp Calls ball->vtable[0x20]() — virtual break method. vtable dispatch 0
7 E:JUMP stricmp ball.jump_cooldown < 1 Plays 3D jump sound at ball position. Sets vel_x=0.002, vel_y=1.0, impact=10, jump_cooldown=10. +0x1F7=10, +0xA7=0.002, +0xA8=1.0, +0x202=10 +200
8 E:ACTION strnicmp(8) Parses XML. <ONCE>TRUE</ONCE>: adds event to ball.action_list if not present (one-shot gate). <SCORE>N</SCORE>: adds N × difficulty_modifier to player score at App+0x5E4+playerIndex×0xA0. Updates ball name with score. +0xCB (list), App+0x5E4 N × difficulty
9 E:TRAJECTORY strnicmp(12) Parses <X>f</X> <Y>f</Y> <Z>f</Z> XML. Writes Vec3 to CollisionMesh+0xCA4/0xCA8/0xCAC (velocity override). CM+0xCA4/CA8/CAC 0
10 N:NOCONTROL stricmp Sets ball.impact_timer = 10 (disables input). +0x202=10 0
11 N:WATER stricmp Sets ball.in_water = 1, ball.water_timer = 10. +0x2D5=1, +0xB6=10 0
12 N:TARPIT stricmp If not already in tar: plays tar sound, stores ball.pos_y as entry height. Sets ball.in_tar=1, clears ball+0x1DA=0. +0xB3=1, +0xB4=pos_y, +0x1DA=0 0
13 N:GOAL manual(7 bytes) ball.goal_reached==0 AND ball.enabled!=0 RACE FINISH. Plays "Goal!" music. Sets scene+0xCD0=1. For multiplayer: copies race timer. Sets App+0x5D6+playerIdx×0xA0=1 (goal reached). Swaps goal textures unlit→lit. If mirror mode: swaps mirrored textures too. Sets App+0x5F0+playerIdx×0xA0=1 (race finished). Updates status text to "Update". scene+0xCD0, App+0x5D6/5FC/5F0 (+playerIdx×0xA0) 0
14 N:MOUSETRAP manual(12 bytes) Reads ball velocity from CollisionMesh+0xCA4/CA8/CAC. Normalizes it. Sets vel_y = 15.0. Scales entire vector by 20.0 (DAT_004cf370). Writes back. Iterates rotator list at scene+0x1D3C, finds matching rotator by ID (rotator+0x10D4 == *param_2+0x47C). On match: plays collision sound, adds rotator to scene+0x2578 (collision list). CM+0xCA4/CA8/CAC (velocity modified), scene+0x2578 (list append) 0

X: prefix events (Anonymous collision events, matched by suffix only)

These use stricmp(event+2, ...) — skipping the 2-character prefix,
so they match any X: prefix (e.g., E:DROPIN, N:DROPIN, etc.).

# Event Suffix Condition Action Ball Fields Written Points
15 DROPIN velocity_mag in range (0.0, >2.0) AND dropin_cooldown < 1 Plays dropin sound. Sets cooldown=50. +0x1F2=50 +200
16 PIPEBONK pipebonk_cooldown < 1 Generates 2 random numbers. Plays random pipe bump sound (pipebump1/2/3). Sets cooldown=10. +0x1F8=10 +100
17 POPOUT popout_cooldown < 1 Plays popout sound. Sets cooldown=50. +0x1F4=50 +100
18 ZIP zip_cooldown < 1 Plays zip sound. Sets cooldown=50. +0x1F5=50 0

Verified Constants

Address Value Type Used By
0x004CF368 0.0 float DROPIN: velocity upper bound (velocity must be < 0.0, i.e., the < 0.0 == == 0.0 check is always true)
0x004CF370 20.0 float N:MOUSETRAP: velocity scale factor
0x004CF48C 2.0 float DROPIN: minimum velocity magnitude (must be > 2.0)
0x41700000 15.0 float N:MOUSETRAP: velocity Y override
0x3B03126F 0.002 float E:JUMP: velocity X drift

Call Sites (28 total)

TowerCollisionEvents     (0x0040DCD0) — primary level collision dispatcher
ExpertCollisionEvents     (0x0040EA6B) — arena mode collision dispatcher
HandleArenaCollisionEvents             (0x00412850) — spinner/bumper/launch collision
NeonCollisionEvents               (0x00410E6F) — arena limit collision
SinkPlatformArenaCollisionEvents  (0x00413C09) — sinking platform collision
+ 23 other Create* and collision handler functions
+ 2 DATA references (vtable entries)

Summary

DispatchCollisionEvents is the universal level collision event handler. It is called
as a fall-through from TowerCollisionEvents and ExpertCollisionEvents after
those functions handle their own specific events. It dispatches 18 different event
types based on the collision event string:

  • Race progression: N:GOAL (finish), N:SECRET (mark secret found)
  • Arena mode: E:LIMIT (track knockoffs), N:UNLOCKSECRET (unlock check)
  • Ball physics: E:JUMP (impulse), E:TRAJECTORY (velocity override),
    N:MOUSETRAP (deflect), E:BREAK (shatter)
  • Environmental: N:WATER (water zone), N:TARPIT (tar zone), N:NOCONTROL (stun)
  • Pipe events: DROPIN, PIPEBONK, POPOUT, ZIP (sound + score)
  • Scoring/tracking: E:ACTION (once-gated score), E:NODIZZY (time record)
  • Switches: E:SAFESWITCH (copy switch data to ball)

The name DispatchCollisionEvents describes only the E:NODIZZY branch (branch #3), which
is one of 18. The function creates nothing — it is a pure event handler that reads
the collision event string and dispatches side effects to the ball, App, and
scene structs.


🔗 Related Documents

Custom Event Plane Implementat

types : modding
keywords :

📂 View source on GitHub


Custom Event Plane Implementation Guide

Overview

This document explains how to add a brand-new event plane type to Hamsterball using a DLL proxy mod. The game's event system is entirely string-matching based — there is no registration table or enum. Adding a new event type means intercepting the string check at runtime.

Prerequisites:

  • MESHWORLD file with E: prefixed geometry (you handle this)
  • bass.dll proxy DLL compiled with MinGW (this guide covers it)
  • Understanding of the collision dispatch pipeline (see EVENT_PLANES.md)

1. The Dispatch Pipeline (What You're Hooking Into)

When the ball intersects an E: named collision mesh, this is the call chain:

Ball_FallUpdate (0x408830, vtable[65] at offset 0x104)
  │
  ├─ Collision_TraverseSpatialTree (0x465EF0)
  │   └─ Finds intersecting mesh buffers → populates physics+0x848 list
  │
  └─ For each collision entry in physics+0x848 list:
      └─ board->vtable[0x1D] (offset +0x74)
          │
          ├─ TowerCollisionEvents  (0x40DCD0) — Tower board (optional override)
          ├─ ExpertCollisionEvents  (0x40E6A0) — arenas (optional override)
          └─ DispatchCollisionEvents (0x40C5D0) — shared base, ALWAYS called last

Key insight: DispatchCollisionEvents is the universal chokepoint. Every level-specific handler (TowerCollisionEvents, ExpertCollisionEvents, and the 25+ unnamed board-specific handlers) processes its own E: events, then falls through to DispatchCollisionEvents as a catch-all. Hooking this one function gives you coverage for ALL board types and ALL game modes.

Calling Convention

void __thiscall DispatchCollisionEvents(void *board, int *ball, int *coll_entry);
  • ECX = board (the Board/scene object — type varies by level)
  • [ESP+4] = ball (pointer to Ball struct)
  • [ESP+8] = coll_entry (pointer to 2-element array: [0]=scene_obj, [1]=mesh_buffer)

The event name string is at:

char *event_name = *(char **)(coll_entry[1] + 0x864);

Per-Ball State

Each ball has a player index at ball+0x18 (int, 0–3 for 4 players). This is the same field the game itself uses for per-player scoring in E:LIMIT and N:GOAL. Use it to index a global array:

static int g_my_flag[4];  // per-player state

void hook(...) {
    int *ball = ...;              // from hook params
    int player_idx = ball[6];     // ball+0x18, int* stride = 0x18
    g_my_flag[player_idx] = 1;
}

2. Hook Architecture

MinGW __thiscall Workaround

MinGW doesn't support __thiscall directly. Use __fastcall with a dummy EDX parameter:

// __thiscall(this, arg1, arg2) == __fastcall(this, dummy_edx, arg1, arg2)
// Both use ECX for first arg, callee cleans 8 bytes of stack.
typedef void (__fastcall *handler_t)(void *this_, void *edx_dummy, void *ball, void *coll_entry);

Inline Hook (JMP Detour)

The hook overwrites the first 5 bytes of DispatchCollisionEvents with a JMP to your handler. A trampoline saves the original 5 bytes + JMP back to original+5:

Original:    [orig 5 bytes] [rest of function]
After hook:  [JMP to hook]  [rest of function]
Trampoline:  [orig 5 bytes] [JMP to original+5]

Your handler calls the trampoline to execute the original function.

ASLR Safety

Hamsterball.exe loads at base 0x400000 (no ASLR on this PE), but the hook code computes the real base at runtime:

DWORD base = (DWORD)GetModuleHandleA(NULL);
void *target = (void *)(0x0040C5D0 + (base - 0x00400000));

3. Reading Event Data

Safe Event Name Access

Always use IsBadReadPtr before reading game memory (MinGW doesn't support __try/__except):

static const char *get_event_name(void *coll_entry) {
    if (!coll_entry) return "(null)";
    if (IsBadReadPtr(coll_entry, 12)) return "(bad-ptr)";

    int *pair = (int *)coll_entry;
    int mesh_buffer = pair[1];
    if (!mesh_buffer || IsBadReadPtr((void *)mesh_buffer, 0x868))
        return "(bad-mesh)";

    int name_ptr = *(int *)(mesh_buffer + 0x864);
    if (!name_ptr || IsBadReadPtr((void *)name_ptr, 1))
        return "(bad-name)";

    return (const char *)name_ptr;
}

Reading Ball Position

static void get_ball_pos(void *ball, float *x, float *y, float *z) {
    *x = *y = *z = 0.0f;
    if (!ball || IsBadReadPtr(ball, 0x170)) return;
    *x = *(float *)((char *)ball + 0x164);
    *y = *(float *)((char *)ball + 0x168);
    *z = *(float *)((char *)ball + 0x16C);
}

Parsing XML-Style Tag Parameters

If your event name includes parameters (e.g., E:MYFLAG<DURATION>100</DURATION>), parse them using the game's own MWParser_ReadTag function at 0x0040xxxx (search Ghidra for MWParser_ReadTag). Alternatively, parse manually with strchr and strstr:

// Parse <TAG>value</TAG> from event name string
static float parse_tag_float(const char *name, const char *tag_name) {
    char open_tag[64], close_tag[64];
    snprintf(open_tag, sizeof(open_tag), "<%s>", tag_name);
    snprintf(close_tag, sizeof(close_tag), "</%s>", tag_name);

    const char *start = strstr(name, open_tag);
    if (!start) return 0.0f;
    start += strlen(open_tag);

    const char *end = strstr(start, close_tag);
    if (!end) return 0.0f;

    char buf[32];
    int len = end - start;
    if (len >= sizeof(buf)) len = sizeof(buf) - 1;
    memcpy(buf, start, len);
    buf[len] = 0;

    return (float)atof(buf);
}

4. Writing Effects

Ball Struct Offsets for Event Effects

Offset Type Name Used By
+0x18 int player_index E:LIMIT, N:GOAL (per-player scoring)
+0xA7 float vert_velocity E:JUMP (0.025 = upward force)
+0xA8 byte vert_velocity_on E:JUMP (= 1, enables vertical velocity)
+0x164 float pos_x Ball position X
+0x168 float pos_y Ball position Y (vertical, Y-up)
+0x16C float pos_z Ball position Z
+0x1F7 int impact_counter E:JUMP (10 = bounce cooldown timer)
+0x202 int freeze_counter E:JUMP / N:NOCONTROL (10 = freeze input)
+0x2D5 byte in_water N:WATER (1 = water physics active)
+0xB3 byte in_tar N:TARPIT (1 = tar physics active)
+0xB6 int zone_timer N:WATER (10 = effect timer in frames)
+0x1DA byte velocity_flag E:LIMIT / N:TARPIT (0 = clear velocity)

Calling Game Functions

To trigger existing game effects (sounds, scoring, etc.), call game functions directly:

// Ball_DizzyImmunity(ball, score) — awards points with difficulty modifier
typedef void (__thiscall *Ball_DizzyImmunity_t)(void *ball, long score);
static Ball_DizzyImmunity_t Ball_DizzyImmunity = NULL;

// Sound_Play3D(sound_ptr, x, y, z) — positional audio
typedef void (__cdecl *Sound_Play3D_t)(void *sound, float x, float y, float z);
static Sound_Play3D_t Sound_Play3D = NULL;

// Sound_PlayChannel(channel) — non-positional audio
typedef void (__cdecl *Sound_PlayChannel_t)(int channel);
static Sound_PlayChannel_t Sound_PlayChannel = NULL;

Sound offsets (from board→App at board+0x878→App):

App Offset Sound
+0x460 Drop-in sound
+0x464 Catapult sound
+0x468 Pop-out sound
+0x46C+idx*4 Pipe bonk sounds (3 variants)
+0x484 Tar sound
+0x49C Jump sound
+0x4CC Popout sound

Board/Scene Access

From the board parameter (ECX in __thiscall):

void *app = *(void **)((char *)board + 0x878);  // App pointer
int difficulty = *(int *)((char *)app + 0x23C); // 0=Pipsqueak, 1=Normal, 2=Frenzied

5. Complete Example: Adding E:MYFLAG

This example adds a custom event plane that:

  1. Sets a per-player flag when touched
  2. Plays the jump sound
  3. Awards 100 points
  4. Applies a small upward bounce

Full Source (custom_event.c)

/*
 * Hamsterball Custom Event Plane DLL — E:MYFLAG
 *
 * Hooks DispatchCollisionEvents (0x40C5D0) to intercept custom E: event names.
 * Built as a bass.dll proxy (loads automatically with the game).
 *
 * Build:
 *   i686-w64-mingw32-gcc -shared -o bass.dll custom_event.c \
 *     -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
 *     -Wl,--add-stdcall-alias
 *
 * Install: Copy bass.dll next to Hamsterball.exe (rename original bass.dll first).
 */

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string.h>
#include <stdlib.h>

/* ── Constants ────────────────────────────────────────────────────────── */

#define GAME_BASE 0x00400000
#define ADDR_DispatchCollisionEvents (GAME_BASE + 0x0000C5D0)
#define TRAMP_SIZE 16

/* ── Types ────────────────────────────────────────────────────────────── */

/* __thiscall workaround: __fastcall with dummy EDX */
typedef void (__fastcall *handler_t)(void *this_, void *edx_dummy,
                                      void *ball, void *coll_entry);

/* Game function typedefs */
typedef void (__thiscall *Ball_DizzyImmunity_t)(void *ball, long score);
typedef void (__cdecl *Sound_Play3D_t)(void *sound, float x, float y, float z);

/* ── Globals ─────────────────────────────────────────────────────────── */

static handler_t g_orig_DispatchCollisionEvents = NULL;
static unsigned char g_tramp[TRAMP_SIZE];

/* Per-player custom flag state (indexed by ball+0x18, max 4 players) */
static int g_my_flag[4] = {0, 0, 0, 0};

/* Game function pointers (resolved at init) */
static Ball_DizzyImmunity_t fn_Ball_DizzyImmunity = NULL;
static Sound_Play3D_t fn_Sound_Play3D = NULL;

/* ── Safe memory helpers ──────────────────────────────────────────────── */

static const char *get_event_name(void *coll_entry) {
    if (!coll_entry) return NULL;
    if (IsBadReadPtr(coll_entry, 12)) return NULL;

    int *pair = (int *)coll_entry;
    int mesh_buffer = pair[1];
    if (!mesh_buffer || IsBadReadPtr((void *)mesh_buffer, 0x868))
        return NULL;

    int name_ptr = *(int *)(mesh_buffer + 0x864);
    if (!name_ptr || IsBadReadPtr((void *)name_ptr, 1))
        return NULL;

    return (const char *)name_ptr;
}

static void get_ball_pos(void *ball, float *x, float *y, float *z) {
    *x = *y = *z = 0.0f;
    if (!ball || IsBadReadPtr(ball, 0x170)) return;
    *x = *(float *)((char *)ball + 0x164);
    *y = *(float *)((char *)ball + 0x168);
    *z = *(float *)((char *)ball + 0x16C);
}

static int get_player_index(void *ball) {
    if (!ball || IsBadReadPtr(ball, 0x20)) return 0;
    return *(int *)((char *)ball + 0x18);
}

/* ── Custom event handler ────────────────────────────────────────────── */

static void handle_my_flag(void *board, void *ball, void *coll_entry) {
    /* Get player index for per-ball state */
    int pidx = get_player_index(ball);
    if (pidx < 0 || pidx > 3) pidx = 0;

    /* Set the custom flag */
    g_my_flag[pidx] = 1;

    /* Play jump sound at ball position */
    if (fn_Sound_Play3D) {
        void *app = *(void **)((char *)board + 0x878);
        if (app && !IsBadReadPtr(app, 0x4A0)) {
            void *jump_sound = *(void **)((char *)app + 0x49C);
            float x, y, z;
            get_ball_pos(ball, &x, &y, &z);
            if (jump_sound)
                fn_Sound_Play3D(jump_sound, x, y, z);
        }
    }

    /* Award 100 points */
    if (fn_Ball_DizzyImmunity)
        fn_Ball_DizzyImmunity(ball, 100);

    /* Small upward bounce (same pattern as E:JUMP) */
    if (ball && !IsBadReadPtr(ball, 0x210)) {
        int *ball_ints = (int *)ball;
        /* Check impact cooldown (ball+0x1F7) */
        if (ball_ints[0x1F7 >> 2] < 1) {
            /* Set vertical velocity */
            *(float *)((char *)ball + 0xA7) = 0.025f;  /* upward force */
            *(unsigned char *)((char *)ball + 0xA8) = 1; /* enable flag */
            ball_ints[0x202 >> 2] = 10;  /* freeze input 10 frames */
            ball_ints[0x1F7 >> 2] = 10;  /* impact cooldown 10 frames */
        }
    }
}

/* ── Hook callback ───────────────────────────────────────────────────── */

void __fastcall hook_DispatchCollisionEvents(void *this_, void *edx_dummy,
                                               void *ball, void *coll_entry) {
    (void)edx_dummy;

    const char *name = get_event_name(coll_entry);

    if (name) {
        /* Check for our custom event */
        if (_stricmp(name, "E:MYFLAG") == 0) {
            handle_my_flag(this_, ball, coll_entry);
            /* Still call original so other events on same geometry fire.
               Return here (skip original) if you want exclusive handling. */
        }

        /* Add more custom events here:
         *
         * if (_strnicmp(name, "E:CUSTOM_SPEED", 14) == 0) {
         *     float power = parse_tag_float(name, "POWER");
         *     handle_speed_pad(this_, ball, coll_entry, power);
         * }
         *
         * if (_stricmp(name, "E:WARP") == 0) {
         *     handle_warp(this_, ball, coll_entry);
         *     return;  // skip original — warp replaces all other events
         * }
         */
    }

    /* Call original DispatchCollisionEvents for standard events */
    if (g_orig_DispatchCollisionEvents)
        g_orig_DispatchCollisionEvents(this_, NULL, ball, coll_entry);
}

/* ── Inline hook engine ──────────────────────────────────────────────── */

static int install_hook(void *target, void *hook, unsigned char *trampoline) {
    DWORD oldProtect;
    unsigned char *t = (unsigned char *)target;

    if (!VirtualProtect(t, TRAMP_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect))
        return 0;

    /* Save original bytes to trampoline */
    memcpy(trampoline, t, TRAMP_SIZE);

    /* Trampoline: original 5 bytes + JMP back to target+5 */
    trampoline[5] = 0xE9;
    *(unsigned long *)(trampoline + 6) =
        (unsigned long)((char *)target + 5 - (char *)(trampoline + 5) - 5);

    /* Make trampoline executable */
    DWORD tp;
    VirtualProtect(trampoline, TRAMP_SIZE, PAGE_EXECUTE_READWRITE, &tp);

    /* Overwrite target with JMP to hook */
    unsigned long rel = (unsigned long)((char *)hook - (char *)target - 5);
    t[0] = 0xE9;
    *(unsigned long *)(t + 1) = rel;

    FlushInstructionCache(GetCurrentProcess(), target, 5);
    return 1;
}

/* ── Init thread ─────────────────────────────────────────────────────── */

static DWORD WINAPI init_thread(LPVOID lpParam) {
    (void)lpParam;
    Sleep(2000);  /* Wait for game to fully load */

    /* Resolve game function addresses */
    DWORD base = (DWORD)GetModuleHandleA(NULL);
    DWORD offset = base - GAME_BASE;

    fn_Ball_DizzyImmunity = (Ball_DizzyImmunity_t)(0x00402400 + offset);
    fn_Sound_Play3D = (Sound_Play3D_t)(0x0040xxxx + offset);
    /* NOTE: Sound_Play3D address needs Ghidra verification before building.
       Search for "Sound_Play3D" in Ghidra function list. */

    /* Install hook on DispatchCollisionEvents */
    void *target = (void *)(ADDR_DispatchCollisionEvents + offset);
    if (install_hook(target, hook_DispatchCollisionEvents, g_tramp)) {
        g_orig_DispatchCollisionEvents = (handler_t)g_tramp;
    }

    return 0;
}

/* ── DLL Entry (bass.dll proxy) ──────────────────────────────────────── */

/* Forward declarations for BASS exports (see hamsterball-dll-modding skill
   for the full 10-export list) */
BOOL WINAPI BASS_Init(void *a, int b, int c, void *d, void *e) { return TRUE; }
void WINAPI BASS_Free(void) {}
BOOL WINAPI BASS_Start(void) { return TRUE; }
void WINAPI BASS_Stop(void) {}
BOOL WINAPI BASS_SetConfig(int a, int b) { return TRUE; }
/* ... remaining BASS exports forwarded to original if needed ... */

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    (void)lpvReserved;
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hinstDLL);
        CreateThread(NULL, 0, init_thread, NULL, 0, NULL);
        break;
    case DLL_PROCESS_DETACH:
        /* Hooks are auto-removed when process exits */
        break;
    }
    return TRUE;
}

6. Build & Test Workflow

Cross-Compile (Linux → Windows DLL)

i686-w64-mingw32-gcc -shared -o bass.dll custom_event.c \
    -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
    -Wl,--add-stdcall-alias

MinGW pitfalls (from experience):

  • fopen() triggers C4996 error → use fopen_s() instead
  • __try/__except not supported → use IsBadReadPtr() before reads
  • dsound.h may need #include <mmeapi.h> before it
  • Always use -static -static-libgcc to bundle runtime DLLs
  • See skill hamsterball-dll-modding for full details

Crash Test (Wine/Xvfb)

# Copy DLL to game directory
cp bass.dll ~/hamsterball-re/originals/installed/extracted/

# Delete cached files so the game re-reads MESHWORLD
rm -f ~/hamsterball-re/originals/installed/extracted/Levels/*.cached

# Launch game on Xvfb
DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 timeout 35 wine Hamsterball.exe &

# After 35 seconds, check if process is still alive
# (most DLL crashes happen during audio init at startup)
sleep 35
if pgrep -x wine > /dev/null; then
    echo "CRASH TEST PASSED"
else
    echo "CRASH TEST FAILED"
fi

What crash testing validates:

  • Hook address is correct (wrong address = instant crash)
  • Calling convention matches (__thiscall / __fastcall)
  • No stack corruption from mismatched RET N
  • Trampoline preserves original instruction boundaries
  • IsBadReadPtr guards prevent access violations

What it does NOT validate:

  • Whether the custom event actually triggers (game renders black on llvmpipe)
  • Whether sound plays correctly
  • Whether physics effects are correct

Visual/gameplay testing is done on real Windows by the user.

Testing on Real Windows

  1. Backup original bass.dllbass_real.dll
  2. Copy compiled bass.dll next to Hamsterball.exe
  3. Delete all *.cached files in the Levels/ folder
  4. Launch the game
  5. Play a level containing your custom E:MYFLAG event plane
  6. Verify the effect triggers (sound, score, bounce)
  7. Use MessageBoxA popups in the hook for debugging if needed:
/* Debug: show popup when event fires */
char buf[256];
snprintf(buf, sizeof(buf), "E:MYFLAG hit! player=%d pos=%.1f,%.1f,%.1f",
         pidx, x, y, z);
MessageBoxA(NULL, buf, "CustomEvent", MB_OK);

7. Design Patterns for Common Custom Events

One-Shot Trigger (disappears after first touch)

static int g_triggered[4] = {0};

void handle_one_shot(void *board, void *ball, void *coll_entry) {
    int pidx = get_player_index(ball);
    if (g_triggered[pidx]) return;  /* already triggered */

    g_triggered[pidx] = 1;
    /* ... apply effect ... */
}

Parameterized Event (parses values from name string)

Use E:SPEEDPAD<POWER>0.5</POWER> in the MESHWORLD:

void handle_speed_pad(void *board, void *ball, void *coll_entry,
                      const char *name) {
    /* Parse <POWER> tag */
    const char *start = strstr(name, "<POWER>");
    float power = 0.5f;  /* default */
    if (start) {
        power = (float)atof(start + 7);
    }

    /* Apply directional force based on collision normal */
    int *pair = (int *)coll_entry;
    int mesh = pair[1];
    if (mesh && !IsBadReadPtr((void *)mesh, 0x30)) {
        float nx = *(float *)(mesh + 0x20);
        float ny = *(float *)(mesh + 0x24);
        float nz = *(float *)(mesh + 0x28);
        /* Add force to ball velocity accumulators */
        *(float *)((char *)ball + 0x170) += nx * power;
        *(float *)((char *)ball + 0x174) += ny * power;
        *(float *)((char *)ball + 0x178) += nz * power;
    }
}

Warp Pad

static void handle_warp(void *board, void *ball, void *coll_entry) {
    /* Parse <X>, <Y>, <Z> target from event name */
    const char *name = get_event_name(coll_entry);
    /* ... parse coordinates ... */

    /* Set ball position directly */
    *(float *)((char *)ball + 0x164) = target_x;
    *(float *)((char *)ball + 0x168) = target_y;
    *(float *)((char *)ball + 0x16C) = target_z;

    /* Clear velocity to prevent slingshot */
    *(float *)((char *)ball + 0x170) = 0.0f;
    *(float *)((char *)ball + 0x174) = 0.0f;
    *(float *)((char *)ball + 0x178) = 0.0f;
}

Cooldown-Limited Trigger (rate-limited)

Same pattern as E:JUMP — use a counter on the ball:

void handle_cooldown(void *board, void *ball, void *coll_entry) {
    int *ball_ints = (int *)ball;
    /* Check cooldown timer (reuse ball+0x1F7 or another offset) */
    if (ball_ints[0x1F7 >> 2] > 0) return;  /* still cooling down */

    ball_ints[0x1F7 >> 2] = 30;  /* 30-frame cooldown */
    /* ... apply effect ... */
}

8. Reference: All Known Event Types

For reference, here are all existing E: and N: events the game recognizes. Custom events should use names that don't collide with these:

DispatchCollisionEvents (0x40C5D0) — Universal Events

Event Match Effect
E:NODIZZY<TIME>N</TIME> prefix Anti-dizzy zone, duration in frames
E:SAFESWITCH or E:SAFESWITCH(data) prefix Copy data to ball+0xC2C
E:LIMIT exact Arena finish line tracking
E:BREAK exact Ball bounce callback (vtable[0x20])
E:JUMP exact Bounce pad: sound + force + 200 score
E:ACTION<ONCE>TRUE</ONCE><SCORE>N</SCORE> prefix One-time score award
E:TRAJECTORY<X>..</X><Y>..</Y><Z>..</Z> prefix Set ball trajectory vector
N:SECRET prefix Mark rotator triggered
N:UNLOCKSECRET prefix Check arena unlock
N:NOCONTROL exact Disable input 10 frames
N:WATER exact Water physics flag + timer
N:TARPIT exact Tar physics flag
N:GOAL exact Race finish sequence
N:MOUSETRAP exact Deflect ball + rotator collision
E:DROPIN suffix match Sound + 200 score (cooldown 50)
E:PIPEBONK suffix match Random sound + 100 score (cooldown 10)
E:POPOUT suffix match Sound + 100 score (cooldown 50)

TowerCollisionEvents (0x40DCD0) — Race-Only Events

Event Effect
E:CATAPULTBOTTOM Launch catapult
E:OPENSESAME Open trapdoor
N:TRAPDOOR Activate trapdoor
E:BITE Set damage=25.0
E:MACETRIGGER Activate all maces
N:MACE Ball bounce on mace

ExpertCollisionEvents (0x40E6A0) — Arena-Only Events

Event Effect
E:CALLHAMMER Spawn hammer (tournament only)
E:HAMMERCHASE Start hammer chase (tournament only)
E:ALERTSAW1 / E:ALERTSAW2 Pre-activate saw blade
E:ACTIVATESAW1 / E:ACTIVATESAW2 Full activate saw blade
E:ALERTJUDGES Reset all judges
E:SCORE<n> Set time on score displays
E:BELL Activate bell + 500 bonus time
E:JUMP Duplicate of base handler

Other Level-Specific Events (in unnamed handlers)

Event Found In Effect
E:HEATON Neon level handler Turn heat on
E:HEATOFF Neon level handler Turn heat off
E:NOPEGS Toob level handler Remove pegs
E:PEGS Toob level handler Add pegs
E:TRAPPOP Toob level handler Pop trap
E:LAUNCH Tower level handler Launch ball

9. Key Addresses Summary

Address Function Purpose
0x0040C5D0 DispatchCollisionEvents Hook target — universal event dispatcher
0x0040DCD0 TowerCollisionEvents Race-specific events (optional secondary hook)
0x0040E6A0 ExpertCollisionEvents Arena-specific events (optional secondary hook)
0x00402400 Ball_DizzyImmunity Award score to ball
0x00465D90 Mesh_FindClosestCollision Raycast collision (for custom collision checks)
0x00465260 Level_LoadCollision How event planes are loaded from MESHWORLD
0x00408830 Ball_FallUpdate Physics tick that triggers collision dispatch
0x00400000 Image base RVA calculations (addr - 0x400000)

MeshBuffer Struct (0x874 bytes)

Offset Type Field
+0x85D byte interactive (1 for N: and E: prefixes)
+0x863 byte no_render (1 for E: prefix only)
+0x864 char* Event name string pointer

Collision Entry (passed as coll_entry)

Offset Type Field
+0x00 void* scene object pointer (Level/Stands)
+0x04 void* Mesh buffer pointer (has +0x864 = name string)

10. Troubleshooting

Hook doesn't fire

  • Verify the DLL is loaded (add MessageBoxA(NULL, "loaded", "", MB_OK) in DllMain)
  • Check that Sleep(2000) delay is long enough for game init
  • Verify hook address: decompile DispatchCollisionEvents at 0x40C5D0 in Ghidra — the first 5 bytes must be a complete instruction (no mid-instruction split)
  • If using bass.dll proxy: ensure the original bass.dll is renamed to bass_real.dll, not deleted

Event name is (null) or (bad-ptr)

  • The coll_entry pointer may be invalid — check IsBadReadPtr guards
  • The mesh buffer at coll_entry[1] may not have an event name (it's regular geometry, not an event plane)
  • Only meshes with E: or N: prefix names have the string at +0x864 populated

Game crashes on event trigger

  • Most common: calling convention mismatch. Verify DispatchCollisionEvents uses __thiscall (ECX = this, callee cleans 8 bytes). Your hook must match.
  • Stack corruption: ensure your hook function doesn't push extra args or use wrong RET N
  • Writing to invalid ball offset: always IsBadReadPtr before writing
  • Calling game functions with wrong calling convention: Ball_DizzyImmunity is __thiscall, Sound_Play3D is __cdecl

Event fires but no effect

  • The event name string might not match exactly. Use _stricmp for case-insensitive exact match, or _strnicmp for prefix match
  • The event might be firing on the wrong ball (AI ball instead of player). Check ball+0x18 to verify player index
  • Sound pointers might be null — verify app+0x49C is non-zero before calling Sound_Play3D

Visual verification on Wine/llvmpipe

  • Game renders black on llvmpipe — visual testing is not possible on Linux
  • Use MessageBoxA popups or OutputDebugStringA logging for verification
  • Full visual/gameplay testing requires real Windows hardware

🔗 Related Documents

Custom N

types : objects
keywords :

📂 View source on GitHub


Custom N: Event Objects

Documentation of how N: (and E:, DN:) event objects work inside Hamsterball's collision system, and how to create new ones for mods.


Table of Contents

  1. Overview
  2. Event Name Prefixes
  3. How Event Names Are Stored
  4. The Collision Dispatch Chain
  5. Complete Event Catalog
  6. Collision Entry Struct Layout
  7. How to Create a Custom N: Object
  8. Reference: Key Addresses

1. Overview

Hamsterball's MESHWORLD level files contain geometry (triangles) organized in an octree. Each piece of geometry can carry a string name — if that name starts with N:, E:, or DN:, the engine treats it as an event trigger. When the ball's physics body collides with that geometry, the engine reads the string and dispatches to the appropriate handler function.

This means N: events are not objects in the traditional sense (they have no update loop, no vtable, no spawning). They are collision geometry with a tagged name. The "object" is purely the string name attached to a mesh strip in Section 6 of the MESHWORLD file.


2. Event Name Prefixes

Prefix Meaning Dispatch Path
N: Natural/environmental event DispatchCollisionEvents (base)
E: Effect/interaction event Level/Arena handler → DispatchCollisionEvents
DN: Delayed natural event Custom vtable override (e.g. SinkPlatformArenaCollisionEvents) → DispatchCollisionEvents
(none) Bare-name event DispatchCollisionEvents (checked at name+2 offset)
(NOCOLLIDE) Suffix in name Skips collision geometry creation entirely — visual only

The (NOCOLLIDE) suffix can be appended to any name (e.g. T:NEONARROW(NOCOLLIDE)) to tell the engine to skip building collision triangles for that mesh. The geometry renders but the ball passes through.


3. How Event Names Are Stored

3.1 MESHWORLD File (Section 6 — Octree Geometry)

In the meshworld binary format, Section 6 is a recursive octree. Each leaf node contains geometry entries, and each geometry entry has a string name. When the engine loads a level, it reads these strings and stores them with the collision mesh buffers.

3.2 In Memory — Level_LoadMeshes (0x465860)

When the level loads, Level_LoadMeshes iterates every collision geometry entry from the source MeshWorld:

// Simplified from decompiled Level_LoadMeshes (0x465860)
for each geometry entry (iVar14) in sourceMesh:
    // Check for (NOCOLLIDE) — skip if present
    if (entry->name != NULL && strstr(entry->name, "(NOCOLLIDE)") != NULL)
        continue;  // skip — visual only, no collision

    // Allocate a new collision mesh buffer (0x874 bytes)
    meshBuf = CreateMeshBuffer(operator_new(0x874));

    // Copy the event name string
    if (entry->name != NULL):
        meshBuf[0x219] = _strdup(entry->name);  // +0x864 = event name string ptr

        // Set flags based on prefix
        if (strnicmp(entry->name, "N:", 2) == 0)
            meshBuf[0x85D] = 1;  // natural event flag

        if (strnicmp(entry->name, "E:", 2) == 0):
            meshBuf[0x85D] = 1;  // event flag
            meshBuf[0x863] = 1;  // effect flag (triggers Level/Arena handler)

3.3 Collision Mesh Buffer Layout (0x874 bytes)

Offset (hex) Offset (dec) Type Description
+0x00 0 vtable* Mesh buffer vtable (0x4D8E70)
+0x04 4 void* Unused / zero
+0x0C 12 AthenaList Triangle list (each 0x60 bytes)
+0x21C 540 int Counter
+0x85D 2141 byte Is event flag (N: or E: prefix)
+0x85E 2142 byte Flag (set to 0)
+0x85F 2143 byte Flag (set to 0)
+0x861 2145 byte Flag (set to 0)
+0x862 2146 byte Flag (set to 0)
+0x863 2147 byte Is effect flag (E: prefix only)
+0x864 2148 char* Event name string (the N:/E: name)

The +0x864 field is the critical one — this is the string pointer that all collision dispatch functions read to determine which event to fire.


4. The Collision Dispatch Chain

4.1 Physics → Collision → Event Dispatch

Ball_FallUpdate (0x408830)
  → Ball_AdvancePositionOrCollision (0x4564C0)
    → Collision_TraverseSpatialTree (0x465EF0)  — finds colliding triangles
      → Ball_FallUpdate builds collision entry list at physics+0x848
        → Board vtable[0x1D] (+0x74) = collision dispatch callback

4.2 Per-Board Collision Handlers (NOT "Level vs Arena")

IMPORTANT: There is no single "TowerCollisionEvents" or "ExpertCollisionEvents" — these were incorrect Ghidra labels. The truth is that almost every board type overrides vtable[0x1D] with its own unique collision handler. Each handler processes board-specific events, then falls through to DispatchCollisionEvents (0x40C5D0) as the shared base.

The base Scene vtable (0x4D0260) sets vtable[0x1D] = DispatchCollisionEvents directly. Only the WarmUp board uses this default. Every other board overrides it.

Arena boards (Rumble) have their OWN separate vtables and collision handlers, distinct from their race-mode counterparts. Every arena handler includes DN:SINKPLATFORM handling (some via a shared SinkPlatformArenaCollisionEvents at 0x413BD0).

Race Boards

Board vtable[0x1D] Address Handler Events Handled Before Base
WarmUp 0x0040C5D0 DispatchCollisionEvents (no override) (none — uses base directly)
Intermediate 0x0040D340 IntermediateCollisionEvents N:BRIDGE
Dizzy 0x0040D500 DizzyCollisionEvents N:WATERWHEEL, N:WHEELEMBED, N:SWIRL
Tower 0x0040DCD0 TowerCollisionEvents E:CATAPULTBOTTOM, E:OPENSESAM, N:TRAPDOOR, E:BITE, E:MACETRIGGER, N:MACE
Expert 0x0040E6A0 ExpertCollisionEvents E:CALLHAMMER, E:HAMMERCHASE, E:ALERTSAW1, E:ALERTSAW2, E:ACTIVATESAW1, E:ACTIVATESAW2, E:ALERTJUDGES, E:SCORE, E:JUMP, E:BELL
Odd 0x0040ED30 OddCollisionEvents E:GRAVITY, N:JUMPFIRST, N:JUMPSECOND, E:SHRINK, E:GROWSOUND, E:GROW, E:DROPLIFT, E:PIPERANDOM, E:LIMIT, E:LIMITX, E:LIMITZ, E:LIMITPIPE1, E:LIMITPIPE2, E:SWALLOW
Beginner 0x004111E0 BeginnerCollisionEvents N:BUMPER
Master (Arena) 0x00412850 MasterCollisionEvents N:SPINNER, N:BUMPER, E:LAUNCH, E:CALLHAMMER, E:HAMMERCHASE, E:CATAPULTBOTTOM
Sky/Neon 0x00410D00 NeonCollisionEvents E:PEGS, E:TRAPPOP, E:NOPEGS, E:HEATON, E:HEATOFF, E:LIMIT
Toob 0x00410020 ToobCollisionEvents E:ALERTSAW2, E:BRANCH(A/B), N:SPINNY, N:SAWTEETH, N:BUMPER
Up 0x004119B0 UpCollisionEvents E:HELPINERTIA, E:UNHELPINERTIA, E:VACPOPOUT, N:SPEEDCYLINDER, N:EXTRATIME
Wobbly 0x0040F9A0 WobblyCollisionEvents N:SQUAREWOBBLY, N:WAVY
Glass 0x00417EB0 GlassCollisionEvents N:GLASS, DN:SINKPLATFORM
Impossible 0x00418360 ImpossibleCollisionEvents N:BOUNCE, N:ONROTATOR, N:ONGEAR, E:HELPINERTIA, E:UNHELPINERTIA

Arena (Rumble) Boards

Arena Board vtable Addr vtable[0x1D] Addr Handler Events Handled Before Base
Beginner Arena 0x004D14F0 0x00413DF0 BeginnerArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
Intermediate Arena 0x004D15C0 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM
Dizzy Arena 0x004D1680 0x00414350 DizzyArenaCollisionEvents N:SWIRL, DN:SINKPLATFORM
Tower Arena 0x004D1740 0x00414570 TowerArenaCollisionEvents E:CATAPULTBOTTOM, DN:SINKPLATFORM
Up Arena 0x004D17F8 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM
Odd Arena 0x004D1980 0x00414DA0 OddArenaCollisionEvents E:GRAVITY(TYPE), DN:SINKPLATFORM
Expert Arena 0x004D18C8 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM
Toob Arena 0x004D1A40 0x00415010 ToobArenaCollisionEvents N:BUMPER, DN:SINKPLATFORM
Wobbly Arena 0x004D1B18 0x00415540 WobblyArenaCollisionEvents N:SQUAREWOBBLY, DN:SINKPLATFORM
Sky/Neon Arena 0x004D1BD8 0x00413BD0 SinkPlatformArenaCollisionEvents DN:SINKPLATFORM
Warmup Arena 0x004D1C80 0x00416140 WarmupArenaCollisionEvents E:LAUNCH, DN:SINKPLATFORM
Impossible Arena 0x004D2298 0x00418600 ImpossibleArenaCollisionEvents N:BOUNCE, DN:SINKPLATFORM
Master Arena 0x004D12B0 0x00412850 MasterCollisionEvents N:SPINNER, N:BUMPER, E:LAUNCH, E:CALLHAMMER, E:HAMMERCHASE, E:CATAPULTBOTTOM

Key observation: The Intermediate, Up, Expert, and Sky/Neon arenas share the same minimal SinkPlatformArenaCollisionEvents (0x413BD0) handler — it only processes DN:SINKPLATFORM then delegates to the base. The Master Arena reuses the exact same MasterCollisionEvents handler as the Master race board. All other arenas have their own unique handlers.

Dispatch chain for any collision event:

Board vtable[0x1D] (board-specific handler)
  ├─ Process board-specific events (if event name matches)
  │   └─ Return early (for some events) OR fall through
  └─ DispatchCollisionEvents (0x40C5D0) — shared base handler
      └─ Process universal events (N:GOAL, N:TARPIT, N:WATER, E:JUMP, etc.)

Note: Some board handlers return early without calling DispatchCollisionEvents for certain events (e.g., Intermediate returns early on N:BRIDGE, Dizzy returns early on N:WATERWHEEL/N:WHEELEMBED/N:SWIRL). Most fall through to the base.

4.3 How the Dispatch Reads the Event Name

All handlers read the event name the same way:

// param_1 = ball (int*)
// param_2 = collision entry (int*), where:
//   param_2[0] = type/source reference
//   param_2[1] = collision object pointer
//
// Event name string = *(char**)(param_2[1] + 0x864)

char *eventName = *(char **)(param_2[1] + 0x864);

// Then compared via __stricmp / __strnicmp:
if (__stricmp(eventName, "N:GOAL") == 0) { ... }
if (__strnicmp(eventName, "N:SECRET", 8) == 0) { ... }

5. Complete Event Catalog

Events are split into two categories:

  1. Universal events — handled by DispatchCollisionEvents (0x40C5D0), the shared base that ALL boards call
  2. Board-specific events — handled by each board's own vtable[0x1D] override BEFORE calling DispatchCollisionEvents

5.1 Universal Events — DispatchCollisionEvents (0x40C5D0)

These events fire on ALL board types because every board handler calls DispatchCollisionEvents at the end.

N: Events (Universal)

Event Name What It Does Ball Fields Affected
N:SECRET Calls Rotator_MarkTriggered(rotator_ptr) — marks a secret as found Reads *param_2 + 0x47C (rotator)
N:UNLOCKSECRET Calls CheckArenaUnlock(this) — unlocks arena content Uses board (this)
N:NOCONTROL Sets impact timer = 10 (ball loses control briefly) ball[0x202] = 10
N:WATER Sets water flag + timer = 10 (water physics effect) ball+0x2D5 = 1, ball[0xB6] = 10
N:TARPIT Plays tar sound, sets tar flag, disables dizzy ball+0x2CC = 1, ball[0x1DA] = 0, ball[0xB4] = pos, plays sound at board+0x484
N:GOAL Finishes the race for the current player Sets App+0x5D6 (finished flag), plays "Goal!" music, copies camera angles, sets App+0x5F0
N:MOUSETRAP Deflects ball trajectory, plays rotator collision sound, adds to rotator list Scales trajectory vector by _DAT_004CF370, sets Y=15.0, iterates rotator list

E: Events (Universal)

Event Name What It Does Key Details
E:NODIZZY<TIME>N</TIME> Grants dizzy immunity N via Ball_DizzyImmunity Parses XML tags with MWParser_ReadTag
E:SAFESWITCH Copies parenthesized data to ball+0xC2C strchr for ( char, copies string
E:LIMIT Tracks arena completions per player Sets ball+0x2E9=1, increments counters at board+0x47B4/B8/BC/C0
E:BREAK Calls ball vtable[0x20] callback (**(code**)(*ball + 0x20))()
E:JUMP Jump pad: 3D sound + force + impact=10 + score 200 ball[0x1F7]=10, ball[0xA7]=0x3B03126F, ball[0xA8]=1, ball[0x202]=10
E:ACTION(ONCE)(SCORE) Score event with optional once-only tracking Parses XML: ONCE=TRUEAthenaList_Append(ball+0xCB, obj), SCORE=N → adds to App+0x5E4 + pIdx*0xA0
E:TRAJECTORY(X,Y,Z) Sets ball trajectory vector Writes to physics+0xCA4/CA8/CAC

Bare-Name Events (Universal — checked at name+2)

These events are checked by skipping the first 2 characters of the name (comparing at name + 2), meaning they work with or without a prefix:

Event Name What It Does Cooldown Field
DROPIN Sound + score +200 ball[0x1F2] (0x32 = 50 frames)
PIPEBONK Random sound + score +100 ball[500] (0x32)
POPOUT Sound + score +100 ball[499] (0x32)
(4th, at PTR_DAT_004cf80c) Sound ball[0x1F5] (0x32)

5.2 Board-Specific Events — Per-Board vtable[0x1D] Handlers

These events are ONLY processed by specific board types. If the ball touches geometry with one of these names on a different board, it will be ignored (the board handler won't match it, and DispatchCollisionEvents doesn't know about it either).

Tower (0x40DCD0)

Event Name What It Does
E:CATAPULTBOTTOM Sets impact=1000, iterates catapult list at board+0x43B8, calls Catapult_Launch
E:OPENSESAM Opens trapdoor — iterates trapdoor list at board+0x4BE8, calls Trapdoor_Open
N:TRAPDOOR Activates trapdoor — iterates trapdoor list, calls Trapdoor_Activate
E:BITE Sets damage = 25.0 at board+0x43A0, clears board+0x43A8
E:MACETRIGGER Activates all maces — sets mace+0x10F0 = 1
N:MACE Ball bounce callback — calls ball vtable[0x20] if mace conditions met

Expert (0x40E6A0)

Event Name What It Does
E:CALLHAMMER Creates BONK popup (difficulty-gated: App+0x23C != 0)
E:HAMMERCHASE Starts hammer chase (difficulty-gated)
E:ALERTSAW1 / E:ALERTSAW2 Alerts saw blade (difficulty-gated)
E:ACTIVATESAW1 / E:ACTIVATESAW2 Activates saw blade (difficulty-gated)
E:ALERTJUDGES Resets all judges via Judge_Reset
E:SCORE (prefix match) Sets score display time via ScoreDisplay_SetTime
E:JUMP Duplicate of base E:JUMP (Expert has its own copy)
E:BELL (prefix match) Activates bell, awards 500 bonus time + creates ScoreObject

Master / Arena (0x00412850 — MasterCollisionEvents)

Event Name What It Does
N:SPINNER Calls Rotator_AddObject — attaches ball to spinner rotator
N:BUMPER Bumper physics: plays sound, scales/reverses velocity, sets board flag at +0x53FC + bumperIndex*4
E:LAUNCH Launch pad: looks up "LAUNCHPOINT" position, sets upward trajectory (Y=16.0), impact=50, creates ArenaScoreParticle explosion particles
E:CALLHAMMER Creates BONK popup (difficulty-gated)
E:HAMMERCHASE Starts hammer chase (difficulty-gated)
E:CATAPULTBOTTOM Same as Tower — iterates catapult list at board+0x584C, calls Catapult_Launch

Dizzy (0x0040D500)

Event Name What It Does
N:WATERWHEEL Sets ball+0x1DE = 1 (water wheel flag), returns early — does NOT call DispatchCollisionEvents
N:WHEELEMBED Embeds ball in wheel: computes relative position to wheel center (board+0x4BB0), applies rotation transform, sets ball+0x30F = 1 (teleport flag), writes new position to ball+0x310/311/312, sets impact=50. Returns early.
N:SWIRL Sets ball+0x779 = 1 (swirl flag), returns early

Intermediate (0x0040D340)

Event Name What It Does
N:BRIDGE Checks if board+0x4384 == 3 (bridge state). If so, sets ball+0x1DE = 1 and returns early. Otherwise falls through to DispatchCollisionEvents.

Odd (0x0040ED30)

Event Name What It Does
E:GRAVITY(TYPE) Changes gravity type: NORMALBall_ResetCollisionMesh, XBall_SetTiltedGravity, ZBall_SetFlatGravity. Parses XML tags.
N:JUMPFIRST Teleports ball to "JUMPPIPE1" position, sets upward trajectory (Y=16.0)
N:JUMPSECOND Teleports ball to "JUMPPIPE2" position, sets upward trajectory (Y=16.0)
E:SHRINK Calls Ball_Shrink (shrinks ball), teleports to "SHRINKCENTER", sets downward trajectory (Y=-1.0)
E:GROWSOUND Plays grow sound (cooldown: ball[0x1FE], 100 frames)
E:GROW Calls Ball_Grow (restores ball size)
E:DROPLIFT Calls Stands_PlayBreakSound on mesh at board+0x436C
E:PIPERANDOM Randomly teleports ball to "PIPERANDOM1" or "PIPERANDOM2" position, zeroes velocity, plays sound
E:LIMIT If ball[0x1D2] == 0: sets ball+0x1DA = 0, ball+0x2E9 = 1 (qualifies for this limit gate)
E:LIMITX Same as LIMIT but checks ball[0x1D2] == 1
E:LIMITZ Same as LIMIT but checks ball[0x1D2] == 2
E:LIMITPIPE1 If ball[0x1] != 0 (pipe 1 flag): qualifies for limit
E:LIMITPIPE2 If ball[0x5] != 0 (pipe 2 flag): qualifies for limit
E:SWALLOW Sets ball+0xBA = 1 (swallow/pipe entry flag)

Beginner (0x004111E0)

Event Name What It Does
N:BUMPER Bumper physics: plays sound, scales/reverses velocity (similar to Master's N:BUMPER), sets board flag at +0x6428 + bumperIndex*4

Sky / Neon Race (0x00410D00 — NeonCollisionEvents)

Event Name What It Does
E:PEGS Increments peg counter at board+0x47F4, sets ball[0x1E2] = 1 (peg hit flag)
E:TRAPPOP Plays rotator sound (difficulty-gated: App+0x23C != 0)
E:NOPEGS Decrements peg counter at board+0x47F4, sets ball[0x1E3] = 1
E:HEATON Adds ball to pendulum list via Pendulum_AddIndex (difficulty-gated)
E:HEATOFF Removes ball from pendulum list (difficulty-gated)
E:LIMIT Removes ball from pendulum list (difficulty-gated, same as HEATOFF)

Toob (0x00410020)

Event Name What It Does
E:ALERTSAW2 Alerts saw at board+0x4384 (difficulty-gated)
E:BRANCH(A) / E:BRANCH(B) Branching pipe: looks up numbered POS/VECTOR pairs from hash table, randomly selects one, teleports ball with scaled trajectory. (A) = normal, (B) = double velocity.
N:SPINNY Calls Rotator_AddBall — awards score from rotator
N:SAWTEETH Deflects ball: reads position from rotator+0x1100, normalizes and scales by 3.0, calls Ball_ApplyTrajectory. Cooldown: ball[0x1F7].
N:BUMPER Bumper physics: plays sound, scales/reverses velocity, sets board flag at +0x6448 + bumperIndex*4

Up (0x004119B0)

Event Name What It Does
E:HELPINERTIA Sets ball[0xA9] = 2.5 (reduces inertia — easier to control)
E:UNHELPINERTIA Sets ball[0xA9] = 5.0 (restores normal inertia)
E:VACPOPOUT Sets ball[0xA1] = 20.0 (vacuum popout force), plays 3D sound
N:SPEEDCYLINDER Calls Pendulum_PlayCollisionSound — speed boost sound from rotator
N:EXTRATIME Awards 500 bonus time: creates ScoreObject "EXTRA TIME:", calls Timer_Decrement, adds to board's score list at +0x8B8. Checks rotator+0x10E4 (once-only flag).

Wobbly (0x0040F9A0)

Event Name What It Does
N:SQUAREWOBBLY Calls Stands_AddObject — adds ball to wobbly platform physics
N:WAVY Calls Blockdawg_AddObject — adds ball to wavy/blockdawg physics

Glass (0x00417EB0)

Event Name What It Does
N:GLASS Sets ball[0x317] = 0xF (glass break counter — 15 hits to break)
DN:SINKPLATFORM Calls Scene_StartCountdown to sink the platform the ball is standing on

Impossible (0x00418360)

Handles events for the Impossible Race (Race of Ages) — the gear/rotator level.

Event Name What It Does
N:BOUNCE Bounces ball off gears: doubles velocity, clamps to min 1.25 / max 3.0 via normalize-and-scale. Gated on ball+0x1DA != 0 (active flag).
N:ONROTATOR Calls Rotator_AddBall — registers ball on the rotator's tracking list with a 10-frame tick counter. See Rotator System below.
N:ONGEAR Calls Catapult_AddObjectConditional — same pattern as Rotator_AddBall but on a Catapult object (guarded by catapult+0x1510 != 0). Registers ball on the gear's tracking list for rotational movement.
E:HELPINERTIA Sets ball[0xA9] = 2.5 (reduces inertia — easier to control on gears)
E:UNHELPINERTIA Sets ball[0xA9] = 5.0 (restores normal inertia)

The Rotator System (Gears, Swirls, Spinny Objects)

The rotator system is how Hamsterball makes spinning objects physically carry the ball. It involves two functions working together: one registers the ball on contact, the other applies rotation every frame.

Rotator_AddBall (0x43B6F0 — formerly misnamed Rotator_AddBall)

This function does NOT set a score. It registers the ball on a rotator's ball-tracking list.

Rotator_AddBall(Scene* scene, Ball* ball):
    // Search existing list for this ball
    for each entry in scene->rotatorList (AthenaList at scene+0x10F0):
        if entry->ball_ptr == ball:
            entry->tick_counter = 10    // RESET to 10 — ball already tracked
            return

    // Ball not in list — add new entry
    entry = malloc(8)                   // 8-byte struct: [ball_ptr, tick_counter]
    entry->ball_ptr = ball
    entry->tick_counter = 10
    AthenaList_Append(scene->rotatorList, entry)

Critical behavior: The counter resets to 10 every frame the ball is touching the rotator's collision surface. Since Ball_FallUpdate fires N:ONROTATOR/N:SPINNY/N:SWIRL on every frame of contact, Rotator_AddBall finds the ball already in the list and just resets the counter. The 10-frame countdown only starts ticking down after the ball leaves the rotator surface — it's a grace/release period, not a carry limit.

Catapult_Update (0x43E600 — shared update for Catapult AND Rotator objects)

Each frame, this function iterates the ball-tracking list and applies the object's rotation matrix:

Catapult_Update(Catapult* this):
    // Build rotation matrix from this->rotation params (this+0x439..0x43C)
    Gfx_ScaleZ(this->rotSpeedZ)
    Gfx_ScaleX(this->rotSpeedX)
    Gfx_ScaleY(this->rotAngle)
    Gfx_ScaleY(this->rotSpeedAngle)

    for each entry in this->ballList (AthenaList at this+0x43E):
        entry->tick_counter -= 1
        if tick_counter < 1:
            free(entry)          // Remove ball from tracking
            continue

        // Get ball position relative to rotator center
        relX = ball->posX (ball+0x164) - this->centerX (this+0x436)
        relY = ball->posY (ball+0x168) - this->centerY (this+0x437)
        relZ = ball->posZ (ball+0x16C) - this->centerZ (this+0x438)

        // Apply rotation matrix to relative position
        newX = mat[0]*relX + mat[1]*relY + mat[2]*relZ + mat[3]
        newY = mat[4]*relX + mat[5]*relY + mat[6]*relZ + mat[7]

        // Write rotated position back to ball
        ball->posX = newX + this->centerX
        ball->posY = newY + this->centerY
        ball->posZ = newZ + this->centerZ

        // Also rotate ball's velocity vector (ball+0xCA4/+0xCA8/+0xCAC)
        // using the same rotation matrix
        ball->velX = mat[0]*velX + mat[1]*velY + mat[2]*velZ + mat[3]
        ball->velY = mat[4]*velX + mat[5]*velY + mat[6]*velZ + mat[7]
        ball->velZ = ...

Struct layout (Catapult/Rotator object):

Offset Type Field
+0x436 float centerX (rotator pivot X)
+0x437 float centerY (rotator pivot Y)
+0x438 float centerZ (rotator pivot Z)
+0x439 float rotSpeedZ
+0x43A float rotSpeedX
+0x43B float rotSpeedAngle
+0x43C float rotAngle (accumulated)
+0x43E AthenaList ballList (tracked balls with tick counters)
+0x10F0 AthenaList rotatorList (Scene-level, used by Rotator_AddBall)
+0x10F8 AthenaList catapultBallList (used by Catapult_AddObjectConditional)
+0x1510 byte active flag (Catapult_AddObjectConditional guard)

Called from 3 collision handlers:

Collision Handler Event Level
ImpossibleCollisionEvents (0x418360) N:ONROTATOR Impossible race (gears)
ToobCollisionEvents (0x410020) N:SPINNY Toob race
DizzyArenaCollisionEvents (0x414350) N:SWIRL Dizzy arena

6. Collision Entry Struct Layout

When Ball_FallUpdate processes collisions, it builds a collision entry struct that gets passed to the dispatch functions.

6.1 Collision Entry (8 bytes — on stack)

struct CollisionEntry {
    int type_or_source_ref;    // +0x00: reference to source (e.g. rotator ptr)
    void* collision_obj;        // +0x04: pointer to collision mesh buffer (0x874 bytes)
};

The dispatch functions access the event name as:

char *eventName = *(char **)(collision_entry.collision_obj + 0x864);

6.2 Collision Mesh Buffer (0x874 bytes — heap allocated)

struct CollisionMeshBuffer {
    void* vtable;               // +0x00: 0x4D8E70
    void* unused;               // +0x04
    AthenaList triangles;       // +0x0C: list of CollisionFace (0x60 bytes each)
    // ... (collision geometry data) ...
    byte is_event;              // +0x85D: 1 if name starts with N: or E:
    byte is_effect;             // +0x863: 1 if name starts with E:
    char* event_name;           // +0x864: the event string (e.g. "N:TARPIT")
};

7. How to Create a Custom N: Object

There are four approaches, ranging from simplest to most complex.

Approach A: MESHWORLD Level Editing (No Code)

Best for: Adding existing event types to new locations in a level.

Edit a .MESHWORLD file's Section 6 octree to add geometry with an event name. The engine automatically processes it — no code changes needed.

  1. Create a mesh with triangles at the desired location
  2. Name the mesh entry N:TARPIT, E:JUMP, N:GOAL, etc.
  3. Delete all .cached files so the game re-reads the .MESHWORLD
  4. The engine will build collision geometry for the named mesh and dispatch the event when the ball touches it

Limitation: You can only use event names that already exist in the engine. The engine ignores unrecognized names (they get collision geometry but no event fires).

See: reference/raptisoft-exporter/ for the meshworld binary format spec.

Approach B: Fake Collision Entry (CEA Script or DLL)

Best for: Triggering existing event logic at arbitrary positions/times without level geometry.

Construct a fake collision entry struct in memory, fill in the event name, and call DispatchCollisionEvents directly. This is the GluebieSpawn pattern — used to trigger N:TARPIT effects when the ball is near Gluebie objects that have no collision geometry of their own.

CEA Script Pattern (from GluebieSpawn.CEA):

// Allocate fake structures
alloc(fake_coll_obj, 0x868)       // collision mesh buffer (only +0x864 matters)
alloc(fake_coll_entry, 8)          // collision entry struct
alloc(tarpit_str, 16)

// Write event name string
tarpit_str:
  db 'N:TARPIT',00

// Set up fake collision object — only +0x864 is read
fake_coll_obj + 0x864:
  dd tarpit_str

// Set up fake collision entry: [source_ref, collision_obj_ptr]
fake_coll_entry:
  dd 0                    // type/source (0 = generic)
  dd fake_coll_obj        // pointer to fake collision object

// Call DispatchCollisionEvents (thiscall: ECX=board, push entry, push ball)
// DispatchCollisionEvents(board, ball, entry):
//   ECX = board (from ball+0x14)
//   push fake_coll_entry
//   push ball
//   call 0040C5D0        // __thiscall, callee cleans 8 bytes (RET 0x8)

DLL (C) Pattern:

// Build a fake collision entry to trigger N:TARPIT
typedef struct {
    int source_ref;
    void* collision_obj;
} CollisionEntry;

// Minimal collision object — only +0x864 is read
static char fake_coll_obj[0x868];
static const char* tarpit_name = "N:TARPIT";
*(const char**)(fake_coll_obj + 0x864) = tarpit_name;

CollisionEntry entry;
entry.source_ref = 0;
entry.collision_obj = fake_coll_obj;

// Call DispatchCollisionEvents (thiscall)
// ECX = board, params = (ball, &entry)
typedef void (__thiscall *DispatchFn)(void* board, int* ball, CollisionEntry* entry);
DispatchFn DispatchCollisionEvents = (DispatchFn)(0x400000 + 0xC5D0);

void* board = *(void**)(ball + 0x14);   // ball+0x14 = board
DispatchCollisionEvents(board, ball, &entry);
// Note: __thiscall — callee cleans 8 bytes from stack (RET 0x8)

Important calling convention note: DispatchCollisionEvents is __thiscall with RET 0x8 — the callee cleans up 8 bytes (the two pushed parameters). When calling from inline assembly, do NOT add esp, 8 after the call. When calling from C with a __thiscall typedef, the compiler handles this automatically.

Approach C: Hook DispatchCollisionEvents (DLL)

Best for: Adding entirely new event names with custom logic.

Hook DispatchCollisionEvents (0x40C5D0) to intercept the event name string before the original function processes it. If the name matches your custom event, execute your logic. Otherwise, fall through to the original.

// Pseudo-code for a DLL hook
void __fastcall hooked_DispatchCollisionEvents(void* board, void* edx,
                                                int* ball, int* entry) {
    char* eventName = *(char**)(entry[1] + 0x864);

    if (eventName) {
        // Custom event handling
        if (_stricmp(eventName, "N:SPEEDBOOST") == 0) {
            // Your custom logic: add velocity to ball
            *(float*)(ball + 0x170) += 50.0f;  // velocity X
            return;  // don't call original
        }
        if (_stricmp(eventName, "N:GRAVITYFLIP") == 0) {
            // Reverse gravity
            *(float*)(ball + 0xC94) *= -1.0f;
            return;
        }
    }

    // Fall through to original DispatchCollisionEvents
    original_DispatchCollisionEvents(board, ball, entry);
}

The event name would be placed in a custom MESHWORLD file's Section 6 geometry (Approach A), or triggered via a fake collision entry (Approach B).

Key advantage: You can create completely new event names that don't exist in the original engine. The engine will build collision geometry for any N:/E: prefixed name (it sets the flags but doesn't validate the name), and your hook can intercept it.

Approach D: Vtable Override (DLL)

Best for: Adding per-object custom collision handling (like SinkPlatform's DN: pattern).

Override the scene's vtable[0x1D] (+0x74) to point to your own collision handler. This is how SinkPlatformArenaCollisionEvents works — it checks for DN:SINKPLATFORM before calling DispatchCollisionEvents.

// Custom collision handler (same signature as DispatchCollisionEvents)
void __fastcall MyCollisionHandler(void* board, void* edx,
                                    int* ball, int* entry) {
    char* eventName = *(char**)(entry[1] + 0x864);

    if (eventName && _stricmp(eventName, "N:MYCUSTOM") == 0) {
        // Custom logic here
        DoSomething(ball, board);
    }

    // Always fall through to base dispatch
    DispatchCollisionEvents(board, ball, entry);
}

// Install: patch board vtable[0x1D] to point to MyCollisionHandler
DWORD oldProtect;
void** vtable = *(void***)board;  // board's vtable
VirtualProtect(&vtable[0x1D], 4, PAGE_EXECUTE_READWRITE, &oldProtect);
vtable[0x1D] = (void*)MyCollisionHandler;
VirtualProtect(&vtable[0x1D], 4, oldProtect, &oldProtect);

8. Reference: Key Addresses

Functions

Address Name Description
0x0040C5D0 DispatchCollisionEvents Base collision event handler (universal N:/E:/bare events)
Race Board Handlers
0x0040D340 IntermediateCollisionEvents Intermediate race board (N:BRIDGE)
0x0040D500 DizzyCollisionEvents Dizzy race board (N:WATERWHEEL, N:WHEELEMBED, N:SWIRL)
0x0040DCD0 TowerCollisionEvents Tower race board (catapult, trapdoor, mace, bite)
0x0040E6A0 ExpertCollisionEvents Expert race board (hammer, saw, judge, bell, score)
0x0040ED30 OddCollisionEvents odd race board (gravity, pipes, shrink/grow, limits)
0x0040F9A0 WobblyCollisionEvents Wobbly race board (N:SQUAREWOBBLY, N:WAVY)
0x00410020 ToobCollisionEvents Toob race board (branch pipes, spinny, sawteeth, bumper)
0x00410D00 NeonCollisionEvents Sky/Neon race board (pegs, heat, limit)
0x004111E0 BeginnerCollisionEvents Beginner race board (N:BUMPER)
0x004119B0 UpCollisionEvents Up race board (inertia, vacpopout, speed cylinder, extra time)
0x00412850 MasterCollisionEvents Master race board AND Master Arena (spinner, bumper, launch, catapult)
0x00417EB0 GlassCollisionEvents Glass race board (N:GLASS, DN:SINKPLATFORM)
0x00418360 ImpossibleCollisionEvents Impossible race board (N:BOUNCE, N:ONROTATOR, N:ONGEAR, E:HELPINERTIA, E:UNHELPINERTIA)
Arena (Rumble) Board Handlers
0x00413BD0 SinkPlatformArenaCollisionEvents Shared handler for Intermediate/Up/Expert/Sky arenas (DN:SINKPLATFORM only)
0x00413DF0 BeginnerArenaCollisionEvents Beginner arena (N:BUMPER, DN:SINKPLATFORM)
0x00414350 DizzyArenaCollisionEvents Dizzy arena (N:SWIRL, DN:SINKPLATFORM)
0x00414570 TowerArenaCollisionEvents Tower arena (E:CATAPULTBOTTOM, DN:SINKPLATFORM)
0x00414DA0 OddArenaCollisionEvents Odd arena (E:GRAVITY, DN:SINKPLATFORM)
0x00415010 ToobArenaCollisionEvents Toob arena (N:BUMPER, DN:SINKPLATFORM)
0x00415540 WobblyArenaCollisionEvents Wobbly arena (N:SQUAREWOBBLY, DN:SINKPLATFORM)
0x00416140 WarmupArenaCollisionEvents Warmup arena (E:LAUNCH, DN:SINKPLATFORM)
0x00418600 ImpossibleArenaCollisionEvents Impossible arena (N:BOUNCE, DN:SINKPLATFORM)
Other
0x00465860 Level_LoadMeshes Loads collision geometry + copies event names to mesh buffers
0x00458970 CreateMeshBuffer Allocates 0x874-byte collision mesh buffer
0x00408830 Ball_FallUpdate Physics update — triggers collision dispatch
0x00465EF0 Collision_TraverseSpatialTree Octree traversal for collision detection
0x00453810 AthenaList_Append Append to AthenaList (used for collision entry lists)

Key Offsets

Offset On Object Type Description
+0x864 Collision Mesh Buffer char* Event name string (the N:/E: name)
+0x85D Collision Mesh Buffer byte Is event flag (N: or E: prefix)
+0x863 Collision Mesh Buffer byte Is effect flag (E: prefix only)
+0x74 Board/Scene vtable func* vtable[0x1D] — collision dispatch callback
+0x14 Ball void* Board pointer
+0x878 Board void* Scene/App pointer
+0x164 Ball float Position X
+0x168 Ball float Position Y
+0x16C Ball float Position Z
+0x170 Ball float Velocity X (accumulated, cleared each frame)
+0x174 Ball float Velocity Y
+0x178 Ball float Velocity Z
+0x2CC Ball byte Tar flag (set by N:TARPIT)
+0x2D5 Ball byte Water flag (set by N:WATER)
+0x202 Ball int Impact timer (set by N:NOCONTROL, E:JUMP, etc.)
+0x23C App int Difficulty enum (0=Pipsqueak, 1=Normal, 2=Frenzied)
+0x5D6 App byte Player finished flag (set by N:GOAL)
+0x5E4 App float Score (per player, +pIdx*0xA0)

Calling Convention

DispatchCollisionEvents is __thiscall:

  • ECX = board/scene pointer (this)
  • Stack param 1 = ball pointer
  • Stack param 2 = collision entry pointer
  • Returns with RET 0x8 (callee cleans 8 bytes)
; Assembly call pattern:
push fake_coll_entry    ; param 2: collision entry
push ball               ; param 1: ball
mov ecx, board           ; this: board
call 0040C5D0           ; DispatchCollisionEvents
; No "add esp, 8" needed — callee cleans stack (RET 0x8)

Appendix: Existing Mod Examples

GluebieSpawn (tools/gluebie_spawn/GluebieSpawn.CEA)

Uses Approach B (fake collision entry) to trigger N:TARPIT when the ball is near Gluebie objects. The Gluebie mesh has no collision geometry, so the mod constructs a fake collision entry with N:TARPIT as the event name and calls DispatchCollisionEvents directly.

Collision Hook (tools/collision_hook/)

A read-only DLL that hooks all three dispatch functions to log every collision event to a CSV file. Useful for discovering which events fire during gameplay and verifying custom N: objects are working.


🔗 Related Documents

Custom Object Types

types : modding
keywords :

📂 View source on GitHub


Custom Object Types — Modding Plan

Goal: Add custom object types to Hamsterball levels that interact with the game engine
without modifying the original EXE.

Architecture Overview

There are two injection points for custom objects:

  1. Ref Points (Section 1) — Game logic objects looked up by name during scene construction
  2. Octree Geoms (Section 6) — Collision/render geometry with named event triggers

Both are purely data-driven — the game loads them from the .MESHWORLD binary file.
No EXE modification is needed to add named objects to levels. The constraint is
that the EXE only recognizes specific name prefixes for factory dispatch and
collision events.

Strategy: Three Approaches (Escalating Complexity)

Approach A: Binary MW File Editing (Data-Only Modding)

What you can do WITHOUT touching the EXE:

  1. Add new E: event zones with existing event names to any level

    • Insert named collision geometry (e.g., E:JUMP, E:NODIZZY<TIME>300</TIME>)
    • The collision dispatcher (DispatchCollisionEvents) will process any E:/N: name it recognizes
    • Unrecognized names are silently ignored (no crash, no effect)
  2. Add new N: interactive collision objects with existing names

    • E.g., add N:GOAL to a new location, add N:TARPIT zones, N:WATER zones
    • These trigger existing game logic via the collision dispatch chain
  3. Reposition existing ref points (START, FLAG, SAFESPOT, etc.)

    • Edit Section 1 coordinates to change spawn points, checkpoint locations
  4. Add BADBALL enemy balls with custom parameters

    • <CHASE>, <HOME>, <SIZE>, <SPINDISTANCE> tags are parsed from the name string
  5. Add factory objects (BRIDGE, TIPPER, BONK, CATAPULT, etc.)

    • The factory dispatcher (CreateLevelObjects) matches by prefix
    • Adding a CATAPULT ref point + associated mesh data creates a working catapult
  6. Change materials/textures on existing geometry

    • Per-geom material data in Section 6 (ambient/diffuse/specular/emissive/power/texture)

Tool needed: A .MESHWORLD binary editor that can parse and re-serialize the format.
The parser in this project's docs/MESHWORLD_OBJECT_SYSTEM.md documents the full format.

Limitations:

  • Cannot create genuinely NEW behavior (new event names are ignored)
  • Cannot add new factory types (the prefix list is hardcoded in the EXE)
  • Cannot change physics parameters (gravity, friction, etc. are in the EXE)

Approach B: DLL Proxy Hooking (Runtime Behavior Extension)

What you can do with a proxy DLL (bass.dll or d3d8.dll):

  1. Hook the collision dispatch to intercept DispatchCollisionEvents (0x40C5D0)

    • Intercept collider->name before the EXE processes it
    • Recognize custom E:CUSTOM_* names and execute custom logic
    • Then call the original DispatchCollisionEvents for standard events
  2. Hook the factory dispatcher CreateLevelObjects (0x4121D0)

    • Intercept ref point names before the EXE matches them
    • Recognize custom prefixes (e.g., CUSTOM_BOSS, CUSTOM_TELEPORTER)
    • Allocate custom game objects, register them in the scene's active object list
  3. Hook Mesh_FindClosestCollision (0x465D90)

    • Add custom collision response for custom named objects
    • Could implement warp pads, speed pads, gravity wells, etc.
  4. Add per-frame update hooks via the scene update loop

    • Hook Scene_UpdateBallsAndState to run custom object logic each frame
    • Access game state through known struct offsets (Scene, Ball, App)

Implementation pattern (bass.dll proxy, proven to work for FPS mods):

// Hook DispatchCollisionEvents to intercept custom event names
typedef void (__thiscall *DispatchCollisionEvents_t)(void* this, int* ball, int* collObj);
DispatchCollisionEvents_t original_DispatchCollisionEvents = NULL;

void __thiscall Hooked_DispatchCollisionEvents(void* this, int* ball, int* collObj) {
    char* eventName = *(char**)(collObj[1] + 0x864);
    
    // Check for custom event names
    if (strnicmp(eventName, "E:CUSTOM_WARP", 17) == 0) {
        // Custom teleport logic: move ball to a target ref point
        Vec3 target = HashTable_Lookup(scene_hashtable, "TELEPORT_TARGET");
        ball->pos_x = target.x;
        ball->pos_y = target.y;
        ball->pos_z = target.z;
        return; // Skip original handler
    }
    
    // Fall through to original handler for standard events
    original_DispatchCollisionEvents(this, ball, collObj);
}

Key addresses for hooking:

  • DispatchCollisionEvents = 0x40C5D0 (base collision event handler)
  • TowerCollisionEvents = 0x40DCD0 (race-specific events)
  • ExpertCollisionEvents = 0x40E6A0 (arena-specific events)
  • CreateLevelObjects = 0x4121D0 (factory dispatcher)
  • CreateExpertLevelObjects = 0x40E250 (arena factory sub-dispatcher)
  • Mesh_FindClosestCollision = 0x465D90 (collision raycast)
  • Scene_SpawnBallsAndObjects = 0x41C5B0 (scene initialization)
  • Scene_UpdateBallsAndState — per-frame update (call via vtable)
  • Image base: 0x400000 (use RVA = addr - 0x400000 for ASLR safety)

Limitations:

  • Requires C/C++ compilation (MinGW cross-compile, proven workflow)
  • Must handle VS2003 CRT heap isolation (use HeapAlloc with game heap, not malloc)
  • All struct offsets must be verified against the specific EXE version
  • DLL proxy pattern is well-established (bass.dll proxy works, documented in skill)

Approach C: Full EXE Patching (Binary Modification)

What you can do by patching the EXE directly:

  1. Add new factory dispatch entries in CreateLevelObjects

    • Extend the __strnicmp chain to recognize new prefixes
    • Allocate and initialize custom game object structs
  2. Add new collision event handlers in DispatchCollisionEvents

    • Extend the __stricmp chain to handle new E:/N: names
  3. Modify physics constants (gravity, friction, max speed, etc.)

  4. Add new vtable entries for custom object update/render/dtor

  5. Patch the octree loader to handle custom data sections

Implementation: Direct binary patching with hex editor or patcher script
(similar to the 8ball AI patches at 0x4083D3, 0x4085D1, 0x408598).

Limitations:

  • Most invasive approach — changes the original EXE
  • Must preserve code alignment and instruction boundaries
  • Limited by available code cave space for new logic
  • Each patch must be version-specific

Recommended Implementation Plan

Phase 1: MW File Editor Tool (Approach A)

Build a Python tool that can:

  1. Parse any .MESHWORLD binary file into structured data
  2. Edit ref points (add/remove/reposition)
  3. Add named collision geometry to the octree
  4. Modify materials and textures
  5. Re-serialize to valid .meshworld format

This enables immediate modding with all existing object types.

Phase 2: Custom Event DLL (Approach B)

Build a bass.dll proxy that:

  1. Hooks DispatchCollisionEvents at 0x40C5D0
  2. Recognizes custom E:CUSTOM_* event names
  3. Implements a registry of custom event handlers (configurable via INI)
  4. Provides custom events: teleport, speed pad, gravity flip, ball size change, etc.

Phase 3: Custom Factory Objects (Approach B+)

Extend the proxy DLL to:

  1. Hook CreateLevelObjects at 0x4121D0
  2. Recognize custom ref point prefixes
  3. Allocate custom game objects with update/render/collision vtables
  4. Register in scene's active object list (+0x2578)
  5. Implement custom object types: moving platforms, collectibles, bosses

Custom Object Type Design Space

Given the engine architecture, feasible custom object types include:

Type Implementation Data Required
Warp Pad Hook DispatchCollisionEvents, E:CUSTOM_WARP → move ball Target ref point in Section 1
Speed Pad Hook DispatchCollisionEvents, E:CUSTOM_SPEED → add velocity Direction + magnitude in name
Gravity Zone Hook DispatchCollisionEvents, E:CUSTOM_GRAVITY → change gravity Direction in XML tags
Size Changer Hook DispatchCollisionEvents, E:CUSTOM_GROW/SHRINK → change ball radius New radius in name
Moving Platform Hook factory, custom prefix → animate mesh Waypoints in Section 1
Collectible Hook DispatchCollisionEvents, N:CUSTOM_COIN → score + disappear Score value in name
Custom Hazard Hook DispatchCollisionEvents, N:CUSTOM_HAZARD → damage/respawn Damage value in name
Checkpoints++ Hook DispatchCollisionEvents, E:CUSTOM_CHECKPOINT → track progress Order in name
Wind Zone Hook per-frame, apply force in region Direction + strength
Ice Surface Hook per-frame, reduce friction in zone Friction value

Key Technical Constraints

  1. Image base: 0x400000. Use RVA = addr - 0x400000, then GetModuleHandle(NULL) + RVA for ASLR safety.
  2. CRT heap: operator_new/malloc crash cross-module (VS2003 CRT critical sections). Use HeapAlloc(GetProcessHeap(), 0, size) directly.
  3. Struct sizes: Ball=0xC60, Scene=varies (0x6000+), App=0x2328, CollisionLevel=0x10D0, MeshBuffer=0x874.
  4. 16384 triangle cap: Game crashes if a single level exceeds 2^14 triangles.
  5. String format: Length-prefixed (int32 + data with NUL terminator).
  6. Axis swap: All positions in MW files are x,z,y (Max Z-up → engine Y-up). NOT in-engine — only in file I/O.
  7. Name writing rule: Exporter only writes geom names where name[1] == ':' or name contains "NOCOLLIDE". Custom objects must use a X: prefix to have their name preserved.
  8. CollisionLevel: Each level's collision geometry is a separate MeshWorld loaded via Level_LoadCollision. Named collision faces are stored as MeshBuffer objects in the MeshWorld's object list.

🔗 Related Documents

D3D8 [[14178578133750|rendering pipeline]] Deep D

types : rendering
keywords :

📂 View source on GitHub


D3D8 Rendering Pipeline Deep Dive

Overview

Hamsterball uses Direct3D 8 (D3D8) for all rendering. The pipeline is organized
into multiple passes with specific render states for each. Graphics_RenderScene
(0x454BC0) is the main entry point called each frame.

Graphics Struct (0x800 bytes)

Offset Type Description
+0x154 IDirect3DDevice8* D3D8 device pointer
+0x224 float[16] View matrix
+0x264 float[16] Projection matrix
+0x2A4 float[16] World matrix
+0x5C void* D3D8 vtable
+0x710 void*[8] Render pass list (8 passes)
+0x730 DWORD Z enable state
+0x734 byte Z write enable
+0x738 DWORD Alpha blend enable
+0x73C float Z near
+0x740 float Z far
+0x748 Matrix* Frustum matrix

Render Pipeline (Graphics_RenderScene 0x454BC0)

Pass 1: Setup Lights

Graphics_SetupLights(param_1);  // +0x454630

Sets up D3D8 light sources before rendering.

Pass 2: Copy Matrices

Copies view/projection/world matrices from Graphics struct to local variables:

  • +0x224 → local_40 (view, 16 floats)
  • +0x264 → local_40 (projection, 16 floats)
  • +0x2A4 → auStack_8c (world, 16 floats)

Pass 3: Set View Matrix

Gfx_SetViewMatrix(param_1, local_40);  // +0x454C18
D3DDevice_SetTransform(D3DTS_VIEW, ...)

Applies view matrix to D3D device.

Pass 4: Compute Frustum (First Pass)

Matrix_ComputeFrustum(Graphics+0x748);
D3DDevice_SetTransform(D3DTS_PROJECTION, 0x100);

Computes view frustum for culling, sets projection.

Pass 5: Render Pass 1 (Opaque Objects)

Iterates 8 render passes at Graphics+0x710:

for (i = 0; i < 8; i++) {
    if (pass[i] != NULL) {
        pass[i]->vtable[0x3]();  // Render pass
    }
}

Pass 1 typically renders opaque geometry (no alpha).

Pass 6: Z-Buffer Management

// Save current Z state
DWORD saved_z_enable = Graphics+0x730;
byte saved_z_write = Graphics+0x734;

// Disable Z write for alpha pass
D3DDevice_SetRenderState(D3DRS_ZWRITEENABLE, FALSE);

// Set viewport Z
Graphics_SetViewportZ(near, far);

// Enable alpha blending  
D3DDevice_SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

Switches from opaque to alpha-transparent rendering.

Pass 7: Render Pass 2 (Alpha Objects)

Renders same 8 passes again but with alpha-enabled states.

Pass 8: Present

D3DDevice_BeginScene();   // vtable[0x88]
D3DDevice_EndScene();     // vtable[0x78]

Presents frame to screen.

Key D3D8 Render States

State Value Purpose
D3DRS_ZENABLE 0x8B Z-buffer enable
D3DRS_ZWRITEENABLE 0x1C Z write enable
D3DRS_ALPHABLENDENABLE 0x22 Alpha blending
D3DTS_VIEW 0x100 View matrix transform
D3DTS_PROJECTION 0x102 Projection matrix transform
D3DTS_WORLD 0x100 World matrix transform

Scene Rendering (Scene_RenderAllObjects 0x45E0E0)

Called within render passes. Iterates all scene objects:

  1. For each gadget in Scene+0x858 (gadget list):
    • gadget->vtable0xC = Render method
  2. For each object in Scene+0x25B8 (object list):
    • object->vtable0xC = Render method

Material & Shader System

Graphics_ApplyMaterialAndDraw (0x455110)

Applies material properties before drawing:

void Graphics_ApplyMaterialAndDraw(Mesh *mesh, Material *mat) {
    // Set diffuse color
    D3DDevice_SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, ...);
    
    // Set ambient
    D3DDevice_SetRenderState(D3DRS_AMBIENTMATERIALSOURCE, ...);
    
    // Set specular
    D3DDevice_SetRenderState(D3DRS_SPECULARMATERIALSOURCE, ...);
    
    // Apply texture
    if (mat->texture != NULL) {
        D3DDevice_SetTexture(0, mat->texture->d3d_tex);
    }
    
    // Draw mesh
    Mesh_Draw(mesh);
}

Texture Loading (Graphics_FindOrCreateTexture 0x455C50)

  • Checks texture cache first
  • Loads PNG/BMP via D3DXCreateTextureFromFile
  • Stores in cache for reuse
  • 7 xrefs - called for all texture loads

Depth Bias & Shadow Rendering

The game uses depth bias for shadow mapping:

Graphics+0x730 = z_enable flag
Graphics+0x73C = z_near (viewport near)
Graphics+0x740 = z_far (viewport far)
Graphics+0x738 = alpha_blend_enable

These are toggled between opaque and alpha passes.

Viewport Management

Graphics_SetViewport (0x454F10)

Sets D3D viewport from Graphics struct:

  • X, Y position
  • Width, Height
  • MinZ, MaxZ (depth range)

Graphics_SetProjection (0x454AB0)

31 xrefs - most called graphics function.
Sets projection matrix with FOV and aspect ratio.

Lighting

Graphics_SetupLights (0x454630)

Configures up to 8 D3D lights:

  • Light type (point/spot/directional)
  • Position/Direction
  • Diffuse/Specular/Ambient colors
  • Attenuation factors

Complete Graphics API

Address Function Purpose
0x4542C0 Graphics_ctor Initialize graphics engine
0x453B50 Graphics_BeginFrame Start frame, clear buffers
0x454BC0 Graphics_RenderScene Main render entry
0x454AB0 Graphics_SetProjection Set perspective projection
0x454B50 Graphics_SetViewport Set viewport dimensions
0x454F10 Graphics_SetViewport Set viewport (8 xrefs)
0x453C90 Graphics_CreateDevice Create D3D8 device
0x455110 Graphics_ApplyMaterialAndDraw Material + mesh draw
0x455C50 Graphics_FindOrCreateTexture Load/cached texture
0x455D60 Graphics_DrawScreenRect 2D screen rectangle (63 xrefs!)
0x455A90 Graphics_PresentOrEnd Present to screen
0x454630 Graphics_SetupLights Configure D3D lights
0x453900 Graphics_ClearViewport Clear color/depth buffers
0x453970 Graphics_SetCullMode2 Set cull mode
0x4539A0 Graphics_SetViewportZ Set depth range
0x454190 Graphics_SetRenderMode Set render mode flags
0x454D30 Graphics_Reset Reset graphics state
0x455A60 Graphics_Defaults Set default render states
0x454550 Graphics_Cleanup Shutdown graphics
0x455360 Graphics_dtor Destructor

🔗 Related Documents

Deep-Dive

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/death-pending-flag-deep-dive.md)


Deep-Dive: Ball+0x2E9 (impact_shatter)

Overview

Ball+0x2E9 is a sticky limit/trajectory flag that, once set, permanently alters ball collision behavior until the ball is fully reconstructed. It is NOT an "on_ramp" flag, NOT a ground-contact flag, and NOT cleared per-frame.

Naming History

Previous documentation labeled this field as on_ramp, on_surface, flag2, or is_teleporting. All of these labels are wrong. The correct name is impact_shatter because it limits trajectory application and can trigger ball shatter.

Initialization

Function Address Action Byte Offset
Ball_ctor2 0x004039E0 *(byte*)((int)this + 0x2E9) = 0 0x2E9 (correct, uses (int) cast)

Setting

Only set in Ball_Update (0x00405E00), at L798:

// Inside collision type 5 (floor) handler:
if (piVar16[0x15] > 1.0  &&  is_shrunk == 0) {
    *(byte*)((int)param_1 + 0x2E9) = 1;   // SET (uses (int) cast = byte offset 0x2E9)
    Scene_SetCamera(param_1[5], param_1, 1);
    Graphics_SetViewport(..., param_1[0x59], param_1[0x5A]);
    // Viewport bounds check → may set swallow flag (param_1[0xBA] = 1)
}

Trigger conditions:

  1. Ball is colliding with a type-5 (floor) surface
  2. Surface speed (piVar16[0x15], collision_obj+0x54) > 1.0
  3. is_shrunk (ball+0xC4C) == 0 (ball is NOT in shrunk state)

When these conditions are met, the flag is set AND:

  • Scene_SetCamera is called (camera follows ball)
  • Graphics_SetViewport sets up the viewport around the ball position
  • A viewport bounds check may set the swallow flag (ball+0xBA = 1), which triggers respawn

Clearing

Ball_FindClosestRespawnPoint (0x00405190) clears the flag at address 0x00405262:

00405262: C6 86 E9 02 00 00 00    MOV byte [ESI+0x2E9], 0

The Ghidra decompilation shows *(undefined1 *)(param_1 + 0x2e9) = 0 without an (int) cast. Previous documentation (ball-ground-detection.md) claimed this was int* arithmetic writing to +0xBA4 instead of +0x2E9. This was wrong. The actual disassembly confirms it is MOV byte [ESI+0x2E9], 0 — a direct byte write to offset 0x2E9 via the ModRM 9E encoding ([ESI+disp32]). The flag IS properly cleared on respawn.

Ball_ctor2 (0x004039E0) also initializes it to 0:

; At ctor2+0x1FE: 88 9E E9 02 00 00 = MOV [ESI+0x2E9], BL  (BL pre-loaded with 0)

Result: impact_shatter is cleared both on respawn (Ball_FindClosestRespawnPoint) and on full reconstruction (Ball_ctor2). It is NOT sticky — the previous "sticky flag" claim was based on an incorrect int* arithmetic assumption that the disassembly disproves.

Effects When Set (impact_shatter = 1)

1. Skip Ball_ApplyTrajectory (L498)

if (piVar16[0] == 1) {   // type 1 = surface/wall collision
    if (piVar16[0x19] == unaff_EBP) {   // belongs to this board
        if (param_1[0xBB] > 1 && impact_shatter == 0) {
            Ball_ApplyTrajectory(param_1);   // BOUNCE/REFLECT
        }

When impact_shatter = 1: Ball_ApplyTrajectory is skipped. The ball does not bounce off walls. This means the ball slides along surfaces instead of reflecting.

2. Trigger Ball_Shatter on Wall Hit (L502)

if (impact_shatter == 1) {
    if (velocity_check_based_on_axis) {
        (*vtable[8])();   // calls Ball_FallDeath = Ball_Shatter!
    }
}

The velocity check depends on ball+0x1D2 (axis selector):

  • Axis 0 (Y): board+0xCA8 >= 0.0 (ball moving up or stationary)
  • Axis 1 (X): board+0xCA4 < 0.0 (ball moving in negative X)
  • Axis 2 (Z): board+0xCAC >= 0.0 (ball moving in positive Z or stationary)

If the ball is moving in the "wrong" direction for the current axis AND hits a wall, the ball shatters (breaks into debris pieces, plays sound, triggers respawn).

Ball_FallDeath (0x00409480, vtable[8]):

  • Sets ball+0x2E8 = 1 (shattered flag)
  • Checks is_shrunk (0xC4C) to select which shatter sound to play
  • Creates 3 Ball_Split_ctor debris pieces at the ball's position
  • Copies velocity, position, radius to each debris piece
  • Creates ArenaScoreParticle objects (score popup particles)
  • This is essentially the same as Ball_Shatter (0x00408D70) but called from a different path

3. Dead Code: Position-Match Shatter (L642)

if (impact_shatter != 0 && param_1[0xC9] == 0) {
    if (ABS(ball_pos - ramp_entry_pos) < _DAT_004cf4f8) {   // threshold = 0.0
        (*vtable[8])();   // Ball_Shatter
    }
}

_DAT_004cf4f8 = 0.0, and ABS(x) < 0.0 is always false for any float. This code path never executes.

Interaction with is_shrunk (0xC4C)

The impact_shatter is only SET when is_shrunk == 0. If the ball is in shrunk state (our half-size mod), impact_shatter cannot be set by type-5 floor collisions. This means:

  • Shrunk balls never get their trajectory limited
  • Shrunk balls never trigger the shatter-on-wall-hit behavior
  • The camera/viewport setup at L798 is also skipped for shrunk balls

This is likely intentional game design: in odd race, when the ball is shrunk inside the shrink pipes, it shouldn't shatter from floor contact.

Summary Table

Aspect Detail
Field Ball+0x2E9 (byte)
Name impact_shatter
Init 0 (by Ball_ctor2)
Set by Type-5 floor collision, speed > 1.0, is_shrunk == 0
Cleared by Ball_FindClosestRespawnPoint (0x00405262: MOV byte [ESI+0x2E9],0) and Ball_ctor2 (0x004039E0+0x1FE: MOV [ESI+0x2E9],BL)
Effect 1 Skip Ball_ApplyTrajectory — no wall bouncing
Effect 2 Trigger Ball_Shatter on wall hit if moving in "wrong" direction
Effect 3 Dead code: position-match shatter (threshold = 0.0, never triggers)
vtable[8] Ball_FallDeath (0x00409480) — shatter function called from this path
Interaction with is_shrunk is_shrunk=1 prevents impact_shatter from being set

🔗 Related Documents

DirectInput & Input Handling S

types : input
keywords :

📂 View source on GitHub


DirectInput & Input Handling System

Overview

Hamsterball uses DirectInput8 (DirectInput8Create at 0x47C7F0) for input from
keyboard, mouse, and joystick devices. The input system supports up to 4 players
with different device types: 1=keyboard, 2=mouse, 4=joystick1, 5=joystick2.

InputDevice Structure (0x91C bytes)

Created by App_CreateInputDevice (0x46C050), actually calls SoundDevice_ctor
(this appears to be an error in naming — it's a generic Device base class).

Key offsets (based on App+0x1E4, 0x1E8 player device pointers):

Offset Type Description
+0x04 int Device type (1=keyboard, 2=mouse, 4=joystick)
+0x10 void* DirectInput device handle
+0x18C DIDEVICEINSTANCE Device instance data
+0x10C int Joystick X axis value
+0x110 int Joystick Y axis value
+0x114 int Joystick Z axis value
+0x434 void* Keyboard key state array

Input Device Initialization

App_Initialize_Full (0x429530) — Input Setup Steps 15-22

Step 15: InputDevice_SetType(App+0x1E4, 1)     // Player 1 = keyboard
Step 16: InputDevice_SetType(App+0x1E8, 1)     // Player 2 = keyboard
Step 17: InputDevice_SetType(App+0x1E4, 2)     // Player 1 = mouse
Step 18: InputDevice_SetType(App+0x1E8, 2)     // Player 2 = mouse
Step 19: InputDevice_SetType(App+0x1E4, 4)     // Player 1 = joystick 1
Step 20: InputDevice_SetType(App+0x1E8, 4)     // Player 2 = joystick 1
Step 21: InputDevice_SetType(App+0x1E4, 5)     // Player 1 = joystick 2
Step 22: InputDevice_SetType(App+0x1E8, 5)     // Player 2 = joystick 2

The game tries all device types during initialization. The final type
is set based on which devices are detected/connected.

DirectInput8Create (0x47C7F0)

Standard DirectInput COM entry point:

result = DirectInput8Create(hinst, 0x800, IID_IDirectInput8W, &di, NULL);

InputDevice Functions

Address Function Description
0x46C050 App_CreateInputDevice Allocate 0x91C device struct
0x46C110 App_CreateInputHandler Create input handler
0x6DFC0 InputDevice_SetType Set device type (1=kb, 2=mouse, 4=joy1, 5=joy2)
0x46E0B0 Input_IsKeyDown Check if DIK key is pressed
0x46EBD0 InputDevice_PollAndRelease Poll device, release buffers
0x46EC30 Ball_GetInputForce Get 2D force vector from input
0x46EE10 InputHandler_Ctor Input handler constructor

Ball Input Force (Ball_GetInputForce 0x46EC30)

Maps raw device input to ball movement force:

Mode 1 — Keyboard

Key state array at: *(int*)(ball + 4) + 0x434
DIK codes at: +0x50C (left), +0x510 (right), +0x514 (up), +0x518 (down)
Check: key_state[DIK_code + 0xC] & 0x80 (high bit = pressed)

UP:   force_y = -0.5  // forward
DOWN: force_y = 1.0   // backward  
LEFT: force_x = -1.0
RIGHT: force_x = 1.0

Mode 2 — Mouse

center_x = gfx->width / 2
center_y = gfx->height / 2
GetCursorPos(&cursor)
force_x = cursor.x - center_x
force_y = cursor.y - center_y
If mouse capture active: SetCursorPos(center_x, center_y) // recenter

Mode 3-6 — Joystick (4 devices)

pos.x = joystick->axis_x / 100
pos.y = joystick->axis_y / 100
NormalizeAndScale to unit circle
force_x = normalized.x
force_y = normalized.y

Output is multiplied by per-ball speed scale (ball+0xC).

Input Checking

Input_CheckKeyCombo (0x428F10)

14 xrefs — checks for keyboard shortcut combos:

  • Checks multiple key codes simultaneously
  • Returns TRUE if all keys in combo are pressed

Input_CheckJoystickButtons (0x428FB0)

1 xref — checks joystick button states.

Input Dispatch (Scene_HandleInput 0x4692F0)

Main input routing (51 xrefs):

1. Clear active_input_object at Scene+0x864
2. Iterate gadgets in Scene+0x858 (AthenaList)
3. For each gadget where input_active (+0x16 != 0):
   - Call gadget->vtable[1](msg1, msg2) // input handler
   - If gadget captures (+0x14 != 0): active = gadget
   - If player1 (+0x19 != 0): channel = App+0x1E4
   - If player2 (+0x15 != 0): channel = App+0x1E8
4. Play sound via App+0x1DC

Mouse Input

Input_OnMouseDown (0x46C760)

2 xrefs — mouse button down event.

Input_OnMouseUp (0x46C430)

2 xrefs — mouse button up event.

Input_OnMouseUpCapture (0x46C3C0)

2 xrefs — mouse up with cursor capture/release.

Key Codes (DIK Constants)

Keyboard uses DirectInput key constants (DIK_*):

DIK Code Key
DIK_UP Arrow Up
DIK_DOWN Arrow Down
DIK_LEFT Arrow Left
DIK_RIGHT Arrow Right
DIK_SPACE Spacebar
DIK_ESCAPE Escape

These are stored in the InputDevice structure and looked up by
Ball_GetInputForce to check key states.

Input in Loading Screen

LoadingScreen_HandleInput (0x42D020)

1 xref — handles input during the loading screen (resource loading phase).
Allows user to skip or cancel during initial load.

Registration Dialog

RegisterDialog_HandleInput (0x4475A0)

4 xrefs — handles input in the registration dialog.
Used for entering registration keys.

Key Input Constants

DirectInput keyboard scan codes used:

  • 0x50C (DIK_LEFT) — stored at InputDevice+0x50C
  • 0x510 (DIK_RIGHT) — stored at InputDevice+0x510
  • 0x514 (DIK_UP) — stored at InputDevice+0x514
  • 0x518 (DIK_DOWN) — stored at InputDevice+0x518

The key state array format:

key_state = *(int*)(device + 0x434)
key_pressed = key_state[DIK_code + 0xC] & 0x80

Input Device Detection

The game attempts to detect and configure input devices during startup.
App_CreateInputHandler (0x46C110) manages device enumeration.

Multiple input types can be configured simultaneously:

  • Player 1 can use keyboard, mouse, or joystick
  • Player 2 can use keyboard, mouse, or joystick
  • Each player's device is stored at App+0x1E4 and App+0x1E8

Input Sound Feedback

Input events trigger sound effects via App+0x1DC:

  • Menu navigation sounds
  • Button click sounds
  • Race start countdown sounds

🔗 Related Documents

Direction Detect Mod

types : mods
keywords :

📂 View source on GitHub


Direction Detect Mod

Detects which direction the hamster ball is facing and displays it as an on-screen compass overlay.

Features

  • On-screen compass: Shows facing direction (N/NE/E/SE/S/SW/W/NW) and heading angle in degrees
  • Velocity display: Shows current XZ velocity vector and speed magnitude
  • Engine facing fields: Cross-references the engine's own facing_direction cos/sin fields at ball+0x194/0x198
  • Toggle: Press D to show/hide the overlay
  • Log file: Writes direction data to direction_log.txt in the game directory for debugging

How It Works

The mod uses two mechanisms:

  1. Background polling thread (~60fps): Finds Player 1's ball by scanning the Scene's ball list (Scene+0x29D4 AthenaList), then reads:

    • Ball velocity X/Z (ball+0x170/0x178) → computes heading via atan2f(vz, vx)
    • Ball speed (ball+0x188)
    • Engine's facing direction cos/sin (ball+0x194/0x198)
  2. Render hook: Hooks Graphics_PresentOrEnd (0x455A90) via a code cave. Draws text before calling the original Present function — this is the correct timing (after viewport clear, before Present/EndScene).

Compass Mapping

The Hamsterball coordinate system is Y-up, with X and Z as the horizontal plane:

  • atan2(vz, vx) gives the heading angle
  • 0° = East (+X), 90° = North (+Z), 180° = West (-X), 270° = South (-Z)

Installation

  1. Rename original bass.dllbass_real.dll in the Hamsterball directory
  2. Copy this bass.dll to the same directory
  3. Launch Hamsterball.exe

Build

i686-w64-mingw32-gcc -shared -o bass.dll direction_detect.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias -lm

Controls

Key Action
D Toggle direction overlay on/off

Display

Three lines of text appear at top-center of the screen (x=360, y=20):

Facing: NE (47 deg)
Vel: (123, -45)  Spd: 131.0
Engine: cos=0.731 sin=0.682

Struct Offsets Used

Offset Type Field
ball+0x018 int player_index (0 = Player 1)
ball+0x164 float position X
ball+0x168 float position Y
ball+0x16C float position Z
ball+0x170 float velocity X
ball+0x174 float velocity Y
ball+0x178 float velocity Z
ball+0x188 float speed (velocity magnitude)
ball+0x194 float facing_direction cos (engine-computed)
ball+0x198 float facing_direction sin (engine-computed)

Architecture

  • Pattern 4 (volatile flag + polling thread): The background thread reads ball state and stores it in volatile globals. The render hook's code cave calls a C function (draw_direction_overlay) that reads these globals and draws text using UI_DrawTextShadow_Wrapper (0x409B90).
  • The code cave at Graphics_PresentOrEnd (0x455A90) does: PUSHFD/PUSHAD → CALL draw_overlay → POPAD/POPFD → execute original 7 bytes → JMP back.
  • Text is drawn using UI_DrawTextShadow_Wrapper (0x409B90, 15 params, RET 0x3C) with white text and black shadow for readability.

🔗 Related Documents

docs/agent-knowledge

types : agent-knowledge

📂 View source on GitHub


docs/agent-knowledge

Self-contained bootstrapping documentation for future agents continuing the Hamsterball reverse-engineering project.

Start here: INDEX.md

This package captures project setup, Ghidra workflow, struct verification, common RE patterns, reimplementation lessons, and known failure modes. It is meant to be read by an autonomous agent with no prior project context.


🔗 Related Documents

Drawbridge & Trapdoor

types : docs
keywords :

📂 View source on GitHub


Drawbridge & Trapdoor: Complete Reverse Engineering

Overview

Both objects are spawned by CreateTowerObjects (factory @ 0x0040D7C0) in Tower Race.
They can be spawned globally using a CEA hotkey, but require different setup.


1. DRAWBRIDGE (Glass_Level)

Factory Spawn Sequence

; At 0x40D956-0x40D9DF in CreateTowerObjects
push 0x113C                ; alloc size = 4412 bytes
call operator_new (0x4BA57B)
add esp, 4
; if alloc succeeded:
mov edx, [esi+0x4370]      ; mesh = Board+0x4370 (Drawbridge MeshWorld)
push edx                   ; param_2 = mesh_ptr
push esi                   ; param_1 = Board
mov ecx, eax               ; this = alloc
call Glass_Level_ctor (0x4384A0)  ; ret 8
; Copy position from param_block+4/+8/+0xC to obj+0x10D8 (3 floats)
push edi                   ; obj
lea ecx, [esi+0x2578]
call AthenaList_Append (0x453810)  ; Board+0x2578 (general list)
push edi                   ; obj
lea ecx, [esi+0x4BE8]
call AthenaList_Append (0x453810)  ; Board+0x4BE8 (drawbridge list)

Constructor: Glass_Level_ctor @ 0x4384A0

Calling convention: __thiscallecx=alloc, push Board, push mesh_ptr → ret 8

Steps:

  1. Calls Stands_ctor(this, mesh_ptr) @ 0x462850 — base class init
    • Sets vtable to 0x4D8FB0 (Stands base)
    • Inits AthenaLists at +0x18, +0x488, +0x8A0, +0xCB8
    • Copies mesh data (vertices, spatial tree) from mesh_ptr
    • Creates Timer (0x44 bytes) at +0x434
  2. Sets vtable to 0x4D5060 (Glass_Level/Impossible_Level vtable)
  3. Stores Board at +0x10D0
  4. Zeroes position at +0x10D8/+0x10DC/+0x10E0
  5. Sets +0x10E4 = 0 (rotation/state)
  6. Sets +0x10E8 = -1.0 (0xBF800000, break direction)
  7. Allocates CollisionLevel (0x10D0 bytes) → calls CollisionLevel_ctorWithLevel(alloc, this) @ 0x465080
    • Stores result at +0x10D4
    • Links Timer from this+0x434 into CollisionLevel+0x434
  8. Creates Timer array (2 × 0x44 bytes) via _eh_vector_constructor_iterator
  9. Allocates TimerArray (0x90 bytes) via 0x475980 → stores at +0x10EC
  10. Looks up chain bridge collision strings:
    • "Chain1Bridge" @ 0x4D5BB8 → stores at obj+0x10F0
    • "Chain2Bridge" @ 0x4D5BA8 → stores at obj+0x10FC
    • "Chain1Wall" @ 0x4D5B9C → stores at obj+0x1120
    • "Chain2Wall" @ 0x4D5B90 → stores at obj+0x112C

Struct Layout (0x113C bytes)

Offset Size Description
+0x000 4 vtable ptr (0x4D5060)
+0x004 4 mesh data ptr (from Stands_ctor)
+0x018 0x420 AthenaList (inline objects)
+0x438 0x50 AthenaList
+0x434 4 Timer ptr (0x44 bytes, owned)
+0x480 4 mesh field (from Stands_ctor)
+0x488 0x418 AthenaList
+0x8A0 0x418 AthenaList
+0xCB8 0x418 AthenaList
+0x10D0 4 Board ptr (parent scene)
+0x10D4 4 CollisionLevel ptr (owned, 0x10D0 bytes)
+0x10D8 4 position X
+0x10DC 4 position Y
+0x10E0 4 position Z
+0x10E4 4 rotation/state (0)
+0x10E8 4 break direction (-1.0 = 0xBF800000)
+0x10EC 4 TimerArray ptr (owned, 0x90 bytes)
+0x10F0 12 Chain1Bridge position (Vec3)
+0x10FC 12 Chain2Bridge position (Vec3)
+0x1108 1 flag (set to 1 in ctor)
+0x1120 12 Chain1Wall position (Vec3)
+0x112C 12 Chain2Wall position (Vec3)
+0x1138 4 state flag

Vtable: 0x4D5060

Index Address Function
[0] 0x438730 scalar_deleting_dtor
[9] 0x45DFD0 SceneObject_CallUpdate wrapper
[18] 0x43F2F0 Update (calls base + chain animation)
[21] 0x45DF90 SceneObject_CallRender (tail-call)
[22] 0x45DF80 SceneObject_CallTimerUpdate (tail-call)
[24] 0x438830 scalar_deleting_dtor (alt)

Collision

  • No dedicated handler in TowerCollisionEvents (0x40DCD0)
  • Collision is via the CollisionLevel sub-object at obj+0x10D4
  • CollisionLevel has its own spatial tree built from the Drawbridge mesh
  • Chain bridge/wall names are looked up in the mesh's entity table for collision triggers

Mesh Dependency

  • Board+0x4370 = MeshWorld("Levels\Level4-Drawbridge")
  • Loaded by LevelBoard_Tower_ctor (0x41E340) at line 72
  • On non-Tower levels: must load manually via MeshWorld_ctor(0x10D0, App+0x174, "Levels\\Level4-Drawbridge")

AthenaList Dependencies

  • Board+0x2578 (general objects) — initialized by Board_ctor on ALL levels ✓
  • Board+0x4BE8 (drawbridge list) — initialized by LevelBoard_Tower_ctor ONLY

2. TRAPDOOR (GlassStands)

Factory Spawn Sequence

; At 0x40DA82-0x40DB5C in CreateTowerObjects
push 0x10F8                ; alloc size = 4344 bytes
call operator_new (0x4BA57B)
add esp, 4
; if alloc succeeded:
push esi                   ; param_1 = Board
mov ecx, eax               ; this = alloc
call GlassStands_Ctor (0x438290)  ; ret 4
; Copy position from param_block+4/+8/+0xC to obj+0x10E0 (3 floats)
push edi                   ; obj
lea ecx, [esi+0x2578]
call AthenaList_Append (0x453810)  ; Board+0x2578 (general list)
push edi                   ; obj
lea ecx, [esi+0x47D0]
call AthenaList_Append (0x453810)  ; Board+0x47D0 (trapdoor list)
; Append sub-objects to collision lists:
mov ecx, [edi+0x10D8]     ; sub-object 1 (Stands)
push ecx
lea ecx, [esi+0xCD4]
call AthenaList_Append     ; Board+0xCD4
mov edx, [edi+0x10DC]     ; sub-object 2 (TipperVisual)
push edx
lea ecx, [esi+0x10EC]
call AthenaList_Append     ; Board+0x10EC
; Append to collision levels:
mov ecx, [esi+0x8AC]      ; Board+0x8AC (primary CollisionLevel)
mov eax, [edi+0x10D8]     ; sub-object 1
mov ecx, [ecx+0x480]      ; CollisionLevel+0x480
push eax
add ecx, 0x1C             ; AthenaList at CollisionLevel+0x480+0x1C
call AthenaList_Append
mov eax, [edi+0x10DC]     ; sub-object 2
mov ecx, [esi+0x8B0]      ; Board+0x8B0 (secondary CollisionLevel)
push eax
add ecx, 0x18             ; AthenaList at CollisionLevel+0x18
call AthenaList_Append

Constructor: GlassStands_Ctor @ 0x438290

Calling convention: __thiscallecx=alloc, push Board → ret 4

Steps:

  1. Reads App from Board+0x878
  2. Calls Stands_ctor(this, App+0x594) @ 0x462850 — base class init with Trapdoor1 mesh
    • App+0x594 = MeshWorld("Levels\Level4-Trapdoor1") — globally pre-loaded!
  3. Sets vtable to 0x4D4FF8 (GlassStands vtable)
  4. Stores Board at +0x10D0
  5. Allocates TipperVisual (0x10D0 bytes) using App+0x59C (CollisionLevel of Trapdoor1)
    • Calls 0x4661A0 (TipperVisual_ctor) with App+0x59C as mesh
    • Stores at +0x10D4
    • Calls Level_LoadMeshes(+0x10D4, this) @ 0x465200
  6. Allocates Stands (0x10D0 bytes) using App+0x598 (MeshWorld of Trapdoor2)
    • Calls Stands_ctor(alloc, App+0x598) @ 0x462850
    • Stores at +0x10D8
  7. Allocates TipperVisual (0x10D0 bytes) using App+0x5A0 (CollisionLevel of Trapdoor2)
    • Calls 0x4661A0 (TipperVisual_ctor) with App+0x5A0 as mesh
    • Stores at +0x10DC
    • Calls Level_LoadMeshes(+0x10DC, +0x10D8) @ 0x465200
  8. Sets +0x10EC = 0 (timer/state)
  9. Sets +0x10F0 = -1.0 (0xBF800000, break direction)
  10. Sets +0x10F4 = 0 (triggered flag)

Struct Layout (0x10F8 bytes)

Offset Size Description
+0x000 4 vtable ptr (0x4D4FF8)
+0x004 4 mesh data ptr (from Stands_ctor)
+0x018 0x420 AthenaList (inline objects)
+0x434 4 Timer ptr (0x44 bytes, owned)
+0x480 4 mesh field
+0x488 0x418 AthenaList
+0x8A0 0x418 AthenaList
+0xCB8 0x418 AthenaList
+0x10D0 4 Board ptr (parent scene)
+0x10D4 4 TipperVisual ptr (Trapdoor1 collision, owned)
+0x10D8 4 Stands ptr (Trapdoor2 mesh, owned)
+0x10DC 4 TipperVisual ptr (Trapdoor2 collision, owned)
+0x10E0 4 position X
+0x10E4 4 position Y
+0x10E8 4 position Z
+0x10EC 4 timer/state (0)
+0x10F0 4 break direction (-1.0)
+0x10F4 4 triggered flag (0 = closed, 10 = opening)

Vtable: 0x4D4FF8

Index Address Function
[0] 0x4383F0 scalar_deleting_dtor
[9] 0x45DFD0 SceneObject_CallUpdate wrapper
[11] 0x4342C0 GlassStands_Dtor (cleanup)
[18] 0x45E0E0 Update (Stands_Update base)
[21] 0x45DF90 SceneObject_CallRender (tail-call)
[22] 0x45DF80 SceneObject_CallTimerUpdate (tail-call)
[24] 0x423E0000 (float data, NOT a function — vtable ends at [23])

Note: vtable at 0x4D4FF8 has only 24 entries (0x60 bytes). The values at [24]=0x423E0000
and [25]=0x42A00000 are float constants (40.0 and 80.0), not function pointers.
The next vtable (Glass_Level at 0x4D5060) starts at [26].

Collision

Handled in TowerCollisionEvents (0x40DCD0):

  1. "N:TRAPDOOR"Trapdoor_Activate(obj) @ 0x438410

    • Iterates Board+0x47D0 (trapdoor list) via Board+0x4BDC (heap array)
    • Matches by obj+0x10D4 or obj+0x10DC == colliding object
    • Sets obj+0x10F4 = 10 (activation timer)
    • Plays 3D sound at obj+0x10E0/+0x10E4/+0x10E8
  2. "E:OPENSESAME"Trapdoor_Open(obj) @ 0x4344D0

    • Iterates Board+0x4BE8 (drawbridge list) via Board+0x4FF4 (heap array)
    • Opens the first trapdoor in the list

Trapdoor_Activate @ 0x438410

if (obj+0x10F4 == 0):           // only if not already activated
    if (obj+0x10EC < threshold): // timer check
        play 3D sound at position
    obj+0x10F4 = 10             // set activation timer

Trapdoor_Open @ 0x4344D0

if (obj+0x10E4 == threshold):
    obj+0x10E4 = 1.0  (0x3F800000)

Mesh Dependency

NONE! Trapdoor uses App-level meshes that are globally pre-loaded:

  • App+0x594 = MeshWorld("Levels\Level4-Trapdoor1") — loaded by resource loader (0x4298C0)
  • App+0x598 = MeshWorld("Levels\Level4-Trapdoor2")
  • App+0x59C = CollisionLevel(App+0x594)
  • App+0x5A0 = CollisionLevel(App+0x598)

AthenaList Dependencies

  • Board+0x2578 (general) — initialized by Board_ctor ✓
  • Board+0x47D0 (trapdoor) — initialized by LevelBoard_Tower_ctor ONLY
  • Board+0xCD4 — initialized by Board_ctor ✓ (or BoardLevel5)
  • Board+0x10EC — initialized by Board_ctor ✓ (or BoardLevel5)
  • Board+0x8AC→+0x480→+0x1C — collision list, exists on all levels
  • Board+0x8B0→+0x18 — collision list, exists on all levels

Global Spawn Requirements

Drawbridge

  1. Load mesh: MeshWorld_ctor(0x10D0, App+0x174, "Levels\\Level4-Drawbridge") → Board+0x4370
  2. Init AthenaList: AthenaList_Init(Board+0x4BE8, 0)
  3. Allocate: operator_new(0x113C)
  4. Construct: Glass_Level_ctor(alloc, Board, Board+0x4370) (thiscall, ret 8)
  5. Set position: obj+0x10D8/+0x10DC/+0x10E0 = X/Y/Z
  6. Append: AthenaList_Append(Board+0x2578, obj)
  7. Append: AthenaList_Append(Board+0x4BE8, obj)

Trapdoor

  1. No mesh loading needed (App+0x594/+0x598/+0x59C/+0x5A0 pre-loaded)
  2. Init AthenaList: AthenaList_Init(Board+0x47D0, 0)
  3. Allocate: operator_new(0x10F8)
  4. Construct: GlassStands_Ctor(alloc, Board) (thiscall, ret 4)
  5. Set position: obj+0x10E0/+0x10E4/+0x10E8 = X/Y/Z
  6. Append: AthenaList_Append(Board+0x2578, obj)
  7. Append: AthenaList_Append(Board+0x47D0, obj)
  8. Append: AthenaList_Append(Board+0xCD4, obj+0x10D8) (sub-object 1)
  9. Append: AthenaList_Append(Board+0x10EC, obj+0x10DC) (sub-object 2)
  10. Append: AthenaList_Append(Board+0x8AC→+0x480→+0x1C, obj+0x10D8) (collision)
  11. Append: AthenaList_Append(Board+0x8B0→+0x18, obj+0x10DC) (collision)

Key Function Addresses

Address Function Calling Convention
0x4BA57B operator_new push size, call, add esp 4
0x4BA74D operator_delete (free) push ptr, call, add esp 4
0x453810 AthenaList_Append ecx=list, push obj, call (ret 4)
0x453210 AthenaList_Init ecx=list, push 0, call (ret 4)
0x461510 MeshWorld_ctor ecx=alloc, push App+0x174, push path, call (ret 8)
0x462850 Stands_ctor ecx=alloc, push mesh_ptr, call (ret 4)
0x465080 CollisionLevel_ctorWithLevel ecx=alloc, push source_level, call (ret 4)
0x465200 Level_LoadMeshes ecx=dest, push source, call (ret 4)
0x4661A0 TipperVisual_ctor ecx=alloc, push mesh_ptr, call (ret 4)
0x4384A0 Glass_Level_ctor (Drawbridge) ecx=alloc, push Board, push mesh, call (ret 8)
0x438290 GlassStands_Ctor (Trapdoor) ecx=alloc, push Board, call (ret 4)
0x438410 Trapdoor_Activate ecx=trapdoor_obj, call
0x4344D0 Trapdoor_Open ecx=trapdoor_obj, call
0x40DCD0 TowerCollisionEvents thiscall(Board, ball, collObj)
0x40D7C0 CreateTowerObjects thiscall, processes MESHWORLD objects
0x41E340 LevelBoard_Tower_ctor thiscall, loads Tower meshes + inits lists

Mesh Path Strings (in .rdata)

Address String Board Offset Used By
0x4D099C Levels\Level4-Drawbridge +0x4370 Drawbridge
(global) Levels\Level4-Trapdoor1 App+0x594 Trapdoor
(global) Levels\Level4-Trapdoor2 App+0x598 Trapdoor

App-Level Resource Loading (0x4298C0)

All trapdoor meshes are loaded GLOBALLY for every level:

App+0x594 = MeshWorld("Levels\\Level4-Trapdoor1")     // mesh
App+0x598 = MeshWorld("Levels\\Level4-Trapdoor2")     // mesh
App+0x59C = CollisionLevel(App+0x594)                   // collision from trapdoor1
App+0x5A0 = CollisionLevel(App+0x598)                   // collision from trapdoor2

Drawbridge mesh is Tower-specific, loaded only by LevelBoard_Tower_ctor.


🔗 Related Documents

Drop-in Levels Folder (Beginne

types : mods

📂 View source on GitHub


Drop-in Levels Folder (Beginner ↔ Intermediate Swapped)

Complete copy of the game's Levels/ folder with Beginner Race and Intermediate Race swapped.

What's swapped

File Original This folder
LevelCascade.MESHWORLD Beginner Race Intermediate Race (Level2 data)
Level2.MESHWORLD Intermediate Race Beginner Race (LevelCascade data)

Everything else is original. No .cached or .bak files included.

CRITICAL: Delete .cached files in YOUR game folder!

The game caches pre-processed level data in .cached files. If any .cached files exist in your game's Levels/ folder, the game will use the cached version and IGNORE your swapped .MESHWORLD files.

Before copying this folder:

  1. Go to your game's Levels/ folder
  2. Delete ALL *.cached files (e.g. level1.cached, level2.cached, Level2-Bridge.cached, etc.)
  3. Delete ALL *.bak files (e.g. Level1.MESHWORLD.bak)
  4. Delete the entire Levels/ folder (or rename to Levels_backup/)
  5. Copy this entire levels/ folder as the new Levels/ folder
  6. Do NOT run the game before installing the bass.dll mod — running the game regenerates .cached files from whatever .MESHWORLD files are present

Level mapping (for reference)

Race name Game filename
Warm-up Race Level1.MESHWORLD
Beginner Race LevelCascade.MESHWORLD
Intermediate Race Level2.MESHWORLD
Dizzy Race Level3.MESHWORLD
Tower Race Level4.MESHWORLD
Up Race LevelUp.MESHWORLD
Expert Race Level5.MESHWORLD
odd race Level6.MESHWORLD
Toob Race Level8.MESHWORLD
Wobbly Race Level7.MESHWORLD
Glass Race LevelGlass.MESHWORLD
Sky Race Level9.MESHWORLD
Master Race Level10.MESHWORLD
Neon Race LevelDark.MESHWORLD
Impossible Race LevelImpossible.MESHWORLD

🔗 Related Documents

Dual Platform Arena v2

types : mods

📂 View source on GitHub


Dual Platform Arena v2

Overview

Custom arena replacement for Warm-Up Arena (Arena-WarmUp.MESHWORLD). Features two large circular platforms with C-shaped railings, connected by a wide bridge.

v2 Fixes (from v1)

Issue in v1 Fix in v2
Players 1-4 cramped together (single START1-1) START2-1 through START2-4 spread across platform at 150-unit intervals
8-ball stuck Removed BADBALL ref points (game spawns 8-balls automatically in arena mode)
Platforms too small (r=200) Bigger platforms: r=350
No railings → balls fall off C-shaped railings on each platform (40 units high, 60° gap toward bridge)
Bridge too narrow (80 units) Wider bridge: 240 units total width
Wrong camera ref name (CameraLocus1) CAMERALOOKAT (matches original game format)
Missing STANDS/SAFESPOT Added STANDS and 3 SAFESPOT ref points

Layout

       Platform A (pink, r=350)        Bridge (brown, 240 wide)        Platform B (blue, r=350)
       center: (-600, 0, 0)                                              center: (+600, 0, 0)
       
  START2-4(-750,67,+150)                                              START2-4(+750,815,+150)
       |                                                                     |
  START2-1(-750,67,-150)    [C-railing gap → bridge → gap← C-railing]   START2-3(+750,67,-150)
       |                                                                     |
  START2-3(-450,67,-150)                                              START2-3(+450,815,-150)
       |                                                                     |
  START2-2(-450,67,+150)                                              START2-2(+450,815,+150)
       
  C-shaped railing: 40 units high, gap faces bridge (60° opening)

Geometry

Component Vertices Triangles Material
Platform A (pink cylinder) 384 128 (0.99, 0.63, 1.0)
Platform B (blue cylinder) 384 128 (0.42, 0.62, 0.91)
Railing A (C-shape, gray) 900 252 (0.7, 0.7, 0.8)
Railing B (C-shape, gray) 900 252 (0.7, 0.7, 0.8)
Bridge (brown box) 24 12 (0.55, 0.42, 0.30)
Total 2384 772 5 geoms

Ref Points (Section 1)

Name Position Purpose
START2-1 (-750, 67, -150) Player 1 spawn (back-left of Platform A)
START2-2 (-450, 67, +150) Player 2 spawn (front-right of Platform A)
START2-3 (-450, 67, -150) Player 3 spawn (back-right of Platform A)
START2-4 (-750, 67, +150) Player 4 spawn (front-left of Platform A)
CAMERALOOKAT (0, 0, 0) Camera orbit center between platforms
PLATFORM × 10 Various Arena platform spawn positions
SAFESPOT × 3 On each platform + bridge Respawn safety points
STANDS (0, -776, 0) Arena spectator stands

C-Shaped Railings

Each platform has a curved railing that covers 300° of the circumference, with a 60° gap facing the bridge. The railing is:

  • Height: 40 units above platform surface
  • Radius: platform_radius + 8 (slightly outside edge)
  • Thickness: 10 units (inner r-5 to outer r+5)
  • Gap direction: Platform A gap faces +X (toward bridge), Platform B gap faces -X
  • Color: Light gray (0.7, 0.7, 0.8)

Installation

# Backup original
cp Levels/Arena-WarmUp.MESHWORLD Levels/Arena-WarmUp.MESHWORLD.bak

# Install custom arena
cp DualPlatformArenaV2.MESHWORLD Levels/Arena-WarmUp.MESHWORLD

Testing Results

Tested in original Hamsterball.exe on Wine/llvmpipe (Xvfb display :99):

  • ✅ Level loads correctly — binary format parses without errors (83158 bytes)
  • ✅ Race starts — timer counting, ball on platform, TARGET: 15 displayed
  • ✅ Ball stays on platform — physics working, ball doesn't fall through
  • ✅ 3D geometry renders — platform surface visible
  • ⚠️ Materials render dark on Wine/llvmpipe (known D3D8 lighting issue — colors will show on real Windows GPU)
  • ⚠️ Camera follows ball closely in race mode — can't see both platforms simultaneously (arena mode uses wider camera)

Regeneration

python3 tools/create_dual_arena_v2.py DualPlatformArenaV2.MESHWORLD

🔗 Related Documents

Entity Limit Fixer v7

types : mods
keywords :

📂 View source on GitHub


Entity Limit Fixer v7

Prevents freezes and crashes when spawning many entities (8-balls, player clones) in Hamsterball arenas.

What it does

When ball count exceeds MAX_BALLS (default 5), all Mesh_FindClosestCollision calls are skipped — returning "no collision" (99999.0f) instead of building an expensive SpatialTree from level geometry each call.

Root cause

Mesh_FindClosestCollision (0x465D90) builds a full SpatialTree + CollisionMesh from level geometry on every call, traverses it, then frees everything. Each call takes ~1ms. Called from 5 call sites:

  1. Ball_Update (0x40651F): 2× per ball per frame
  2. Ball_Update (0x407557): 2× per ball per frame
  3. Ball_FindClosestRespawnPoint (0x405C46): ~16× per fallen ball per frame
  4. Scene_UpdateArenaPhysics (0x4406EE): 1× per ball per frame
  5. Ball_FindMeshCollision (0x4039CD): 1× per BounceBall per frame

With 10 balls: ~50+ SpatialTree builds per frame → 50ms+ → freeze.

v7 approach

Instead of patching each call site individually (v5/v6 only covered 3 of 5), v7 patches Mesh_FindClosestCollision's entry point directly. A global flag SKIP_COLLISIONS is set once per frame at Scene_UpdateBallsAndState entry when ball count > MAX_BALLS. When the flag is set, the function returns immediately with 99999.0f — covering all 5 call sites with a single patch.

Patch Address Description
A 0x41B540 Hook Scene_UpdateBallsAndState → set SKIP_COLLISIONS flag
B 0x465D90 Hook Mesh_FindClosestCollision → skip if flag set
C 0x4BA58D operator_new → return NULL (crash protection)
D 0x4083D9 Skip AI O(N²) loop 1 when flag set
E 0x408548 Skip AI O(N²) loop 2 when flag set

Build

i686-w64-mingw32-gcc -shared -o bass.dll bass_proxy.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias

Adjustable parameters (CE address list)

  • MAX_BALLS (default 5): Ball count threshold for skipping collision
  • SKIP_COLLISIONS: Runtime flag (1=skip, 0=normal), set automatically

Behavior

  • ≤5 balls: Full collision detection, game runs 100% normally
  • >5 balls: All Mesh_FindClosestCollision calls skip the expensive SpatialTree build. Balls won't collide with level geometry but still bounce off each other (ball-ball collision is a separate system). Balls that fall still respawn normally.

Files

  • EntityLimitFixer.CEA — CE AutoAssembler script
  • bass_proxy.c — DLL proxy source
  • bass.dll — Compiled DLL proxy

🔗 Related Documents

Entity Performance Fix

types : mods
keywords :

📂 View source on GitHub


Entity Performance Fix

Problem

The game freezes/crashes when too many entities (balls, clones, 8-balls) are active in arenas, especially when they fall/respawn simultaneously.

Root Cause Analysis (Ghidra decompilation trace)

Bottleneck 1: O(N²) Ball-Ball Collision

Function: Scene_UpdateBallsAndState (0x41B540) → Ball_Update (0x405E00)

Each frame, the game iterates ALL balls in the AthenaList at Scene+0x29D4 and calls vtable[4] (Ball_Update) for each. Ball_Update internally:

  • Allocates a SpatialTree (0x20 bytes) via operator_new at 0x40689A
  • Allocates a CollisionNode (0x14 bytes) via operator_new at 0x4068F7
  • Recursively traverses the spatial tree via Collision_TraverseSpatialTree (0x465EF0)
  • Iterates all collision entries (O(N) per ball)
  • For ball-ball collision (type==1): full physics response per pair

Total cost: O(N²) per frame for N balls, plus 0x34 bytes of heap allocation per ball per frame.

Bottleneck 2: O(N²) Mass Despawn

Function: AthenaList_Remove (0x453690)

When balls fall off the arena, they're removed from the AthenaList. The remove function does:

  1. malloc(count * 4) — allocate a new array
  2. Copy all entries except the removed one
  3. free(old_array)
  4. Decrement 0x20 (32) iterator indices

This is O(N) per removal. When M balls fall simultaneously: O(M × N) total.

Bottleneck 3: Heavy Respawn Scan

Function: Ball_FindClosestRespawnPoint (0x405190)

Scans ALL spawn points in Scene+0x1518 AthenaList. For each spawn point:

  • stricmp / strstr string comparisons
  • Distance calculation (3D magnitude)
  • When a candidate is found: calls Mesh_FindClosestCollision (0x465D90) which builds a full spatial tree from scratch (AthenaList_Init → CollisionMesh_ctor → SpatialTree_ctor → CollisionMesh_AddTriangle → Ball_AdvancePositionOrCollision → full cleanup)

Cost: O(spawn_points) per respawn, with a heavy collision mesh build per candidate.

Bottleneck 4: Per-Add Heap Realloc

Function: AthenaList_Append (0x453780)

Does realloc(ptr, (count+1)*4) PER ADD. O(N) heap churn per spawn, causes fragmentation.

Bottleneck 5: Iterator Limit

Function: AthenaList_NextIndex (0x4532B0)

Wraps at 0xFF (255). Hard limit on concurrent nested iterations — if exceeded, iterators corrupt each other's indices.

Solution: CE AutoAssembler Script

Four hooks that cap work per frame:

Hook Address Function Effect
1 0x41B540 Scene_UpdateBallsAndState entry Reset per-frame counters, store ball count
2 0x41B58A First loop vtable[4] call Skip non-player balls when count > cap
3 0x41B62F Second loop vtable[4] call Same skip for secondary entity list
4 0x405190 Ball_FindClosestRespawnPoint entry Throttle respawns to N per frame

Skip Logic

  • Player balls (ball+0x18 != -1) always get full physics
  • Non-player balls use hash-based rotation: (ball_ptr >> 4) XOR frame_counter
  • Each frame, MAX_BALLS_PER_FRAME / total_count fraction of non-player balls get processed
  • All balls get processed within ceil(count / MAX) frames
  • At 60 FPS with 100 balls and MAX=18: each ball updates every ~6 frames (10 Hz)

Performance Impact

Ball Count Without Fix With Fix (MAX=18) Improvement
50 ~50 balls × 50 collisions = 2500 ops ~18 × 50 = 900 ops 2.8x
100 ~100 × 100 = 10000 ops ~18 × 100 = 1800 ops 5.5x
200 ~200 × 200 = 40000 ops ~18 × 200 = 3600 ops 11x

Adjustable Parameters (CE Address List)

  • MAX_BALLS_PER_FRAME (default 18): Max balls with full physics per frame
  • MAX_RESPAWNS_PER_FRAME (default 3): Max respawns per frame
  • BALL_COUNT: Current ball count (read-only, for monitoring)

Usage

  1. Open Cheat Engine, attach to Hamsterball.exe
  2. File → Load → select EntityPerformanceFix.CEA
  3. Enable the script (check the box)
  4. Add MAX_BALLS_PER_FRAME and MAX_RESPAWNS_PER_FRAME to your address list to adjust at runtime

Tuning Guide

  • Still lagging? Decrease MAX_BALLS_PER_FRAME to 10-12
  • Balls too jerky? Increase MAX_BALLS_PER_FRAME to 24-30
  • Respawns too slow? Increase MAX_RESPAWNS_PER_FRAME to 5-8
  • Respawn storms still lag? Decrease MAX_RESPAWNS_PER_FRAME to 1-2

🔗 Related Documents

EntityLimitFixer v5

types : tools
keywords :

📂 View source on GitHub


EntityLimitFixer v5

Prevents freezes AND crashes when spawning many entities (8-balls, clones) in arenas.

What changed from v4

v4 still froze because it only patched:

  • operator_new (crash fix)
  • AI loops (partial — only 8-balls)
  • Respawn throttle (too aggressive — every 3rd frame)

v5 fixes the freeze at the source by patching the THREE compounding per-frame costs:

The Freeze Root Cause (traced via GhidraMCP)

Per frame at 30 balls (10 fallen clones, 16 respawn points):

  1. Ball_FindClosestRespawnPoint (called every frame per fallen clone):
     → iterates ALL 16 respawn points
     → for EACH: calls Mesh_FindClosestCollision at 0x405C46
       (builds SpatialTree + CollisionMesh + traverses geometry + frees)
     → 10 × 16 = 160 spatial tree builds per frame

  2. Ball_Update (called per ball per frame):
     → calls Mesh_FindClosestCollision at 0x40651F (floor collision)
     → calls Mesh_FindClosestCollision at 0x407557 (wall collision)
     → 20 × 2 = 40 spatial tree builds per frame

  3. Ball_AI_ChaseNearest (called per 8-ball per frame):
     → two O(N) loops scanning ALL balls = O(N²) total
     → 30 × 30 = 900 iterations per frame

  TOTAL: ~200 spatial tree builds + thousands of allocs per frame
  → game thread takes >16ms → FREEZE

v5 Patches (all verified via GhidraMCP disassembly)

# Address What How
1 0x4BA58D operator_new crash Replace CALL CRT_ThrowBadAlloc with XOR EAX,EAX; POP ESI; RET — return NULL instead of throwing
2a 0x40651F Mesh_FindClosestCollision #1 in Ball_Update Cave: if ball count > MAX_BALLS, skip call and write 99999 (no collision)
2b 0x407557 Mesh_FindClosestCollision #2 in Ball_Update Same cave pattern — skip when too many balls
3 0x405C46 Mesh_FindClosestCollision in RespawnPoint NOP the CALL — pick nearest respawn by distance without collision check
4a 0x4083D9 AI Loop 1 (near ball bonus) Cave: skip O(N²) loop when count > MAX_BALLS
4b 0x408548 AI Loop 2 (find nearest target) Cave: skip O(N²) loop when count > MAX_BALLS
5 0x405190 Ball_FindClosestRespawnPoint Cave: throttle to every 60th frame (was 3rd in v4)

Key insight

Mesh_FindClosestCollision (0x465D90) is the most expensive per-ball operation — it builds a full SpatialTree from level geometry, traverses it for collision, then frees everything. Called 2× per ball in Ball_Update + up to 16× per fallen clone in RespawnPoint search = the main freeze cause.

v5 skips these calls when ball count exceeds MAX_BALLS, and NOPs the respawn-point collision check entirely (the ball just teleports to the nearest point without path-checking — fine for gameplay).

Usage

  1. Rename bass.dllbass_real.dll in your Hamsterball folder
  2. Copy this bass.dll into the game folder
  3. Launch the game

The mod waits 5 seconds after DLL load, verifies all byte signatures match, then applies patches.

Configuration

Change MAX_BALLS at top of source and recompile:

  • 20 = conservative (very stable)
  • 30 = balanced (default)
  • 50 = aggressive
  • 99 = max (won't crash, may stutter at very high counts)

Change RESPAWN_THROTTLE for respawn frequency:

  • 60 = once per second at 60fps (default)
  • 30 = twice per second
  • 120 = every 2 seconds (very stable but slow respawns)

Build

i686-w64-mingw32-gcc -shared -o bass.dll bass_proxy.c \
    -lwinmm -Wl,--enable-stdcall-fixup \
    -O2 -static -static-libgcc -Wl,--add-stdcall-alias

🔗 Related Documents

Event Plane System

types : physics
keywords :

📂 View source on GitHub


Hamsterball Event Plane System

Overview

Event planes are invisible collision meshes in Hamsterball levels. They use the E: name prefix in the .COL (collision) binary file. When the ball enters an event plane's collision volume, the game dispatches behavior based on the name string (e.g., E:JUMP, E:LIMIT, E:NODIZZY<TIME>300</TIME>).

This document covers:

  1. How event planes are loaded from MESHWORLD/.COL files
  2. How they're stored in memory (MeshBuffer struct)
  3. How collision detection finds them
  4. How the 3-tier dispatch system routes events by name
  5. Every known event type with exact addresses, parameters, and Ball/Scene offsets

1. Loading Pipeline

Source: Level_LoadCollision (0x00465260)

The .COL file is loaded alongside the .MESHWORLD visual mesh. The collision loader reads:

Header: 24 bytes → MeshWorld+0x45C (transform/flags)
int32:   sublevel_count
  if < 1 → single-mesh mode (read objects below)
  else   → multi-level mode (recursive sub-load)

Per collision object (single-mesh mode):

  1. CreateMeshBuffer(0x874) — allocate a MeshBuffer (2148 bytes)
  2. Append to MeshWorld->object_list (+0x2C)
  3. Read int32 name_length then char[name_length] — the name string
  4. Prefix detection (case-insensitive strnicmp):
    • "N:" prefix → buf->interactive = 1 (offset +0x85D) — visible + collidable
    • "E:" prefix → buf->interactive = 1 (+0x85D) AND buf->no_render = 1 (+0x863) — invisible event trigger
  5. Read int32 face_count
  6. For each face: allocate 0x60 bytes, read 9 floats (v0 xyz, v1 xyz, v2 xyz), compute face normal via cross product, store per-vertex normals (flat shading), append to object's face list

Key Point: E: objects are NEVER rendered

The no_render flag (+0x863) causes Scene_RenderAllObjects (0x45E0E0) to skip them entirely. They exist only in the collision mesh for trigger detection.


2. MeshBuffer Struct (Collision Object) — 0x874 bytes

Offset Size Field Description
+0x000 4 vtable* Virtual table pointer
+0x00C 0x418 AthenaList Face list (triangle faces)
+0x217 1 byte render_flag (= 0 for collision objects)
+0x85D 1 byte interactive_flag (1 for N: and E: prefixes)
+0x863 1 byte no_render_flag (1 for E: prefix only)
+0x864 4 char** Name string pointer (e.g., "E:JUMP")

CollisionFace Layout (0x60 bytes per triangle)

Offset Field
+0x00 float v0.x, v0.y, v0.z
+0x0C float normal.x, normal.y, normal.z (flat shading)
+0x18 float v1.x, v1.y, v1.z
+0x24 float normal.x, normal.y, normal.z
+0x30 float v2.x, v2.y, v2.z
+0x3C float normal.x, normal.y, normal.z

3. Collision Detection → Event Name Retrieval

When a collision is detected, the handler receives a collObj pointer (2-element array of pointers). The event name is retrieved as:

char *eventName = *(char **)(collObj[1] + 0x864);  // name string from MeshBuffer

This is the exact same string that was loaded from the .COL file. The dispatch code then does stricmp/strnicmp comparisons on this string to determine which behavior to trigger.


4. Two-Tier Dispatch System

Collision events are dispatched through a 2-tier handler chain. Level and Arena handlers are parallel, not chained — they never call each other. Both delegate to the shared base handler (DispatchCollisionEvents) as the final step. The scene's vtable determines which top-level handler runs.

Note: Ball_AdvancePositionOrCollision (0x4564C0) handles only geometric collision detection (velocity integration, mesh intersection via CollisionLevel->vtable[0x1C], max-speed clamping). It does NOT dispatch event-name-based collision events. Event dispatch is triggered from the ball update chain (ball->vtable[0x10], called by Scene_UpdateBallsAndState).

Tier 1a: TowerCollisionEvents (0x0040DCD0) — Level Events

Signature: void __thiscall TowerCollisionEvents(Scene *scene, Ball *ball, Collider *collider)

Handles level-specific events (catapults, trapdoors, maces, bite damage), then calls DispatchCollisionEvents.

Tier 1b: ExpertCollisionEvents (0x0040E6A0) — Arena Events

Signature: void __thiscall ExpertCollisionEvents(Scene *scene, Ball *ball, Collider *collider)

Handles arena/rumble events (hammers, saw blades, judges, bells), then calls DispatchCollisionEvents.

Base Tier: DispatchCollisionEvents (0x0040C5D0) — Shared Base Handler

Signature: void __thiscall DispatchCollisionEvents(void *this, int *ball, int *collObj)

Handles ALL common events. This is always called last regardless of level type.


5. Complete Event Reference

5a. Base Handler Events — DispatchCollisionEvents (0x0040C5D0)

Event String Match Type Condition Effect Ball/Scene Offsets
N:SECRET __strnicmp 8 chars Mark rotator as triggered *collObj + 0x47C
N:UNLOCKSECRET __strnicmp 14 chars Check arena unlock
E:NODIZZY __strnicmp 9 chars Anti-dizzy zone. Parses <TIME>value</TIME> tags via MWParser_ReadTag. Duration passed to Ball_DizzyImmunity
E:SAFESWITCH __strnicmp 12 chars (0xC) Switch state. If no ( in name: clear ball+0x30B=0. If ( found: strcpy(ball+0xC2C, paren_content) ball+0x30B, ball+0xC2C
E:LIMIT __stricmp exact Arena finish line. Clear velocity flag, set limit-hit ball+0x1DA=0, ball+0x2E9=1, board+0x47B4-47C0
E:BREAK __stricmp exact Call ball bounce callback ball->vtable[0x20]()
E:JUMP __stricmp exact impactCounter < 1 Bounce pad. Play 3D jump sound, set cooldown=10, upward force=0.025, freeze=10 frames, +200 score ball+0x1F7=10, ball+0xA7=0.025f, ball+0xA8=1, ball+0x202=10
E:ACTION __strnicmp 8 chars Parse XML tags: <ONCE>TRUE</ONCE> = one-time trigger (track in ball+0xCB list), <SCORE>500</SCORE> = award points with difficulty modifier ball+0xCB (once list)
E:TRAJECTORY __strnicmp 12 chars Parse <X>, <Y>, <Z> tags. Set ball collision mesh direction mesh+0xCA4/CA8/CAC
N:NOCONTROL __stricmp exact Disable ball input for 10 frames ball+0x202=10
N:WATER __stricmp exact Set water flag, start 10-frame timer ball+0x2D5=1, ball+0xB6=10
N:TARPIT __stricmp exact first entry only Play tar sound. Set in-tar flag, clear velocity ball+0xB3=1, ball+0x1DA=0
E:DROPIN __stricmp(eventName+2, "DROPIN") dist > threshold && dropinCounter < 1 Play dropin sound, set cooldown 50 frames, +200 score ball dropinCounter=50
E:PIPEBONK __stricmp(eventName+2, "PIPEBONK") pipebonkCounter < 1 Play random pipe sound (3 variants), set cooldown 10 frames, +100 score ball pipebonkCounter=10
E:POPOUT __stricmp(eventName+2, "POPOUT") popoutCounter < 1 Play popout sound, set cooldown 50 frames, +100 score ball popoutCounter=50
N:GOAL strncmp 7 chars !ball.finished && ball.active Set goalReached=1, play "Goal!" music, mark player finished
N:MOUSETRAP strncmp 12 chars RNG seed, deflect ball direction × trap speed

Note on E:DROPIN, E:PIPEBONK, E:POPOUT: These three events use eventName+2 comparison, skipping the "E:" prefix. The name in the .COL file is still "E:PIPEBONK" etc.

5b. Level Handler Events — TowerCollisionEvents (0x0040DCD0)

Event String Match Condition Effect Scene Offsets
E:CATAPULTBOTTOM stricmp exact ball.cooldown < 1 Set cooldown=1000, find matching catapult in list, store ball pointer, launch catapult, play sound scene+0x43B8 (catapult list), catapult+0x10EC (ball ptr)
E:OPENSESAME stricmp exact Open first trapdoor scene+0x4BEC
N:TRAPDOOR stricmp exact Activate matching trapdoor(s) scene+0x47D0
E:BITE stricmp exact Set damage timer=0, damage amount=25.0 scene+0x43A8=0, scene+0x43A0=25.0
E:MACETRIGGER stricmp exact Set all maces active mace list+0x5000, mace+0x10F0=1
N:MACE stricmp exact mace active & speed ≠ 80.0 Call BounceForce on ball ball->vtable[0x20]()

5c. Arena Handler Events — ExpertCollisionEvents (0x0040E6A0)

Event String Match Condition Effect Scene Offsets
E:CALLHAMMER stricmp exact tournament only Create hammer popup scene+0x436C
E:HAMMERCHASE stricmp exact tournament only Start hammer chase scene+0x436C
E:ALERTSAW1/2 stricmp exact tournament only Pre-activate saw blade scene+0x4370/0x4374
E:ACTIVATESAW1/2 stricmp exact tournament only Full activate saw blade scene+0x4370/0x4374
E:ALERTJUDGES stricmp exact Reset all judge objects scene+0x4FC8
E:SCORE<n> strnicmp 7 chars Parse number suffix, set time on score displays
E:JUMP stricmp exact cooldown < 1 Duplicate of base handler: play sound, bounce, +200 score ball+0x1F7=10, ball+0xA7, ball+0xA8
E:BELL strnicmp 6 chars Activate bell, +500 bonus time if not racing, show "EXTRA TIME:" popup scene+0x4FD4

6. Tag Parsing System (MWParser)

Several events embed XML-style parameters in the name string. These are parsed by MWParser_ReadTag():

Event Tag Format Parameters
E:NODIZZY <TIME>value</TIME> duration in frames
E:ACTION <ONCE>TRUE</ONCE> <SCORE>500</SCORE> once-only trigger, score points
E:TRAJECTORY <X>value</X> <Y>value</Y> <Z>value</Z> direction vector components
BADBALL <CHASE>value</CHASE> <HOME> <SIZE>value</SIZE> <SPINDISTANCE>value</SPINDISTANCE> AI ball behavior tags

MWParser_ReadTag returns:

struct MWTag {
    void*  release;   // +0x00: vtable with dtor
    char*  tag_name;   // +0x04: tag key (e.g., "TIME", "CHASE")
    char*  tag_value;  // +0x08: tag value as string (e.g., "300", "25.0")
};

SAFESWITCH uses a different pattern — parenthesized data: E:SAFESWITCH(data)strchr(eventName, '(') extracts the content.


7. N: vs E: Prefix Distinction

Prefix interactive (+0x85D) no_render (+0x863) Rendered Collidable Examples
N: 1 0 Yes Yes N:WALL, N:WATER, N:GOAL, N:TRAPDOOR
E: 1 1 No Yes E:JUMP, E:LIMIT, E:NODIZZY, E:PIPEBONK
(none) 0 0 Yes No Regular visual geometry
  • N: objects are visible geometry that also trigger gameplay events (walls, water zones, goal lines)
  • E: objects are invisible trigger volumes (jump pads, boundaries, switches)
  • Unprefixed objects are visual-only, non-interactive geometry

8. MESHWORLD Section 3 Object System (Visual Spawners)

Section 3 of the meshworld format contains named reference objects used for spawning game objects (not collision events). These use different prefixes:

  • Access path: Scene+0x8AC → MeshWorld* → MeshWorld+0x480 → section_root
  • Object array: section_root+0xCA0
  • Object count: section_root+0x898
  • Per-object layout:
    • +0x00: char* name (e.g., "BADBALL25", "START0-0", "E:JUMP")
    • +0x04: float x, y, z (position)
    • +0x14: float rot_y (rotation)

The CreateLevelObjects factory (0x4121D0) scans these names with strnicmp to instantiate game objects:

Name Prefix Object Ctor Size Scene Offset
BRIDGE Bridge mesh config +0x436C/4370
TIPPER Tipper 0x1104 +0x2578
BONK Bonk 0x1200 +0x540C
BBRIDGE1/2 BreakBridge 0x1100 +0x5418/541C
POPCYLINDER PopCylinder 0x10E8 +0x5428
BLOCKDAWG1/2 Blockdawg 0x1154 +0x2578
CATAPULT Catapult 0x1108 +0x584C
GLUEBIE Gluebie 0x110C +0x6080
BADBALL<tags> BadBall (AI) 0xC98

9. Adding Custom Event Plane Types

To add a new event type to the game (e.g., E:MYNEWEVENT):

In the .COL file:

  1. Name your collision object "E:MYNEWEVENT" — the E: prefix automatically sets interactive=1, no_render=1
  2. Append tag parameters if needed: "E:MYNEWEVENT<DURATION>100</DURATION>"
  3. The name string is stored verbatim at MeshBuffer+0x864 and passed to the collision dispatcher

In the collision handler:

  1. Add a stricmp/strnicmp check in the appropriate handler:
    • TowerCollisionEvents (0x40DCD0) for level-specific events
    • ExpertCollisionEvents (0x40E6A0) for arena-specific events
    • DispatchCollisionEvents (0x40C5D0) for universal events
  2. The event name is read from *(char **)(collObj[1] + 0x864)
  3. Use strnicmp for prefix matching (allows suffix data like tags)
  4. Use stricmp for exact matching (no suffix data needed)
  5. For tag parameters, use MWParser_ReadTag() to parse XML-style <TAG>value</TAG> syntax
  6. For parenthesized data, use strchr(eventName, '(') to extract (data)

Key ball offsets for writing effects:

Offset Type Name Description
+0xA7 float vert_velocity Vertical force (E:JUMP uses 0.025)
+0xA8 byte vert_velocity_on Vertical velocity enable flag
+0xB3 byte in_tar In tar zone flag
+0xB6 int water_timer Water/zone effect timer (frames)
+0xCB AthenaList once_list One-time trigger records (E:ACTION)
+0x1DA byte velocity_flag Velocity/direction clear
+0x1F7 byte impact_counter Jump/bounce cooldown timer
+0x202 int freeze_counter Input freeze timer
+0x2D5 byte in_water In water zone flag
+0x2E9 byte limit_hit Finish line reached flag (⚠ sticky — never cleared in Ball_Update, NOT ground contact)
+0x30B byte safe_switch Switch state (0 = clear)
+0xC2C char[?] safe_switch_data Switch data buffer
+0xCA4 Vec3 collision_direction Trajectory direction vector

Key scene offsets:

Offset Type Name Description
+0x43A0 float damage_amount Bite damage value (25.0)
+0x43A8 int damage_timer Damage effect timer
+0x43B8 AthenaList catapult_list Catapult objects
+0x47D0 AthenaList door_list Trapdoor objects
+0x4BEC AthenaList first_door First trapdoor reference
+0x4FD4 Bell* bell_obj Bell object
+0x5000 AthenaList mace_list Mace objects
+0x540C Bonk* bonk_ref Bonk object reference

10. Data Flow Summary

.COL Binary File
  │
  ├─ Level_LoadCollision (0x465260)
  │   ├─ Read object name string (e.g., "E:JUMP")
  │   ├─ Detect "E:" prefix → set interactive=1, no_render=1
  │   ├─ Store name at MeshBuffer+0x864
  │   └─ Build collision face list (triangles + normals)
  │
  ├─ Ball Physics Update
  │   └─ Mesh_FindClosestCollision — DDA ray traversal
  │       └─ Returns collObj pointing to MeshBuffer
  │
  ├─ Collision Handler Dispatch (2-tier, vtable-driven)
  │   ├─ TowerCollisionEvents (0x40DCD0) — Tower board
  │   │   └─ Level-specific events first (CATAPULTBOTTOM, OPENSESAME, etc.)
  │   │   └─ delegates to DispatchCollisionEvents
  │   ├─ ExpertCollisionEvents (0x40E6A0) — arenas
  │   │   └─ Arena events first (CALLHAMMER, SAW, BELL, etc.)
  │   │   └─ delegates to DispatchCollisionEvents
  │   └─ DispatchCollisionEvents (0x40C5D0) — shared base, always called last
  │       └─ eventName = *(collObj[1] + 0x864)
  │       └─ stricmp/strnicmp dispatch to specific handlers
  │
  └─ Scene_RenderAllObjects (0x45E0E0)
      └─ Skip objects with no_render flag (+0x863)

11. E:BLACKOUT — Not Found

E:BLACKOUT does not exist in any decompiled code. No string matching "blackout", "BLACKOUT", "BlackOut", or "black_out" was found. This event type either:

  • Does not exist in the original game (possibly a mod/community addition)
  • Uses a different internal name
  • Is handled in an undiscovered code path

If you want to implement E:BLACKOUT for custom levels, add it as a new stricmp case in DispatchCollisionEvents and handle it with whatever effect you want (e.g., fade screen to black, toggle visibility).


🔗 Related Documents

Expert Race Objects

types : docs
keywords :

📂 View source on GitHub


Expert Race Objects: Complete Reverse Engineering

Overview

Expert Race (Level5) contains 6 unique object types. All can be spawned globally.

Objects

Object String String Addr Alloc Size Constructor Ret Vtable
BONK (Hammer) "BONK" 0x4CFA4C 0x1200 0x438850 0x10 0x4D5120
FAN "FAN" 0x4CFA48 0x1188 0x438C20 0x14 0x4D5180
SAWBLADE "SAWBLADE" 0x4CFA28 0x111C 0x434660 0x10 0x4D5240
BRIDGE "BRIDGE" 0x4CF678 0x10FC 0x4396F0 0x14 0x4D51E0
JUDGE "JUDGE" 0x4CFA14 0x1100 0x43A150 0x10 0x4D52B8
BELL "BELL" 0x4CFA0C 0x10E8 0x434D70 0x10 0x4D5330

Factory Function

Arena factory: CreateExpertLevelObjects @ 0x40E250 (ret 0x10)

  • Handles ALL 6 object types
  • Called via Board vtable[33] at 0x40E250
  • Also handles post-creation events (E:CALLHAMMER, E:HAMMERCHASE, E:ALERTSAW1/2, etc.)

Race factory: @ 0x414BD0 (ret 0x10)

  • Only handles FAN
  • Falls through to FACTORY_RACE_BASE (0x4133E0) for unknown refs
  • Appends FAN to BOTH Board+0x2578 (general) AND Board+0x47E0

Constructor Calling Conventions

BONK (Hammer) — 4 stack params, ret 0x10

ecx = this (alloc 0x1200)
push Board        ; param_1
sub esp, 0xC      ; pos XYZ (3 floats from refEntry+4)
call 0x438850     ; ret 0x10
  • Reads Board+0x878 → App → App+0x174 (D3D device)
  • Calls Stands_ctor(0x461740) with D3D device — creates geometry internally
  • Sets vtable 0x4D5120
  • Stores Board at obj+0x10D0
  • After factory: stored at Board+0x436C, collision sub-object at obj+0x10F8

FAN — 5 stack params, ret 0x14

ecx = this (alloc 0x1188)
push refEntry[0x14]  ; param_5 (ext_flag integer, NOT mesh)
sub esp, 0xC        ; pos XYZ
push Board           ; param_1
call 0x438C20        ; ret 0x14
  • Reads Board+0x878 → App → App+0x174 (D3D device)
  • Calls Stands_ctor(0x461740) with D3D device
  • Sets vtable 0x4D5180
  • Sets obj+0x10E8=0.5, obj+0x10EE=1, obj+0x10F0=500.0
  • Factory post-checks: "SLOW" → obj+0x10EC=1, "SUPER" → obj+0x10ED=1, "UP" → call 0x434580
  • 5th param (ext_flag) is read by ctor but only as a float for position adjustment

SAWBLADE — 4 stack params, ret 0x10

ecx = this (alloc 0x111C)
push Board        ; param_1
sub esp, 0xC      ; pos XYZ
call 0x434660     ; ret 0x10
  • Reads Board+0x878 → App → App+0x174 (D3D device)
  • Sets vtable 0x4D5240
  • After factory: "1" → stored at Board+0x4370, call 0x434AB0(obj, push 1)
  • After factory: "2" → stored at Board+0x4374, call 0x434AB0(obj, push 2)

BRIDGE — 5 stack params, ret 0x14

ecx = this (alloc 0x10FC)
push refEntry[0x14]  ; param_5 (ext_flag)
sub esp, 0xC        ; pos XYZ
push Board           ; param_1
call 0x4396F0        ; ret 0x14
  • READS Board+0x4378 (bridge mesh) — calls Stands_ctor(0x462850) with it
  • Sets vtable 0x4D51E0
  • After factory: "1" → Append(Board+0x4380, obj), "2" → Append(Board+0x4798, obj)
  • After factory: "NEG" → obj+0x10F8 = -1.0f (0xBF800000)

JUDGE — 4 stack params, ret 0x10

ecx = this (alloc 0x1100)
push Board        ; param_1
sub esp, 0xC      ; pos XYZ
call 0x43A150     ; ret 0x10
  • Reads Board+0x878 → App → App+0x174 (D3D device)
  • Sets vtable 0x4D52B8
  • After factory: Append(Board+0x4BBC, obj)

BELL — 4 stack params, ret 0x10

ecx = this (alloc 0x10E8)
push Board        ; param_1
sub esp, 0xC      ; pos XYZ
call 0x434D70     ; ret 0x10
  • Reads Board+0x878 → App → App+0x174 (D3D device)
  • Calls Stands_ctor(0x461740) with D3D device
  • Sets vtable 0x4D5330
  • After factory: stored at Board+0x4FD4, Append(Board+0x2578, obj)

Mesh Loading

LevelBoard_Expert_ctor @ 0x41EA40 loads:

  • Board+0x4378 = MeshWorld_ctor(0x10D0, App+0x174, "Levels\Level5-Bridge") @ 0x4D0ABC
  • Board+0x437C = CollisionLevel_ctorWithLevel(0x10D0, Board+0x4378) @ 0x465080
  • Board+0x4BB0/4BB4/4BB8 = 3× hammyjudge objects (0x18 bytes, ctor 0x471C20, mesh "meshes\hammyjudge")
  • Board+0x4344 = "Fight!" string ptr (0x4D0AA0)

Pre-loaded by resource loader (0x4298C0): NONE — all Expert meshes are level-specific.

AthenaList Initialization

LevelBoard_Expert_ctor initializes:

  • Board+0x4380 (saw1 bridge list)
  • Board+0x4798 (saw2 bridge list)
  • Board+0x4BBC (judge list)

On non-Expert levels, these are NOT initialized → must call AthenaList_Init(0x453210).

Board+0x2578 (general list) is initialized by Board_ctor on ALL levels.

Collision Events

TowerCollisionEvents @ 0x40E6A0 (Expert Board vtable[29] at 0x74):

Event String Addr Handler Action
E:CALLHAMMER 0x4CFAC8 0x438B30 Calls Board+0x436C (BONK) to chase ball
E:HAMMERCHASE 0x4CFAB8 0x438BB0 Activates hammer chase on Board+0x436C
E:ALERTSAW1 0x4CFAAC 0x434770 Alerts Board+0x4370 (SAW1)
E:ALERTSAW2 0x4CFAA0 0x434770 Alerts Board+0x4374 (SAW2)
E:ACTIVATESAW1 0x4CFA90 0x434A50 Activates Board+0x4370 (SAW1)
E:ACTIVATESAW2 0x4CFA80 0x434A50 Activates Board+0x4374 (SAW2)
E:ALERTJUDGES 0x4CFA70 iterates Board+0x4BBC Cycles through judge list
E:SCORE 0x4CFA68 creates 3D sound Sets score, plays sound
E:JUMP 0x4CF890 creates 3D sound Jump pad effect

On non-Expert levels: Objects have physical collision (via internal CollisionLevel) but special events won't fire because the collision handler is level-specific.

Object Updates

Each object's vtable contains update/render functions:

  • vtable[0x54] = render function (called each frame)
  • vtable[0x58] = update function (called each frame)
  • vtable[0x60] = collision response

Global Spawn Requirements

No mesh needed (create from D3D device):

  • BONK, FAN, SAWBLADE, JUDGE, BELL
  • Just need: alloc + Board + position

Mesh needed:

  • BRIDGE: requires Board+0x4378 = MeshWorld("Levels\Level5-Bridge")
    • Load via: MeshWorld_ctor(0x10D0, App+0x174, "Levels\Level5-Bridge") @ 0x461510
    • String at 0x4D0ABC

AthenaList_Init required:

  • Board+0x4380 (for BRIDGE "1")
  • Board+0x4798 (for BRIDGE "2")
  • Board+0x4BBC (for JUDGE)

Post-spawn Board field writes:

  • BONK → Board+0x436C
  • SAWBLADE → Board+0x4370 (saw1) or Board+0x4374 (saw2)
  • BELL → Board+0x4FD4

🔗 Related Documents

Factory Object System

types : objects
keywords :

📂 View source on GitHub


Hamsterball Factory Object System — Comprehensive Analysis

Overview

The Hamsterball object factory system uses a dispatch pattern: each Board level loads
MeshWorld sub-meshes during construction (in the BoardLevel*_ctor), storing MeshWorld
pointers at specific Board offsets. Then, a per-level Create*Objects factory function is
called for each named object in the MESHWORLD file, using __strnicmp to match object
name prefixes and instantiate the appropriate game object with position/rotation data from
a param_block.

Key Constants

  • Image base: 0x00400000
  • Board (this/ecx) + 0x878 = App pointer
  • App + 0x23C = difficulty enum (0 = easy, non-zero = hard)
  • Board + 0x2578 = general active objects list (AthenaList)
  • param_block layout: Position X/Y/Z at +4/+8/+0xC, Rotation X/Y/Z at +0x10/+0x14/+0x18
  • MeshWorld allocation size: 0x10D0 bytes
  • MeshNode allocation size: 0x18 bytes
  • CollisionLevel allocation size: 0x10D0 bytes

1. Factory Function: CreateLevelObjects (0x004121D0)

Used by: Dizzy Race (Level3), Master Race, and other levels using Level3-style objects.

Object Name Match Alloc Size Constructor Board Mesh Offset Board Store Offset AthenaList(s) Difficulty Gate Position Fields Rotation Fields
BRIDGE strnicmp "BRIDGE", 6 N/A (no alloc) None — configures existing mesh +0x436C +0x437C, +0x4380, +0x4384 (pos) No param_4+4/+8/+0xC
TIPPER strnicmp "TIPPER", 6 0x1104 Tipper_ctor +0x4394 (mesh), +0x4398 (visual mesh) +0x2578 YES (App+0x23C != 0) obj+0x10D8/+0x10DC/+0x10E0 obj+0x10E4/+0x10E8/+0x10EC
TipperVisual (sub-alloc) 0x10D0 TipperVisual_ctor +0x4398 obj+0x10D4
BONK strnicmp "BONK", 4 0x1200 Bonk_ctor +0x540C +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC
BBRIDGE1 strnicmp "BBRIDGE1", 8 0x1100 BreakBridge_ctor +0x5410 +0x5418 +0x2578 No param_4+4/+8/+0xC
BBRIDGE2 strnicmp "BBRIDGE2", 8 0x1100 BreakBridge_ctor +0x5414 +0x541C +0x2578 No param_4+4/+8/+0xC
POPCYLINDER strnicmp "POPCYLINDER", 11 0x10E8 PopCylinder_ctor +0x5420 +0x2578, +0x5428 No param_4+4/+8/+0xC
BLOCKDAWG1 strnicmp "BLOCKDAWG1", 10 0x1154 Blockdawg_ctor +0x5840 +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC
BLOCKDAWG2 strnicmp "BLOCKDAWG2", 10 0x1154 Blockdawg_ctor +0x5844 — (sets obj+0x1152=1) +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC
CATAPULT strnicmp "CATAPULT", 8 0x1108 Catapult_ctor +0x5848 — (sets obj+0x1100=1) +0x2578, +0x584C No obj+0x10D8/+0x10DC/+0x10E0
GLUEBIE strnicmp "GLUEBIE", 7 0x110C Gluebie_ctor +0x607C +0x6080, +0x2578 YES (App+0x23C != 0, inverted: skips if 0) obj+0x10D4/+0x10D8/+0x10DC

BLOCKDAWG Path Lookups

  • BLOCKDAWG1: Level_FindObjectByName(Board+0x8AC, "DAWGPATH1") → path index passed to ctor
  • BLOCKDAWG2: Level_FindObjectByName(Board+0x8AC, "DAWGPATH2") → path index passed to ctor

2. Factory Function: CreateUpLevelObjects (0x004117B0)

Used by: Up Race (LevelUp).

Object Name Match Alloc Size Constructor Board Mesh Offset AthenaList(s) Difficulty Gate Position Fields Notes
LIFTER strnicmp "LIFTER", 6 0x10F4 Rotator_ctor_sound +0x4784 +0x2578 No param_4+4/+8/+0xC Parses _atol(param_1+6) for numeric arg
SPEEDCYLINDER strnicmp "SPEEDCYLINDER", 13 0x150C Pendulum_ctor +0x4788 +0x2578 No param_4+4/+8/+0xC Uses __ftol2 for float→int conversion
TIMEBUTTON strnicmp "TIMEBUTTON", 10 0x10E8 Rotator_ctor_nosound +0x478C +0x2578 No param_4+4/+8/+0xC Returns obj via param_2/param_3

3. Factory Function: CreateMechanicalObjects (0x00417FE0)

Used by: Impossible Race and other mechanical levels.

Object Name Match Alloc Size Constructor Board Mesh Offset AthenaList(s) Difficulty Gate Position Fields Rotation Fields Notes
LOOPER strnicmp "LOOPER", 6 0x1500 Looper_ctor +0x436C +0x2578 No param_4+4/+8/+0xC
GEAR strnicmp "GEAR", 4 0x1514 Gear_ctor +0x4370 +0x2578 No param_4+4/+8/+0xC (pos1) + param_4+0x10/+0x14/+0x18 (pos2) Both position AND rotation from param_block
BIGGEAR strnicmp "BIGGEAR", 7 0x1514 Gear_ctor +0x4374 +0x2578 No param_4+4/+8/+0xC (pos1) + param_4+0x10/+0x14/+0x18 (pos2) Sets obj+0x10F4=0x3F000000 (0.5f scale). Checks strstr("TOUCH")→obj+0x1510=1
ROTATOR strnicmp "ROTATOR", 7 0x1508 Rotator_ctor +0x4378 +0x2578 No param_4+4/+8/+0xC Sets obj+0x10E8=±1.0f (RNG_Rand direction). obj+0x10D4 = result
PENDULUM strnicmp "PENDULUM", 8 0x1504 Pendulum_ctor +0x437C +0x2578 No param_4+4/+8/+0xC Returns obj via param_2/param_3

4. Factory Function: CreateExpertLevelObjects (0x0040E250)

Used by: Expert Race (Level5).

Object Name Match Alloc Size Constructor Board Mesh Offset Board Store Offset AthenaList(s) Difficulty Gate Position Fields Notes
BONK strnicmp "BONK", 4 0x1200 Bonk_ctor +0x436C +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC
FAN strnicmp "FAN", 3 (via PTR_DAT_0x4CFA48) 0x1188 TowerLevel_Ctor +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC + param_4+0x14 (extra float) Checks strstr("SLOW")→obj+0x10EC=1, strstr("SUPER")→obj+0x10ED=1, strstr("UP")→Sound_InitChannels
SAWBLADE strnicmp "SAWBLADE", 8 0x111C Sawblade_Level_Ctor +0x2578 YES (App+0x23C != 0) param_4+4/+8/+0xC strstr("1")→Board+0x4370=Sawblade, Sawblade_SetBreakSound(1). strstr("2")→Board+0x4374=Sawblade, Sawblade_SetBreakSound(2)
BRIDGE (Expert) strnicmp "BRIDGE", 6 0x10FC Spinner_Level_ctor +0x2578 (conditional) No param_4+4/+8/+0xC + param_4+0x14 strstr("1")→AthenaList+0x4380. strstr("2")→AthenaList+0x4798. strstr("NEG")→obj+0x10F8=0xBF800000 (-1.0f)
JUDGE strnicmp "JUDGE", 5 0x1100 Gear_Level_ctor +0x4BBC No param_4+4/+8/+0xC
BELL strnicmp "BELL", 4 0x10E8 Tipper_Level_Ctor +0x4FD4 +0x2578 No param_4+4/+8/+0xC

5. Factory Function: CreateSpinny (0x004143D0)

Used by: Toob Race (Level8), Master Race, and general-purpose spinny levels.

Object Name Match Alloc Size Constructor Board Mesh Offset AthenaList(s) Difficulty Gate Position Fields Notes
SPINNY strnicmp "SPINNY", 6 0x1508 Rotator_ctor +0x47E0 +0x2578 No param_4+4/+8/+0xC Falls through to CreatePlatformOrStands if no match

6. Factory Function: CreateLifter (0x00414A20)

Used by: Lifter levels (Level6, Impossible).

Object Name Match Alloc Size Constructor Board Mesh Offset AthenaList(s) Difficulty Gate Position Fields Notes
LIFTER strnicmp "LIFTER", 6 0x10F4 Rotator_ctor_sound +0x47E0 +0x2578 No param_4+4/+8/+0xC Parses _atol(param_1+6) for numeric arg. Falls through to CreatePlatformOrStands if no match

7. Factory Function: Scene_CreateObject_Gear (0x00418760)

Used by: Impossible Race and other gear levels.

Object Name Match Alloc Size Constructor Board Mesh Offset AthenaList(s) Difficulty Gate Position Fields Rotation Fields Notes
GEAR strnicmp "GEAR", 4 0x1514 Gear_ctor +0x47E0 +0x2578 No param_4+4/+8/+0xC (pos1) + param_4+0x10/+0x14/+0x18 (pos2) Both position AND rotation from param_block Sets obj+0x10F4=0x40000000 (2.0f scale). Falls through to CreatePlatformOrStands

8. Board Level Constructors — Mesh Loading & Board Offsets

LevelBoard_Dizzy_ctor (0x0041D060) — "Dizzy Race"

  • Vtable: PTR_LevelBoard_Dizzy_dtor_004d0890
  • AthenaLists: +0x4378, +0x4790
  • Race data: "DIZZYRACE", Display: "Dizzy!", Race name: "DIZZY RACE"
  • Gravity: (0, 1.0, 0)
Mesh Path VA (String) Board Offset Collision Offset Object Type
Levels\Level3-Tipper 0x4D07E8 +0x436C +0x4370 TIPPER mesh
Levels\Level3-WaterWheel 0x4D0794 +0x4BA8 +0x4BAC WATERWHEEL (spinning floor)
Levels\Level3-Swirl 0x4CFFE0 +0x4BC4 +0x4BC8 SWIRL mesh
Levels\Level3-Gluebie 0x4D0728 +0x4374 (no separate collision) GLUEBIE mesh

LevelBoard_Tower_ctor (0x0041E340) — "Tower Race"

  • Vtable: PTR_LevelBoard_Tower_dtor_004d0a08
  • AthenaLists: +0x43B8, +0x47D0, +0x4BE8, +0x5000
  • Race data: "TOWERRACE", Display: "Happy Rush", Race name: "TOWER RACE"
  • Gravity: (1.0, 0.5, 0)
Mesh Path VA (String) Board Offset Collision Offset Object Type
Levels\Level4-Catapult 0x4D09B8 +0x436C (none) CATAPULT mesh
Levels\Level4-Drawbridge 0x4D099C +0x4370 (none) DRAWBRIDGE/BREAKBRIDGE mesh
Meshes\YellowLink 0x4D0988 +0x4374 (MeshNode, 0x18 bytes) YellowLink visual
Levels\Level4-Mace 0x4D0974 +0x4378 (none) MACE mesh
Levels\Level4-Windmill 0x4D095C +0x437C (none) WINDMILL mesh
Meshes\Chomper 0x4D094C +0x4390 (MeshNode, 0x18 bytes) CHOMPER visual mesh
Levels\Level4-Turret 0x4D0932* +0x43B4 (none) TURRET mesh

*Note: String at 0x4D0932 has a prefix byte "pB" before "Levels\Level4-Turret" (likely Ghidra parsing artifact; actual string starts at the "L").

LevelBoard_Up_ctor (0x00420390) — "Up Race"

  • Vtable: PTR_BoardLevel_Generic_dtor3_004d11a0
  • AthenaLists: +0x436C
  • Race data: "UPRACE", Display: "Up Race", Race name: "UP RACE"
  • Gravity: (1.0, 0, 1.0)
Mesh Path VA (String) Board Offset Object Type
levels\levelup-lifter 0x4D1160 +0x4784 LIFTER mesh
levels\levelup-speedcylinder 0x4D1140 +0x4788 SPEEDCYLINDER mesh
levels\levelup-button 0x4D1128 +0x478C TIMEBUTTON mesh

LevelBoard_Expert_ctor (0x0041EA40) — "Expert Race"

  • Vtable: PTR_LevelBoard_Expert_dtor_004d0b00
  • AthenaLists: +0x4380, +0x4798, +0x4BBC
  • Race data: "EXPERTRACE", Display: "Fight!", Race name: "EXPERT RACE"
  • Gravity: (1.0, 0, 0)
Mesh Path VA (String) Board Offset Collision Offset Object Type
Levels\Level5-Bridge 0x4D0ABC +0x4378 +0x437C BRIDGE/SPINNER mesh
meshes\hammyjudge 0x4D0AA8 +0x4BB0 (MeshNode, 0x18 bytes) JUDGE visual (1)
meshes\hammyjudge 0x4D0AA8 +0x4BB4 (MeshNode, 0x18 bytes) JUDGE visual (2)
meshes\hammyjudge 0x4D0AA8 +0x4BB8 (MeshNode, 0x18 bytes) JUDGE visual (3)

LevelBoard_Toob_ctor (0x0041F4B0) — "Toob Race"

  • Vtable: PTR_BoardLevel_Toob_dtor_004d0e78
  • Vec3 array at +0x438C (8 elements of 0x418 each)
  • Race data: "TOOBRACE", Display: "Rodenthood", Race name: "TOOB RACE"
  • Gravity: (0.5, 0.5, 1.0)
Mesh Path VA (String) Board Offset Object Type
Levels\Level8-Spinny 0x4D0E38 +0x436C SPINNY mesh
Levels\Level8-Saw 0x4D0E24 +0x4370 SAW/SAWBLADE mesh
Levels\Level8-Fallout 0x4D0E0C +0x4374 FALLOUT mesh
Levels\Level8-Blockdawg1 0x4D0DF0 +0x4378 BLOCKDAWG1 mesh
Levels\Level8-Blockdawg2 0x4D0DD4 +0x437C BLOCKDAWG2 mesh

BoardLevel_Master_Ctor (0x004206D0) — "Master Race"

  • Vtable: PTR_BoardLevel_Generic_dtor4_004d12b0
  • Vec3 array at +0x439C (4 elements of 0x418 each)
  • AthenaLists: +0x5428, +0x584C, +0x5C64, +0x6080
  • Race data: "MASTERRACE", Display: "Master Theme", Race name: "MASTER RACE"
  • Gravity: (0.5, 0.5, 0.5)
Mesh Path VA (String) Board Offset Collision Offset Object Type
Levels\Level2-Bridge 0x4D055C +0x436C +0x4370 BRIDGE mesh
Levels\Level10-2PBridge 0x4D127C +0x4374 +0x4378 BBRIDGE/2P mesh
Levels\Level3-Tipper 0x4D07E8 +0x4394 +0x4398 TIPPER mesh + visual
Levels\Level10-Bridge1 0x4D1264 +0x5410 (none) BBRIDGE1 mesh
Levels\Level10-Bridge2 0x4D124C +0x5414 (none) BBRIDGE2 mesh
levels\level9-popcylinder1 0x4D0F5C +0x5420 (none) POPCYLINDER1 mesh
levels\level9-popcylinder2 0x4D0F40 +0x5424 (none) POPCYLINDER2 mesh
Levels\Level8-Blockdawg1 0x4D0DF0 +0x5840 (none) BLOCKDAWG1 mesh
Levels\Level8-Blockdawg2 0x4D0DD4 +0x5844 (none) BLOCKDAWG2 mesh
Levels\Level4-Catapult 0x4D09B8 +0x5848 (none) CATAPULT mesh
Levels\Level3-Gluebie 0x4D0728 +0x607C (none) GLUEBIE mesh

ArenaBoard_Tower_ctor (0x004228C0) — "Tower Arena"

  • Vtable: PTR_ArenaBoard_Odd_DeletingDtor_004d1740
  • AthenaLists: +0x47E4, +0x4C00
  • Race name: "TOWER ARENA", Display: "Happy Rush"
Mesh Path VA (String) Board Offset Object Type
Levels\Level4-Mace 0x4D0974 +0x47E0 MACE mesh
Levels\Level4-Catapult 0x4D09B8 +0x4BFC CATAPULT mesh
Levels\Level4-Turret 0x4D0932 +0x5018 TURRET mesh

9. Complete String Address Table (.rdata section)

Object Name Strings (for strnicmp matching)

VA File Offset String
0x4CF678 0xCF678 BRIDGE
0x4CF680 0xCF680 GLUEBIE
0x4CF688 0xCF688 SWIRL
0x4CF690 0xCF690 WATERWHEEL
0x4CF69C 0xCF69C TIPPER
0x4CF818 0xCF818 PIPEBONK
0x4CF900 0xCF900 N:BRIDGE
0x4CF928 0xCF928 N:SWIRL
0x4CF930 0xCF930 N:WHEELEMBED
0x4CF940 0xCF940 N:WATERWHEEL
0x4CF960 0xCF960 TURRET
0x4CF968 0xCF968 CHOMPER
0x4CF970 0xCF970 TRAPDOOR
0x4CF97C 0xCF97C WINDMILL
0x4CF988 0xCF988 DRAWBRIDGE
0x4CF994 0xCF994 MACE
0x4CF99C 0xCF99C CATAPULT
0x4CF9A0 0xCF9A0 CATAPULT (alt offset)
0x4CFA0C 0xCFA0C BELL
0x4CFA14 0xCFA14 JUDGE
0x4CFA28 0xCFA28 SAWBLADE
0x4CFA48 0xCFA48 FAN
0x4CFA4C 0xCFA4C BONK
0x4CFB1C 0xCFB1C LIFTER
0x4CFCC0 0xCFCC0 BLOCKDAWG3
0x4CFCD8 0xCFCD8 BLOCKDAWG2
0x4CFCF0 0xCFCF0 BLOCKDAWG1
0x4CFD2C 0xCFD2C SPINNY
0x4CFD4C 0xCFD4C N:SPINNY
0x4CFDB8 0xCFDB8 POPCYLINDER
0x4CFE40 0xCFE40 TIMEBUTTON
0x4CFE4C 0xCFE4C SPEEDCYLINDER
0x4CFEB4 0xCFEB4 BBRIDGE2
0x4CFEC0 0xCFEC0 BBRIDGE1
0x4CFF04 0xCFF04 BONKBASH
0x4CFF10 0xCFF10 BONKPOPUP
0x4CFF98 0xCFF98 levels\arena-WarmUp
0x4CFEF8 0xCFEF8 N:SPINNER
0x4D01C0 0xD01C0 PENDULUM
0x4D01CC 0xD01CC ROTATOR
0x4D01DC 0xD01DC BIGGEAR
0x4D01E4 0xD01E4 GEAR
0x4D01EC 0xD01EC LOOPER
0x4D0144 0xD0144 FLICKRING
0x4CFF5C 0xCFF5C STANDS
0x4CFF64 0xCFF64 PLATFORM

Mesh Path Strings

VA File Offset String Used By
0x4CF8E0 0xCF8E0 levels\level1 Beginner Race
0x4CF8F0 0xCF8F0 levels\level2 Intermediate Race
0x4CF918 0xCF918 levels\level3 Dizzy Race (main mesh)
0x4CF950 0xCF950 levels\level4 Tower Race (main mesh)
0x4CFC1C 0xCFC1C levels\level7 Wobbly Race (main mesh)
0x4CFCA8 0xCFCA8 levels\level8 Toob Race (main mesh)
0x4CFDA8 0xCFDA8 levels\level9 Sky Race (main mesh)
0x4CFE30 0xCFE30 levels\levelup Up Race (main mesh)
0x4CFEA4 0xCFEA4 levels\level10 Master/2P Race (main mesh)
0x4D01A8 0xD01A8 levels\levelimpossible Impossible Race (main mesh)
0x4D055C 0xD055C Levels\Level2-Bridge Intermediate BRIDGE mesh
0x4D07E8 0xD07E8 Levels\Level3-Tipper Dizzy TIPPER mesh
0x4D0794 0xD0794 Levels\Level3-WaterWheel Dizzy WATERWHEEL (spinning floor) mesh
0x4CFFE0 0xCFFE0 Levels\Level3-Swirl Dizzy SWIRL mesh
0x4D0728 0xD0728 Levels\Level3-Gluebie Dizzy GLUEBIE mesh
0x4D09B8 0xD09B8 Levels\Level4-Catapult Tower CATAPULT mesh
0x4D099C 0xD099C Levels\Level4-Drawbridge Tower DRAWBRIDGE mesh
0x4D0974 0xD0974 Levels\Level4-Mace Tower MACE mesh
0x4D095C 0xD095C Levels\Level4-Windmill Tower WINDMILL mesh
0x4D0932 0xD0932 Levels\Level4-Turret Tower TURRET mesh (note: prefixed with "pB" in raw)
0x4D094C 0xD094C Meshes\Chomper Tower CHOMPER visual mesh (MeshNode)
0x4D0988 0xD0988 Meshes\YellowLink Tower YellowLink visual (MeshNode)
0x4D33EC 0xD33EC Levels\Level4-Trapdoor2 Tower TRAPDOOR2 mesh
0x4D3404 0xD3404 Levels\Level4-Trapdoor1 Tower TRAPDOOR1 mesh
0x4D0ABC 0xD0ABC Levels\Level5-Bridge Expert BRIDGE/SPINNER mesh
0x4D0AA8 0xD0AA8 meshes\hammyjudge Expert JUDGE visual (MeshNode, ×3)
0x4D0E38 0xD0E38 Levels\Level8-Spinny Toob SPINNY mesh
0x4D0E24 0xD0E24 Levels\Level8-Saw Toob SAWBLADE mesh
0x4D0E0C 0xD0E0C Levels\Level8-Fallout Toob FALLOUT mesh
0x4D0DF0 0xD0DF0 Levels\Level8-Blockdawg1 Toob/Master BLOCKDAWG1 mesh
0x4D0DD4 0xD0DD4 Levels\Level8-Blockdawg2 Toob/Master BLOCKDAWG2 mesh
0x4D0F28 0xD0F28 levels\level9-trapdoor Sky TRAPDOOR mesh
0x4D0F40 0xD0F40 levels\level9-popcylinder2 Sky/Master POPCYLINDER2 mesh
0x4D0F5C 0xD0F5C levels\level9-popcylinder1 Sky/Master POPCYLINDER1 mesh
0x4D1160 0xD1160 levels\levelup-lifter Up LIFTER mesh
0x4D1140 0xD1140 levels\levelup-speedcylinder Up SPEEDCYLINDER mesh
0x4D1128 0xD1128 levels\levelup-button Up TIMEBUTTON mesh
0x4D124C 0xD124C Levels\Level10-Bridge2 Master BBRIDGE2 mesh
0x4D1264 0xD1264 Levels\Level10-Bridge1 Master BBRIDGE1 mesh
0x4D127C 0xD127C Levels\Level10-2PBridge Master 2P BRIDGE mesh
0x4D20DC 0xD20DC Levels\LevelImpossible-Pendulum Impossible PENDULUM mesh
0x4D20FC 0xD20FC Levels\LevelImpossible-Rotator Impossible ROTATOR mesh
0x4D211C 0xD211C Levels\LevelImpossible-BigGear Impossible BIGGEAR mesh
0x4D213C 0xD213C Levels\LevelImpossible-Gear Impossible GEAR mesh
0x4D2158 0xD2158 Levels\LevelImpossible-Looper Impossible LOOPER mesh
0x4D3308 0xD3308 Levels\Level6-Lifter Impossible LIFTER mesh
0x4D3390 0xD3390 Meshes\sawblade Expert SAWBLADE visual (MeshNode)
0x4D3468 0xD3468 Meshes\Bell Expert BELL visual (MeshNode)
0x4D5C10 0xD5C10 levels\level5-bonk Expert BONK mesh

Sound Path Strings

VA File Offset String
0x4D2BC8 0xD2BC8 sounds\breakbridge
0x4D2C00 0xD2C00 sounds\speedcylinder
0x4D2D14 0xD2D14 sounds\bell
0x4D2DE8 0xD2DE8 sounds\catapult
0x4D2E54 0xD2E54 sounds\bridgeslam
0x4D2E68 0xD2E68 sounds\gearclank

10. Board Offset Summary (Consolidated)

Board Offset Type Description Used By
+0x2578 AthenaList Active game objects list (ALL objects) All factories
+0x436C MeshWorld* Bridge/Tipper/Catapult/Spinny/Looper mesh (varies by level) Dizzy, Tower, Expert, Master, Impossible
+0x4370 CollisionLevel*/MeshWorld* Collision for +0x436C / or secondary mesh Dizzy, Tower, Expert, Master
+0x4374 MeshWorld* Gluebie mesh (Dizzy) / 2PBridge (Master) / Fallout (Toob) Dizzy, Master, Toob
+0x4378 AthenaList*/MeshWorld* Dizzy: AthenaList / Tower: Mace / Expert: Bridge / Toob: Blockdawg1 Dizzy, Tower, Expert, Toob
+0x437C CollisionLevel*/MeshWorld* Dizzy: collision / Tower: Windmill / Expert: collision / Toob: Blockdawg2 Dizzy, Tower, Expert, Toob
+0x4380 AthenaList Expert: Spinner bridge list 1 Expert
+0x4388 float Master: 0x42340000 (40.0f) Master
+0x438C Vec3List[8] Toob: vector array (0x418 × 8) Toob
+0x4390 MeshNode* Tower: Chomper visual mesh Tower
+0x4394 MeshWorld* Tipper mesh Dizzy, Master
+0x4398 CollisionLevel* Tipper visual/collision Dizzy, Master
+0x439C Vec3List[4] Master: vector array (0x418 × 4) Master
+0x43A0-A8 float[3] Tower: zero-init (0, 0, 0) Tower
+0x43B4 MeshWorld* Tower: Turret mesh Tower
+0x43B8 AthenaList Tower: object list Tower
+0x4790 AthenaList Dizzy: secondary list Dizzy
+0x4798 AthenaList Expert: Spinner bridge list 2 Expert
+0x47D0 AthenaList Tower: list 2 Tower
+0x47E0 MeshWorld* Spinny/Gear/Lifter mesh (Impossible, Spinny, Lifter factories) Impossible, Spinny, Lifter
+0x47E4 AthenaList Tower Arena: list Tower Arena
+0x4784 MeshWorld* Up: Lifter mesh Up
+0x4788 MeshWorld* Up: SpeedCylinder mesh Up
+0x478C MeshWorld* Up: TimeButton mesh Up
+0x4BA8 MeshWorld* Dizzy: WaterWheel mesh Dizzy
+0x4BAC CollisionLevel* Dizzy: WaterWheel collision Dizzy
+0x4BC4 MeshWorld* Dizzy: Swirl mesh Dizzy
+0x4BC8 CollisionLevel* Dizzy: Swirl collision Dizzy
+0x4BE8 AthenaList Tower: list 3 Tower
+0x4BFC MeshWorld* Tower Arena: Catapult mesh Tower Arena
+0x4FD4 void* Expert: BELL object pointer Expert
+0x5000 AthenaList Tower: list 4 Tower
+0x5018 MeshWorld* Tower Arena: Turret mesh Tower Arena
+0x540C void* Bonk object pointer (Level3/Master) Dizzy, Master
+0x5410 MeshWorld* BreakBridge1 mesh Master
+0x5414 MeshWorld* BreakBridge2 mesh Master
+0x5418 void* BreakBridge1 object pointer Master
+0x541C void* BreakBridge2 object pointer Master
+0x5420 MeshWorld* PopCylinder mesh Master
+0x5424 MeshWorld* PopCylinder2 mesh (Master only) Master
+0x5428 AthenaList PopCylinder list Master
+0x5840 MeshWorld* BlockDawg1 mesh Master, Toob
+0x5844 MeshWorld* BlockDawg2 mesh Master, Toob
+0x5848 MeshWorld* Catapult mesh Master
+0x584C AthenaList Catapult list Master
+0x5C64 AthenaList (unknown - Master) Master
+0x607C MeshWorld* Gluebie mesh Master
+0x6080 AthenaList Gluebie list Master
+0x868 char* Board display name string All
+0x870 int Race ID from App+0x14+0x1DC All
+0x878 App* App pointer All
+0x8AC Scene* Scene pointer (for Level_FindObjectByName) All
+0x29B4 char* Race name string All
+0x4344 char* Display/theme string All

11. Object Allocation Size Summary

Object Type Alloc Size Constructor Called From
Tipper 0x1104 Tipper_ctor CreateLevelObjects
TipperVisual 0x10D0 TipperVisual_ctor CreateLevelObjects
Bonk 0x1200 Bonk_ctor CreateLevelObjects, CreateExpertLevelObjects
BreakBridge (BBRIDGE1/2) 0x1100 BreakBridge_ctor CreateLevelObjects
PopCylinder 0x10E8 PopCylinder_ctor CreateLevelObjects
BlockDawg 0x1154 Blockdawg_ctor CreateLevelObjects
Catapult 0x1108 Catapult_ctor CreateLevelObjects
Gluebie 0x110C Gluebie_ctor CreateLevelObjects
Lifter 0x10F4 Rotator_ctor_sound CreateUpLevelObjects, CreateLifter
SpeedCylinder 0x150C Pendulum_ctor CreateUpLevelObjects
TimeButton 0x10E8 Rotator_ctor_nosound CreateUpLevelObjects
Looper 0x1500 Looper_ctor CreateMechanicalObjects
Gear 0x1514 Gear_ctor CreateMechanicalObjects, Scene_CreateObject_Gear
BigGear 0x1514 Gear_ctor (same, with scale 0.5) CreateMechanicalObjects
Rotator 0x1508 Rotator_ctor CreateMechanicalObjects, CreateSpinny
Pendulum 0x1504 Pendulum_ctor CreateMechanicalObjects
Spinny 0x1508 Rotator_ctor CreateSpinny
Fan (TowerLevel) 0x1188 TowerLevel_Ctor CreateExpertLevelObjects
Sawblade 0x111C Sawblade_Level_Ctor CreateExpertLevelObjects
Spinner (Expert Bridge) 0x10FC Spinner_Level_ctor CreateExpertLevelObjects
Judge 0x1100 Gear_Level_ctor CreateExpertLevelObjects
Bell 0x10E8 Tipper_Level_Ctor CreateExpertLevelObjects
MeshWorld 0x10D0 MeshWorld_ctor All Board ctors
MeshNode 0x18 MeshNode_ctor Tower, Expert Board ctors
CollisionLevel 0x10D0 CollisionLevel_ctorWithLevel All Board ctors

12. Difficulty Gate Summary

Objects that ONLY spawn on Hard difficulty (App+0x23C != 0):

  • TIPPER (CreateLevelObjects)
  • BONK (CreateLevelObjects, CreateExpertLevelObjects)
  • BLOCKDAWG1 (CreateLevelObjects)
  • BLOCKDAWG2 (CreateLevelObjects)
  • GLUEBIE (CreateLevelObjects — inverted: skipped when App+0x23C == 0)
  • FAN (CreateExpertLevelObjects)
  • SAWBLADE (CreateExpertLevelObjects)

Objects that spawn regardless of difficulty:

  • BRIDGE, BBRIDGE1, BBRIDGE2, POPCYLINDER, CATAPULT (CreateLevelObjects)
  • LIFTER, SPEEDCYLINDER, TIMEBUTTON (CreateUpLevelObjects)
  • LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM (CreateMechanicalObjects)
  • SPINNY (CreateSpinny)
  • GEAR (Scene_CreateObject_Gear)
  • BRIDGE(Expert), JUDGE, BELL (CreateExpertLevelObjects)

13. param_block Layout (Confirmed)

The param_4 pointer points to a structure with:

+0x00: (header/flags)
+0x04: float position.X
+0x08: float position.Y  
+0x0C: float position.Z
+0x10: float rotation.X (or secondary position for GEAR/BIGGEAR)
+0x14: float rotation.Y (or extra float for FAN/Spinner)
+0x18: float rotation.Z

For most objects, position is copied from param_4+4/+8/+0xC.
For GEAR/BIGGEAR, both position (param_4+4/+8/+0xC) AND rotation (param_4+0x10/+0x14/+0x18) are used.
For FAN and Expert BRIDGE (Spinner), param_4+0x14 is passed as an additional float parameter.


🔗 Related Documents

File Formats

types : project
keywords :

📂 View source on GitHub


Hamsterball File Formats

MESHWORLD Format (.MESHWORLD)

Binary level format containing object definitions with positions, rotations, and properties.

Header

  • Bytes 0-3: Object count (uint32 LE)
  • Bytes 4-7: First object type string length (uint32 LE)

Object Types (confirmed from Arena-Beginner.MESHWORLD)

  • START1-1, START2-1, START2-2 - Player start positions (1P and 2P)
  • FLAG02, FLAG04, FLAG06, FLAG07 - Checkpoint flags
  • SAFESPOT - Safe landing spot
  • CAMERALOOKAT - Camera look-at target (from earlier strings analysis)
  • PLATFORM - Platform object

Arena-SpawnPlatform.MESHWORLD Analysis

First 0x22 bytes: Header with object count = 0x22 (34)
Then each object contains:

  • 4 bytes: string length
  • N bytes: null-terminated type string (e.g., "START2-2\0")
  • Floats: position (x, y, z), orientation, scale data
  • Per-object variable-length data

Key floats observed:

  • 0x3F800000 = 1.0f (scale/identity matrix values)
  • 0xC2480000 = -50.0f
  • 0x41555555 ≈ 13.33f (y-position values)
  • 0x3E2B01F6 ≈ 0.167f (small scale factors)
  • 0x3F550000 ≈ 0.832f
  • 0xC2000000 = -32.0f

Special object type: N:SINKPLATFORM - sinking platforms
E:NODIZZY<TIME>50</TIME> - modifier with XML-like embedded properties

Sub-objects within MESHWORLD

Each MESHWORLD contains:

  1. Vertex data (arrays of floats for positions, normals, UVs)
  2. Object definitions with type strings
  3. Embedded XML-like properties (e.g., <TIME>50</TIME>)
  4. Texture references (e.g., "PinkChecker.bmp", "BlackCheckerFlag.png")
  5. Face/index data near end of file

MESH Format (.MESH)

Custom binary 3D mesh format used for game models.

Header Structure

Offset  Size  Description
0x00    4     Version (uint32 LE) = 1
0x04    4     Mesh name length (uint32 LE)
0x08    N     Mesh name (null-padded ASCII)

Post-Header Data (Sphere.MESH)

After name string:

  • Position: 3 floats (x, y, z) = (0, 0, 0)
  • Scale: 3 floats (1.0, 1.0, 1.0)
  • Unknown: 3 floats (1.0, 1.0, 1.0) - possibly bounding box extents
  • Material properties: diffuse color RGB (3 floats), ambient (3 floats), specular (3 floats)
  • Specular power: 1 float (25.0)
  • More transform/material data

Then texture references:

4 bytes: texture name length (uint32 LE)
N bytes: texture filename (null-terminated, e.g., "HamsterBall.png")

After textures: vertex data arrays.

Known Meshes

  • Sphere.MESH - Default hamster ball
  • Sphere+Tar.MESH - Ball with tar texture
  • SphereBreak1.MESH, SphereBreak2.MESH - Broken ball fragments
  • Hamster.MESH, Hamster-Trot1/2/3.MESH, Hamster-Waiting.MESH - Hamster model + animations
  • Sawblade.MESH, SawFace.MESH - Sawblade obstacle
  • Various obstacle models (Fanblades, Chomper, etc.)

HS.CFG Format (Save/Config)

Binary format, 1300 bytes:

  • Fixed-size records with null-padded strings
  • Player names: "R. FINK", "SQUEAKS", "HAMMSTURABBI", "PEEPUMS", "MR. RAPTIS"
  • Appears to store high scores per race category
  • Each record: 64-byte name field + score data

RaceData.xml Format

XML structure per race:

<RACENAME>
    <TIME>seconds</TIME>
    <PAR>seconds</PAR>
    <WEASEL>time</WEASEL>
    <GOLD>time</GOLD>
    <SILVER>time</SILVER>
    <BRONZE>time</BRONZE>
    <CAM>float</CAM>  <!-- camera parameter -->
</RACENAME>

Race Categories (in order)

  1. BEGINNERRACE (60s, par 47s)
  2. CASCADERACE (50s, par 25s)
  3. INTERMEDIATERACE (45s, par 35s)
  4. DIZZYRACE (40s, par 35s)
  5. TOWERRACE (35s, par 35s)
  6. UPRACE (30s, par 25s)
  7. NEONRACE (30s, par 25s)
  8. EXPERTRACE (30s, par 20s)
  9. ODDRACE (30s, par 20s)
  10. TOOBRACE (25s, par 20s)
  11. WOBBLYRACE (25s, par 20s)
  12. GLASSRACE (25s, par 10s)
  13. SKYRACE (25s, par 5s)
  14. MASTERRACE (55s, par 2s)
  15. IMPOSSIBLERACE (50s, par 2s)

Jukebox.xml Format

Music configuration mapping race names to MO3 song indices:

<SONG> * comment *
    <NAME>display name</NAME>
    <HEX>hex_index</HEX>
</SONG>

The HEX value is a position/pattern index into the MO3 music file.

Font Description Format

Binary font metric file in Fonts/<name>/font.description:

  • Entry count (uint32)
  • Glyph count/character height
  • Per-glyph entries: character code, x position (float), width (float), UV coordinates
  • Paired with texture atlas PNGs (Data0.png, Data1.png, etc.)

MO3 Music Format

Music/Music.mo3 - MO3 format (BASS library tracker format)

  • Contains all game music in a single module
  • Indexed by HEX values from Jukebox.xml
  • Uses BASS_MusicLoad/BASS_MusicPlayEx for playback

Sound Effects

All in Sounds/ directory as OGG Vorbis files.
Loaded via BASS library (BASS_ChannelSetAttributes, BASS_ChannelStop).


🔗 Related Documents

Font & Text Rendering System

types : rendering
keywords :

📂 View source on GitHub


Font & Text Rendering System

Overview

Hamsterball uses a custom bitmap font system with a binary descriptor format.
Fonts are loaded from font.description files with associated dataN.png glyph
atlases. The font renderer supports both 2D and 3D text rendering.

Font Loading (LoadFont 0x457130)

font.description Binary Format

Header:
  uint32 page_count;              // Number of glyph atlas pages
  uint32 glyph_count;            // Number of glyph definitions

Pages (page_count entries):
  // Each page: "%s\\data%d.png" → Graphics_FindOrCreateTexture()
  // Textures loaded as PNG files, alpha-enabled

Glyphs (glyph_count entries):
  uint8  char_code;               // ASCII character code (1 byte)
  uint32 x_advance;               // Horizontal advance pixels (4 bytes)
  uint8[4] unknown1;              // Unknown field (4 bytes)
  uint8[4] unknown2;              // Unknown field (4 bytes)
  uint8[4] unknown3;             // Unknown field (4 bytes)
  float  height;                  // Glyph height (4 bytes)
  uint32 unknown4;               // Unknown (4 bytes)
  uint32 unknown5;               // Unknown (4 bytes)
  uint32 unknown6;               // Unknown (4 bytes)
  // Total: 33 bytes per glyph entry

FontList Structure (large, ~0x1490 bytes)

Offset Type Description
+0x00 vtable* FontList_ScalarDtor (0x4D8E30)
+0x04 void* Graphics device pointer (param_1)
+0x08 AthenaList Texture pages
+0x420 int Glyph count
+0x424 int Max glyph height (updated during load)
+0x428 float Scale (default 1.0f = 0x3F800000)
+0x42C char[5120] Per-character flags (0x500 * 4 = 0x1400) — glyph availability
+0x43C void*[256] Per-character sprite slots (256 * 0x14)
+0x434 int[256] Per-character x position offsets
+0x10E0 int[256] Per-character y position offsets
+0x430 int[256] Per-character widths

Font Load Process

  1. Zero-initialize glyph table (0x500 * 4 bytes at +0x42C)
  2. Open {font_dir}\\font.description with _open(path, 0x8000) (O_RDONLY)
  3. Read page_count and glyph_count from header
  4. For each page: load {font_dir}\\data{N}.png via Graphics_FindOrCreateTexture
  5. For each glyph: read 33 bytes, call Gadget_AddSpriteSlot with glyph params
  6. Track max height at FontList+0x424
  7. Close file

Font Rendering

Font_DrawGlyph (0x457440)

Renders a string character by character:

for each char in text:
  if char == '\n':
    y_offset += line_height (+0x424)
    x_position = start_x
  elif glyph_available[char] (+0x42C offset):
    if scale == 1.0:
      Sprite_DrawRect(glyph_sprite, x + glyph_x_offset, y + glyph_y_offset)
    else:
      Scene_CreateObject4f(glyph_sprite, x, y, 
                           scaled_width, scaled_height, ...)
  advance x by glyph width

Font_DrawCentered (0x42C870)

8 xrefs — renders text centered at position:

  1. Measure text width with Font_MeasureText
  2. Center x = position - (width / 2)
  3. Call Font_DrawGlyph at centered position

Font_MeasureText (0x456E20)

56 xrefs — measures pixel width of text string:

  1. Sum per-character x_advance values
  2. Account for scale factor
  3. Return total width in pixels

Font_DrawGlyph3D (0x457690)

1 xref — renders text in world-space 3D coordinates.
Used for floating text in game world (goal markers, score displays).

Font_WordWrap (0x456E80)

3 xrefs — word-wraps text to fit within a maximum width.
Inserts newline characters where needed.

Font_RenderToTextureComplex (0x472340)

4 xrefs — renders text to an off-screen texture for compositing.
Used for complex text rendering (shadows, outlines, gradients).

Font_RenderChannels (0x4A91D0)

2 xrefs — renders font glyph channels (for font building).

Font Processing Pipeline (SDF Builder)

The game includes a full SDF (Signed Distance Field) font builder:

  • Font_DecodeGlyphBits (0x4AD716) — Decode glyph bitmap from font description
  • Font_IncrementGlyphMask (0x4B4CF2) — Advance glyph mask for coverage
  • Font_ComputeGlyphBounds (0x4B4D5E) — Compute bounding box of glyph
  • Font_SplitGlyphQuadrants (0x4B5096) — Quadtree split for SDF generation
  • Font_ComputeGlyphCentroid (0x4B51E9) — Compute glyph center of mass
  • Font_ComputeGlyphCoverage (0x4B5321) — Compute pixel coverage percentage
  • Font_CompositeGlyph (0x4B60EE) — Composite glyph onto final texture

This pipeline generates the dataN.png glyph atlas files from TrueType fonts
during development. The runtime only loads the pre-built atlases.

Font Resources

Font Files (5 fonts loaded in App_ResourceLoader)

App Offset Path Point Size Usage
+0x318 fonts\showcardgothic28 28pt Title text, HUD
+0x31C fonts\showcardgothic14 14pt Small labels, scores
+0x320 fonts\showcardgothic16 16pt Medium labels
+0x324 fonts\arialnarrow12bold 12pt UI body text
+0x328 fonts\showcardgothic72 72pt Large display text

Each font directory contains:

  • font.description — Binary font descriptor (header + glyph defs)
  • data0.png, data1.png, ... — Glyph atlas pages

Font Drawing API

Address Function Description
0x457130 LoadFont Load font from font.description file
0x457440 Font_DrawGlyph Render text string (2D)
0x457690 Font_DrawGlyph3D Render text in 3D world space
0x42C870 Font_DrawCentered Center-aligned text rendering
0x456E20 Font_MeasureText Measure pixel width of text
0x456E80 Font_WordWrap Word-wrap text to max width
0x472340 Font_RenderToTextureComplex Render to off-screen texture
0x4A91D0 Font_RenderChannels Render glyph channels
0x4AD716 Font_DecodeGlyphBits Decode glyph bitmap
0x4B4CF2 Font_IncrementGlyphMask Advance glyph mask
0x4B4D5E Font_ComputeGlyphBounds Compute glyph bounding box
0x4B5096 Font_SplitGlyphQuadrants Quadtree split for SDF
0x4B51E9 Font_ComputeGlyphCentroid Compute glyph center
0x4B5321 Font_ComputeGlyphCoverage Compute pixel coverage
0x4B60EE Font_CompositeGlyph Composite glyph onto atlas
0x473940 FontFormatString_Parse Parse printf-style format strings
0x475390 Menu_AddFont Add font to menu system
0x472570 MeshWorld_BuildFontMeshes Build 3D text meshes from font data

Format String Parsing (FontFormatString_Parse)

Parses printf-style format strings with %d, %s, %f, etc.
Uses CRT_ParseFormatString (0x4F7820) for format specifier extraction.

Supports format flags: digits, ., -, +, #, (standard printf).

Menu Text Positioning

Menu items use Matrix_Scale4x4 for text scaling:

  • Each item has a 4-component scale: (X1, X2, Y1, Y2)
  • (1.0, 1.0, 1.0, 1.0) = normal size
  • (0.75, 1.0, 0.75, 1.0) = 75% width for emphasized items
  • (1.0, 0.5, 0.5, 1.0) = half Y-size for "FRENZIED!" difficulty

🔗 Related Documents

FPS Mod

types : tools
keywords :

📂 View source on GitHub


Hamsterball FPS Mod

A drop-in mod for the original Hamsterball Windows game that lets you set a custom FPS cap.

What it does

  • Reads TargetFPS and RenderFPS from hamsterball_fps.ini
  • Writes those values into the game's live App struct at App+0x16C and App+0x170
  • Optionally enables the old "uncap" mode (Uncap=1) which NOPs the render-skip throttle and forces immediate swap intervals

Installation (Windows)

  1. Copy these files into your Hamsterball game folder:
    • bass.dll (the proxy)
    • hamsterball_fps.ini
    • install_fps_mod.bat (optional helper)
  2. Double-click install_fps_mod.bat, OR manually:
    • Rename the original bass.dllbass_real.dll
    • Rename hamsterball_fps_mod.dllbass.dll
  3. Edit hamsterball_fps.ini and set your desired FPS.
  4. Launch the game normally.

Uninstallation

Run uninstall_fps_mod.bat, or manually delete the proxy bass.dll and rename bass_real.dll back to bass.dll.

Configuration

[FPS]
TargetFPS=144
RenderFPS=144

[Uncap]
Uncap=0
  • TargetFPS: update tick rate cap (default in original game: 88)
  • RenderFPS: render frame cap (default in original game: 75)
  • Uncap=1: additionally remove the render-skip check and vsync limits (may affect physics/timing)

Building from source (Linux + MinGW)

cd tools/bass_fps_proxy
make
make package   # creates hamsterball-fps-mod.zip

How it works

The game loads bass.dll at startup. This proxy forwards every BASS call to the real bass_real.dll and, after the game has initialized, patches the live App singleton via the pointer at 0x005341E0.

Offsets used:

Field Offset Original value
target_fps 0x16C 88
render_fps 0x170 75

Warning

Very high FPS can change physics behavior (ball friction, collision response, timer drift). Test carefully. The uncap mode in particular can cause the well-known "white screen" timing issue on some systems.


🔗 Related Documents

FPS Uncap

types : tools
keywords :

📂 View source on GitHub


Hamsterball FPS Uncap — bass.dll Proxy

What This Does

Hamsterball has a built-in FPS limiter that caps rendering at ~100 FPS (in practice ~75 FPS on most systems due to 15.6ms timer granularity). This proxy DLL replaces bass.dll — the game ships it and imports 10 audio functions from it — with a shim that forwards all audio calls to the original (bass_real.dll) while patching the game's rendering limiter in memory.

Result: the game renders as fast as the GPU can push frames — uncapped FPS, butter-smooth motion.

Why bass.dll?

The game imports bass.dll for audio. Windows DLL search order loads our proxy first (same directory as the EXE), so our bass.dll loads instead of the original, then loads the original as bass_real.dll and forwards every function call to it. This is a classic "DLL proxy" technique — audio works identically, but our code runs inside the game's process with full memory access.

The Failed Approaches (and why they failed)

Before arriving at the final solution, we went through several iterations. Understanding why each failed is critical for understanding why the final approach works.

Attempt 1: Patch fps_target (100 → 1000)

Theory: The game has a struct field fps_target (at App+0x830) set to 100 in the constructor. Raising it to 1000 should raise the FPS cap.

Why it failed: fps_target is read by an idiv instruction in App_Run that computes render_interval = time_slice / fps_target. This idiv runs once at the top of App_Run, before the main loop begins. Our patches fire 500ms after DLL load (via a background thread) — by then, App_Run has already executed the idiv and stored render_interval in a stack variable. Patching the struct value after that point has zero effect on the running game.

Attempt 2: Patch the idiv instruction itself

Theory: Instead of patching the struct value, patch the idiv instruction in the code so it divides by 1000 instead of 100.

Why it failed: Same timing problem. The idiv executes once at function entry. Even if we patch the instruction bytes, the computation has already happened — the result is sitting on the stack as a local variable. Changing the code after it runs is useless.

Also: raising fps_target without first fixing the timer causes the frame_time threshold to go negative, producing a white screen. The game's GetTickCount() returns 15.6ms-resolution values (Windows default), so when fps_target is high enough that the expected frame time (1/FPS) is smaller than the timer resolution, the subtraction underflows. This is why you can't just raise fps_target — you must also hook GetTickCount with QueryPerformanceCounter first.

Attempt 3: Patch with short 3-byte patterns

Theory: Use byte-pattern matching to find and patch the cmp eax, 10 (spin counter cap) instruction.

Why it failed: The 3-byte pattern 83 F8 0A (cmp eax, 10) matches 13 locations in the .text section — most of which have nothing to do with the frame limiter. Overwriting all of them corrupted audio, D3D8 initialization, and rendering code paths. The game showed a white screen with no sound.

Fix: Restricted to a 9-byte context pattern (inc eax; cmp eax, 10; mov [esp+0x10], eax; jge) that uniquely identifies the spin-counter location. This fixed the corruption but didn't fix the white screen (because the root cause was the idiv timing issue, not the spin counter).

Attempt 4: The jbe NOP (WORKS)

The breakthrough: Stop trying to patch computed values that are baked into stack variables at init time. Instead, patch the render decision — a jbe (jump-if-below-or-equal) instruction that is checked every single frame.

At address 0x46BF55 in the game loop, there's a jbe skip_render instruction (76 5D). If the elapsed time since the last render is below the threshold, it jumps over the render path — skipping Present(). NOPing this jump (90 90) means the game always falls through to the render path, every single loop iteration, regardless of any timing value.

This works regardless of when our patch fires, because the instruction is re-evaluated every frame. It doesn't matter that render_interval was computed once at init — the jbe that gates rendering is checked continuously.

The Final Solution (v7)

Patches Applied (4 total)

# Patch Address Original Bytes Patched Bytes Purpose
1 GetTickCount IAT hook kernel32.dll IAT entry (pointer) hooked_GetTickCount High-resolution timing — replaces 15.6ms GetTickCount with QPC-based sub-ms precision
2 Render-skip jbe NOP VA 0x46BF55 76 5D 90 90 Forces rendering every frame — the core FPS uncap
3 vsync disable (×2) Pattern match C7 86 F8 01 00 00 01 00 00 00 C7 86 F8 01 00 00 00 00 00 80 Sets D3DPRESENT_INTERVAL_ONED3DPRESENT_INTERVAL_IMMEDIATE (disables vsync, 2 locations)
4 timeBeginPeriod(1) Win32 API call N/A N/A Requests 1ms timer resolution from Windows

Why each patch is needed

  1. GetTickCount → QPC hook: Without this, timeBeginPeriod(1) alone is unreliable on Windows 10 v2004+ / Windows 11. The game's GetTickCount() calls return 15.6ms-resolution values, and the frame-time threshold calculation can underflow (go negative) when FPS targets are high. The QPC hook gives us sub-millisecond precision (100ns on most systems).

  2. jbe NOP (the core patch): This is the one that actually uncaps FPS. The game loop at 0x46BF55 checks whether enough time has passed since the last render. If not, it jumps over the Present() call. NOPing this jump forces a render every iteration. Because this instruction is checked every frame, it doesn't matter that our patch fires 500ms after startup.

  3. vsync disable: The game calls CreateDevice with D3DPRESENT_PARAMETERS.PresentationInterval = D3DPRESENT_INTERVAL_ONE (vsync on). This caps FPS at the monitor refresh rate (60 Hz for most). Patching the constant from 1 to 0x80000000 (D3DPRESENT_INTERVAL_IMMEDIATE) disables vsync so the GPU can present as fast as it renders. There are 2 occurrences in the binary.

  4. timeBeginPeriod(1): Requests that Windows use 1ms timer resolution instead of the default 15.6ms. This improves the resolution of Sleep(), WaitForSingleObject(), and other timing functions the game may use internally.

What is NOT patched (and why)

  • fps_target (100): Left at the original value. It's only read once at init to compute render_interval, and we bypass that entire code path with the jbe NOP. Patching it would risk the white-screen underflow bug if our QPC hook hasn't initialized yet.
  • fps_divisor (75): Same — it's an init-time computed value. The jbe NOP makes it irrelevant.
  • Spin counter cap: The game has a counter that force-renders after 10 skipped frames (a failsafe to prevent total freeze). Since we never skip renders, this counter never increments, so patching it is unnecessary.

File Layout

tools/bass_fps_proxy/
├── bass_fps_proxy.c    — Full proxy DLL source (311 lines)
├── bass_exports.def    — Module definition file (exports the 10 BASS functions)
├── bass.dll            — Compiled proxy (statically linked)
└── README.md           — This file

How to Build

Prerequisites

  • MinGW cross-compiler: i686-w64-mingw32-gcc (32-bit, since Hamsterball.exe is PE32 i386)
  • On Ubuntu/Debian: apt install gcc-mingw-w64-i686

Build Command

i686-w64-mingw32-gcc -shared -o bass.dll bass_fps_proxy.c \
    bass_exports.def -lwinmm \
    -Wl,--enable-stdcall-fixup -O2 \
    -static -static-libgcc -Wl,--add-stdcall-alias

Critical flags:

  • -static -static-libgcc: Statically links libgcc, otherwise the DLL depends on libgcc_s_dw2-1.dll which won't exist on the target machine. Must include both flags.
  • -Wl,--enable-stdcall-fixup and -Wl,--add-stdcall-alias: Ensures the exported function names match what the game imports (some imports use decorated names like _BASS_Init@20, some use plain names).
  • -lwinmm: Links timeBeginPeriod/timeEndPeriod.
  • bass_exports.def: Defines the 10 exports with their ordinals.

Verify No External Dependencies

i686-w64-mingw32-objdump -p bass.dll | grep "DLL Name"

Should show only kernel32.dll, user32.dll, winmm.dll, and bass_real.dll — NOT libgcc_s_dw2-1.dll.

How to Install

  1. In your Hamsterball game folder, rename the original bass.dll to bass_real.dll
  2. Copy the compiled bass.dll into the same folder
  3. Launch the game

The proxy will:

  • Load bass_real.dll and forward all 10 BASS audio functions to it (audio works normally)
  • After 500ms, apply the 4 patches in a background thread
  • Write a log file named Hamsterball_fps.log in the same folder

How to Remove

Delete the proxy bass.dll, rename bass_real.dll back to bass.dll.

Log Output

The DLL writes Hamsterball_fps.log next to the EXE:

Hamsterball FPS Uncap Proxy v7.0
=================================
Patches applied: 4
  GetTickCount IAT hook: OK (QPC-based sub-ms timing)
  render-skip jbe NOP @0x46BF55: OK (always render)
  vsync: INTERVAL_ONE -> INTERVAL_IMMEDIATE
  timer: timeBeginPeriod(1)
  fps_target: 100 (ORIGINAL - correct physics)
  QPC frequency: 10000000 Hz (100.000 ns resolution)

If the jbe patch fails (bytes don't match), it will show FAILED (bytes mismatch) — this means the game EXE is a different version than expected.

Technical Details

The jbe Instruction at 0x46BF55

In the game loop (App_Run / GameLoop at VA 0x46BD80), after computing physics and input, the game checks whether it's time to render:

; ... time calculation ...
cmp eax, [render_interval]    ; has enough time elapsed?
jbe skip_render               ; 0x76 0x5D — if not, skip Present()
; ... D3D Present() ...
skip_render:
; ... continue loop ...

The jbe (jump if below or equal) at 0x46BF55 is the gatekeeper. NOPing it (0x90 0x90) makes the CPU unconditionally fall through to the render path.

The vsync Pattern

Original:     C7 86 F8 01 00 00 01 00 00 00
              mov dword ptr [esi+0x1F8], 0x00000001   ; INTERVAL_ONE
Patched:      C7 86 F8 01 00 00 00 00 00 80
              mov dword ptr [esi+0x1F8], 0x80000000   ; INTERVAL_IMMEDIATE

This patches the PresentationInterval field of D3DPRESENT_PARAMETERS before CreateDevice is called. Two occurrences exist (likely one for each of the game's two CreateDevice paths — windowed and fullscreen).

The GetTickCount IAT Hook

We walk the PE import table, find the kernel32.dll import for GetTickCount, and replace the function pointer with our own hooked_GetTickCount(). Our version uses QueryPerformanceCounter to get sub-millisecond precision:

static DWORD WINAPI hooked_GetTickCount(void)
{
    LARGE_INTEGER now;
    QueryPerformanceCounter(&now);
    return (DWORD)((now.QuadPart * 1000ULL) / g_qpc_freq.QuadPart);
}

This is necessary because timeBeginPeriod(1) alone is insufficient on Windows 10 v2004+ / Windows 11 — the OS no longer reliably honors the 1ms request for GetTickCount.

Key Addresses

All addresses assume image base 0x400000 (Hamsterball.exe default, no ASLR):

Address Description
0x46BD80 Game loop (GameLoop / App_Run body)
0x46BF55 jbe skip_render — the frame render gate (patched to NOP)
0x4278E0 WinMain
0x4FD680 App global instance
0x4BA57B Game's operator_new

Version History

Version Approach Result
v1-v2 Patch fps_target + fps_divisor White screen (timer underflow)
v3-v4 Add GetTickCount QPC hook + timeBeginPeriod Audio works, FPS still capped at ~75
v5-v6 Raise fps_target to 1000 + fix timer first Audio works, ~111 FPS ceiling (vsync still on)
v7 (early) Add vsync disable + short 3-byte patterns White screen (pattern overmatch corruption)
v7 (mid) Fix patterns to 9-byte context White screen (idiv timing issue — patches fire too late)
v8 Remove idiv/fps_target patches, keep only jbe NOP + vsync + timer Works — uncapped FPS
v7.0 (final) Clean rewrite with absolute addressing + jbe NOP Works — uncapped FPS, clean code

🔗 Related Documents

FPS Unlock

types : tools
keywords :

📂 View source on GitHub


Hamsterball FPS Unlock

Removes the game's built-in frame rate caps (100 Hz update / 75 Hz render).

What It Does

Hamsterball has two FPS caps:

  1. Update cap: 100 Hz (App+0x16C = 100, frame_time = 1000/100 = 10ms)
  2. Render cap: 75 Hz (App+0x170 = 75, render_time = 1000/75 = 13ms)

The game loop in App_Run (0x46BD80) checks if enough time has passed since the last update/render. If not, it skips the frame and calls Sleep(0). There's also a conditional jump (JBE at 0x46BF55) that skips the Present call if the render rate isn't met.

This DLL:

  • Sets App+0x16C = 1000 (update cap → 1000 Hz)
  • Sets App+0x170 = 1000 (render cap → 1000 Hz)
  • NOPs out the render-skip JBE at 0x46BF55 (always render)

Files

File Description
fps_unlock.dll The unlock DLL — inject into Hamsterball.exe
fps_unlock.log Created on first run — confirms patches applied

Usage

Method 1: DLL Injection (recommended)

Use any DLL injector (including the one in tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/):

  1. Copy fps_unlock.dll to your Hamsterball game folder
  2. Launch Hamsterball.exe
  3. Inject fps_unlock.dll using your preferred injector
  4. Check fps_unlock.log to confirm patches applied

Method 2: D3D8 Proxy (auto-load)

Rename fps_unlock.dll to d3d8.dll and place it next to Hamsterball.exe. The game will auto-load it on startup. The DLL forwards all D3D8 calls to the real d3d8.dll while applying the FPS patch.

(Note: proxy mode requires additional forwarding code — see the d3d8_proxy_logger tool for an example of how to build a full proxy DLL.)

What Changed (Technical Details)

App_Ctor (0x46DC40) — Default Values

param_1[0x5b] = 100;   // App+0x16C = target FPS (100 Hz)
param_1[0x5c] = 0x4b;  // App+0x170 = render FPS (75 Hz)

App_Run (0x46BD80) — Frame Timing

// Update cap: 1000 / App+0x16C = 10ms per update tick
frame_time = 1000 / app->target_fps;   // 0x46BDF5: IDIV [ESI+0x16C]

// Render cap: 1000 / App+0x170 = 13ms per render
render_time = 1000 / app->render_fps;  // 0x46BDAC: IDIV [ESI+0x170]

// Render-skip check at 0x46BF55:
// JBE 0x46BFB4  → skip Present if not enough time elapsed
// Patched to: NOP NOP (always render)

Patches Applied

Address Original Patched Description
App+0x16C 100 (0x64) 1000 (0x3E8) Update FPS cap
App+0x170 75 (0x4B) 1000 (0x3E8) Render FPS cap
0x46BF55 76 5D (JBE) 90 90 (NOP) Render-skip conditional

Limitations

  • Vsync: The game uses D3D8 Present with default presentation interval. If your GPU driver forces vsync, the actual FPS will be limited to your monitor refresh rate. Disable vsync in your GPU control panel for true uncapped FPS.
  • Physics: The game's physics simulation runs at the update rate. At very high FPS, physics may behave differently (faster/smoother). The game was designed for 100 Hz max.
  • Menu: The FPS unlock affects both gameplay and menus.

Building

make

Cross-compiled with MinGW i686 (32-bit Windows target).


🔗 Related Documents

fps_mod_custom

types : mods

📂 View source on GitHub


fps_mod_custom

Custom FPS mod (writes App+0x16C/0x170)

Files

  • hamsterball_fps_mod.c — C source code
  • hamsterball_fps_mod.dll — Compiled DLL (PE32 i386)
  • hamsterball-fps-mod.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

fps_unlock_standalone

types : mods

📂 View source on GitHub


fps_unlock_standalone

Standalone fps unlock DLL (injector, not bass proxy)

Files

  • fps_unlock.c — C source code
  • fps_unlock.dll — Compiled DLL (PE32 i386)

Proxy Type

Standalone DLL (not a bass proxy). Use an DLL injector or launcher tool.


🔗 Related Documents

fps_unlock_v9

types : mods

📂 View source on GitHub


fps_unlock_v9

Uncaps FPS (v9, QPC timer + vsync disable)

Files

  • bass_fps_proxy.c — C source code
  • bass.dll — Compiled DLL (PE32 i386)
  • hamsterball-fps-uncap-bass-v9.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

Fun Ball Mod

types : mods

📂 View source on GitHub


Fun Ball Mod

Info

  • File: bass.dll (proxy)
  • Effect: Spawns player 1's ball as the FunBall mesh every race
  • Android-safe: No IAT hooks, no code caves, no D3D API calls

What it does

The game loads the FunBall mesh (index 10) into the ball mesh array at
board+0x244 but never assigns it — Ball_ctor2 always sets ball+0x754=0
(Sphere). This mod writes ball+0x754=10 every frame (20fps) so the game
renders the FunBall mesh for player 1's ball at the start of every race.

No button activation needed — it's automatic.

Build

i686-w64-mingw32-gcc -shared -o bass.dll fun_ball.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy mod bass.dll to game folder
  3. On Android/Wine: set DLL override to native for bass.dll

🔗 Related Documents

Function Map

types : decompilation
keywords :

📂 View source on GitHub


Hamsterball - Function Map

Binary: Hamsterball.exe (MD5: 7d25019366b8d7f55906325bd630d7fe)
Total functions: 3,781 (Ghidra analysis)
Documented: 3781/3781 (100%)
User-labeled: 975+
Sessions: 14-28 (50→71.6%), 29-39 (71.6→96%), 40-45 (96→97.6%), 46 (97.6→100%)

Entry Point and Lifecycle

Address Name Description
0x004BB4C8 entry CRT entry point (GetVersionEx, heap init, etc.)
0x004278E0 WinMain Game entry: Init → Run → Shutdown
0x00429530 App_Initialize_Full 26-step init sequence
0x0046BB40 App_Initialize 12-step base init (vtable calls + D3D8)
0x0046BD80 App_Run Game loop (PeekMessage → Update → Render)
0x0046BA10 App_Shutdown Cleanup on exit

Graphics Subsystem (Direct3D 8)

Address Name Description
0x00455380 Graphics_Initialize D3D8 init, adapter check, device creation
0x00455A60 Graphics_Defaults Set default render states
0x00453B50 Graphics_BeginFrame Begin frame/render setup
0x00455A90 Graphics_PresentOrEnd Present frame or end scene
0x004542C0 Graphics_ctor Graphics constructor (vtable 0x4D88A0, init render context, texture cache, frustum)
0x00455360 Graphics_dtor Graphics destructor (cleanup + optional free)
0x00454550 Graphics_Cleanup Release D3D objects, free texture path, clear cache
0x00454000 Graphics_SetTexturePath Set custom texture prefix path (strdup at +0x7D8)
0x00454060 D3DFMT_ToString Convert D3DFORMAT enum to debug string
0x00454B50 Graphics_SetViewport Set viewport dimensions
0x00454D30 Graphics_Reset Reset device with new params, CreateDevice twice
0x00455D60 Graphics_DrawScreenRect Draw 2D screen rectangle (x,y,w,h → TLVERTEX triangle strip). 63 xrefs.
0x00455110 Graphics_ApplyMaterialAndDraw Apply material/render states + draw textured geometry. 17 xrefs.
0x00454190 Graphics_SetRenderMode Set shading mode, reset vertex shader, re-apply render states
0x00455B80 Graphics_SetStreamBuffers Set vertex buffer stream sources for rendering
0x00457FA0 RenderContext_Init Initialize render context struct (0x50 bytes, vtable 0x4D8E68)
0x00401160 Graphics_SetViewportClip Set viewport clipping bounds from 4x4 matrix

Audio Subsystem (BASS)

Address Name Description
0x0046A020 LoadMusicFile BASS_MusicLoad wrapper
0x0046A4D0 LoadJukebox Parse jukebox.xml
0x00442CE0 OptionsMenu_ctor Options screen (Resolution, Fullscreen, Color, Safe Mode, Volume, Key Remap, Mouse, Pause)
0x00487E40 BASS_SetConfig IAT thunk
0x00487E46 BASS_Start IAT thunk
0x00487E4C BASS_Init IAT thunk
0x00487E52 BASS_Free IAT thunk
0x00487E58 BASS_Stop IAT thunk
0x004794B0 BASS_ErrorGetCode IAT thunk
0x004794B6 BASS_MusicLoad IAT thunk
0x004794BC BASS_ChannelStop IAT thunk
0x004794C2 BASS_ChannelSetAttributes IAT thunk
0x004794C8 BASS_MusicPlayEx IAT thunk

Level/Game World System

Address Name Description
0x0045DE30 LoadMeshWorld Load .meshworld level file
0x004706E0 MeshWorld_ctor MeshWorld object constructor (0x488 bytes)
0x00470930 MeshWorld_Parse Parse MESHWORLD text format (*MATERIAL, *MESH, etc.)
0x00458970 CreateMeshBuffer Allocate mesh vertex/index buffer
0x00458A80 InitMaterialArray Initialize material array
0x0040D1C0 GetLevelPath Get path for level (levels/levelN)
0x0040A120 LoadRaceData Parse racedata.xml

Game Object Factories

Address Name Description
0x0040FA20 CreateBumper Create BUMPER1/2/3/4 objects
0x00413CE0 CreateBumper2 Create bumper variant
0x00410D00 NeonCollisionEvents Create E:LIMIT boundary
0x004117B0 CreateUpLevelObjects Create SPEEDCYLINDER
0x00412850 HandleArenaCollisionEvents Create N:SPINNER
0x0040E250 CreateExpertLevelObjects Create SAWBLADE
0x0040BF50 CreateMouseTrap Create MOUSETRAP
0x0040C5D0 DispatchCollisionEvents Create E:NODIZZY
0x00438B30 CreateBonkPopup Create BONKPOPUP feedback
0x0041D060 LevelBoard_Dizzy_ctor BoardLevel3 constructor
0x0040BAA0 CreateSecretObjects Create SECRET and SECRETUNLOCK objects
0x0043DFB0 Secret_ctor Secret object constructor (0x10EC bytes)
0x004121D0 CreateLevelObjects Multi-factory: BRIDGE, TIPPER, BONK, BBRIDGE1/2, POPCYLINDER, BLOCKDAWG1/2, CATAPULT, GLUEBIE
0x004133E0 CreatePlatformOrStands Create PLATFORM and STANDS objects
0x00417FE0 CreateMechanicalObjects Multi-factory: LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
0x00437040 Platform_ctor Platform object constructor (0x10FC bytes)
0x00462850 Stands_ctor Stands/Audience constructor (0x10D0 bytes)
0x00435800 Looper_ctor Looper (loop-de-loop) constructor (0x1500 bytes)
0x00437590 Gear_ctor Gear/BigGear constructor (0x1514 bytes)
0x00435940 Rotator_ctor Rotator constructor (0x1508 bytes)
0x00437700 Pendulum_ctor Pendulum constructor (0x1504 bytes)
0x00437960 Tipper_ctor Tipper (tipping platform) constructor (0x1104 bytes)
0x004661A0 TipperVisual_ctor Tipper visual component
0x00465200 TipperVisual_Attach Attach visual to tipper
0x00438850 Bonk_ctor Bonk (hammer) constructor (0x1200 bytes)
0x00436D70 BreakBridge_ctor Breakable bridge constructor (0x1100 bytes)
0x00436EE0 PopCylinder_ctor Pop cylinder (pop-up obstacle) constructor (0x10E8 bytes)
0x0043C310 Blockdawg_ctor Blockdawg (block creature) constructor (0x1154 bytes)
0x00437E10 Catapult_ctor Catapult constructor (0x1108 bytes)
0x00437CB0 Gluebie_ctor Gluebie (glue blob) constructor (0x110C bytes)

Camera System

Address Name Description
0x00413280 CameraLookAt Set camera look-at (CAMERALOOKAT)

Menu/UI Screens

Address Name Description
0x0042DE50 MainMenu_ctor Main menu (LET'S PLAY, HIGH SCORES, OPTIONS, CREDITS, EXIT)
0x00442CE0 OptionsMenu_ctor Options (Resolution, Fullscreen, Color, Volume, Remap, Mouse)
0x004254E0 CreditsScreen_ctor Credits scrolling screen (formerly Physics_Init)
0x0042B470 HighScoreEntry_ctor High score entry screen (name input + score display)
0x0042BD40 HighScoreEntry_Render Render high score entry UI
0x0042E060 GameSelectionScreen_ctor Tournament difficulty selector
0x0044FD60 SaveTourneyDialog_ctor Save tournament dialog
0x004476B0 RegisterDialog_ctor Register/purchase dialog
0x0042E6F0 QuitRaceMenu Race quit confirmation menu

Ball Physics System

Address Name Description
0x00403100 Ball_SetTiltedGravity Set gravity plane to tilted (value 1, normal -1,0,0)
0x00403150 Ball_SetFlatGravity Set gravity plane to flat (value 2, normal 0,0,1)
0x00403850 Ball_SetTrajectory Set ball trajectory direction + force scale
0x00403750 Ball_ApplyTrajectory Apply trajectory force (normalize+scale direction, play sound, frame counter=100)
0x00403980 Ball_FindMeshCollision Wrapper for Mesh_FindClosestCollision
0x00401DD0 Ball_CreateTrailParticles Create trail particles (10 iterations, spawn 0x28 byte objects)
0x00401920 Ball_RenderShadow Render ball shadow (scale by radius*constant, position at ball XYZ)
0x00402BC0 AthenaList_SetIndex Set list iteration index

Collision System

Address Name Description
0x00465EF0 Collision_TraverseSpatialTree Recursive octree traversal for collision tests
0x00453780 AthenaList_Append Append item to dynamic AthenaList

Level Rendering

Address Name Description
0x0040B090 Level_InitScene Init camera/scene (projection, fog, find CAMERALOCUS)
0x0040B420 Level_RenderDynamicObjects Render moving objects (platforms) using Timer position
0x0040B570 Level_RenderObjects Iterate objects calling vtable+0x0C (render callback)
0x0040B600 Level_UpdateAndRender Full update pass: merge lists, pre-render, shadow, cleanup
0x0040B9C0 Level_SetObjectTransform Set world transform from position data
0x0040ACA0 Level_SelectCameraProfile Select camera profile by level difficulty (4-15)

Level/Collision Loading

Address Name Description
0x004652E0 CollisionLevel_ctor Collision-only level (.meshcollision format)
0x00465260 Level_LoadCollision Load binary collision mesh (planes, objects, AABB)
0x004624C0 Level_Cleanup Level destructor (free objects, VBs, textures)

Arena Initialization

Address Name Description
0x00413C20 ArenaLevel_WarmUp_Init Initialize Warm-Up arena (levels\arena-WarmUp)
0x00416F40 ArenaLevel_Neon_Init Initialize Neon arena (levels\arena-neon)

Game Logic

Address Name Description
0x00469CF0 GameUpdate Main update tick
0x00428160 PauseGame Pausing (RightButtonPause)
0x0042FAD0 QuitRace Quit current race
0x0042E6F0 QuitRaceMenu Race quit menu
0x004298C0 TimerDisplay Race timer display
0x004254E0 CreditsScreen_ctor Credits scrolling screen (formerly mislabeled Physics_Init)
0x0040ABA0 CheckArenaUnlock Check arena unlock conditions
0x0040A420 CheckPurchaseOrHighScore Purchase reminder / high score check

Config/Save System

Address Name Description
0x004279F0 LoadOrSaveConfig Config load/save dispatcher
0x0042AE80 LoadConfig Load HS.CFG
0x0042B6E0 SaveConfig Save HS.CFG
0x00433AC0 GameSelectionManager Tournament save/load (TOURNAMENT.SAV)

Font/Text Rendering

Address Name Description
0x00457130 LoadFont Load font.description + PNG glyphs
0x00456E20 Font_MeasureText Measure text string width for centering
0x00457440 Font_DrawGlyph Core glyph rendering (1 call = 1 glyph quad)
0x004013A0 UI_DrawTextCenteredAbsolute Draw centered text (x - width/2)
0x00409C60 UI_DrawTextCentered Draw centered text with shadow
0x004012C0 UI_DrawTextShadow Draw text with shadow (offset + main)

DRM System

Address Name Description
0x00429200 ESellerate_Init eSellerate DRM initialization

Math Utilities

Address Name Description
0x00401AA0 Vec3_NormalizeAndScale Normalize 3D vector and scale to length param_1. 59 xrefs — most common math utility.
0x00401D60 Matrix_TransformVec3 Transform 3D vector by 4x3 matrix. 15 xrefs.
0x00453150 Matrix_Scale4x4 Set 4x4 matrix row scale values.
0x00453200 Matrix_Identity Set matrix to identity (vtable pointer).
0x004532E0 AthenaList_SortedInsert Insert with insertion-sort (ascending/descending).
0x00458B50 Matrix_ScaleTransform Create 4x4 matrix by scaling source rows.
0x0040A050 Color_RandomRGBA Generate 32-bit color from 4 random bytes.
0x004580D0 AABB_ContainsPoint Test if point inside AABB (6 floats).
0x00401010 Vec3_Copy Copy 4 floats (Vec3+padding) from source, preserves vtable
0x00401040 Vec3_Init Init Vec3: set vtable 0x4CF300 + copy 4 floats from source
0x00401070 Vec3_dtor Vec3 deleting destructor: reset to identity, optionally free
0x004016c0 Vec3_Scale Multiply Vec3 by scalar: out = scalar * this
0x00401890 Vec3_DivideByScalar In-place Vec3 division via reciprocal multiply with g_one (1.0f)
0x004018c0 Vec3_AddTwo Add two Vec3s: out = this + rhs
0x004018f0 Vec3_AddInPlace In-place Vec3 addition: this += rhs
0x00401d20 Vec3_Distance Euclidean distance between two 3D points (6 float params)
0x0040a020 AthenaList_GetAt Get element by index with bounds check; returns 0 if OOB

Misc/Utility

Address Name Description
0x00429450 FinishLoad Finish loading (calls final setup)
0x00457AD0 Timer_Init High-resolution timer init
0x00457A40 Timer_Cleanup Timer cleanup

Key Global Data

Address Name Description
0x004FD680 g_App Global App singleton object
0x004D9CDC MeshWorld_vtable MeshWorld virtual function table

App Object Layout (from App_Initialize_Full)

Offset Field Description
0x000 vtable Virtual function table pointer
0x004 hInstance Windows HINSTANCE
0x008 cmdShow nCmdShow parameter
0x054 registryKey Registry/config key
0x05C targetFPS Target frame rate
0x15C width Window width
0x158 windowed Windowed mode flag
0x160 height Window height
0x174 graphics Graphics object pointer
0x17C audioSystem audio system pointer
0x180 d3dDevice D3D device pointer
0x200 initialized Init complete flag
0x208 initStep Current init step string
0x240 cursor Loaded cursor handle
0x278 shadowTexture Shadow texture
0x534 musicHandle BASS music handle
0x538 musicChannel2 Second music channel
0x53C musicChannel1 First music channel
0x550 gameMode1 Game mode object (1)
0x554 gameMode2 Game mode object (2)
0x558 gameMode3 Game mode object (4)
0x55C gameMode4 Game mode object (5)
0x914 playCount Play count from registry
0x159 quitFlag Game quit flag
0x15A someFlag Another control flag
0x156 updateDisabled Update disabled flag
0x5A frameTimeMs Frame delta time
0x5B fpsDenominator FPS denominator
0x5D renderTarget Render target ptr
0x65 frameCounter Per-second frame counter
0x84 profilingSection Current profiling section name
0x1B4 versionString Version string
0x1CC loadedCount Loaded objects counter

Texture System

Address Name Description
0x00471750 LoadMesh Load discrete .mesh files (uses same parser as MESHWORLD)

Graphics Pipeline (27-step init)

Address Name Description
0x00455380 Graphics_Initialize Full 27-step D3D8 init
0x00453C90 Graphics_CreateDevice D3D8 CreateDevice with format fallbacks (0x4b, 0x4f, 0x49, 0x47, 0x50, 0x4d)
0x0042C810 Graphics_InitRenderStates Step 20: Initialize render states
0x00454F10 Graphics_SetViewport Step 22: SetViewport(0,0,w,h)
0x00454AB0 Graphics_SetProjection Step 23: SetProjection(10.0, 5000.0)
0x00454630 Graphics_SetupLights Step 25: Setup D3D8 lights

Collision Event System

Address Name Description
0x0040C5D0 DispatchCollisionEvents Main collision event dispatcher (E:NODIZZY, E:SAFESWITCH, E:LIMIT, E:BREAK, E:JUMP, E:ACTION, E:TRAJECTORY, N:NOCONTROL, N:WATER, N:TARPIT, N:GOAL, N:MOUSETRAP, N:SECRET, N:UNLOCKSECRET, DROPIN, PIPEBONK, POPOUT)
0x0040E6A0 ExpertCollisionEvents Arena-specific collision handler (E:CALLHAMMER, E:HAMMERCHASE, E:ALERTSAW1/2, E:ACTIVATESAW1/2, E:ALERTJUDGES, E:SCORE, E:JUMP, E:BELL + delegates to DispatchCollisionEvents)
0x0040DCD0 TowerCollisionEvents Level-specific collision handler (E:CATAPULTBOTTOM, E:OPENSESAME, N:TRAPDOOR, E:BITE, E:MACETRIGGER, N:MACE + delegates to DispatchCollisionEvents)
0x00434770 Saw_AlertActivate Activate saw blade (alert mode - clear flag, play 3D sound)
0x00434A50 Saw_Activate Activate saw blade (full - set active flag, play 3D sound)
0x00434C40 Judge_Reset Reset judges (clear active flag, re-add to list)
0x00434C80 ScoreDisplay_SetTime Set score display time with format string
0x00434E20 Bell_Activate Activate bell (play sound, set animation)
0x00438BB0 Hammer_ChaseStart Start hammer chase (set chasing flag, copy positions)
0x00434290 Catapult_Launch Catapult launch (set active + timer)
0x004344D0 Trapdoor_Open Open trapdoor (set scale from 0 to 1.0)
0x00438410 Trapdoor_Activate Activate trapdoor (play 3D sound, set timer)

Sound System (BASS)

Address Name Description
0x00459860 Sound_Play3D Play 3D positioned sound (set position via BASS, play channel)
0x004597B0 Sound_PlayChannel Play sound channel (check if playing, dispatch from pool or allocate)
0x004595B0 Sound_StartSample Start BASS sample (via vtable: reset, volume, 3D position)
0x00466750 Sound_CalculateDistanceAttenuation 3D distance attenuation (find closest listener, apply min/max rolloff)

Race/Timer System

Address Name Description
0x0044C260 RaceResultPopup_ctor Race end popup showing rank + TIME'S UP! / OUT OF TIME! text
0x004298C0 TimerDisplay Race timer display

Audio Pipeline

Address Name Description
0x00474540 Audio_Init BASS_Init (tries -1, then 0 device), BASS_Start, BASS_SetConfig
0x0046A310 Audio_PlayMusic BASS_MusicPlayEx wrapper
0x0046A020 LoadMusicFile BASS_MusicLoad wrapper (already labeled)

Supported Display Modes (from Graphics_Initialize)

  • 640x480 (0x280 x 0x1E0)
  • 800x600 (0x320 x 0x258)
  • 1024x768 (0x400 x 0x300)
  • 1280x1024 (0x500 x 0x400)

Supported Refresh Rates

  • 60Hz (0x14)
  • 70Hz (0x15)
  • 72Hz (0x16)
  • 75Hz (0x17)
  • 80Hz (0x18)
  • 85Hz (0x19)
  • 90Hz (0x1a)
    |- 100Hz+ (0x17 fallback)|

Scene System (Core Lifecycle)

Address Name Description
0x00419770 Scene_dtor Master scene destructor. Destroys all AthenaList items (dynamic +0x335, sub-objects +0x43B, static +0x22E), frees level clones (+0x22B/+0x22C), resources (+0x21F, +0xE92), clears 8+ AthenaLists, destroys matrix arrays, cleanup
0x00419C00 Scene_Update Main scene tick. Demo timer (buy dialog), unpause check, ball position updates from +0xA75 list, camera tracking from App+0x5DC, screen offset animation (+0xA6E, -10/frame to 0 or -800), update+delete static objects in +0x22E, vmethods +0x4C/0x50/0x54/0x58 (render passes), iterate dynamic objects +0xD8B
0x0041A2E0 Scene_Render Main render dispatch by player count. Mode 0 (1P): render3D/renderObjects/renderOverlay then HUD. Mode 1 (2P): setup camera from player list +0x3A38, same. Mode 2 (3-4P split): per-viewport Graphics_SetViewport, camera per player
0x00419FA0 Scene_SetCamera Camera positioning. Ball pos from +0x758 + scene offset +0x434C. Boundary check +0x3F1C: distance clamp with sqrt+falloff. Object+0x744 noise randomizer. Timer +0x3F2C: snap to ball for N frames. FOV 0.9 from +0x29BC
0x0040DFA0 Scene_RenderWithCamera Two-pass camera render: front face then 180-degree back face. Camera angle +0x43A0, Y offset +0x43A4, X offset +0x43B0, iterator vtable +0x4390
0x00418870 Scene_CreateObject4f Create object at (x,y,z,w) with BaseObject vtable 0x4CF584, set position via FUN_45D450
0x0040C0F0 Scene_CreateFlags Scan for FLAG/SMALLFLAG objects, create Flag (0x8C size), append to +0x2160, scale SMALLFLAG by constant
0x0040C270 Scene_CreateSigns Scan for SIGN objects, create Sign (0x10FC size), SIGN-TARPIT gets tar texture, added to +0xCD4 and render lists
0x0040C430 Scene_CreateDynamicObjects Generic object creation loop via vmethod +0x84, append to +0x335 (dynamic) and level lists, also +0x43B sub-objects
0x00419B70 Scene_ForEachBall_SetVelocity Iterate ball list +0x29D4, call Ball_SetVelocity on each

SceneObject Class (vtable 0x4D934C, size 0xD4)

Address Name Description
0x0046B4F0 SceneObject_ctor Constructor. Sets vtable, initializes 3 matrix transforms (base scale +0x94, rotation +0xA8, world +0xBC), pos=0, visible +0x88=1, zOrder +0x8C=-1, radius +0xCC=sqrt(const), type +0xD0=3
0x0046B650 SceneObject_dtor Destructor: calls Cleanup then optionally frees
0x0046B3F0 SceneObject_Cleanup Reset vtable, unlink from scene list (+0x710-0x730), reset 3 identity matrices
0x0046B4D0 SceneObject_SetVisible Set visibility flag +0x88, call vmethod +0xC
0x00453BD0 Scene_RegisterObject Assign ID to obj+0x8C, call vmethod +0xC (init), store in scene array +0x710+id*4

SceneObject Structure Layout

Offset Type Field Description
+0x000 void* vtable Virtual function table pointer (0x4D934C)
+0x004 int gfxContext Graphics context pointer
+0x008-0x01C int[6] field_08 Zeroed at init
+0x088 char visible Visibility flag (1=visible)
+0x08C int zOrder Z-order / object ID (-1 default)
+0x094 float[5] baseScaleMatrix Base scale 4x4 matrix (identity 1.0)
+0x0A8 float[5] rotationMatrix Rotation 4x4 matrix (zero at init)
+0x0BC float[5] worldMatrix World transform 4x4 matrix (zero at init)
+0x0CC float radius Bounding radius = sqrt(global_constant)
+0x0D0 int type Object type (3 default)

Level Setup Functions

Address Name Level Path Special Features
0x00416270 Scene_SetupLevelDark levels\leveldark 2-player SceneObjects when !multiplayer, App+0x5DC/+0x67C
0x0040E190 Scene_SetupLevel5 levels\level5 Simple load (no extras)
0x0040EA90 Scene_SetupLevel6 levels\level6 LAUNCH01/02/03 + CHROMESHADOW positions, launcher timer +0x10DD=200
0x0040F360 Scene_SetupLevel7 levels\level7 Simple load (no extras)
0x00410830 Scene_SetupLevel9 levels\level9 PILLAR list, MAGNIFYER on hard, CLOUDSCAPE, fog + projection setup
0x00411F60 Scene_SetupLevel10 levels\level10 4 bumpers, FUN_436FC0 removal on easy, TarBubble list, multiplayer append
0x004110D0 Scene_SetupLevelCascade levels\levelcascade 8 bumpers (N:BUMPER%d 0-7) — Beginner Race (internal name: Cascade)
0x00411540 Scene_SetupLevelUp levels\levelup Initial ball pos (0,50,0), VAC-IN/VAC-OUT vacuum tubes

Reflection Rendering Passes

Address Name Object Count Offset Base Float Offset
0x00410670 Scene_RenderReflectiveObjects 8 +0x438C +0x644C
0x00411380 Scene_RenderReflectiveObjects7 8 +0x436C +0x642C
0x00412DC0 Scene_RenderReflectiveObjects4 4 +0x439C +0x53FC

Extended Utility Functions

Address Name Description
0x00453180 Vec3_Init Initialize Vec3 with vtable 0x4CF300, 3 floats, default scale 1.0
0x0040A0B0 Matrix_TransformVec3 Transform 3D vector by 4x4 matrix: result = M * v
0x00426E90 StdString_Assign MSVC std::string::assign (SSO 0xF threshold, word+byte copy)
0x0040A040 NoOp Empty function (58 xrefs, default/no-op handler)
0x00409AC0 BaseObject_Init Set vtable pointer to 0x4CF584 (54 xrefs)
0x00409D60 RepeatCall Call function pointer N times (34 xrefs)

Ball Extended Functions

Address Name Description
0x0040AF90 Ball_GetTransform Read ball transform (+0x6C/+0x70/+0xC0/+0xC4) into output struct
0x0040AF00 SceneObject_InitAtPosition Initialize SceneObject at (x,y) with Vec3 vtable

UI and Input (Extended)

Address Name Description
0x00409B90 UI_DrawTextShadow_Wrapper Wraps UI_DrawTextShadow with Vec3 default params
0x00428F10 Input_CheckKeyCombo Check key combos. param_1=2: escape. 0-3: iterate 4 input bindings, 50-frame debounce

Game State Management (Extended)

Address Name Description
0x004287C0 App_StartRace Restart audio, setup race, play sound, free dialogs, start music (vol 1.0/0.5)
0x00428ED0 Difficulty_GetTimeModifier Time modifier by difficulty: 0=easy, 1=normal, 2=hard, default=0.0
0x00413BD0 SinkPlatformArenaCollisionEvents Match "DN:SINKPLATFORM" name, call sinking behavior, then base handler

Level Texture/Scale Assignment

Address Name Description
0x004130E0 Level_AssignTextures Match object textures by ID against 20-slot table at App+0x2C8
0x00411BA0 Level_AssignTexturesAndScales Set scale based on max(x,y,z) vs threshold, then match textures

Scene Vtable Layout (0x4D0260, 36 entries)

Slot Offset Address Name Description
0 +0x00 0x425020 Scene_DeletingDtor Destructor + free if flag&1
1 +0x04 0x419C00 Scene_Update Main tick (9-step: input, update pipeline, ball update+respawn, race-end, countdown, objects, cameras, render, HUD)
2 +0x08 0x41A2E0 Scene_Render 1P/2P/split render dispatch
3 +0x0C 0x4692F0 Scene_HandleInput Iterate menu items, check input, set current item at +0x864, play sound
4 +0x10 0x469220 Scene_ActivateCurrentItem Call vmethod+0x10 on current menu item (+0x864)
5 +0x14 0x4130A0 Scene_vmethod5 Unknown (arena-specific override)
6 +0x18 0x469280 Scene_SelectCurrentItem Call vmethod+0x0C on current menu item (+0x864)
7 +0x1C 0x409D90 Scene_NoOp Empty (no-op stub, 3 bytes)
8 +0x20 0x40B400 Level_RenderDynamicObjects Level dynamic object rendering
9 +0x24 0x44B840 NoOp_return Empty stub (default)
10 +0x28 0x44B840 NoOp_return Empty stub (default)
11 +0x2C 0x4692A0 Scene_ClearCurrentItem Set current item ptr (+0x864) to NULL
12 +0x30 0x4692A0 Scene_ClearCurrentItem Same as slot 11
13 +0x34 0x44B840 NoOp_return Empty stub (default)
14 +0x38 0x409DA0 Scene_DestroyScene Calls FUN_4693C0 then SaveAndCleanup
15 +0x3C 0x469430 Scene_NotifyObjects Iterate AthenaList, call FUN_4699D0 on each item
16 +0x40 0x419740 Scene_SetDestroyed Set +0x2C=1 destroyed flag
17 +0x44 0x4692B0 Scene_SaveAndCleanup Calls FUN_469AC0 (save + cleanup)
18 +0x48 0x40B090 Level_InitScene Level scene initialization
19 +0x4C 0x41B130 Scene_HandleRaceEnd Race timer decrement, lap 3->4 transition, Game Over, RaceResultPopup
20 +0x50 0x41B540 Scene_UpdateBallsAndState Iterate ball lists, SetCamera, Ball_Update, destroy finished balls
21 +0x54 0x40A040 NoOp Empty (58 xrefs)
22 +0x58 0x41A540 Scene_ProcessRaceEnd Countdown timer, check all balls finished, scene transition on expire
23 +0x5C 0x409DE0 Scene_HandleBallFinish Ball finish state machine: start→countdown(150f)→finish→result popup→done
24 +0x60 0x40B420 Level_RenderDynamicObjects_2 Alternate dynamic render
25 +0x64 0x40B600 Level_UpdateAndRender Combined update + render
26 +0x68 0x40B570 Level_RenderObjects Level object rendering
27 +0x6C 0x41B710 Scene_RenderScoreHUD Draw tournament title, countdown progress bar, "Score" text, Player 2, timer
28 +0x70 0x41BFD0 Scene_RenderTimerHUD Draw race timer, split screen divider, time display, overlay popups
29 +0x74 0x40C5D0 Scene_vmethod29 Game logic override
30 +0x78 0x44B840 NoOp_return Empty stub (default)
31 +0x7C 0x41AC70 Scene_vmethod31 Game logic override
32 +0x80 0x41C5B0 Scene_SpawnBallsAndObjects vmethod[32]: spawn balls (GameObject_ctor), scan SAFESPOT, CreateBadBall/MouseTrap/Secret/Flags/Signs/DynamicObjects
33 +0x84 0x419750 Scene_method33 Near Scene_dtor helper
34 +0x88 0x44B840 NoOp_return Empty stub (default)
35 +0x8C 0x41A9A0 Scene_ComputeInputForceDirection Computes 3D force vector from strongest player input across balls in slot. Ghidra label "Scene_ComputeLighting" is a MISNOMER. Scale: 0.12 human, different for AI.

SceneObject Vtable Layout (0x4D934C, 10 entries)

Slot Offset Address Name Description
0 +0x00 0x46B650 SceneObject_dtor Destructor (sets vtable, flags cleanup)
1 +0x04 0x46B490 SceneObject_SetPosition Sets position (this+0x08..0x10) then calls vmethod+0x0C
2 +0x08 0x46B4B0 SceneObject_SetScale Sets scale (this+0x14..0x1C) then calls vmethod+0x0C
3 +0x0C 0x46B670 SceneObject_Render Build world matrix from base+rotation+scale transforms, call D3D SetTransform+SetMaterial
4 +0x10 0x46B4D0 SceneObject_SetVisible Toggle visibility flag (+0x88)
5 +0x14 - (padding) Non-code sentinel value
6 +0x18 - (padding) Non-code sentinel value
7 +0x1C 0x46B9F0 SceneObject_DeletingDtor Calls BaseDtor then free if flag&1
8 +0x20 0x46B910 SceneObject_BaseDtor (function exists but not created)
9 +0x24 0x46B980 SceneObject_vmethod9 (function exists but not created)

Rumble/Arena Board Initialization Functions

Address Name Level Path Special Features
0x00413C20 ArenaLevel_WarmUp_Init levels\arena-WarmUp Simple load + clone
0x00413CE0 ArenaBoard_Beginner_Init levels\arena-beginner 4 bumpers (N:BUMPER%d), Level_ctor + Clone
0x00414180 ArenaLevel_Intermediate_Init levels\arena-intermediate Simple load + clone
0x00414240 ArenaLevel_Dizzy_Init levels\arena-dizzy Extra Level3-Swirl loaded, no bumpers
0x004144B0 ArenaLevel_Tower_Init levels\arena-tower Simple load + clone
0x00414960 ArenaLevel_Up_Init levels\arena-up Simple load + clone
0x00414B10 ArenaLevel_Expert_Init levels\arena-expert Simple load + clone
0x00414CE0 ArenaLevel_Odd_Init levels\arena-Odd Simple load + clone
0x00414F00 ArenaLevel_Toob_Init levels\arena-Toob 5 bumpers (N:BUMPER%d)
0x004153A0 ArenaLevel_Wobbly_Init levels\arena-Wobbly Simple load + clone
0x004158C0 ArenaLevel_Sky_Init levels\arena-Sky PILLAR name scan via __strnicmp, append to +0x11FB
0x00416080 ArenaLevel_Master_Init levels\arena-Master Simple load + clone
0x00416F40 ArenaLevel_Neon_Init levels\arena-neon Scale matrix setup for dynamic objects, SceneObject at +0x11F9
0x00417DF0 ArenaLevel_Glass_Init levels\arena-glass Simple load + clone
0x00418540 ArenaLevel_Impossible_Init levels\arena-impossible Simple load + clone

Common ArenaBoard pattern: Level_ctor → Level_Clone → CameraLookAt → vmethod+0x80 (post-init). Simple arenas have just load+clone; complex ones add bumper objects, pillar scans, or SceneObject decorations.

Board (Tournament) Constructors

Address Name Tournament Sub-Levels
0x00419030 Board_ctor (base) Base tournament board constructor
0x0041F4B0 Board_ctor (Toob) "Rodenthood" Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1, Level8-Blockdawg2
0x0041D060 LevelBoard_Dizzy_ctor (tournament) Tournament level 3 board

Key SceneObject Methods

Address Name Description
0x0046B4F0 SceneObject_ctor Constructor (0xD4 bytes, vtable 0x4D934C, init pos/scale/rot matrices)
0x0046B650 SceneObject_dtor Destructor (sets vtable, flags cleanup)
0x0046B3F0 SceneObject_Cleanup Cleanup helper (clears lists, resets state)
0x0046B4D0 SceneObject_SetVisible Toggle visibility at +0x88
0x0046B490 SceneObject_SetPosition Set position (3 floats at +0x08) + vmethod+0x0C
0x0046B4B0 SceneObject_SetScale Set scale (3 floats at +0x14) + vmethod+0x0C
0x0046B670 SceneObject_Render D3D world matrix build + SetTransform + SetMaterial
0x0046B860 SceneObject_BaseDtor Iterate child AthenaList, call each dtor(1), clear list
0x0046B9F0 SceneObject_DeletingDtor Calls BaseDtor then free if scalar deleting

Tournament and Menu System

Address Name Description
0x00427080 Tournament_AdvanceRace Advance to next race in tournament; 15-case switch creates Board_ctor for each level (1-15), saves score, difficulty time bonus, saves race timestamps
0x00428060 App_ShowResults Create results screen (FUN_426030 ctor), save to App+0x228, dispatch to scene manager
0x0042EA30 PracticeMenu_ctor "Practice Menu" scene, "CHOOSE A PRACTICE RACE!", 14 race items with thumbnail textures (practice-level1.png through practice-impossible.png), lock check via App+0x851-0x865 flags
0x004366E0 Scene_SetRaceActive Sets +0x10EC=1 (race active flag), 62 xrefs

Tournament Level Mapping (from Tournament_AdvanceRace switch)

Case Level Board Constructor Size
1 Warm-Up FUN_41CA40 0x436C
2 Beginner FUN_4200E0 0x644C
3 Intermediate FUN_41CB20 0x438C
4 Dizzy LevelBoard_Dizzy_ctor 0x4BE0
5 Tower FUN_41E340 0x5418
6 Up FUN_420390 0x4790
7 Expert FUN_424440 0x4394
8 Odd FUN_41EA40 0x4FD8
9 Neon FUN_41ED80 0x43B0
10 Toob FUN_41F4B0 0x646C
11 Wobbly FUN_41F110 0x4388
12 Glass FUN_424A90 0x4390
13 Sky FUN_41F930 0x47F8
14 Master FUN_4206D0 0x6498
15 Impossible FUN_424C20 0x4380

Level Unlock Flags (App+0x851-0x865)

Offset Level Texture
+0x851 Dizzy practice-level3.png
+0x852 Tower practice-level4.png
+0x853 Up practice-up.png
+0x854 Expert practice-level5.png
+0x855 Odd practice-level6.png
+0x856 Toob practice-level7.png
+0x857 Wobbly practice-level8.png
+0x858 Sky practice-level9.png
+0x859 Master practice-level10.png
+0x863 Neon practice-neon.png
+0x864 Glass practice-glass.png
+0x865 Impossible practice-impossible.png

RNG / Random System

Address Name Xrefs Description
0x0045dd60 RNG_Rand 193 PRNG with 55-entry circular buffer; returns (buf[read]+buf[write])>>6 % range
0x0045dd60 RNG_Rand(signed) - When param_2=1 and RNG_Rand(2)==1, returns negative value

Graphics Transform Pipeline

Address Name Xrefs Description
0x00457b10 Matrix44_Zero 12 Clear 4x4 matrix; zero all 16 entries, set diagonals to 1.0
0x00457b50 Gfx_SetPosition 69 Set world position via D3D SetTransform
0x00457bb0 Gfx_RotateY 15 Rotate around Y axis (negate + look-at construction)
0x00457c60 Gfx_ScaleX 40 Scale X axis with render-state multiplier
0x00457c90 Gfx_ScaleY 35 Scale Y axis with render-state multiplier
0x00457cc0 Gfx_ScaleZ 26 Scale Z axis with render-state multiplier
0x00457fd0 Matrix4_Identity 40 Set identity vtable + zero (Vec3 base init)
0x00425fe0 Gfx_SetAlphaBlendState 9 Set D3D alpha blend render states (0xD, 0xE → mode 3)
0x00427940 Gfx_SetCullMode 13 Set D3D cull mode (none/CW/CCW) based on +0x708 and specular flag

Wave / Math System

Address Name Xrefs Description
0x00457da0 Wave_Sin 38 sin(time * frequency * 2π/360)
0x00457dc0 Wave_Cos 23 cos(time * frequency * 2π/360)

UI List System (vtable 0x4D6A70)

Address Name Xrefs Description
0x00448f20 SimpleMenu_ctor 15 "Simple Menu" base ctor with item list, up/down scrollers
0x004490a0 UIListItem_ctor - Init 0x444-byte item with Vec3 + AthenaList
0x004497f0 UIList_AddItem 86 Add named item (text, subtext, colors, SceneObject, height)
0x00449430 UIList_AddSpacer 29 Add empty spacer item with height only
0x004494d0 UIList_ScrollUpdate 17 Scroll logic + mouse wheel + vtable dispatch
0x00449b00 UIList_Cleanup 27 Free all items (strings, SceneObjects, Vec3Lists)
0x00449c20 UIList_HandleKeyNav 18 Handle up/down/pgup/pgdn key navigation
0x00449d40 UIList_Render 18 Draw all items: gradient bars, text, icons, scroll arrows
0x0044a570 UIList_Layout 18 Compute text widths, position SceneObjects, set scrollers
0x0044a8b0 UIList_SetTextByName 27 Find item by subtext, replace display text
0x00449750 UIList_ActivateCurrentItem 18 Activate selected item: Back→650, Continue→50, else callback
0x0044ad50 ArenaScoreParticle_ctor 12 Init ArenaScoreParticle vtable + difficulty scale (0.02/0.03/0.04)

Rumble Board System

Address Name Xrefs Description
0x004217b0 ArenaBoard_ctor 15 Init board with "ArenaBoard" string, timer, base score=6000
0x00421880 ArenaBoard_dtor 24 Cleanup timer, release SceneObjects, call Scene_dtor
0x00421910 ArenaBoard_Render 16 Draw timer bar, round info, difficulty status, "TIE BREAKER!"
0x00421fe0 ArenaBoard_Update 16 Check round end, resolve ties, play "Game Over" music
0x00458e60 ToggleTimer_Init 12 Initialize round timer
0x00458e80 ToggleTimer_Cleanup 32 Cleanup round timer
0x00458e90 ToggleTimer_Tick 12 Tick round timer countdown

Scene / Rendering Pipeline

Address Name Xrefs Description
0x0045e0e0 Scene_RenderAllObjects 33 Main render: Graphics_BeginFrame → sort objects (opaque/alpha/shadow) → draw
0x00460450 Scene_RenderBallShadow 38 Render ball shadow: Ball_Render + depth bias pass
0x0045df80 SceneObject_CallUpdate 47 Dispatch +0x434 vtable[1] (Update)
0x0045df90 SceneObject_CallRender 47 Dispatch +0x434 vtable[2] (Render)
0x00437130 Scene_StartCountdown 11 Start race countdown (3..2..1..GO with param=400 or 50 frames)

Sprite System (vtable 0x4D8F84)

Address Name Xrefs Description
0x0045d0c0 Sprite_ctor 30 Init with texture, RenderContext, material defaults
0x0045d660 Sprite_RenderQuad 15 Render textured quad using material + draw primitive

Dialog System

Address Name Xrefs Description
0x00440e70 OkayDialog_ctor 12 "Okay Dialog" with caption text + "OKAY!" button

Core Utility Functions

Address Name Xrefs Description
0x004531e0 Vec3_Init 13 Set Vec3 vtable + zero position + w=255.0
0x00453250 Vec3List_Free 283 Free Vec3List + member data at +0x103
0x004536a0 AthenaList_GetSize 60 Return list count at +4
0x00453640 AthenaList_FindByValue - Linear search for value, returns index or -1
0x00443990 AthenaString_Clear 14 Free string buffer, reset to 15-char inline capacity
0x0044b840 Noop 280 Empty stub function (vtable placeholder)
0x00401480 GameObject_dtor 9 Release timers, free Vec3Lists, cleanup matrices
0x00453610 AthenaList_ContainsValue 10 Check if value exists in list (returns count>0)
0x00453820 AthenaList_MergeSorted 10 Merge sorted list param into this list
0x004598c0 Float_IsBetween 10 Test if param1 minus param2 is between two global bounds
0x00469510 AthenaString_Set 10 Copy string param into AthenaString (free old, strdup, set length)
0x0046a140 MusicPlayer_SetTempoScale 10 Set tempo scaling based on music BPM and delta time
0x00477860 FileStream_SeekRead 10 File stream seek and read data
0x00460530 Level_FindObjectByName 9 Find level game object by string name (stricmp), returns ptr or 0
0x00467d30 Buffer_Free 9 Free buffer: free ptr+4, zero ptr+4/+8/+C
0x00480032 CRT startup helper 5 C runtime init
0x00480c4d CRT helper 5 C runtime helper
0x0048a560 Math/alloc helper 5 Runtime helper

Session 2026-04-13 Additions — Ball Physics & Rendering

Address Name Xrefs Description
0x004015b0 Ball_SetupCollisionRender 4 Initialize collision mesh render objects from level collision data
0x004016f0 Ball_ApplyForceV2 4 Alternate force application (gravity plane aware, ice/dizzy/tube multipliers)
0x00402c10 Ball_RenderWithCollision 10 Ball render: check collision planes, render shadow, apply scaling, end frame
0x00425340 ArenaBoard_DeletingDtor 4 ArenaBoard destructor (calls ArenaBoard_dtor, then free)
0x004280e0 App_ShowMainMenu 4 Create MainMenu (0xCDC bytes) and store at App+0x224
0x004288b0 App_StartTournamentRace 4 Start tournament race: config mirror/mode, create level scene, advance race
0x0042c7c0 Graphics_SetScaleAndPosition 4 Set identity scale matrix then set position (x,y) on Graphics object
0x00443e30 QuitDialog_ctor 4 Quit Dialog constructor — "Quit Dialog" with "YES"/"NO" buttons
0x004468c0 ToggleTimer_TickWrapper 4 Wrapper: calls ToggleTimer_Tick at offset +0x110C
0x00446b80 RegisterDialog_ValidateSerial 5 Validate serial number using XOR cipher with key "54138", write DXCaps to registry

Session 2026-04-13 Additions — Register/Security Dialog

Address Name Xrefs Description
0x004475a0 RegisterDialog_HandleInput 4 Handle keyboard input for register dialog (char add, length check)
0x00447920 RegisterDialog_Render 4 Render "REGISTER HAMSTERBALL!" screen, name/serial fields, buy button
0x00448890 RegisterDialog_HandleKey 4 Handle key navigation (Tab name/serial, arrows, Enter=validate, Esc=close)
0x00448a40 UITimer_dtor 4 UI timer destructor (set vtable, cleanup)
0x00449240 UIList_AddIconItem 4 UI list add item with icon (0x444-byte UIListItem, text+subtext+RGBA+icon+height)

Session 2026-04-13 Additions — Sound System

Address Name Xrefs Description
0x00459660 Sound_LoadOggOrWav 10 Load sound file: try .ogg first, then .wav fallback
0x00459310 Sound_LoadOgg - Load OGG Vorbis file, create D3D sound buffer, add to channel list
0x00458ee0 Sound_Play3DAtPosition - Play 3D positioned sound (get channel, call vtable+0x3C with position)
0x00459810 Sound_GetNextChannel 10 Get next sound channel from circular buffer (with wrap-around)

Documentation Progress

Session Total Functions Documented % New Labels
Initial 3,988 1,700 42.8%
Session 1 3,888 1,768 45.5% 37
Session 2026-04-13a 3,811 1,870 49.1% 25+
Session 2026-04-13b 3,811 1,892 49.6% 27+
Session 2026-04-13c 3,811 1,908 50.1% 39

Session 2026-04-13b Additions — Core Engine & Scene Systems

Address Name Xrefs Description
0x00498200 BitStream_ReadBits 95 Bitstream reader - reads N bits from byte-aligned stream with bit-level positioning
0x004737f0 AthenaString_Assign 52 String copy/assign operator (copies src into dest buffer)
0x004792fb D3DX_DetectShaderProfile 48 Detect pixel shader version (1.x/2.x/3.x) based on D3DX runtime
0x00492bda Vertex_Transform 32 Vertex coordinate transformation between spaces (mode 1/2/3)
0x00466c70 AthenaString_Format 98 String format wrapper - calls Sprintf with object's internal buffer, returns ptr
0x004bbdfd AthenaString_Sprintf - In-memory sprintf using FILE struct trick for vsnprintf
0x00469990 Scene_AddObject 77 Add SceneObject to Scene - checks uniqueness, appends, sets back-ref, vtable notify
0x0046f390 Scene_BeginFrame 39 Begin scene frame - Graphics_BeginFrame + vtable[6] callback
0x00460da0 Scene_RenderFrame 38 Full frame render pipeline - iterates scenes/objects, assigns render indices
0x00461370 Scene_RenderOpaque 38 Opaque render pass - iterates objects calling vtable[0x28], renders mesh entries
0x00461890 Scene_LoadMeshWorld 38 Load mesh world from stream - creates MeshBuffers with materials/textures/flags
0x00461f00 Scene_Subdivide 38 Create 3D grid of mesh objects by dividing bounding box, test visibility per cell
0x00462100 Scene_SubdivideRandom 38 Random grid subdivision using seeded RNG, tests cell visibility
0x004629e0 Scene_LoadCached 37 Load .cached scene file - materials, meshes, transforms, objects, bounding box
0x0046a750 Window_Notify 37 Send WM_COPYDATA (0x4A) message to window with formatted string
0x004605e0 AthenaHashTable_Lookup 36 Hash table case-insensitive string lookup with bucket iteration
0x004691c0 SceneObject_dtor 31 SceneObject destructor - frees Vec3Lists, restores base vtable
0x004693c0 Scene_AddAllObjects 25 Batch-add all SceneObjects from internal list
0x00469600 MWParser_ReadTag 24 XML/SGML tag parser - finds ... pairs from stream
0x004695d0 StreamReader_dtor 24 Stream/file reader destructor - close handle, free buffer
0x004694f0 Sprite_DrawColoredRect (was 0x45d450) 23 Draw colored rectangle with random RGBA vertex colors
0x00472af0 AthenaString_Init 18 Default string constructor - set vtable, zero fields, flags
0x00472f30 RegKey_Close 18 Close registry key handle via RegCloseKey
0x00473670 AthenaString_CopyCtor 16 String copy constructor from source string object
0x004740d0 AthenaString_WriteTag 16 Build XML tag string: content concatenation
0x00489610 Pool_FreeList 16 Memory pool free list traversal with reference counting
0x00459b24 Graphics_InitShaderDispatch 19 Shader init dispatch thunk (D3DX detect + indirect jump)
0x0045a439 Graphics_SetRenderState 29 Render state dispatch thunk (profile detect + indirect jump)
0x004ab9b8 DivCeil 15 Ceiling division utility: (a-1+b)/b

Key Data Items

Address Name Description
0x004D2334 s_BACK String "BACK"
0x005341CC g_renderIndex Global render index counter
0x004F7360 PTR_OBJ_VTABLE Pointer to object vtable (used by Ball, GameObject, Scene, etc.)

Session 2026-04-13c Additions — Scene, Path, Game, Registry

Address Name Xrefs Description
0x00467bf0 Path_GetPosition 14 Interpolate X/Z from path splines at time t, Y=0
0x004690f0 Gadget_ctor 11 Generic Gadget constructor (vtable 0x4d9170, SceneObject-derived)
0x00469240 Gadget_Activate 9 Activate gadget callback (vtable[2] with RNG params)
0x00457de0 Math_Atan2Angle 13 Atan2 angle with quadrant adjust, degrees-per-unit scaling
0x004561e0 Material_Init 11 Zero-initialize 0x64-byte material/transform array
0x004562b0 Material_Copy 8 Copy 0x64 bytes (4x4 matrix + extras)
0x00458020 AABB_Update 12 Expand axis-aligned bounding box by 3D point
0x00458130 Math_FastDistance2D 8 Approximate integer distance (Bresenham weights 102/246)
0x0045d300 Sprite_DrawRect 12 Draw rect with position+size, random RGBA, 2-triangle strip
0x0046b2a0 MeshBuffer_dtor 11 Destroy mesh buffer list items + free Vec3List
0x00472fd0 RegKey_WriteDword 13 Write DWORD via RegSetValueExA
0x00473080 RegKey_ReadDword 13 Read DWORD via RegQueryValueExA
0x00471c20 MeshNode_ctor 9 Load mesh file into scene graph node (vtable 0x4d9c48)
0x00472c70 Math_Lerp 9 Linear interpolation: a + (b-a)*t
0x0042c870 Font_DrawCentered 8 Draw text centered at (x,y) position
0x004351f0 GameLevel_ctor 8 Game level constructor (Stands_ctor, Level_Clone, sound channel)
0x00402400 Ball_DizzyImmunity 7 Reset +0x2EC, update max at +0x2F4 if param exceeds current
0x00425f90 App_CompleteRace 7 Complete race - increment counter, trigger state transitions, clear flag
0x00426b30 String_AllocBuffer 7 Allocate string buffer with size
0x0042b190 ConfirmMenu_ctor 6 Confirmation menu (BACK/BACK2TOURNAMENT, DONE), vtable 0x4d39d0
0x00473500 AthenaString_AssignCStr 75 AthenaString assign from C string (free old, alloc new, copy)
0x004736b0 AthenaString_dtor 85 AthenaString destructor - frees buffer, sets vtable to base dtor
0x00473a50 AthenaString_AssignCRLF 21 AthenaString assign CRLF ("\r\n")
0x004bae43 AthenaString_SprintfToBuffer 71 sprintf into char buffer via fake FILE struct
0x00473050 RegKey_WriteBool 30 Write boolean to registry via RegSetValueExA (REG_BINARY)
0x00473130 RegKey_ReadBool 28 Read boolean from registry via RegQueryValueExA
0x00473170 RegKey_ReadString 23 Read string from registry with fallback attempts
0x00470150 SceneObject_RenderFull 40 Full render with ball+material+strips, alpha-aware material application
0x00470440 SceneObject_RenderSingleObj 39 Render a single SceneObject with material/strip dispatch
0x0046fbb0 SceneObject_ComputeCollisionSphere 38 Compute bounding sphere from AABB, call Ball_CheckCollisionPlanes
0x0045dfd0 SceneObject_CheckCollision 38 Thunk: compute bounding sphere + check collision planes
0x0049336b D3DDevice_SetFPUControl 29 Set FPU control word from device state
0x0049338e Mesh_InitTexture 32 Initialize texture object from surface desc (D3D texture init)
0x00493671 Mesh_DrawWithTransform 31 Draw mesh with temporary transform override
0x0049373d Mesh_ClearColorVertices 30 Zero out vertices matching the clear color (transparency hack)
0x0047dfb9 Graphics_DrawIndexedPrimitive 25 D3D DrawIndexedPrimitive wrapper via vtable
0x004a458c longjmp_with_cleanup 34 CRT longjmp with optional cleanup callback
0x004a45aa seh_filter_invoke 21 Invoke SEH exception filter callback at offset 0x44
0x004c02e7 LeaveCriticalSection_indexed 23 LeaveCriticalSection by index into global array
0x0046f3b0 Scene_BeginFrameThenRender 39 Begin graphics frame then invoke render callback
0x0046f3d0 MeshWorld_ctor 39 MeshWorld constructor from filename and strip count
0x00472770 SceneObject_BuildStrips 39 Builds triangle strips for SceneObject rendering
0x004ba754 __ftol2 358 CRT float-to-int64 conversion (compiler intrinsic)
0x004bc7c8 __errno 40 CRT __errno - returns thread-local errno pointer
0x004bc7d1 __doserrno 23 CRT __doserrno - returns thread-local DOS errno pointer
0x004bcda8 __security_init_cookie 34 CRT security cookie initialization
0x004bac20 strstr 22 CRT strstr - string search with SIMD optimization
0x004bc0d1 strtok 50 CRT strtok - thread-safe string tokenizer
0x00461740 Level_ctor 12 Level constructor: inits base, vtable 0x4D8FB0, 4 AthenaLists, Timer + LevelState 0x10D4 bytes
0x004694f0 SceneObject_sub1_ctor 12 Simple SceneObject sub-ctor: set vtable 0x4D91BC, null +0x08/+0x10, visible +0x14=true
0x0049721e MeshNode_DeletingDtor 30 MeshNode deleting destructor: calls base dtor, then frees if param_1 & 1
0x00472c20 AthenaHashTable_ctor 15 Athena hash table constructor: set vtable, call internal init, then set final vtable 0x4CF584
0x00472ec0 RegKey_Open 15 Open registry key: HKLM first, fall back HKCU, create if needed
0x0047df9a Graphics_DrawIndexedPrimitiveUP 14 D3D DrawIndexedPrimitiveUP wrapper via vtable dispatch at offset 0x2C
0x00489710 AthenaList_IterateNext 15 Advance linked list iterator to next node
0x00489bd0 AthenaList_Sort_14 15 Sort list elements (comparator index 0xE / 14)
0x0048a1a0 AthenaList_FreeAll 10 Free all list nodes: Pool_FreeList on head and items
0x0048cd87 D3D8_DebugSetMute 14 D3D8 DebugSetMute: load d3d8.dll/d3d8d.dll, call DebugSetMute
0x004c8ff7 _check_file_access 14 Check if file path is accessible (directory vs file), uses GetFileAttributesA
0x004ab4a0 __libm_sse2_tan 14 SSE2 tan() implementation with range reduction and polynomial approx
0x004ad5ac Zlib_FreeIf 12 Conditional free: only frees if both params non-null
0x004ad63c Zlib_UpdateHash 14 Zlib deflate hash update (calls update + insert)
0x004adf0c Zlib_FlushWithCRC 14 Zlib flush with CRC validation, chunked writes
0x004b5ae1 CRT_Noop2 14 Empty function (no-op)
0x004ba61e CRT_ThrowStringTooLong 10 Throw C++ exception "string too long"
0x004bacc0 strchr 11 CRT strchr - find char in string with SIMD optimization
0x004bc350 strcpy 12 CRT strcpy - string copy with SIMD optimization
0x004a167b Matrix4x4_Multiply_SSE2 12 4x4 matrix multiply using SSE2 packed float ops
0x00402a20 Ball_SetVec3AtOffset 12 Set 3 floats at offset 0xca4 in ball (camera/force vector)
0x00402a50 GameObject_sub2_dtor 12 GameObject deleting destructor variant 2
0x00402a70 Ball_DrawArenaScoreText 25 Draw rumble score text at viewport using Graphics_SetViewport + UI_DrawTextCenteredAbsolute
0x00405100 Ball_InitPhysicsDefaults 14 Set ball physics defaults: radius=0.5, friction=0.2, max_speed=35.0, gravity=6.0
0x00405d90 GameObject_sub_ctor 14 GameObject subclass constructor: vtable 0x4CF494, scale=1.0, visibility flag, +0x80c=0x32
0x00405dd0 GameObject_sub_dtor 12 GameObject deleting destructor variant 1
0x00405e00 Ball_Update 400+ Main ball physics tick: timer, collision, velocity integration, reflection, sound, camera tilt, spin. Core function!
0x00408390 Ball_AI_ChaseNearest 60 AI steering: finds nearest opponent ball, applies force toward it, sine wave wandering fallback
0x00408830 Ball_FallUpdate 40 Ball update when fallen: shrinks ball, handles scale change, trail cleanup
0x00408d10 Ball_Split_ctor 14 Split ball constructor: vtable 0x4CF560, +0xc60=5, calls scene function
0x00408d70 Ball_Shatter 50 Arena: marks parent ball for despawn, spawns 3 AI split balls (IDs 1/2/4). Called from FollowBall_Update 0x43ECC0, NOT from E:JUMP
0x00409480 Ball_SplitAndExplode 70 Creates 2 split balls + circular ArenaScoreParticle explosion pattern (0-360 degrees)
0x0040a920 Scene_CreateGameOverMenu 25 Creates game-over UI based on game state: single quit, multiplayer quit, or menu mode
0x0040ae50 Sprite_DrawCentered 12 Sprite draw centered at position: Sprite_DrawRect with center offset
0x0040b960 SceneObj_SetBounds 14 SceneObject bounds setter: sets +0x14-20, identity matrix if scale != 1.0
0x0040d280 Scene_LoadLevel2 15 Load level 2: "levels\level2", clone, init scene
0x0040d390 Scene_LoadLevel3 25 Load level 3: "levels\level3", clone, init scene, collect TarBubble objects
0x0040d6d0 Scene_LoadLevel4 20 Load level 4: "levels\level4", clone, init scene, special setup (flag +0x80=1, camera bounds)
0x00401590 SceneObj_CallVtable18 8 SceneObject vtable call at offset 0x18
0x00401a60 Vec3_Length 8 Vec3 length: sqrt(xx + yy + z*z), min 1.0

New Functions (Session 2026-04-13)

Address Name Xrefs Description
0x4531b0 Vec3_SetScalar 6 Set all 3 components to same value, w=1.0
0x4536b0 AthenaList_InsertAt 7 Insert value at index, reallocate array
0x453e90 Gfx_TransformX 7 Screen Y transform: y * viewMatrix + viewport
0x453eb0 Gfx_TransformY 7 Screen Z transform: z * viewMatrix + viewport
0x453970 Graphics_SetCullMode2 5 Set D3D cull mode via vtable call
0x44aca0 SimpleList_vtbl_Init 7 SimpleList vtable pointer init
0x44b850 UIListItem_vtbl_Init 6 UIListItem vtable pointer init
0x426d20 StdString_Erase 6 String erase: delete substring at position
0x426de0 StdString_Substr 5 String substring extraction
0x426f30 PlayerProfile_ctor 5 Player profile constructor (0x280 bytes)
0x443a10 StdString3_Clear 5 Clear string, free heap buffer
0x443f50 GameObject2_dtor 5 GameObject destructor variant
0x44fda0 TourneyMenu_ctor 6 Tournament menu constructor
0x458220 AABB_TriangleIntersect 6 AABB vs triangle edge intersection test
0x458320 AABB_TriangleTest6Edges 6 Full 6-edge AABB-triangle intersection
0x457f10 Collision_PointInTriangle 5 Barycentric point-in-triangle test

New Functions (Session 2026-04-13 Part 2 - Vec3/Ball/Scene)

Address Name Description
0x00401090 Scene_SetSoundMode Set scene audio mode: dispatches vtable[50](0x16, mode), stores at +0x708
0x00401100 Gfx_PackColorRGB Pack 3 float RGB channels to 24-bit color via __ftol2 + shift/OR
0x004011c0 SceneObj_SetScale Set uniform scale factor on SceneObject, create scale matrix
0x00401220 Scene_ZoomIn Increase scene viewport zoom by g_zoomStep, call SetProjection
0x00401270 Scene_ZoomOut Decrease scene viewport zoom by g_zoomStep, call SetProjection
0x00401640 Timer_dtor Timer deleting destructor: calls Timer_Cleanup, optionally frees
0x00401660 Ball_SetName Set ball display name at +0xC28, copies string, sets type ID=200
0x00402030 Ball_SetTargetPos Set/interpolate target position with smooth damping threshold
0x00402150 Ball_CheckProximity Check position proximity radius, store integer distance result
0x00402200 Ball_Shrink odd race shrink ball: set is_shrunk+0xC4C=1, shrink radius to 13.0, play 3D sound
0x00402270 Ball_Grow odd race grow ball: clear is_shrunk, set radius=26.0, physics=5.0
0x00402290 GameObject_Render Render game obj: scale, depth layer toggle, Sprite_RenderQuad, copy result
0x004027f0 Ball_dtor Ball deleting destructor: calls Ball_Cleanup, optionally frees
0x004029c0 CollisionMesh_SetSpeed DEAD CODE. Writes roll_friction (+0xC64) and scaled_dir (+0xC98), immediately overwritten by Ball_Update Phase 14. Does NOT control ball speed.
0x457b80 Gfx_SetPositionAndRender 5
0x458f10 Vtable_CallOffset48 7
0x473940 FontFormatString_Parse 9
0x4749b0 StdString_CompareSubstr 8
0x47c830 StreamReaderVtbl_Init 8
0x489660 D3DX_SurfaceClipBlit 9
0x489da0 Pool_Reset 8
0x48a820 D3DX_Uninit 8
0x42e220 DifficultyMenu_ctor 5
0x42e840 OptionsMenu_RenderControls 5
0x42f550 OptionsMenu_dtor 5
0x4363f0 Platform_ctor 5
0x465260 Level_LoadCollision 6
0x465d90 Mesh_FindClosestCollision 10
0x465ef0 Collision_TraverseSpatialTree 3
0x492bc4 D3DX_AssemblyOp8 8
0x498200 BitStream_ReadBits 95

New Functions (Session 2026-04-13 Continued)

Address Name Xrefs Description
0x4531b0 Vec3_SetScalar 6 Set all 3 components to same value, w=1.0
0x4536b0 AthenaList_InsertAt 7 Insert value at index, reallocate array
0x453e90 Gfx_TransformX 7 Screen Y transform: y * viewMatrix + viewport
0x453eb0 Gfx_TransformY 7 Screen Z transform: z * viewMatrix + viewport
0x453970 Graphics_SetCullMode2 5 Set D3D cull mode via vtable call
0x44aca0 SimpleList_vtbl_Init 7 SimpleList vtable pointer init
0x44b850 UIListItem_vtbl_Init 6 UIListItem vtable pointer init
0x426d20 StdString_Erase 6 String erase: delete substring at position
0x426de0 StdString_Substr 5 String substring extraction
0x426f30 PlayerProfile_ctor 5 Player profile constructor (0x280 bytes)
0x443a10 StdString3_Clear 5 Clear string, free heap buffer
0x443f50 GameObject2_dtor 5 GameObject destructor variant
0x44fda0 TourneyMenu_ctor 6 Tournament menu constructor
0x458220 AABB_TriangleIntersect 6 AABB vs triangle edge intersection test
0x458320 AABB_TriangleTest6Edges 6 Full 6-edge AABB-triangle intersection
0x457f10 Collision_PointInTriangle 5 Barycentric point-in-triangle test
0x463790 Vec3_CrossProduct 7 Vector cross product: this x param2 -> param1
0x46b1a0 AthenaListObj_ctor 7 AthenaList object constructor
0x4738b0 StdString_AppendN 7 Append N chars from string with strncat
0x47c840 Sprite_CalcTexCoords 8 Calculate sprite UV coordinates
0x4602f0 Scene_CollectByNameFilter 6 Collect scene objects by name filter
0x46ec30 Ball_GetInputForce 7 Get ball input force (keyboard/mouse/joystick)
0x46f670 Mesh_SaveAndFree 5 Save mesh to file, free buffers
0x473b10 AthenaString_AssignFormatted 6 Format and assign AthenaString
0x46a440 Audio_PlayMusicAtSpeed 6 Play music with tempo/speed adjustment
0x466ac0 Scene_UpdateChildren 5 Traverse scene tree, update children
0x467750 Array_CopyDWords 5 Copy N dwords from src to dst
0x429520 Game_SetInProgress 5 Mark game in progress (sets +0x200=1)
0x440dd0 Graphics_DrawRectAndReset 5 Draw rect, then reset matrix to identity
0x44be80 ScoreObject_ctor 5 SceneObject constructor (vtable PTR_RaceGoalReached_Render) — creates object used for race goal rendering and rotator ball tracking
0x44fd40 SimpleList_dtor 5 SimpleList destructor
0x459610 Scene_RenderIfVisible 5 Render scene object if visible flag set
0x470650 Array_FillDWords 5 Fill N dwords with same value
0x472da0 Transform_ctor 5 Transform object constructor
0x418760 Scene_CreateObject_Gear 6 Create GEAR object in scene
0x418930 Gear_AdvanceAlongPath 6 Gear path following (8-dir gradient descent)
0x4730c0 Registry_ReadFloat 5 Read float from Windows registry
0x473740 StdString_AppendCharN 5 Append N copies of a char
0x46e0b0 Input_IsKeyDown 5 Check input state (keyboard/mouse/joystick)
0x456150 Ray_SetDirection 3 Set ray direction, normalize, compute length
0x456390 Mesh_Clear 3 Clear mesh: free vertex/index buffers
0x456e80 Font_WordWrap 3 Word-wrap text to fit width
0x458000 Collision_InitDefaultAABB 3 Initialize default collision AABB bounds
0x4583f0 AABB_TriangleIntersect2 4 Double AABB-triangle test wrapper
0x4581d0 Vec2_Distance 3 2D distance: sqrt(dx²+dy²)
0x458190 Collision_GradientEval_Stub 4 Empty stub for collision gradient evaluation
0x46f340 MeshBuffer_Cleanup 3 Cleanup mesh buffer, free resources
0x46fd60 Mesh_AddVertex 4 Add vertex to mesh buffer (8 floats)
0x46c970 Texture_SetDimensions 3 Set texture dimensions and UV scale

Menu System (Session 2026-04-13 Batch 4)

Address Name Xrefs Description
0x42f810 TimeTrialMenu_ctor 3 "Time Trial Menu" → extends PracticeMenu with race items + lock checks
0x42fc10 PartyMenu_ctor 3 "Time Trial Menu" ( Party Race! ) → extends PracticeMenu, vtable 0x4D4738
0x42fc40 ArenaMenu_ctor 3 Arena menu with 14 arena items (Warm-Up to Impossible), lock icons, vtable 0x4D47B8
0x4326d0 MPMenu_ctor 3 Multiplayer menu: Party Race, Rodent Rumble, controller config (1-4P)

Scene/Texture/Rendering (Session 2026-04-13 Batch 4)

Address Name Xrefs Description
0x44aa40 Scene_FindTextureByName 4 Find texture by case-insensitive name, return ptr+dimensions+width
0x44ab00 Scene_FindTextureDimensions 4 Find texture and measure text width via Font_MeasureText
0x44abf0 Scene_AddTextureToList 4 Add texture reference to scene list by name, mark dirty at +0xCBC
0x443ac0 SceneObject_RenderScaled 3 Render object scaled (ScaleX, SetPosition, vtable calls + Timer)
0x4410c0 SceneObject_FreeStrings 3 Free 2 string pointers, re-init BaseObject, call SceneObject_dtor
0x453c50 Texture_RemoveRef 3 Decrement texture refcount, remove from cache and free when 0
0x457a50 Graphics_DisableRenderState 3 Thunk → Graphics_SetRenderState (disable mode)

Game Flow (Session 2026-04-13 Batch 4)

Address Name Xrefs Description
0x428c50 App_StartPracticeRace 3 Start practice/tournament race: calls App_StartRace, sets up PlayerProfile, calls Tournament_AdvanceRace
0x434580 Sound_InitChannels 3 Allocate sound channels for object, get next sample, play 3D positioned sound, set timer 0x140
0x43b6f0 Rotator_AddBall 3 Register ball on rotator tracking list (8-byte entry [ball_ptr, tick=10]). Resets tick to 10 if ball already tracked. Formerly misnamed ScoreObject_SetScore. Called from N:ONROTATOR, N:SPINNY, N:SWIRL.
0x43e600 Catapult_Update 4 Per-frame update: decrements tick counters, applies rotation matrix to tracked balls' position (+0x164/+0x168/+0x16C) and velocity (+0xCA4/+0xCA8/+0xCAC). Frees entries when counter reaches 0. Shared by catapult launch and rotator/gear systems.
0x43e9c0 Catapult_AddObjectConditional 3 Register ball on catapult/gear tracking list (guarded by +0x1510). Same 8-byte entry pattern as Rotator_AddBall. Called from N:ONGEAR.
0x434290 Catapult_Launch 2 Launch pad activation: sets catapult+0x10F0=1 (active), +0x10F4=50 (launch timer). Called on E:CATAPULTBOTTOM.
0x44bef0 Timer_Decrement 4 Timer tick: value = end_value - 100, set flag at +0x2A
0x448620 ScoreDisplay_DeletingDtor 3 ScoreDisplay scalar deleting destructor
0x4470d0 ScoreDisplay_dtor 3 Clean up ScoreDisplay: free strings, timers, BaseObjects (5), SceneObject_dtor
0x44acb0 UIList_Clear 4 Empty stub (returns 0), clear/reset UI list

Board Level Constructors (Session 2026-04-13 Batch 5)

Address Name Xrefs Description
0x41CA40 LevelBoard_WarmUp_ctor 1 Board constructor for Warm-Up race (Level 1), calls Board_ctor, sets "Board (Warm-Up)", loads BEGINNERRACE data
0x41CB10 LevelBoard_WarmUp_dtor 1 Board destructor for Warm-Up race, sets vtable to 0x4D04A8, calls Scene_dtor
0x41CB20 LevelBoard_Intermediate_ctor 1 Board constructor for Intermediate race (Level 2), loads Level2-Bridge, creates level clone, TipperVisual_Attach
0x41CC80 LevelBoard_Intermediate_dtor 1 Board destructor for Intermediate race
0x41D450 LevelBoard_Dizzy_dtor 1 Board destructor for Dizzy race (Level 3), frees Vec3Lists at +0x11E4 and +0x10DE
0x41E340 LevelBoard_Tower_ctor 1 Board constructor for Tower race (Level4), loads 6 levels: Level4-Catapult, Level4-Drawbridge, Level4-Mace, Level4-Windmill, Level4-Turret, plus YellowLink and Chomper meshes
0x41E640 LevelBoard_Tower_dtor 1 Board destructor for Tower race, frees Vec3Lists at multiple offsets
0x41EA40 LevelBoard_Expert_ctor 1 Board constructor for Expert race (Level5), loads Level5-Bridge, creates clone, 3x hammyjudge meshes
0x41EC90 LevelBoard_Expert_dtor 1 Board destructor for Expert race
0x41ED80 LevelBoard_Odd_ctor 1 Board constructor for odd race (Level6), sets "Board (Odd)", ODDRACE data
0x41EE70 LevelBoard_Odd_dtor 1 Board destructor for odd race
0x41F110 LevelBoard_Wobbly_ctor 1 Board constructor for Wobbly race (Level 12), loads 7 levels: Level7-Wobbly1 through Level7-Wobbly7
0x41F3C0 BoardLevel12_Wobbly_dtor 1 Board destructor for Wobbly race
0x41F720 BoardLevel_Toob_dtor 1 Board destructor for Toob race

Collision System (Session 2026-04-13 Batch 5)

Address Name Xrefs Description
0x456D80 CollisionMesh_ctor 1 CollisionMesh constructor: initializes triangle list for mesh collision detection, sets up AthenaLists
0x456120 CollisionMesh_AddTriangle 1 Add triangle to collision mesh: appends to list at +0x430, sets back-pointer at +0x08
0x4564C0 Ball_AdvancePositionOrCollision 1 Ball physics: advance position with collision detection. Uses spatial tree for mesh traversal, handles material accumulation, collision response. Core physics function.
0x463330 SpatialTree_ctor 1 SpatialTree (octree) constructor for collision spatial partitioning
0x4632E0 SpatialTree_Free 1 Free spatial tree nodes and cleanup

CRT/Misc

Address Name Xrefs Description
0x4C5F5A CRT_amsg_exit 10 CRT error message exit (__amsg_exit)

Newly Documented Functions (Session 2026-04-13)

Address Name Xrefs Description
0x440390 Scene_UpdateArenaPhysics 1 Arena physics update: wave motion, collision detection, sound triggers
0x466cf0 CollisionNode_ctor 1 Collision node constructor: sets friction values (0.1), vtable
0x477020 CollisionNode_BaseInit 1 Collision node base initialization
0x459aba Triangle_Interpolate2D 1 2D triangle interpolation (barycentric coords)
0x453c00 Graphics_InitShaderProfileThunk 1 Shader profile init thunk
0x441150 UI_CheckKeyCombo 2 UI key combo handler (calls vtable on press)
0x41f7e0 Scene_HandleRaceEnd_ClampZoom 1 Clamps camera zoom levels after race end
0x430330 PauseArenaMenu_ctor 2 Pause Rumble menu constructor (RESUME, OPTIONS, QUIT)
0x4601a0 Scene_MarkDirty 2 Recursively marks scene objects as dirty (tree traversal)
0x470680 MeshWorld_ClearObjectLists 3 Clears AthenaLists on marked objects during cleanup
0x474e70 Menu_dtor 1 Menu destructor: frees 10 Vec3List entries, clears AthenaLists
0x475000 Menu_DeletingDtor 1 Menu scalar deleting destructor
0x441190 OkayDialog_Render 2 Okay dialog render: draws gradient bars, "OKAY!" button
0x4415b0 UI_SetQuarterViewport 1 Sets quarter-screen viewport dimensions (50x50)
0x446710 UI_ConfirmYes 1 UI confirm handler ("YES" dialog response)
0x424c10 ArenaBoard_DeletingDtor 1 ArenaBoard scalar deleting destructor
0x460220 Scene_ResetObjectSlots 1 Resets scene object slots, re-registers objects
0x453100 Rect_ContainsPoint 1 Point-in-rectangle test (AABB containment)
0x4415f0 UI_ResetViewportToQuarter 1 Resets viewport to quarter-screen (4x 0x32 values)
0x442540 UI_SetPauseRightButtonText 1 Sets "PAUSE W/RIGHT BUTTON: %s" text in UIList
0x4431c0 UIList_DeletingDtor 1 UIList scalar deleting destructor
0x441660 UIList_dtor 1 UIList destructor: initializes 5 AthenaHashTables, cleanup
0x4502f0 ArenaBoard_Menu_dtor 1 ArenaBoard menu destructor: vtable, timer, UIList cleanup
0x445230 Scene_StartTournament 1 Starts tournament mode: creates TourneyMenu, sets music tempo
0x446730 Tourney_SaveTournament 1 Saves tournament to "DATA\tournament.sav"
0x450960 Tourney_AdvanceRound 1 Advances tournament to next round, creates new TourneyMenu
0x4508f0 Tourney_SetCurrentLevel 1 Sets tournament current level, material color
0x4610e0 MeshWorld_CollectTrianglesInAABB 3 Recursive AABB triangle collection (collision mesh query)
0x467730 Array_Fill 1 Simple array fill (memset-like)
0x468050 Vector_Init 1 Vector/array initializer (allocates dword array)
0x469090 SpatialNode_SwapBuffers 1 Spatial tree node buffer swap (copy/move semantics)
0x4707b0 MeshObject_dtor 1 Mesh object destructor: releases textures, frees mesh data
0x471c00 MeshObject_DeletingDtor 1 MeshObject scalar deleting destructor
0x475020 MeshWorld_AddTexture 2 Adds texture to mesh world (creates 0x48 byte texture object)
0x476009 Bit_ShiftByte 1 Bit shift helper for byte values
0x4471e0 StdString_Replace 1 String replace operation (std::string-like)
0x448410 UI_TextEdit_PasteFromClipboard 2 Pastes text from Windows clipboard to UI
0x4492d0 UIList_AddItem 1 Adds item to UIList (creates UIListItem, copies string)
0x441640 SceneObject_DeletingDtor 1 SceneObject scalar deleting destructor
0x4722e0 AthenaVector_Init 1 Athena vector/list initializer
0x473030 Registry_SetValue 4 Windows registry value setter (RegSetValueExA wrapper)
0x451b90 TourneyMenu_ctor 1 Tournament menu constructor: difficulty, music, scoring thresholds
0x462380 SpatialTree_CloneToLevel 2 Recursively clones spatial tree to Level object
0x456870 Mesh_DeletingDtor 1 Mesh scalar deleting destructor
0x479000 Sound_EnumerateDevices 1 Sound device enumeration callback registration
0x480032 D3DTexture_ResizeAndValidate 5 D3D texture resize and compression format validation

Session 17 - SSE2/Math/Texture/AthenaString/Gfx Functions

Address Name Xrefs Description
0x4b22ab BitStream_ReadBitsSSE2 19 SSE2 bitstream reader: reads N bits from compressed stream, skips 0xFF markers
0x4a0f3a Matrix_Inverse4x4_SSE2 8 SSE2 4x4 matrix inverse using cofactor expansion + Newton-Raphson reciprocal
0x4a6b80 SSE2_SetFPControlWord 9 Sets FPU control word from param, stores to global DAT_00535280
0x4aba49 Mem_Zero 8 Optimized memset-to-zero: dword loop + byte tail
0x4ad576 Malloc_OrLongjmp 9 Safe malloc wrapper, longjmps on failure with "Out of Memory"
0x4bc360 StrCat_Fast 9 Optimized strcat with dword-aligned null detection (0x7efefeff trick)
0x4b8564 BitStream_CopyToOutput 8 Bitstream copy-to-buffer with checksum callback and double-buffer
0x4c183e Noop2 8 Empty no-op function stub
0x4c7152 ReturnZero 8 Stub function returning 0
0x459d96 D3DX_ShaderDispatch0 3 D3DX shader dispatch: calls D3DX_DetectShaderProfile then vtable[0x4F7194]
0x459e34 D3DX_ShaderDispatch1 4 D3DX shader dispatch via PTR 0x4F71B8
0x459ed1 D3DX_ShaderDispatch2 4 D3DX shader dispatch via PTR 0x4F71CC
0x467e40 AthenaList_Ctor 4 AthenaList constructor: sets vtable 0x4E998C, calls StdString_Substr
0x46a0d0 Audio_StopChannel 4 BASS_ChannelStop on channel at this+0x08
0x46b840 SceneObject_EmptyListCtor 3 Sets vtable to SceneObject_DeletingDtor(0x4D9368), inits AthenaList
0x46db10 App_Shutdown 4 App shutdown: destroys window, releases 5 COM objects, CoUninitialize
0x46dfa0 NetworkConnection_Ctor 3 NetworkConnection init: sets "Not Connected", +0x0C=1.0f
0x472990 Gadget_LabelCtor 4 UI label widget ctor: Gadget+label, vtable 0x4D9E68, name at +0x878
0x69b20 UIWidget_HitTest 3 UI hit-test: checks +0x420 rect or iterates children for point containment
0x73480 AthenaString_Reserve 4 String reserve: allocates new buffer, copies old string, free old
0x73600 AthenaString_Length 3 Recalculates string length from buffer, caches at +0x14
0x73640 AthenaString_Find 4 strstr wrapper: finds substring, returns offset or -1
0x77010 D3DX_RegistryGetter 4 Sets vtable from DirectX registry path struct at 0x4DA65C
0x75dc0 CRC32_Compute 3 CRC32 using 256-entry lookup table at 0x4F7534
0x77670 Vec3_ClosestPointOnLine 3 Projects point onto line segment, clamps t, returns nearest point
0x77970 Texture_StreamRead 3 Texture streaming read with 1KB chunks and progress callback
0x77ac0 Texture_BinarySearch 4 Texture binary search for load position optimization
0x77d60 Texture_LoadFromStream 4 Texture load from stream with D3D validation and retry
0x78680 Texture_dtor 4 Texture destructor: frees sub-textures, pixel buffers, refcount release
0x45ace5 D3DX_ShaderDispatch_4 4 D3DX shader dispatch with 4 params via PTR 0x4F7214
0x45ad75 D3DX_ShaderDispatch_4b 4 D3DX shader dispatch with 4 params via PTR 0x4F71F8
0x45ae05 D3DX_ShaderDispatch_2a 4 D3DX shader dispatch with 2 params via PTR 0x4F7208
0x45aea1 D3DX_ShaderDispatch_2b 4 D3DX shader dispatch with 2 params via PTR 0x4F720C
0x45af3e D3DX_ShaderDispatch_2c 4 D3DX shader dispatch with 2 params via PTR 0x4F7210
0x45afdc D3DX_ShaderDispatch_3 4 D3DX shader dispatch with 3 params via PTR 0x4F7238
0x45b521 D3DX_ShaderDispatch_noarg 4 D3DX shader dispatch with no args via PTR 0x4F71FC
0x45b7a8 D3DX_ShaderDispatch_noarg2 4 D3DX shader dispatch via PTR table
0x45baeb D3DX_ShaderDispatch_noarg3 4 D3DX shader dispatch via PTR table
0x45bc10 D3DX_ShaderDispatch_noarg4 4 D3DX shader dispatch via PTR table
0x45bf20 D3DX_ShaderDispatch_noarg5 4 D3DX shader dispatch via PTR table
0x8a160 Texture_SetPool 6 Sets texture pool on struct (calls init then assigns param)
0x8a180 Pool_dtor 6 Pool destructor: frees pool list, zeroes 8 dwords
0x8a560 Texture_ValidateOrBuild 5 Validates texture generation or rebuilds from source data
0x8a860 Texture_DestroyBuffers 5 Destroys indexed buffer list (frees each entry then array)
0x8a8d0 Mesh_Init 3 Mesh initializer: zeroes 8 dwords, callocs 0xCA8 byte sub-struct
0x8a900 Mesh_dtor 6 Mesh destructor: frees vertex/index/shader buffers, zeroes 8 dwords
0x89df0 Pool_Free 4 Frees two pool lists and zeroes 0x14 dwords
0x89e20 Texture_ComputeChecksum 4 Iterates byte stream summing values, marks 0x80000000 on non-0xFF terminator
0x8b1c0 Pool_Alloc 6 Pool allocator: 8-byte aligned alloc with linked-list overflow
0x8b2a0 VertexDeclaration_dtor 3 Vertex declaration destructor: frees buffer at +0x4C, zeroes 0x18 dwords
0x8b530 VertexShader_dtor 3 Vertex shader destructor: frees vertex/index buffers, releases DX objects
0x80c4d Gfx_ResizeBuffers 5 D3D render target resize: recreates vertex/index buffers, validates device
0x472340 Font_RenderToTextureComplex 4 Complex font rendering to texture with vertex buffers and shaders

Session 18 - ArenaBoard/BoardLevel Scalar Destructors

Address Name Description
0x425360 ArenaBoard_CollSlices_scalar_dtor Scalar deleting destructor for CollSlices board
0x425380 ArenaBoard_CollSlices_dtor Destructor for CollSlices board
0x4253e0 ArenaBoard_Expert_ScalarDtor Scalar deleting destructor for Expert Arena board
0x425400 ArenaBoard9_PopCylinder_ScalarDtor Scalar deleting destructor for ArenaBoard9 PopCylinder
0x425420 BoardLevel14_RaceOfAges_Scene_scalar_dtor Scalar deleting destructor for RaceOfAges scene
0x425440 ArenaBoard_Odd_ScalarDtor Scalar deleting destructor for Odd Arena board
0x425460 BoardLevel_Glass_scalar_dtor Scalar deleting destructor for Glass board level
0x425480 ArenaBoard_ScalarDtor Scalar deleting destructor for ArenaBoard
0x4254c0 ArenaBoard_Level_ScalarDtor Scalar deleting destructor for ArenaBoard level

Session 18 - CreditsScreen, MusicPlayer, TourneyMenu

Address Name Description
0x425920 CreditsScreen_dtor CreditsScreen destructor
0x425ac0 CreditsScreen_Render CreditsScreen render/layout
0x425d80 CreditsScreen_Layout CreditsScreen layout calculation
0x425f70 CreditsScreen_scalar_dtor CreditsScreen scalar deleting destructor
0x426030 MusicPlayer_ctor MusicPlayer constructor
0x4260c0 MusicPlayer_dtor MusicPlayer destructor
0x426130 MusicPlayer_scalar_dtor MusicPlayer scalar deleting destructor
0x426150 MusicPlayer_Render MusicPlayer render/update
0x4264a0 TourneyMenu_GetRaceName Get race name string for tournament
0x4264b0 TourneyMenu_WriteSave Write tournament save data
0x4265a0 TourneyMenu_LoadSaveAndShow Load tournament save and display
0x4266f0 TourneyMenu_Advance Advance tournament to next round
0x426780 TourneyMenu_CreateBoard Create board for tournament race
0x426af0 TourneyRaceEntry_scalar_dtor TourneyRaceEntry scalar deleting destructor
0x431d00 TourneyRaceEntry_scalar_dtor2 TourneyRaceEntry alternate scalar deleting destructor
0x432d20 TourneyMenu_Render TourneyMenu render function

Session 18 - AthenaString

Address Name Description
0x426bae AthenaString_MoveAssign AthenaString move assignment operator
0x426c50 AthenaString_Reserve AthenaString buffer reserve/capacity

Session 18 - Pendulum/Rotator/CollisionFace

Address Name Description
0x434030 Rotator_PlayCollisionSound Play collision sound for Rotator
0x434070 Rotator_Render Rotator render function
0x4362c0 Pendulum_Cleanup_vtable Pendulum vtable cleanup thunk
0x4362d0 Pendulum_Render Pendulum render function
0x436390 Pendulum_AddIndex Add index to Pendulum object
0x436530 Rotator_Cleanup_vtable Rotator vtable cleanup thunk
0x4366f0 Rotator_ctor Rotator constructor
0x4367d0 Rotator_Cleanup Rotator cleanup/release
0x4367e0 Rotator_StartSound Rotator start sound playback
0x436860 CollisionFace_ctor CollisionFace constructor
0x436910 CollisionFace_dtor_vtable CollisionFace vtable destructor thunk
0x436920 Rotator_ctor_sound Rotator constructor (sound variant)
0x436a10 Rotator_Cleanup_vtable2 Rotator vtable cleanup thunk variant 2
0x436a20 Pendulum_ctor Pendulum constructor
0x436b10 Pendulum_Cleanup Pendulum cleanup/release
0x436b70 Pendulum_PlayCollisionSound Play collision sound for Pendulum
0x436c10 Rotator_ctor_nosound Rotator constructor (no sound variant)
0x436ce0 Rotator_Cleanup_vtable3 Rotator vtable cleanup thunk variant 3
0x436cf0 Rotator_TriggerSound Rotator trigger/start sound
0x436e40 Pendulum_scalar_dtor Pendulum scalar deleting destructor
0x436e60 Pendulum_Cleanup_vtable2 Pendulum vtable cleanup thunk variant 2
0x436e70 Pendulum_ResetAndFire Reset and fire Pendulum
0x436fb0 Rotator_Cleanup_vtable4 Rotator vtable cleanup thunk variant 4
0x436fc0 Rotator_RemoveAndFree Remove and free Rotator object
0x4371a0 Rotator_Cleanup_vtable5 Rotator vtable cleanup thunk variant 5
0x4371f0 Rotator_MarkTriggered Mark Rotator as triggered
0x4372f0 Rotator_Cleanup_vtable6 Rotator vtable cleanup thunk variant 6

Session 18 - Level_Cleanup Variants

Address Name Description
0x433f00 Level_Cleanup_vtable1 Level vtable cleanup thunk variant 1

Session 21 - Scene Rendering, Mesh, Font, Huffman, D3DTexture, Sound, CRT

Address Name Description
0x0045ec30 Scene_Render3DObjects Main 3D object renderer: transforms vertices, frustum culls, submits triangle strips with D3D draw calls
0x00468600 Path_ComputeSegmentLengths Compute segment lengths from point pairs using sqrt(dx²+dy²)
0x0045afdc D3DX_ShaderThunk_Profile1 Thunk: calls D3DX_DetectShaderProfile(1) then indirect via PTR_FUN_004f7238
0x0045b521 D3DX_ShaderThunk_Profile1b Thunk: calls D3DX_DetectShaderProfile(1) then indirect via PTR_FUN_004f71fc
0x0045bf20 D3DX_ShaderThunk_Profile1c Thunk: calls D3DX_DetectShaderProfile(1) then indirect via PTR_FUN_004f71e8
0x0047c990 Mesh_Dtor Mesh destructor: set vtable, free ptrs +0x20/+0x28/+0x30, clear AthenaList, free Vec3Lists
0x0047d160 Mesh_FindElement Search array of 8-float elements (stride 0x424) for matching entry, returns index or -1
0x0047d2a0 Mesh_AddElement If not found, append 8 floats + 0 at stride 0x424 and increment count
0x0047d020 Mesh_ConnectElements Build adjacency between two mesh elements (param_2, param_3) via edge list
0x0047d680 FileHandle_Dtor File handle destructor: close handle, free buffer
0x00479be0 App_GetProductVersion Uses Version API (GetFileVersionInfoSizeA/A/VerQueryValueA) to extract ProductVersion string
0x00478780 BitStream_ReadValue Read int64 from bitstream array with stride 0x10; can sum all values
0x004ab9c8 Math_AlignUp Round up: ((a-1+b)/b)*b — align value to multiple of second arg
0x004ab9e0 Array_CopyElements Copy param_5 elements of param_6 bytes each from src to dst array-of-pointers
0x004abfaf DSound_SetVolume Set volume on sound object via vtable call (opcode 0x35)
0x004ad569 CRT_FreeIfNotNull Free ptr if non-null, simple null-check wrapper
0x004b2101 Huffman_BuildTable Build canonical Huffman decode table from 16-byte code length table (deflate-style)
0x004b2a13 BitStream_FlushAndReset Flush accumulated bits, zero the history buffer, reset position, set timestamp
0x004b670c Sound_DecodeFrame Decode a sound frame: handles types 4 (stop note), 5 (callback), 6 (sub-decode)
0x0048300c D3DTexture_Init Create D3D texture object with vtable PTR_004db360, init format/flags, register with parent
0x00483a44 D3DTexture_InitLocked Same as Init but with locked vtable PTR_004db3d0 (render target variant)
0x00486f30 D3DTexture_NullDtor Trivial destructor that just sets vtable to PTR_LAB_004db4c8
0x004b0e6b CRT_FreeParam2 Callback wrapper: just frees param_2 (2nd arg), ignores param_1
0x004ad716 Font_DecodeGlyphBits Decode glyph bitmap from compressed font data based on bit depth (1/2/4/8)

Session 21 (continued) - Level ctors/dtors, more naming

Address Name Description
0x004383f0 Glass_Level_scalar_dtor Glass level scalar deleting destructor
0x004384a0 Glass_Level_ctor Glass level constructor: Stands init, clone level, init timers, lookup Chain1/2 Bridge/Wall hashtables
0x00438730 Impossible_Level_scalar_dtor Impossible level scalar deleting destructor
0x00438830 Cascade_Level_scalar_dtor Cascade level scalar deleting destructor
0x00438b10 WarmUp_Level_scalar_dtor WarmUp level scalar deleting destructor
0x00438f30 Tower_Level_scalar_dtor Tower level scalar deleting destructor
0x004396f0 Spinner_Level_ctor Spinner level constructor: Stands init, position/velocity, timer, scale X/Z, clone level, 100 score, 1.0 factor
0x00439850 Intermediate_Level_scalar_dtor Intermediate level scalar deleting destructor
0x00439870 Impossible_Level_Update Impossible level update: countdown timer, gravity, scale/position, sound
0x00439b90 Sawblade_Level_scalar_dtor Sawblade level scalar deleting destructor
0x0043a150 Gear_Level_ctor Gear level constructor: RNG for speed/offset, 400ms time, ScoreDisplay init
0x00430430 TourneyRaceEntry_Dtor TourneyRaceEntry destructor: set vtable, call UIList_Cleanup

Session 21 (continued 2) - Level dtors, UI, String, RaceResults

Address Name Description
0x00446860 QuitToDesktop_scalar_dtor QuitToDesktop scalar deleting destructor
0x00446880 GameObject2_scalar_dtor GameObject2 scalar deleting destructor
0x004468a0 TourneyContinueDialog_scalar_dtor TourneyContinueDialog scalar deleting destructor
0x004469e0 App_CreateConfirmMenu Create ConfirmMenu dialog, add to scene
0x00446a60 App_CreateHighScoreEntry Create HighScoreEntry dialog, add to scene
0x00446ae0 QuitToDesktop_Execute Execute quit: set cull mode, start race, show results, show main menu
0x00447370 ScoreDisplay_scalar_dtor ScoreDisplay scalar deleting destructor
0x00447390 StdString_Insert Insert count bytes from ptr at position in std::string (SSO-aware)
0x00447500 StdString_TruncateToWidth Erase chars from end until Font_MeasureText width <= 0x13F (319px)
0x00447570 StdString_InsertCStr Insert C string at position (strlen + call StdString_Insert)
0x0044af60 RaceResults_scalar_dtor RaceResults scalar deleting destructor
0x0044af80 RaceResults_Init RaceResults init: set vtable, Matrix_Identity
0x0044b8a0 RaceResults_ctor RaceResults constructor: init timers, random congratulatory text, score thresholds
0x0044bfc0 RaceResults_Render Render race results: colored rect, shadow text for title/place/time
0x0044c450 RaceResults_Reset Reset: free sub-object, cleanup timer, restore vtable
0x0044c7d0 RaceResults_Update Update race results: advance timers, check completion, sound/voice
0x0044ca80 RaceResultsMenu_scalar_dtor RaceResultsMenu scalar deleting destructor
0x0044cb10 RaceResultsMenu_ctor RaceResultsMenu constructor: title, subtitle, player entries, timer

Session 22 - Path/Sprite/Gfx/Math Functions

Address Name Description
0x00457370 FontList_Dtor FontList destructor: set vtable PTR_004d8e30, iterate/remove textures, clear list, null 0x100 entries, Vec3List_Free
0x0045cb88 Matrix_BuildRotationZYX Build 4x4 rotation matrix from Euler ZYX angles (sin/cos with global factor 0x4d5c84)
0x0045d8f0 Ball_RenderWithMaterial Render ball with material: Ball_Render, Graphics_ApplyMaterialAndDraw, vtable 0x120 call
0x0045dfe0 Gfx_SetupAlphaRenderState Setup alpha render state: cull mode, texture stage 0x152, alpha test 0x16, blend 0x1d/7/0x89, texture 0x1b
0x0045dcf0 RNG_SeedSmall Seed small RNG: 53-entry additive PRNG with state at +0xc (mask 0x3fffffff), 0x35 iterations
0x0045dde0 Gfx_SetBlendState Set D3D blend state: vtable 0xfc calls (0, 0xd, 2) and (0, 0xe, 2), state check at +0x704
0x0045d030 Sprite_Reset Sprite reset: set vtable PTR_004d8f84, remove texture ref, Matrix_Identity
0x0045d0a0 Sprite_ScalarDtor Sprite scalar destructor: calls Sprite_Reset, then free if flag bit 0 set
0x0045d1d0 Sprite_Ctor Sprite constructor: vtable PTR_004d8f84, RenderContext_Init, sets 3 scale pairs from image dims
0x0045dab0 Sprite_DrawRotatedQuad Draw rotated quad: 5-point star pattern via sin/cos waves around center, 4 RGBA corner colors
0x00468780 Path_BuildVertexStrips Build vertex strips from path array: interleaved pos/uv/color quads, stride-7 format, vertex count = (count-1)*4
0x00468f30 Path_ComputeSegDeltas Compute segment deltas from path: outputs 4-float {0,0,delta,start} per segment
0x00469580 FileHandle_Open Open file: close prev handle, free buffer, _open with 0x8000 flags, set status flag
0x00467c60 Array_CopyDWordsThunk Thunk: just calls Array_CopyDWords
0x00467e00 DualBuffer_Free Free dual buffer: frees two pointers at +0x14 and +0x04, zeros 6 DWORDs
0x00468510 PathGroup_Init PathGroup init: zero 15 DWORDs across 5 groups of 3
0x00466cd0 Transform_SetDefaultScale Set transform default scale: NoOp + write 0.1f at +0xc and +0x10
0x00466d50 Transform_ScalarDtor Transform scalar destructor: calls sub_466cc0 then free if flag bit 0 set
0x00467780 Array_CopyBackward Copy DWORDs backward: src→dest in reverse, count in bytes
0x004677b0 Matrix_SolveGaussElim Gaussian elimination with partial pivoting: solves Ax=b for matrix at param_1
0x00467cc0 Array_FillAndAdvance Fill DWORD array and advance: Array_FillDWords, returns ptr + count
0x00467cf0 Vector_InsertRange Insert range into vector: copy [param_2, param_3) into this container, update end ptr
0x004685e0 PathGroup_PushPair PathGroup push pair: Vector_PushBack of 4 then 8 onto two vectors offset by 0x10
0x004692d0 SceneObject_ScalarDtor SceneObject scalar destructor: calls SceneObject_dtor then free if flag bit 0 set

Session 22 (continued) - App Lifecycle, Input, Gfx Lighting

Address Name Description
0x0046c050 App_CreateInputDevice Allocate 0x91c bytes, call FUN_00466620 (input device ctor), store at App+0x178
0x0046c0b0 App_CreateAudioDevice Allocate 0x424 bytes, call Audio_Init, store at App+0x17c
0x0046c170 App_FrameUpdate Frame update: GetCursorPos, WindowFromPoint, poll input/audio/collision, GameUpdate
0x0046c200 App_ResetFrame Reset frame: Scene_ResetCameraAndFrameCount, then Graphics_Clear
0x0046c260 App_TickGameUpdate Thunk: call FUN_00469a60(App+0x184) - tick game update
0x0046c290 App_OnMouseDown Mouse down handler: SetCapture, set button flags, UIWidget_HitTest, dispatch to widget
0x0046f100 Gfx_ApplyLightingState Set D3D lighting state: specular enable, light enable, material emission, 0x39 state
0x0046f1e0 Gfx_ResetLighting Reset lighting: disable specular, set render state 0x1b=0, material type 0, state 0x39
0x0046ca20 App_ParseGUID Parse GUID from string: MultiByteToWideChar + CLSIDFromString, store at this+0x44
0x0046dc20 App_ScalarDtor App scalar destructor: calls App_Shutdown then free if flag bit 0
0x0046dc40 App_Ctor App constructor: vtable, size 640/480, cursors, CoInitialize, input device 0x848
0x0046e910 KeyboardDevice_ScalarDtor KeyboardDevice scalar destructor: calls KeyboardDevice_dtor then free
0x0046ebd0 InputDevice_PollAndRelease Poll DInput device, acquire on error, release 4 sub-devices
0x00471c60 Vector_FillResize Fill vector with count DWORDs, update end pointer

Session 23 — Math, Sound, D3D, Stream, Spatial (74.4→75.2%)

Address Name Xrefs Description
0x4637f0 Vec3_Abs 2 Compute absolute value of 3-component vector (fabs each)
0x45b345 Matrix_BuildLookAt 1 Compute view matrix from eye, target, up vectors (cross products)
0x45b48d Matrix_BuildPerspectiveFOV 1 Compute perspective projection matrix from FOV, aspect, near, far
0x45b1f9 D3D_Thunk4 1 D3D dispatch thunk through PTR_FUN_004f7230 (4 params)
0x45b227 D3D_Thunk0 1 D3D dispatch thunk through PTR_FUN_004f722c (0 params)
0x45b238 D3D_Thunk5 1 D3D dispatch thunk through PTR_FUN_004f7234 (5 params)
0x45be69 D3D_ThunkIndirect 1 D3D dispatch thunk through DAT_004f71e4
0x457690 Font_DrawGlyph3D 1 Render 3D text — iterate chars, render with Ball_RenderWithMaterial
0x476270 D3DX_ErrorHandler_Ctor 1 Init D3DX error handler — set vtable 0x4DA194 + store param
0x466060 Level_CloneTree 1 Recursively clone Level tree — create CollisionLevel for each child
0x466320 SoundChannel_Ctor 1 Init sound channel vtable 0x4D90E8 with 7 zero fields
0x466480 SpatialTree_ComputeOffset 1 Compute vector offset with cross-product area for spatial tree
0x4664f0 Sound_LoadAndAppend 1 Create AthenaList entry, load sound via Sound_LoadOggOrWav
0x466620 SoundDevice_ctor 1 Sound device constructor — vtable 0x4D911C, 16 channels, DS init
0x466a10 SoundDevice_UpdateChannels 1 Tick sound channels, play on timer expire, remove expired
0x466b80 SoundDevice_Play3DAll 1 Iterate all sound channels, call Sound_Play3DAtPosition per entry
0x466c50 SoundDevice_DeletingDtor 1 Scalar deleting destructor for SoundDevice
0x466c90 SoundBuffer_ScalarDtor 1 Free buffer at +4, scalar deleting destructor
0x466cc0 D3DX_RegistryGetter_Ctor 1 Init D3DX registry getter — set vtable 0x4D9128
0x468700 D3DXSkinMesh_Init 1 Initialize D3DX skin mesh — two vector resizes
0x4632f0 SpatialTree_SetDefaults 1 Set spatial tree defaults: scale=0.1, depth=6, min=0.9
0x4633b0 SpatialTree_DeletingDtor 1 Scalar deleting destructor — free then delete
0x463880 SpatialTree_ForEach 1 Iterate tree children calling vtable[0x20] on each
0x4694a0 StreamReader_DeletingDtor 1 Scalar deleting destructor calling StreamReader_dtor
0x4694c0 StreamReader_dtor 2 Free 2 buffers at +4/+8, set vtable 0x4D91B8
0x4698d0 FileStream_DeletingDtor 1 Close file handle +0x10, free buffer +0x8
0x469920 MeshWorld_ctor_v2 1 MeshWorld constructor v2 — vtable 0x4D91C4

| 0x4699d0 | MeshWorld_RemoveObject | 1 | Remove object from mesh list, clear current references, call dtor |
| 0x469a40 | MeshWorld_ClearCurrent | 1 | Clear current item by calling vtable+0x30 |
| 0x469a80 | MeshWorld_CallNext | 1 | Call vtable+0x24 on next item at +0x424 |
| 0x469ac0 | MeshWorld_SkipOrAdvance | 1 | Skip current (param_2!=0) or advance to next if matches param_1 |
| 0x469c30 | MeshWorld_dtor2 | 1 | Clear list, iterate items calling dtor(1), Vec3List_Free |
| 0x469ec0 | MeshWorld_ActiveUpdate | 1 | Iterate objects, skip inactive (+0xB), set obj ID, call vtable+8 |
| 0x469f50 | MeshWorld_CallVtable34 | 1 | Iterate objects calling vtable+0x34 |
| 0x469fc0 | MeshWorld_DeletingDtor2 | 1 | Scalar deleting destructor for MeshWorld variant |
| 0x469fe0 | MusicChannel_Ctor | 1 | Init vtable 0x4D91D8, AthenaList, volume=1.0, flags=0 |
| 0x46a180 | MusicChannel_FadeUpdate | 1 | Fade BASS volume up/down with BASS_ChannelSetAttributes |
| 0x46a250 | MusicChannel_Cleanup | 1 | Free all channel buffers, clear AthenaList, Vec3List_Free |
| 0x46a4b0 | MusicChannel_DeletingDtor | 1 | Scalar deleting destructor for MusicChannel |

Session 25 - Mesh/Vertex/D3D thunks, Array utilities (63.8%)

Address Name Description
0x0047ead8 DynArray_Grow Dynamic array grow: realloc with doubled capacity
0x0047ebae FindInSmallIntArray Search small int array for matching value
0x00480c4d MeshBuffer_Allocate Allocate mesh vertex/index buffer
0x00489217 SetFileSecurityW IAT thunk for SetFileSecurityW
0x0048a560 AthenaList_SortMerge AthenaList merge sort implementation
0x0048a860 MeshGroup_dtor MeshGroup destructor
0x0048ce30 TextureCache_RecursiveFree TextureCache recursive free (tree traversal)
0x0048eb20 BuildSDFGrid Build signed distance field grid
0x004912e4 ComputeScanlineZBuffer Compute scanline Z-buffer for software rendering
0x00496e13 Mesh_SetVertexFormat Set mesh vertex format/declaration
0x0045b72c D3DX_Thunk3Param D3D dispatch thunk through PTR_FUN_004f7204 (3 params)
0x0047e026 WideString_MatchSlot Match ushort value against 3-slot array, return index (0/1/2)
0x0047e152 WideArray_Grow Realloc wchar array with capacity doubling
0x0047e359 ShortArray_FindMatch3 Search short array of size 3 for matching value
0x0047e5bd Mesh_BuildAttributeIndices Build mesh attribute/index remapping tables (vertex→attrib, 0xFFFF sentinel)
0x0047e725 Mesh_CollectUniqueAttributes Collect unique attribute IDs from mesh into dynamic array (doubles capacity)
0x0047e855 UshortArray_PushBack Push ushort element with doubling capacity
0x0047e914 Mesh_RemapVertexIndices Remap vertex/adjacency indices via lookup tables (stride 0xC)
0x0047ea05 Mesh_SwapVertices Swap two mesh vertices + adjacency/attribute/blend data
0x0047eb76 MeshData_Init Init mesh data struct: set 3 params, zero counts at +0x18/+0x2C
0x0047ec49 Mesh_SwapVertexData Swap vertex data between indices using alloca temp buffer (stride from +0x2C)
0x0047edc2 MeshSubMesh_Init Init sub-mesh struct: set 3 params, zero counts at +0x10/+0x20
0x0047ee1b MeshEdge_GetFirstVertex Get first valid vertex from 4-entry edge list (+0x14 base offset)
0x0047ef1e MeshAttribute_FindIndex Search attribute short array for matching value, return found+index
0x0047f02a Mesh_UnlinkVertex Unlink vertex from doubly-linked adjacency list (0xC stride)
0x0047f17e Mesh_LinkVertexToHead Link vertex to head of hash bucket (stride-6 linked list)
0x0047f363 VertexDecl_CopyVertexData Copy vertex data between declarations: blend weights, position, normal, texcoords
0x0047f58e VertexDecl_CreateFromFVF Create vertex declaration from FVF (Flexible Vertex Format) code
0x0047e263 DWord6Array_Grow Grow DWORD-6-stride array with realloc (init 0x28 bytes, security cookie)

Session 25 (continued) - Mesh Pipeline Functions

Address Name Description
0x0047f6c7 Mesh_RenderSubset Render mesh subset by attribute ID via D3D DrawIndexedPrimitive (HW or SW vertex processing)
0x0047f993 Mesh_WeldVertices Weld duplicate vertices: build adjacency hash, split edges, remap indices, fill adjacency
0x0047fdc0 Mesh_RemoveEdgeFromHash Remove edge entry from hash bucket linked list (find matching v0/v1/v2, unlink)
0x0047fe0e Mesh_FindBestDuplicateVertex Find best matching duplicate vertex by comparing cross-product magnitudes with epsilon
0x0047feec Mesh_ComputeTriCrossProduct Compute scalar triple product of two 3D triangles for vertex welding comparison
0x00480501 Mesh_ValidateAdjacency Validate mesh adjacency: check face refs, vertex bounds, edge integrity, attribute matching

Session 25 (continued 2) - Mesh Pipeline, Rendering, Validation

Address Name Description
0x00480501 Mesh_ValidateAdjacency Validate mesh adjacency: check face refs, vertex bounds, edge integrity, attribute matching
0x004806da Mesh_DrawSubsetIndexed Draw mesh subset by attribute ID using D3D DrawIndexedPrimitive (indexed path)
0x00480813 Mesh_WeldVertices16 16-bit variant of WeldVertices: build adjacency, weld duplicate verts, split edges
0x00481101 Mesh_ValidateAdjacency16 16-bit variant of ValidateAdjacency: check 16-bit face/edge/vertex refs
0x0048130c Mesh_ValidateAttributes Check attribute IDs match face ranges in attribute table

Session 25 (continued 3) - Mesh Copy, Adjacency, Optimize

Address Name Description
0x00481432 Mesh_CopyFrom Copy mesh data from source: indices, adjacency, attributes, vertices, blend data
0x00481637 Mesh_CopyFrom16 16-bit index variant of Mesh_CopyFrom (ushort adjacency stride 6)
0x0048183d Mesh_AdjacencyIteratorNext Advance adjacency iterator across mesh edges (32-bit), handle boundary/interior
0x00481900 Mesh_AdjacencyIteratorNext16 16-bit variant of Mesh_AdjacencyIteratorNext
0x00482010 Mesh_OptimizeFaces Optimize mesh face order for vertex cache, compact vertex remapping

Session 25 (continued 4) - Mesh Optimization Pipeline

Address Name Description
0x00482137 Mesh_OptimizeVertices Vertex cache optimization: swap/reorder vertices for cache locality, compact index remap
0x00482218 Mesh_SortByAttribute Sort mesh by attribute ID: build attr table, remap vertices/faces, D3DX ID3DXMesh Optimize style

Session 25 (continued 5) - Mesh Helper Functions

Address Name Description
0x0047dffd FindInSmallIntArray32 Find int value in 3-element int array, return index or (2+not_found)
0x0047e8f6 Mesh_RemapVertexAttribute32 Remap vertex attribute through lookup table (if flag bit 2 set at +0x0C)
0x0047e96b Mesh_SwapVertices32 Swap vertex data between two indices using alloca temp buffer (32-bit stride)

Session 25 (continued 6) - Vec/Math, Level, Sound, FlagWaver, App, Input, Vector STL

Address Name Description
0x0045c17b Vec2_Normalize Normalize 2D vector (length-squared check, divide by sqrt)
0x0045c32f Vec3_Normalize Normalize 3D vector (length-squared check, divide by sqrt)
0x0045c208 Matrix_TransformVec4x3 Transform vector by 4x3 matrix (last row implicit [0,0,0,1])
0x0045c48e Gfx_ProjectToViewport Project 3D coords to viewport UV using render state flags
0x0045c61b Gfx_UnprojectToNDC Unproject screen coords back to normalized device coords via viewport rect
0x0045c7c1 D3D_ThunkShaderDispatch4 D3D thunk dispatch with 4 params
0x0045caae Matrix_BuildOuterProductScale Build 4x4 matrix from outer product of vec scaled and subtracted from identity
0x00461460 SceneObject_BaseInit Base init for scene objects — AthenaList_Init, Vec3_Init, string buffer
0x00461680 SceneObject_BaseClear Clear/reset scene object base — Vec3List_Free, Matrix_Identity, free string
0x004629c0 Level_DeletingDtor Scalar deleting destructor for Level (calls Level_Cleanup, free if flag)
0x004650e0 Level_dtor Level destructor — set vtable 0x4D9068, call Level_Cleanup
0x00465240 Level_DeletingDtor2 Scalar deleting destructor variant 2 for Level
0x00465860 Level_LoadMeshes Create MeshWorld, create MeshBuffers, parse (NOCOLLIDE)/N:/E: prefixes, create CollisionLevels
0x00466570 Level_ReadSoundVolume Read "Sound Volume" float from registry, default 1.0
0x004665e0 Audio_ClampPanValue Convert float to int pan value, clamp minimum at -2000/-10000
0x004668a0 SoundDevice_dtor SoundDevice destructor — save volume to registry, free AthenaLists, vec3list
0x00467b40 Spline_EvalCubic Evaluate cubic spline (t^3a + t^2b + t*c + d), clamp t to array bounds
0x00467c30 Array_CopyDWords3 Copy dwords from begin to end into destination
0x00467c80 Array_CopyDWords4 Thunk to Array_CopyDWords
0x00467d60 MultiBuffer_Free Free 6 allocated buffers with size/capacity fields at 0x10-byte intervals
0x00467ea0 Exception_DeletingDtor Scalar deleting destructor for std::exception
0x00467ec0 Exception_dtor Exception destructor — free SSO string, call base ~exception
0x00467f00 Exception_AssignCStr Exception constructor from C string — StdString_Assign
0x00467f40 Exception_ThrowVectorLength Throw "vector too long" exception
0x00467fc0 Exception_CopyCtor Exception copy constructor — base copy + StdString_Substr
0x004680b0 Vector_InsertN STL vector::insert — insert N elements at position (realloc/memmove)
0x00468350 Vector_Assign STL vector::assign — copy from another vector, realloc if needed
0x00468490 Vector_Resize STL vector::resize — grow or trim to count
0x00468570 Vector_PushBack STL vector::push_back — append element, realloc via InsertN if needed
0x00469a60 UIWidget_CallVtable20 Call vtable+0x20 on object at +0x424 if non-null
0x00469aa0 UIWidget_CallVtable28 Call vtable+0x28 on object at +0x424 if non-null
0x00469b20 UIWidget_HitTest Find widget under point — check rect bounds, iterate back-to-front
0x00469be0 UIWidget_UpdateHover Update hover — find widget under point, call vtable+0x30 leave / vtable+0x2c enter
0x0046a0e0 RegKeyList_AppendStr Allocate string+value pair, append to AthenaList at +0xC
0x0046a3c0 RegKeyList_CopyFromSibling Iterate sibling list entries, call RegKeyList_AppendStr for each
0x0046a6e0 RaptisoftUtil_Ctor Constructor — FindWindowA("Raptisoft Utility"), set vtable
0x0046a7f0 RaptisoftUtil_DeletingDtor Destructor — Window_Notify "*** END RAPTISOFT SESSION ***"
0x0046a820 FlagWaver_dtor Free vertex buffer, release D3D resource, identity matrix
0x0046a8a0 FlagWaver_AllocBuffers Allocate vertex buffer array, create D3D vertex buffer
0x0046a930 FlagWaver_AdvancePhase Increment wave phase offset at +0x28
0x0046a940 FlagWaver_DeletingDtor Scalar deleting destructor for FlagWaver
0x0046a960 FlagWaver_UpdateVertices Compute water wave positions (sin/cos), normals, vertex averaging for smooth surface
0x0046af30 FlagWaver_Ctor Constructor — vtable 0x4D9344, 10 segments, wave params, alloc buffers
0x0046b070 FlagWaver_Render Render water ripple — update vertices, test ball intersection, draw with vertex buffer
0x0046b200 RenderList_AppendCopy Allocate new RenderContext and copy data, or append existing to list
0x0046b360 RenderList_FreeAndClear Iterate list calling dtor(1), then AthenaList_Free
0x0046b3d0 MeshBuffer_DeletingDtor Scalar deleting destructor for MeshBuffer
0x0046bca0 App_WriteDisplaySettings Write Fullscreen/ScreenWidth/ScreenHeight to registry
0x0046bd00 App_ReadDisplaySettings Read Fullscreen/ScreenWidth/ScreenHeight from registry
0x0046bff0 App_CreateGraphics Allocate Graphics object (0x7DC), store at App+0x174
0x0046c110 App_CreateInputHandler Allocate InputHandler (0x438), store at App+0x180
0x0046c3c0 Input_OnMouseUpCapture Decrement capture count, ReleaseCapture if zero, notify interceptor "MouseUp"
0x0046c430 Input_OnMouseUp ReleaseCapture, notify interceptor or widget with vtable+0x18, clear button flags
0x0046c760 Input_OnMouseDown Hit-test widget, call vtable+0x1c with button param
0x0046c7c0 App_SetFullScreen Toggle fullscreen/windowed, Graphics_Reset, AdjustWindowRect, SetWindowPos
0x0046c9e0 App_FrameTick If not paused, render + update scene
0x0046cb00 App_CreateScoreDisplay Allocate 0x8A4 score display object, add to scene
0x0046cb70 App_SetTitleString Free and replace title string at App+0x1B4

Session 25 (cont7) — 44 renames — 67.5% documented

Address Name Description
0x0046f010 SceneObject_dtor Destroys scene object, iterates list calling dtor(1), frees sub-objects
0x0046f0e0 SceneObject_DeletingDtor Deleting dtor for SceneObject
0x0046f310 SpriteAnim_Ctor Initializes sprite animation with vtable 004d9c48
0x0046f7c0 SpriteAnim_SetRange Sets animation range (prev=current, new=param)
0x0046f7d0 SpriteAnim_InitFromCalcTexCoords Initializes sprite anim from texture coords (8 calls)
0x0046f8d0 MeshWorld_BuildVertexBuffer Builds vertex buffer from mesh object lists, creates D3D VB
0x0046fb50 SpriteAnim_DeletingDtor Deleting dtor for SpriteAnim, frees callback and data
0x0046fcc0 MeshWorld_CollectRenderLists Collects render lists from mesh objects into target list
0x0046ff50 SafeCallDtor Calls vtable[2] (dtor) on object ptr if non-null
0x0046ff60 MeshWorld_OptimizeAll Optimizes all meshes, builds vertex buffers
0x00471ce0 Throw_VectorTooLong Throws "vector too long" exception
0x00472570 MeshWorld_BuildFontMeshes Builds font meshes via Font_RenderToTextureComplex
0x00472a30 ArenaBoard_dtor ArenaBoard destructor, frees sub-object and calls SceneObject_dtor
0x00472a50 ArenaBoard_TickDown Decrements countdown, triggers vtable callbacks at zero
0x00472a80 ArenaBoard_RenderThenFree Calls render (vtable+0x48) then free (vtable+0x40)
0x00472ad0 ArenaBoard_DeletingDtor Deleting dtor for ArenaBoard
0x00472b20 HitBox_PointInBounds AABB point-in-bounds check with float comparisons
0x00472b80 D3DTexture_Ctor Initializes D3D texture object with vtable 004d9ecc
0x00472c00 D3DTexture_DeletingDtor Deleting dtor for D3D texture
0x00472d80 BaseObject_DeletingDtor Deleting dtor for BaseObject
0x00472e20 AthenaHashTable_DeletingDtor Deleting dtor for hash table (calls ctor then frees)
0x00472ea0 RegKey_Ctor Initializes registry key object with vtable 004d9f08
0x00472f50 RegKey_SetSoftwarePath Builds "SOFTWARE%s" registry path
0x00473000 RegKey_WriteDWORD Writes DWORD value to registry via RegSetValueExA
0x00473100 RegKey_QueryValue Queries registry value via RegQueryValueExA
0x00473220 RegKey_DeletingDtor Deleting dtor for RegKey, frees path string
0x00473260 eSellerate_ExtractDLL Extracts eSellerateEngine.dll from resources to Windows dir
0x00473355 eSellerate_ExtractDLLNull Calls eSellerate_ExtractDLL with NULL module
0x0047335d eSellerate_ReadAffiliateKey Reads affiliate key from SOFTWARE\eSellerate registry
0x00473460 StdString_FreeBuffer Frees internal buffer, zeroes capacity/size/ptr
0x00473480 StdString_Reserve Reallocates buffer to param_1+1 size
0x00473580 StdString_AssignN strncpy-assigned string with length param
0x00473600 StdString_RecalcLen Recalculates string length if dirty flag set
0x00473640 StdString_FindSubstr strstr wrapper returning index or -1
0x004736f0 AthenaString_AssignCStrFree Assign C string then free source
0x00473a70 AthenaString_AssignCStrFree2 Variant of assign-CStr-then-free
0x00473ac0 AthenaString_AssignFree Assign string then free source
0x00473ba0 AthenaString_Substr Substring extraction by index/length
0x00473cd0 AthenaString_EraseRange Erase characters in range
0x00473e20 AthenaString_EraseSubstr Find and erase substring
0x00474000 AthenaString_Truncate Truncate string to N characters
0x004742b0 MWParser_DumpTags Dump tags from MW parser to string
0x004743f0 MusicChannel_LoadAndAppend Create MusicChannel, load file, append to list
0x00474480 MusicDevice_SetVolume Set BASS config volume
0x004744b0 MusicDevice_ReadVolume Read "Music Volume" from registry
0x00474510 MusicDevice_MuteToggle Toggle BASS mute on/off
0x004746a0 MusicDevice_dtor Destroy music device, free channels, BASS_Stop/Free
0x00474780 MusicDevice_FadeAll Update fade on all music channels
0x004747e0 MusicDevice_DeletingDtor Deleting dtor for MusicDevice
0x00474800 Menu_Ctor Menu class constructor, inits 8 AthenaLists
0x00474900 LoaderGadget_OK "LoaderGadget::OK" - handles OK button
0x00474930 Menu_SetDirty Sets dirty flag (+0x2d64)
0x00474940 Menu_MergeAllLists Merge all 7 category lists into main list
0x00474a30 LoaderGadget_Tick Load progress tick, updates count/percentage

Session 26 — 30 renames — 69.1% documented

Address Name Description
0x00473e70 AthenaString_ReplaceAt Replace substring at position with new string
0x00473fb0 AthenaString_ReplaceSubstr Find substr, erase, insert replacement at same position
0x00475cec StrLen Null-safe strlen (character count)
0x00475d03 StrNEq strncmp-like comparison, returns 1 if equal up to N chars
0x00475dec Hash_MixString Hash mix over string bytes using MixKey
0x00475e15 Hash_MixStringUpper Hash mix with Char_ToUpper on first string
0x00475e60 NoiseTable_Init Seed RNG and fill 0x200-entry noise table at 0x5341f0
0x00475e87 StrLen_Delay8 strlen with 8-iteration delay loop (anti-analysis?)
0x00475ed4 LicenseKey_ComputeHash Compute license hash from strings then mask + add constant
0x00475ef9 LicenseKey_EncryptBlock 100-element permutation cipher with key expansion
0x00477010 CollisionObj_Init Init collision object with vtable PTR_LAB_004da65c
0x00477060 Ray_SphereIntersect Ray-sphere intersection test, returns hit T or flags
0x00477120 Vec3_ProjectOntoRay Project point onto ray (origin + dir), result = dir*t + origin
0x004771d0 Ray_PlaneIntersectT Ray-plane intersection, returns T or flags
0x00477240 Vec3_DotDiff dot(a,n) - dot(b,n) — difference of dot products
0x00477280 Vec3_DotDiffAbs abs(dot(a,n) - dot(b,n)) — absolute dot difference
0x004772c0 Vec3_Reflect Reflect vector v across plane normal n: result = v - 2*dot(v,n)*n
0x00477330 AABB_FromSphere Compute AABB min/max from sphere center, velocity, radius
0x004774b0 Tri_TestPointInside Test if point is inside triangle using cross product edge tests
0x00477670 Segment_ClosestPoint Find closest point on line segment to a point
0x004777c0 Vec3_ProjectOntoPlane Project vector onto plane: result = v - (dot(v-origin,n))*n
0x00477830 MeshArchive_SetPosition Set position and reset pool via vtable
0x00477970 MeshArchive_ReadChunks Read 0x400-byte chunks from stream
0x00477ac0 MeshArchive_BuildTree Recursive binary search tree for mesh data
0x00477d60 MeshArchive_LoadSubmesh Load submesh with bitstream and D3DX retry loop
0x00477f10 MeshArchive_LoadAll Load all meshes from archive with position tracking
0x00478320 MeshArchive_ReleaseBuffers Release sub-buffers, set state=2
0x00478340 MeshArchive_LoadFrame Frame-level load with bitstream, mesh groups, position tracking
0x00478680 MeshArchive_dtor Destructor: free all sub-resources, D3D resources, mesh groups
0x00478800 MeshArchive_SeekRead Seek to position and read mesh data with bitstream

Session 26 (cont2) — 10 renames — 69.3% documented

Address Name Description
0x00478b90 MeshArchive_GetSubmeshPtr Get pointer to submesh at index (or first if -1)
0x00478bd0 MeshArchive_ReadVertices Read vertex data from current frame, apply signed-compression decode
0x00478d60 MeshArchive_LoadComplete Full load: reset position, read chunks, build tree, load all, seekread
0x00478e70 MeshArchive_ctor Constructor: init vtable, pool, D3D resources, mesh groups, position tracking
0x00478f80 MeshArchive_InitAndLoad Create archive and fully load mesh data from stream
0x00479050 Reg_ReadD3DValue Read DWORD value from HKLM/software\Microsoft\rd3D registry
0x004793e0 SceneList_Ctor Init SceneList with vtable PTR_FUN_004da6b4, empty AthenaList
0x00479400 SceneList_Reset Reset SceneList (vtable + Vec3List_Free)
0x00479460 SceneList_DeletingDtor Deleting dtor for SceneList (Reset then free if bit 0 set)
0x004794d0 SplashScreen_Ctor Splash screen ctor: brand.png + raptisoftlogo.png sprites, showcardgothic16 font

Session 28 — 21 renames — 70.1% documented

Address Name Description
0x00456cd0 Ball_InitBattleMode Initialize ball for battle mode: sets physics params (friction 0.18, bounciness 1.0, radius 400.0), speed
0x00456f70 Gadget_AddSpriteSlot Add sprite slot to gadget (sprite list with texture index + dimensions)
0x0045b104 D3DThunk_AcquireThreadFocus D3D thunk: DetectShaderProfile + acquire thread focus
0x0045b25f Matrix_BuildScaleTranslation Build combined scale+translation matrix (16-float array, scale*1.0, translation, 0s)
0x0045b74c Matrix_BuildRotationAxisScaled Build rotation matrix around axis with scale factor (cos/sin of angle*scale)
0x0045ba24 D3DThunk_Present D3D thunk: DetectShaderProfile + Present dispatch
0x0045bb6f D3DThunk_SetClipPlane D3D thunk: indirect SetClipPlane dispatch
0x0045bd1d D3DThunk_DrawIndexedPrimitiveUP D3D thunk: DetectShaderProfile + DrawIndexedPrimitiveUP (6 params)
0x0045bd47 D3DX_SlerpQuad D3DX spherical lerp for quaternions (4 floats, uses 1-t)*t weighting
0x0045bd9e D3DThunk_SetVertexShader D3D thunk: DetectShaderProfile + SetVertexShader dispatch
0x0045bdde D3DX_SquadInterpolate D3DX squad interpolation between quaternions with blend factor
0x0045c273 Matrix_TransformPoint2D 2D point transform: mat*vec2 (reads mat[0..5], vec2, outputs vec2)
0x0045c2f3 Matrix_TransformVector2D 2D vector transform: mat*vec2 without translation (reads mat[0..5], vec2)
0x0045c3cf Matrix_TransformPoint3D 3D point transform: mat*vec3 with perspective divide (reads mat[0..15])
0x0045c7fa Matrix_BuildFullTransform Build full transform matrix with scale/translation offsets and optional negation
0x00479630 SceneObject_DeletingDtor SceneObject deleting destructor: release 3 render callbacks then dtor
0x004796b0 SceneObject_FadeAlpha SceneObject fade alpha state machine: states 0-3 cycle through fade timers and callbacks
0x00479820 SceneObject_ScalarDtor SceneObject scalar destructor (free if bit 0 set)
0x00479a30 GetOSVersionString Build OS version string: "Windows NT/9x X.X - Build YYYY"
0x00479dc0 BugTracker_SetUserAgent Set bug tracker user agent string
0x00479e20 BugTracker_Dtor BugTracker destructor: restore exception filter, delete GDI objects, call string dtor

Session 28 (cont) — 16 renames — Vec3_Normalize, WebClient, Matrix, COM, DDSURFACEDESC — 70.5% documented

Address Name Description
0x0048c24c Vec3_Normalize Normalize 3D vector in-place (with fast inverse sqrt, handles zero)
0x0048c316 Matrix_Multiply4x4_InPlace 4x4 matrix multiply allowing in-place (out = a*b, temp buf if overlap)
0x0048c3f9 Matrix_Multiply4x4 4x4 matrix multiply (out = a*b with temp buf for aliasing)
0x0048c487 Matrix_InitVTable Initialize matrix math vtable (7 entries: identity, transpose, inverse, mul, mul2, adjoint, normalize)
0x0048c560 WebClient_WndProc WebClient window proc: handles WM_TIMER and WM_ASYNC message for DNS/connect/send
0x0048c7d0 WebClient_Ctor Construct WebClient object with vtable, 3 AthenaStrings, Winsock init, WebWindow class registration
0x0048c940 WebClient_Dtor Destroy WebClient: release window, deregister Winsock, destroy 3 strings
0x0048c9c0 WebClient_FetchURL Parse URL, build HTTP GET request, create window, DNS resolve, connect and send
0x0048cc70 WebClient_DeletingDtor WebClient deleting dtor (dtor + free if bit 0 set)
0x0048cc90 WebClient_RecvBuf_Dtor Release WebClient receive buffer (free allocated ptr)
0x0048cd0c WebClient_RecvBuf_DeletingDtor Receive buffer deleting dtor
0x0048cd28 COM_QueryInterface COM IUnknown QueryInterface: validate IID, set out pointer, call AddRef
0x0048cd6f COM_IUnknown_Init Initialize COM IUnknown struct (vtable, refcount=1, ptrs=0)
0x0048ce17 WebClient_InitResponse Zero-initialize response fields (7 dwords cleared)
0x0048ce89 TextureCache_DeletingDtor TextureCache deleting dtor: Tree_FreeRecursive then free
0x0048cea5 DDSURFACEDESC_ParsePixelFormat Parse DDSURFACEDESC pixel format (D3DFMT enum), allocate palette, decompress RLE/compressed surfaces

Session 29 — 6 renames — 70.7% documented

Address Name Description
0x0048e59c DDTexture_CreateMipChain DirectDraw texture mipmap chain creation (checks DDSD sig, parses pixel fmt, allocates surface levels, handles DXT compression)
0x00490400 DDSurface_CopyRects DirectDraw surface rectangle copy (validates size match, optionally copies palette, pixel copy via scanline loop)
0x00490538 DDSurface_Blt3PointFilter DirectDraw surface BLT with 3-tap bilinear filter (for 1/2 downscaling)
0x00490a2f DDSurface_Blt4PointFilter DirectDraw surface BLT with 4-tap filter dispatch (dispatches to sub-filters by texture format)
0x00490e52 DDSurface_Blt5PointFilter DirectDraw surface BLT with 5-tap filter (for 1/4 downscaling, bilinear + detail preservation)
0x004917ca DDSurface_Blt3PointWBuffer DirectDraw surface BLT with 3-tap filter + Z buffer (scans Z range, computes span endpoints, bilinear filter)

Session 30 — Matrix, Vec2, IMDCT, PNG, Font renames (71.6%→71.9%)

Address Name Description
0x0049dd82 Matrix_BuildFromQuatScaleTranslate Builds 4x4 matrix from quaternion + scale + translation (7-param SSE2)
0x0049ef1a Matrix_BuildFromTanAngles Builds rotation matrix from tangent of X/Y angles
0x0049f5f3 Matrix_BuildFromNormTanAngles Normalizes angles then builds rotation matrix from tan
0x004a0860 Matrix_BuildFromQuaternion3x3 Builds 3x3 rotation matrix from quaternion with optional determinant
0x004a1362 Vec2_NormalizeSSE2 Normalizes 2D vector using SSE2 reciprocal square root
0x004a15f0 Vec2_TransformMatrixPerspDivide Transforms vec2 by 2x4 matrix with perspective divide
0x004a304d Script_ParseComponentList Bytecode parser: extracts component list with nibble-encoded data
0x004a503c Object_SetDirtyFlag Sets bit 0 in flags word at offset +0x60
0x004a8026 IMDCT_TransformBlock IMDCT 16-point transform for audio decoder
0x004a91d0 Font_RenderChannels Renders font glyph channels via Huffman decode and callback
0x004aba27 Mem_Copy128Blocks Copies data in 128-byte blocks (param3 << 7 >> 2 dwords)
0x004ae18e PNG_ParsePLTE PNG PLTE (palette) chunk parser with IHDR check and CRC
0x004b0220 IMDCT_ForwardTransform Forward IMDCT/DCT transform with fixed-point cosine tables

Session 34 — CPUID, D3DXSkinMesh, MeshStrip, StreamReader, CRT, AthenaList, D3DX texture (83.3%→84.8%)

Address Name Description
0x0045dd60 CPUID_CheckMMX Checks CPUID feature bit 23 (MMX support)
0x0045dd6f CPUID_IsMMXAvailable Returns MMX availability flag from cached CPU features
0x0045dd7e CPUID_Check3DNow Checks CPUID extended features for 3DNow support
0x0045dda0 CPUID_DetectFeatures Runs CPUID leaf 1 and stores feature flags
0x0045ddb0 CPUID_GetProcessorFeatures Returns cached processor feature flags
0x0045ddc0 CPUID_CheckProcessorFeature Tests specific bit in processor feature flags
0x0045e010 CopyVec2 Copies 2-component float vector
0x0045e190 D3DXSkinMesh_Ctor Initializes D3DX skin mesh object with vtable and defaults
0x0045e340 D3DXSkinMesh_InitTimerDefaults Initializes timer-related defaults for skin mesh
0x0045e6a0 D3DXSkinMesh_CreateStrip Creates strip from skin mesh data
0x0045e770 D3DXSkinMesh_CopyStripData Copies strip data between buffers
0x0045ea30 D3DXSkinMesh_RebuildList Rebuilds internal list structure for skin mesh
0x0045eb40 D3DXSkinMesh_DestroyAllObjects Destroys all managed objects in skin mesh
0x0045ec80 D3DXSkinMesh_GenerateStrips Generates triangle strips from mesh data
0x0045ed90 D3DXSkinMesh_DeletingDtor Deleting destructor for D3DX skin mesh
0x0045ee70 MeshStrip_ComputeWinding Computes winding order for mesh strip
0x0045ef00 MeshStrip_GenerateNextStrip Generates next strip in sequence
0x0045f0a0 StreamReader_InitVtbl Initializes stream reader vtable
0x0045f2b0 StreamReader_ReadLine Reads a line from stream
0x0045f360 StreamReader_OpenFile Opens file for stream reading
0x0045f490 StreamReader_DeletingDtor_Close Deleting destructor that also closes file handle
0x0045f5e0 Mesh_ConnectFaceEdges Connects face edges in mesh topology
0x0045f680 Mesh_AddFace Adds face to mesh
0x0045f8e0 Mesh_SwapFaceEntries Swaps two face entries in mesh
0x0045f990 Mesh_RemapFaceIndices Remaps face indices according to remap table
0x0045fa90 Mesh_ReorderFacesByAdjacency Reorders faces by adjacency for cache coherence
0x0045fb50 D3DX_CreateErrorHandler Creates D3D error handler
0x0045fd60 VertexDecl_ParseDeclarationType Parses vertex declaration type from D3D declaration
0x0045ff50 VertexDecl_CopyToBuffer Copies vertex declaration data to buffer
0x00460020 VertexDecl_WriteBlendWeights Writes blend weight data to vertex buffer
0x0047ee44 SmallIntArray_Init Initializes small integer array
0x0047ee8b SmallIntArray_Find Finds value in small integer array
0x0047eeb2 SmallIntArray_Push Pushes value onto small integer array
0x0047eed6 ShortArray_Init Initializes short array
0x0047ef46 ShortArray_Push Pushes value onto short array
0x0047efd7 ShortArray_GetNextEdge Gets next edge from short array iterator
0x0047ef6c Mesh_GetNextEdge Gets next edge in mesh edge traversal
0x0047efb7 Mesh_GetPrevVertex Gets previous vertex in mesh vertex list
0x0047f091 Mesh_LinkVertexToHead Links vertex to head of vertex list
0x0047f0d0 Mesh_DecrementVertex Decrements vertex reference count
0x0047f0fa Mesh_UnlinkShortVertex Unlinks short vertex from list
0x0047f1ca Mesh_DecrementShortVertex Decrements short vertex reference count
0x0047f1fb D3D_InitDisplayModes Initializes available D3D display modes
0x0047f5a4 VertexDecl_ParseFVF Parses FVF (Flexible Vertex Format) flags
0x00481366 MeshIter_InitEdge Initializes mesh edge iterator
0x004813c4 MeshIter_InitShortEdge Initializes mesh short edge iterator
0x004819ec MeshData_AttributeSort Sorts mesh data by attribute group
0x00481dbd MeshData_SplitVerticesByAttribute Splits vertices sharing attributes into separate copies
0x00482611 MeshData_SplitShortVertsByAttr Splits short vertices by attribute group
0x00482881 MeshData_OptimizeVertexOrder Optimizes vertex order for cache performance
0x004829c6 MeshData_OptimizeFaceOrder Optimizes face order for vertex cache coherence
0x00482ab0 MeshData_FreeAdjBuffer Frees adjacency buffer
0x00482aba MeshData_FindOrAddAttr Finds or adds attribute entry
0x00482adf MeshData_FindOrAddShortAttr Finds or adds short attribute entry
0x00482b04 MeshData_InitVertexAdj Initializes vertex adjacency data
0x00482bf0 MeshData_RemoveFace Removes face from mesh data
0x00482c55 MeshData_FindBestNextEdge Finds best next edge for strip generation
0x00482d2d MeshData_InitShortVertexAdj Initializes short vertex adjacency data
0x00482e29 MeshData_RemoveShortFace Removes short face from mesh data
0x00482ea4 MeshData_FindBestNextShortEdge Finds best next short edge for strip generation
0x00486e92 D3DXMesh_DrawSubset Draws a mesh subset by attribute group
0x00486f10 CRT_InitLocaleVtable Initializes C runtime locale vtable
0x00486f80 CRT_FormatInteger Formats integer for printf-style output
0x00487070 CRT_FormatFloatOutput Formats floating-point output with precision
0x00487410 CRT_NormalizeFP80 Normalizes 80-bit extended precision float
0x00487520 CRT_ClassifyFP Classifies floating-point value (NaN/Inf/Zero/Normal)
0x004875c0 CRT_FormatFloat Formats float/double per printf format spec
0x00487870 CRT_FormatSpecifier Parses and applies printf format specifier
0x00487b30 CRT_ParseFormatString Parses printf format string
0x00487e5e CRT_InitCriticalSection Initializes critical section for thread safety
0x00487e70 D3DXMesh_OptimizeInPlace Optimizes mesh in place with flags
0x00489280 AthenaList_FreeAllChunks Frees all chunks in Athena list buffer
0x00489540 AthenaList_SplitChunk Splits a chunk in Athena list buffer
0x004897a0 AthenaList_WriteDword Writes DWORD to Athena list buffer
0x004897f0 AthenaList_ReadByte Reads byte from Athena list buffer
0x00489830 AthenaList_ReadDword Reads DWORD from Athena list buffer
0x00489b70 CRT_FltControl87 Wraps _control87 to set x87 FPU control word
0x00490054 D3DX_MipMap_16bit_555 Generates mipmap for 16-bit 555 format surface
0x00490175 D3DX_MipMap_16bit_565 Generates mipmap for 16-bit 565 format surface
0x004902c1 D3DX_MipMap_8bit Generates mipmap for 8-bit palettized surface
0x00490629 D3DX_CopyRects_Point Copies rects between surfaces with point (nearest) sampling
0x0049082b D3DX_CopyRects_Stretch Copies rects between surfaces with stretch blt
0x004913e4 D3DX_TransformTex_Bilinear Bilinear texture transform for D3DX

Session 35 — D3DXMesh pipeline, D3DTexture, MeshData, Sort (84.8%→85.4%)

Address Name Description
0x00482f9c D3DResource_ReleaseA COM Release - decrements refcount, calls D3DDevice_ReleaseResourcesA when hits 0
0x00482fd4 D3DResource_ReleaseB COM Release variant B - same pattern, different resource cleanup
0x0048316c D3DXMesh_GenerateAdjacency Generates face adjacency for 32-bit index mesh (locks IB, iterates with MeshIter_InitEdge)
0x004833df D3DXMesh_WeldVertices Welds vertices by position hash + distance check (32-bit indices, heapsort)
0x00483856 Sort_IndexHeapByFloat Heapsort of index array keyed by float values
0x00483981 Mesh_FindWeldVertex Hash-lookup for vertex welding (xyz hash, adjacency chain check, 32-bit indices)
0x00483ba4 D3DTexture_CreateFromDesc Creates D3D texture from description (pool/usage/size, handles Init/Locked paths)
0x00483d6d D3DTexture_CopyLockedData Copies mesh data into locked texture (16-bit indices, 32→16-bit conversion)
0x00483f2a D3DTexture_CopyIndexData Copies mesh index data into texture (32-bit to 16-bit index conversion)
0x004840f9 D3DXMesh_GenerateAdjacency16 16-bit index version of GenerateAdjacency (MeshIter_InitShortEdge)
0x0048436d D3DXMesh_WeldVertices16 16-bit index version of WeldVertices (uses Sort_IndexHeapByFloat)
0x004847eb Mesh_FindWeldVertex16 16-bit vertex weld hash lookup (ushort adjacency chain checking)
0x004848b7 D3DTexture_CopyLockedData16 16-bit locked data copy (32→16-bit index truncation)
0x00484a87 D3DTexture_CopyIndexData16 16-bit index data copy (D3DTexture_ResizeAndValidate path)
0x00484c3a D3DTexture_CopySurfaceData Copies surface data between D3D textures (vertex format conversion)
0x00484d0f D3DXMesh_Stripify Converts mesh to triangle strips (32-bit indices, uses MeshData_RemoveFace)
0x00484de5 D3DXMesh_StripifyOptimized Optimized stripification with vcache-awareness (32-bit, SmallIntArray)
0x004850e2 D3DXMesh_Stripify16 16-bit strip conversion using short vertex adjacency
0x004851d8 D3DXMesh_StripifyOptimized16 16-bit optimized stripification with ShortArray vcache
0x00485525 D3DTexture_CreateSimple Simplified texture creation from dimensions/usage/pool flags
0x00485610 D3DTexture_CloneFromDesc16 Clones texture via 16-bit copy paths (CopyIndexData16/LockedData16/SurfaceData)
0x004857d9 D3DXMesh_ConvertAdjacencyToStrip Builds adjacency and converts to strips (32-bit, MeshData_Init)
0x00485ad9 D3DXMesh_ConvertAdjacencyToStrip16 16-bit adjacency-to-strip conversion (MeshSubMesh_Init)
0x00485dd7 D3DXMesh_OptimizeFull Full mesh optimization pipeline (adjacency, attr sort, vcache, strip, reorder)

CRT Exception Handling (Session 46)

Address Name Description
0x004ba59f Exception_ScalarDeletingDtor Exception scalar deleting destructor
0x004ba5bb Exception_VbaseDtor Exception vbase destructor
0x004ba5c6 CRT_ThrowInvalidStringPosition Throws "invalid string position"
0x004ba65e BadAlloc_VbaseDtor bad_alloc vbase destructor
0x004ba669 BadAlloc_ScalarDeletingDtor bad_alloc scalar deleting destructor
0x004ba69d CRT_ThrowBadAlloc Throws "bad allocation"
0x004ba6fe CRT_InitFPFuncTable Initializes CRT floating-point function pointers
0x004ba736 CRT_InitFPState Initializes FP state (FDIV check)
0x004ba8ac EH_FrameHandler C++ exception frame handler
0x004ba997 CRT_PurecallPush Push handler onto _purecall list
0x004ba9bf CRT_PurecallCheck Check if handler in _purecall list
0x004baa2c EH_CallSettingFrame Call __CallSettingFrame wrapper
0x004baa7d EH_NotifyCB C++ EH notification callback
0x004bac02 CRT_SaveSEHContext Save SEH context (EAX/EBP/ret)
0x004793d8 DllEntryPoint DLL entry point

CRT Standard Library (Session 46)

Address Name Description
0x004baf41 CRT_ArrayUnwind1 Array unwind helper (4 args)
0x004bafa3 CRT_ArrayUnwind2 Array unwind helper (alt params)
0x004bb411 CRT_LeaveCritSec8 LeaveCriticalSection(8)
0x004bb429 CRT_mkdir CRT mkdir wrapper
0x004bb455 CRT_remove CRT remove/delete file wrapper
0x004bb6ed CRT_LeaveCritSec4a LeaveCriticalSection(4) variant
0x004bb77d CRT_LeaveCritSec4b LeaveCriticalSection(4) variant
0x004bb92c CRT_LeaveCritSec4c LeaveCriticalSection(4) variant
0x004bb9a1 CRT_Lock8 __lock(8) wrapper
0x004bb9aa CRT_LeaveCritSec8b LeaveCriticalSection(8) variant
0x004bb9b3 CRT_Initterm Run init/term function arrays
0x004bba18 CRT_ExitProcess CRT ExitProcess with cleanup
0x004bbb0d CRT_ExitProcessNoCleanup ExitProcess without full cleanup
0x004bbbc0 CRT_UnlockFile __unlock_file wrapper
0x004bbc1a CRT_UnlockFileEbp __unlock_file via EBP
0x004bbc24 CRT_fsopen __fsopen wrapper
0x004bbc40 FPU_AsinWrapper FPU asin() wrapper
0x004bbc5d FPU_AsinHelper FPU asin error handling
0x004bbd0b FPU_IsFinite Check if double is finite
0x004bbd20 FPU_ClassifyDouble Classify double (FP_CLASS)
0x004bbe60 FPU_RoundDouble CRT round double with error handling
0x004bbf81 StdException_Ctor std::exception constructor
0x004bc058 TypeInfo_Dtor type_info destructor
0x004bc081 TypeInfo_ScalarDeletingDtor type_info scalar deleting destructor
0x004bc339 CRT_UnlockFileEbp14 __unlock_file via EBP+0x14
0x004bc768 CRT_vsprintf CRT vsprintf implementation
0x004bc9c8 CRT_UnlockFileEbp8 __unlock_file via EBP+8
0x004bcabe CRT_LeaveCritSec4d LeaveCriticalSection(4) variant
0x004bcc68 CRT_UnlockFileEbp8b __unlock_file via EBP+8 variant
0x004bcd9d CRT_UnlockFileEbp14b __unlock_file via EBP+0x14 variant
0x004bce60 CRT_qsort CRT qsort implementation
0x004bd37d CRT_DivCeil Ceiling division helper
0x004bd397 CRT_GetLocalePtr Get locale pointer
0x004bd39d CRT_FreeLocale Free locale struct
0x004bd467 CRT_SetupThreadLocale Setup thread locale
0x004bd55b CRT_LeaveCritSec12 LeaveCriticalSection(12)
0x004bd760 FPU_FdivThunk FPU fdiv thunk (SSE/fallback)
0x004bd7bd FPU_FdivHelper FPU fdiv software emulation
0x004bd982 FPU_RoundCheck FPU round check (no-op if already round)
0x004bdf67 CRT_cvtToScientific CRT double-to-scientific conversion
0x004bdfdb CRT_FormatFractional Format fractional part of float
0x004be077 CRT_cftof Float format (%f)
0x004be0df CRT_cftoe Float format (%e scientific)
0x004be1d0 CRT_SetFPUAffinity Set FPU affinity mask
0x004be36b CRT_ReleaseTranslator Release SEH translator count
0x004be3cb CRT_NLSStrLen NLS string length
0x004be3e8 EH_CallCatchBlock EH catch block caller
0x004be52b EH_UnwindAndRestore Unwind and restore EH state
0x004be772 EH_SearchEnclosingCatch Search for enclosing catch handler
0x004be832 EH_SearchExceptionHandler Full EH exception handler search
0x004beb18 CRT_MTDeleteLocks Free TLS + delete MT locks
0x004beba7 CRT_MTInitTLS Init TLS slot for multithreading
0x004bec6c CRT_StrtodScan strtod scanning/parser
0x004bed07 CRT_GetCharType Get character type info (NLS)
0x004bef25 CRT_WriteFormatted Full printf-style formatted output
0x004bf708 CRT_CheckThrowInfo Validate throw info, call SEH handler
0x004bf761 CRT_SetUnhandledExceptionFilter SetUnhandledExceptionFilter wrapper
0x004bf7db CRT_LeaveCritSec4e LeaveCriticalSection(4) variant
0x004bf7e4 CRT_RuntimeError Runtime error message box
0x004bfaf8 CRT_ScanMBCSWhitespace Skip whitespace in MBCS string
0x004bfc28 CRT_ParseCommandLine Parse command line into argv
0x004bfd94 CRT_GetMainArgs __getmainargs implementation
0x004bff58 CRT_IOInit CRT I/O initialization (stdin/stdout/stderr)
0x004c0fc9 CRT_UnlockFileHandle __unlock_fhandle wrapper

Session 2690 — 10 New Function Decompilations

Address Name Description
0x00458220 AABB_TriangleIntersect Test if triangle edge (2 vertices + FPU 3rd) overlaps AABB. Called 6× by AABB_TriangleTest6Edges for SAT collision test. Returns 1 if intersection, 0 otherwise.
0x00465EF0 Collision_TraverseSpatialTree Recursive octree traversal for broad-phase collision. Walks spatial tree nodes, at leaf level tests each face vertex against query AABB via AABB_ContainsPoint. Matches appended to output AthenaList. Called from Stands_BuildCollision + self-recursive.
0x00498200 BitStream_ReadBits Bit-level reader for Ogg Vorbis audio streams. Reads N bits (1-32) from byte buffer at arbitrary bit alignment, handles multi-byte crossing. 20+ xrefs from Vorbis header parsing (ID/Comment/Setup). Part of statically-linked Vorbis decoder.
0x0049675E D3DXMesh_ComputeLightingFromNormals D3DX8 software mesh lighting: computes per-vertex 565-format colors from vertex normals × fixed light direction matrix. Clamps to [0,255], packs into 16-bit color stream. Called from D3DXMesh_ComputeLighting565 and ComputeLightingThenAssemble.
0x00425F90 App_CompleteRace Finalizes a completed race: increments counter (+0x7C8), calls graphics vtable+0xFC twice to re-enable ZENABLE and ZWRITEENABLE render states, clears pending flag (+0x704=0). Called from 7 render contexts.
0x00420DA0 Board_Master_Update Custom Update for Master Race (L14) ONLY — all other levels use default Scene_Update (0x419C00). Handles: Scene_Update + vtable calls, random sparkle particle spawn (1/11, NOT a nag screen — Ghidra mislabeled the particle ctor as "RegisterDialog_Render"), ball-vs-mechanical-object collision (distance check → velocity normalize + 3D sound + 3 trail particles), falling ball gravity + respawn. Confirmed unique via binary search: 0x420DA0 appears exactly once in the binary (vtable 0x4D12B4).
0x00435B00 CollisionLevel_PlayBreakSound Plays 3D positional "break" sound at collision level position. Chains through parent→sound_mgr→sample. Sets 1.0f cooldown at +0x10E4. Called when breakable objects shatter.
0x004B3878 Audio_DecodeOutputBuffer MO3 audio codec: decodes compressed frame to PCM output buffer. Manages stream state (source ptr, frame size, consumed count), calls codec vtable for decode, output converter for format transform, post-processor for remaining samples. Accumulates decoded count.
0x0046C290 App_OnMouseDown Window message handler for mouse clicks. Calls SetCapture, sets button flags (L/R/M at +0x1C8/9/A), stores mouse X/Y, delegates to mouse interceptor if set (+0x1B0) or hit-tests UIWidget tree and dispatches click to widget vtable+0x14.
0x004912E4 ComputeScanlineZBuffer D3DX8 software rasterizer: pre-computes per-pixel Z-buffer for bilinear texture filtering. Allocates width×16 bytes, computes V coordinate + fractional weight + adjacent texel V for each scanline pixel. Clamp/wrap mode based on param_1. Used by D3DX_TransformTex_Bilinear and DDSurface_Blt3PointWBuffer.

🔗 Related Documents

Function Name Verification Rep

types : analysis
keywords :

📂 View source on GitHub


Function Name Verification Report

Total functions decompiled: 3,978
Functions analyzed for name accuracy: All 3,978
Confirmed misnomers: 7

Summary

All 3,305 previously-undecompiled functions have been decompiled from GhidraMCP and saved to analysis/ghidra/decompilations/batch_auto/. Each function was analyzed for name/behavior mismatches using the following methods:

  • String literal analysis (what does the function actually reference?)
  • Call graph analysis (what functions does it call?)
  • Cross-reference verification (who calls it and how?)
  • Constructor call counting (does 'Create' actually create multiple types?)
  • Rendering call detection (is a 'Tick' actually doing rendering?)

Confirmed Misnomers

TimerDisplay (0x004298c0)

  • Severity: CRITICAL
  • Suggested name: LoadingScreenGadget_Factory
  • Description: Named 'TimerDisplay' but is actually the LoadingScreenGadget factory function. Allocates 0x3628 bytes for a LoadingScreenGadget object and loads ALL game resources: 5 fonts (showcardgothic28/72/14/16, arialnarrow12bold), 40+ textures (hammy1-3.png, blueblot.png, goal.png, locktile.png, arrow1.png, etc.), 14 meshes, 7 levels, 55 sounds. The only timer-related string is 'timerblot.png'. This is the main game asset loading function, not a timer display.
  • Evidence: operator_new(0x3628) + LoadingScreenGadget_Ctor + 199 string literals for fonts/textures/meshes

RegisterDialog_Render (0x00447920)

  • Severity: MODERATE
  • Suggested name: PurchaseScreen_Render
  • Description: Named 'RegisterDialog_Render' suggesting a generic dialog, but this is specifically the game PURCHASE/REGISTRATION screen. Shows 'REGISTER HAMSTERBALL!', 'CLICK HERE TO BUY!', customer name/serial number input fields, and 'UNLOCK!' button. The name is technically correct (it renders a registration dialog) but 'PurchaseScreen_Render' would be more descriptive.
  • Evidence: Strings: 'REGISTER HAMSTERBALL!', 'CLICK HERE TO BUY!', 'CUSTOMER NAME:', 'SERIAL NUMBER:', 'UNLOCK!'

DispatchCollisionEvents (0x0040c5d0)

  • Severity: CRITICAL
  • Suggested name: LevelObjectCollisionHandler
  • Description: Named 'DispatchCollisionEvents' after just ONE of ~15 event types it handles. This is actually the GENERAL level object collision/event handler called from 28+ sites. Handles: N:SECRET, N:UNLOCKSECRET, E:NODIZZY, E:SAFESWITCH, E:LIMIT, E:BREAK, E:JUMP, E:ACTION, E:TRAJECTORY, N:NOCONTROL, N:WATER, N:TARPIT, DROPIN, PIPEBONK, POPOUT, N:GOAL, N:MOUSETRAP. Does NOT create anything - it's a collision response handler.
  • Evidence: 28 xrefs (all UNCONDITIONAL_CALL), 0 ctor calls, 18 __stricmp calls dispatching events

RaceGoalReached_Tick (0x0044df70)

  • Severity: MODERATE
  • Suggested name: RaceResultsScreen_Render
  • Description: Named 'Tick' but is primarily a RENDER function. Displays the race results screen with 'BEST RACE TIME:', 'WEASEL'S TIME:', 'BROKEN BALLS:', 'DIZZIED BALLS:', 'BRONZE TIME:', 'SILVER TIME:' and 'Click the mouse to continue!'. Has 56 Matrix_Scale4x4 calls and 14 UI_DrawTextCentered calls. Should be named RaceResultsScreen_Render.
  • Evidence: 56 Matrix_Scale4x4 + 14 UI_DrawTextCentered + race result strings

CreateExpertLevelObjects (0x0040e250)

  • Severity: MODERATE
  • Suggested name: CreateMechanicalObjects2
  • Description: Named 'CreateExpertLevelObjects' but creates 6 different object types: BONK, SLOW/SUPER, UP, SAWBLADE, BRIDGE, and calls Bonk_ctor, TowerLevel_Ctor, Sawblade_Level_Ctor, Spinner_Level_ctor, Gear_Level_ctor, Tipper_Level_Ctor. Should be named after the category of objects it creates, not just one.
  • Evidence: 6 distinct _ctor calls, handles BONK/SLOW/SUPER/UP/SAWBLADE/BRIDGE/NEG/JUDGE/BELL strings

CreateUpLevelObjects (0x004117b0)

  • Severity: MODERATE
  • Suggested name: CreateRotatorsAndPendulums
  • Description: Named 'CreateUpLevelObjects' but creates Rotator and Pendulum objects. Handles LIFTER, SPEEDCYLINDER, TIMEBUTTON strings but the actual objects created are Rotator_ctor_sound, Rotator_ctor_nosound, and Pendulum_ctor.
  • Evidence: 3 distinct _ctor calls: Rotator_ctor_sound, Rotator_ctor_nosound, Pendulum_ctor

HandleArenaCollisionEvents (0x00412850)

  • Severity: MODERATE
  • Suggested name: CreateArenaObjects
  • Description: Named 'HandleArenaCollisionEvents' but handles N:SPINNER, N:BUMPER, E:LAUNCH, LAUNCHPOINT, EXPLODEHELPER, E:CALLHAMMER, E:HAMMERCHASE, E:CATAPULTBOTTOM. Only creates ArenaScoreParticle_ctor. This is an arena-specific object factory, not just a spinner creator.
  • Evidence: Handles 8 different event/object types, only 1 ctor call (ArenaScoreParticle_ctor)

Notes

  • Many 'Render' functions were flagged as suspicious for 'no draw calls', but this is a FALSE POSITIVE: D3D8 rendering uses vtable dispatches ((**code**)(...) patterns) which don't show up as named function calls. These names are CORRECT.
  • CRT library functions (scanf, printf, pow, RaiseException) and codec functions (IDCT, IMDCT, Inflate, Vorbis) are correctly named — they're VS2003 CRT and bundled codec libraries.
  • D3DX functions (CreateTextureFromFile, CreateMeshFromFormat, OptimizeMesh, WeldVertices, SkinMesh) are correctly named — they're the D3DX utility library bundled with the game.
  • The Create* function naming pattern is systematically misleading: each 'Create' function is actually a factory for MULTIPLE object types, named after just one. This appears to be how the original developers named them (after the first/primary object type in the switch statement).

🔗 Related Documents

Game Loop & Window Management

types : decompilation
keywords :

📂 View source on GitHub


Game Loop & Window Management

Overview

The game loop (App_Run at 0x46BD80) implements a fixed-timestep update with
variable rendering. The WinMain (0x4278E0) sets up the window and enters
the main loop.

App_Run — Game Loop (0x46BD80)

void App_Run(App *app) {
    Timer_Init(timer);
    fps_target = app->fps_divisor;  // +0x170 — frames per second divisor
    frame_time = 0;
    last_tick = 0;
    last_render_tick = 0;
    app->last_tick = GetTickCount();
    
    while (!app->quit_flag) {  // +0x159 = quit flag
        frame_count = 0;
        Sleep(0);  // Yield to other processes
        app->phase_name = "Background";  // +0x210
        
        // Calculate milliseconds per frame
        app->ms_per_frame = 1000 / app->fps_divisor;  // +0x168
        
        // FPS counter update (every 1 second)
        now = GetTickCount();
        if (fps_counter_target < now) {
            if (app->show_fps) {  // +0x1AC = show FPS flag
                AthenaString_SprintfToBuffer(app->fps_string, format);
            }
            app->frame_counter = 0;  // +0x194
            fps_counter_target = now + 1000;
        }
        
        // Process Windows messages
        while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
            if (app->quit_flag) goto cleanup;
            TranslateMessage(&msg);
            DispatchMessageA(&msg);
        }
        
        if (app->quit_flag) break;
        
        // Fixed-timestep update
        do {
            now = GetTickCount();
            
            // If we're ahead of schedule OR too many catch-up frames
            if ((now - app->last_tick) < (app->ms_per_frame - 5) || 
                catchup_frames > 9) {
                app->phase_name = "Render";
                catchup_frames = 0;
                
                // Only render if enough time has passed
                if ((1000 / fps_target) - 5 + last_render_tick < now) {
                    if (app->gfx != NULL && 
                        (!app->minimized || !app->active)) {
                        app->frame_counter++;
                        Graphics_BeginFrame(app->gfx, timer);
                        app->vtable[0x24]();  // Pre-render
                        app->vtable[0x28]();  // Render
                        app->vtable[0x2C]();  // Post-render
                        Graphics_PresentOrEnd(app->gfx, TRUE);
                    }
                    last_render_tick = GetTickCount();
                }
                break;
            }
            
            // Logic update (fixed timestep)
            app->update_count++;  // +0x18C
            app->phase_name = "Update";
            Graphics_BeginFrame(app->gfx, timer);
            app->vtable[0x20]();  // Update (game logic)
            
            // Advance tick counter
            prev_tick = app->last_tick;
            app->last_tick = prev_tick + app->ms_per_frame;
            
            // Prevent tick drift (clamp to 1 second behind)
            if (1000 < (GetTickCount() - (prev_tick + app->ms_per_frame))) {
                app->last_tick = GetTickCount() - 1000;
            }
            
            frame_count++;
        } while (frame_count < 1);  // Max 1 update per frame
    }
    
cleanup:
    Timer_Cleanup(timer);
}

App Struct — Game Loop Offsets

Offset Type Description
+0x159 byte Quit flag (non-zero = exit loop)
+0x15A byte Minimized flag
+0x16C byte Show FPS flag
+0x18C int Update count (total updates)
+0x194 int Frame counter (FPS display)
+0x164 int ms_per_frame (1000 / fps_divisor)
+0x168 int FPS divisor (target FPS)
+0x170 int fps_divisor (backup)
+0x1A0 char* FPS display string (+0x198?)
+0x1AC byte show_fps (1 = display FPS counter)
+0x210 char* Phase name ("Background"/"Update"/"Render")
+0x174 GfxEngine* Graphics engine pointer

Vtable Dispatch Calls

The game uses vtable calls for the 4 main update phases:

Vtable Offset Call Description
vtable[0x20] Update Game logic (physics, input, AI)
vtable[0x24] Pre-render Pre-render setup (camera, visibility)
vtable[0x28] Render Draw all scene objects
vtable[0x2C] Post-render UI overlays, HUD, menus

WinMain (0x4278E0)

Entry point:

  1. Register window class (Athena class)
  2. Create window (1024x768 default, configurable)
  3. Initialize D3D8 device via Graphics_CreateDevice
  4. Call App_Initialize_Full (all subsystems)
  5. Enter App_Run game loop
  6. On exit: LoadOrSaveConfig (saves settings/shutdown)

Window Management

Window Procedure

Standard Windows message handler:

  • WM_PAINT: Render invalidation
  • WM_SIZE: Handle resize/minimize
  • WM_ACTIVATE: Pause/unpause on focus change
  • WM_CLOSE: Set quit flag
  • WM_KEYDOWN/WM_KEYUP: Buffered input

Minimize Handling

When minimized (app->minimized ≠ 0):

  • Game logic still updates (vtable[0x20])
  • Rendering is skipped (only render when not minimized)
  • Sleep(0) yields CPU during background

Timer System

Timer_Init / Timer_Cleanup

High-resolution timer using QueryPerformanceCounter:

  • Timer_Init: Initialize performance counter
  • Timer_Cleanup: Release timer resources

Frame Timing

  • Target: 1000 / fps_divisor ms per frame (typically ~33ms = 30fps)
  • Max catch-up frames: 9 (prevents spiral of death)
  • Tick drift clamp: max 1 second behind real time
  • Render throttled: only when 1000/fps - 5ms have elapsed

Graphics_BeginFrame (0x453B50)

Called before both update and render:

  1. Clear back buffer
  2. Begin D3D scene
  3. Set default render states
  4. Update timer

Graphics_PresentOrEnd (0x455A90)

Called after render:

  1. End D3D scene
  2. Present back buffer (swap chain)
  3. Reset render state tracker

Frame Budget

At 30 FPS:

  • Update budget: ~33ms per logic tick
  • Render budget: ~28ms (33 - 5ms margin)
  • Catch-up limit: 9 frames before skipping
  • Background: Sleep(0) between frames

Key App Offsets (Game State)

Offset Type Description
+0x178 Scene* Current scene
+0x184 Scene* Loading screen scene
+0x1DC SoundChannel* Input event sound
+0x1E4 InputDev* Player 1 device
+0x1E8 InputDev* Player 2 device
+0x708 int Game state (3 = racing)
+0x174 GfxEngine* Graphics engine
+0x850 bool Mirror mode flag
+0x86C uint8[0x50] Best time data
+0x8BC uint8[0x50] Medal data

🔗 Related Documents

Game Object Factory System

types : objects
keywords :

📂 View source on GitHub


Hamsterball Game Object Factory System

Overview

The factory system creates all game objects from MESHWORLD level data. Each
level file contains named objects (N: and E: prefixed). The factories match
object name strings and instantiate the appropriate C++ object.

Factory Dispatch Chain

Level data → Named objects parsed from MESHWORLD
  ├─ CreateLevelObjects (0x4121D0) — main dispatcher
  │    ├─ BRIDGE, TIPPER, BONK, BBRIDGE1/2, POPCYLINDER
  │    ├─ BLOCKDAWG1/2, CATAPULT, GLUEBIE
  │    └─ Falls through to CreatePlatformOrStands
  ├─ CreatePlatformOrStands (0x4133E0)
  │    ├─ PLATFORM → Platform_ctor (0x371040, 0x10FC bytes)
  │    └─ STANDS → Stands_ctor (0x462850, 0x10D0 bytes)
  ├─ CreateSpinny (0x4143D0) — ROTATOR → Rotator_ctor
  ├─ CreateLifter (0x414A20) — LIFTER → Lifter_ctor + falls through
  ├─ CreateWobbly1 (0x415460) — WOBBLY1-8 → GameLevel_ctor + falls through
  ├─ CreateMechanicalObjects (0x417FE0) — LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
  ├─ CreateBumper (0x40FA20) — BUMPER1-4 → MeshWorld + 8x Scene_CollectByNameFilter
  ├─ CreateExpertLevelObjects (0x40E250) — Multi-factory for arenas
  │    ├─ BONK → Bonk_ctor (0x1200 bytes)
  │    ├─ "UP?" → TowerLevel_Ctor (0x1188 bytes)
  │    ├─ SAWBLADE1/2 → Sawblade_Level_Ctor (0x111C bytes)
  │    ├─ SPINNER/BRIDGE → Spinner_Level_ctor (0x10FC bytes)
  │    ├─ JUDGE → Gear_Level_ctor (0x1100 bytes)
  │    └─ BELL → Tipper_Level_Ctor (0x10E8 bytes)
  ├─ CreateBadBall (0x40BCA0) — BADBALL → Ball_ctor
  ├─ CreateSecretObjects (0x40BAA0) — SECRET / SECRETUNLOCK
  ├─ CreateMouseTrap (0x40BF50) — MOUSETRAP → MouseTrap_ctor
  ├─ NeonCollisionEvents (0x410D00) — E:LIMIT → Level collision
  └─ CreateUpLevelObjects (0x4117B0) — SPEEDCYLINDER

CreateLevelObjects (0x4121D0) — Detailed

Main factory, called per named object in the level mesh. Uses __strnicmp for
prefix matching. All created objects are appended to Scene+0x2578.

Object Types

Object Name Match Length Object Size Constructor Scene Storage
BRIDGE 6 Configures mesh +0x436C (mesh), +0x4370 (collision)
TIPPER 6 0x1104 Tipper_ctor +0x2578
BONK 4 0x1200 Bonk_ctor +0x2578, +0x540C
BBRIDGE1 8 0x1100 BreakBridge_ctor +0x2578, +0x5418
BBRIDGE2 8 0x1100 BreakBridge_ctor +0x2578, +0x541C
POPCYLINDER 11 0x10E8 PopCylinder_ctor +0x2578, +0x5428
BLOCKDAWG1 10 0x1154 Blockdawg_ctor +0x2578
BLOCKDAWG2 10 0x1154 Blockdawg_ctor (flag+0x1152=1) +0x2578
CATAPULT 8 0x1108 Catapult_ctor +0x2578, +0x584C
GLUEBIE 7 0x110C Gluebie_ctor +0x2578, +0x6080

Gating

  • TIPPER, BONK, BLOCKDAWG1/2, GLUEBIE are gated behind App+0x23C != 0
    (multiplayer/arena mode flag)
  • BRIDGE with "(NOCOLLIDE)" in name skips collision mesh setup

Object Position Source

All objects read position from param_4 (transform matrix):

  • param_4+0x04: X position
  • param_4+0x08: Y position
  • param_4+0x0C: Z position
  • param_4+0x10: X rotation
  • param_4+0x14: Y rotation (also used for scale in some objects)
  • param_4+0x18: Z rotation

CreateExpertLevelObjects (0x40E250) — Arena Multi-Factory

This is actually a combined factory that handles 6 different object types
for arena levels. All created objects get multiplayer gating (App+0x23C).

Object Types

Object Name Match Object Size Constructor Scene Storage
BONK 4 0x1200 Bonk_ctor +0x2578, +0x436C
"UP?" 3 0x1188 TowerLevel_Ctor +0x2578
SAWBLADE 8 0x111C Sawblade_Level_Ctor +0x2578, +0x4370 or +0x4374
SPINNER/BRIDGE 6 0x10FC Spinner_Level_ctor +0x2578, +0x4380 or +0x4798
JUDGE 5 0x1100 Gear_Level_ctor +0x2578, +0x4BBC
BELL 4 0x10E8 Tipper_Level_Ctor +0x2578, +0x4FD4

Special Flag Handling

  • SLOW in name: obj+0x43B = 1 (TowerLevel slow mode)
  • SUPER in name: obj+0x10ED = 1 (TowerLevel super mode)
  • UP in name: Sound_InitChannels(obj, 1) (TowerLevel with sound)
  • SAWBLADE1/2: stored at Scene+0x4370/0x4374, Sawblade_SetBreakSound(n)
  • SPINNER1/2: stored in list at Scene+0x4380/0x4798
  • NEG in SPINNER name: obj+0x43E = -1.0 (0xBF800000, reverse rotation)
  • JUDGE: added to judge list at Scene+0x4BBC
  • BELL: stored as single reference at Scene+0x4FD4

CreateBumper (0x40FA20)

Creates a dedicated bumper level with 8 named bumper objects:

  1. new MeshWorld(0x10D0, graphics, "levels\\level8")
  2. new CollisionLevel(0x10D0, meshWorld) — clone for collision
  3. Level_InitScene(this)
  4. 8 iterations: Scene_CollectByNameFilter("N:BUMPER%d", base+0x10E3)
  5. vtable[0x80]() — post-init callback

Stored: this+0x22B = MeshWorld, this+0x22C = CollisionLevel

Scene Object List Offsets

Offset Type Description
+0x2578 AthenaList All active game objects
+0x436C void* Bonk/Hammer reference
+0x4370 void* Sawblade 1
+0x4374 void* Sawblade 2
+0x4378 AthenaList (level list 1)
+0x4380 AthenaList Spinner list 1
+0x4790 AthenaList (level list 2)
+0x4798 AthenaList Spinner list 2
+0x4BBC AthenaList Judge (Gear) list
+0x4FD4 void* Bell reference
+0x540C void* Bonk reference (TowerLevel)
+0x5410 void* BreakBridge mesh 1
+0x5418 void* BreakBridge object 1
+0x5414 void* BreakBridge mesh 2
+0x541C void* BreakBridge object 2
+0x5420 void* PopCylinder mesh
+0x5428 AthenaList PopCylinder list
+0x5840 void* Blockdawg1 mesh
+0x5844 void* Blockdawg2 mesh
+0x5848 void* Catapult mesh
+0x584C AthenaList Catapult list
+0x607C void* Gluebie mesh
+0x6080 AthenaList Gluebie list

Scene Spawn Flow (0x41C5B0)

Scene_SpawnBallsAndObjects creates player balls and game objects for a race:

  1. Create GameObject (0xC60 bytes) for each ball
  2. Set start positions via "START%d-%d" naming pattern
  3. Scan "SAFESPOT"/"SAFEPOS" objects for safe landing zones
  4. Create BadBall/MouseTrap/SecretObjects/Flags/Signs
  5. Create dynamic objects (spinning platforms, traps)

CreateMechanicalObjects (0x417FE0)

Object Constructor Size Notes
LOOPER Looper_ctor 0x1500 Looping animated platform
GEAR Gear_ctor 0x1514 Spinning gear (with path)
BIGGEAR Gear_ctor 0x1514 Larger gear variant
ROTATOR Rotator_ctor 0x1504 Rotating platform
PENDULUM Pendulum_ctor 0x1504 Swinging pendulum

Board Construction Pattern

Every Board (race level) follows the same construction pattern in BoardLevelX_ctor:

  1. Board_ctor(this, app) — base class, vtable 0x4D0260
  2. Set level-specific vtable (e.g., 0x4D0890 for Dizzy)
  3. Init AthenaLists at +0x4378 and +0x4790
  4. Set title string and race name
  5. Load sub-levels as MeshWorld + CollisionLevel pairs
  6. Allocate sound channel
  7. Set physics: gravity vector, Matrix_Identity
  8. LoadRaceData(this, "RACENAME")
  9. Set display string

🔗 Related Documents

Game State & Race Lifecycle

types : gameplay
keywords :

📂 View source on GitHub


Game State & Race Lifecycle

Overview

Hamsterball's game state is managed through App struct mode flags and a set of
state transition functions. The race lifecycle goes: Menu → Loading → Race → Result → Menu.

App State Machine

App Struct Mode Fields

Offset Type Description
App+0x174 GfxEngine* Graphics engine (set cull mode on start)
App+0x178 Scene* Current scene
App+0x184 Scene* Loading screen scene
App+0x1DC SoundChannel* Sound channel for input events
App+0x1E4 InputDev* Player 1 input device
App+0x1E8 InputDev* Player 2 input device
App+0x21C void* State object (freed on start race)
App+0x220 void* Menu/dialog object (freed on start race)
App+0x224 void* Secondary dialog (freed on start race)
App+0x228 void* Tertiary dialog (freed on start race)
App+0x22C LoadingScreenGadget* Resource loader
App+0x534 MusicPlayer* Primary music channel
App+0x53C MusicPlayer* Secondary music channel
App+0x708 int Game state (3=racing)
App+0x7D2 byte Cull mode flag

Game States

The game uses numeric state values stored at App+0x708:

  • State 3: Active racing
  • Other states managed by vtable dispatch from App_GameLoop (0x46BD80)

Race Start Sequence

App_StartRace (0x4287C0)

1. Scene_UpdateChildren(App+0x178)  // Update all objects
2. Gfx_SetCullMode(App+0x174)       // Set backface culling
3. Set D3D render state vtable[50]   // 0x16 = 2 or 3 depending on flag
4. App+0x708 = 3                    // Set game state to "racing"
5. Free old state objects at:
   - App+0x21C (vtable[0x40]() destructor)
   - App+0x228 (vtable[0x40]() destructor)  
   - App+0x224 (vtable[0x40]() destructor)
   - App+0x220 (direct destructor call with param 1)
6. If music[0] exists: MusicPlayer_SetTempoScale(music[0], 1.0)
7. If music[1] exists: MusicPlayer_SetTempoScale(music[1], 0.5)

App_StartTournamentRace (0x4288B0)

Same as StartRace but with tournament-specific initialization.

App_Start2PRace (0x429230)

Two-player split-screen race initialization.

App_StartPracticeRace (0x428C50)

Practice mode - same track load but no tournament tracking.

App_StartMPRace (0x428B20)

Multiplayer race init - sets up arena mode.

Race End Sequence

App_CompleteRace (0x425F90)

When the ball crosses the finish line:

  1. Calculate elapsed time from timer
  2. Compare against best time (App+0x86C[player_index*4])
  3. If better: update best time and save to registry
  4. Determine medal: Bronze/Silver/Gold based on time thresholds
  5. Create RaceGoalReached object:
    • Display "GOAL REACHED!" text (800ms timer)
    • Show weasel rank sprite ("textures\ranks\weasel.png")
    • Update best time blob at App+0x86C
  6. Progress tournament if applicable

Quit Race

QuitRace (0x42FAD0)

Exits current race back to menu:

  1. Set game state to menu mode
  2. Free scene objects
  3. Restore menu display
  4. Reset music tempo

QuitRaceMenu (0x42E6F0)

In-race quit confirmation menu.

Level Data Loading

LoadRaceData (0x40A120)

Loads MESHWORLD data for a specific level:

  1. Determine level path from race type
  2. MeshWorld_ctor to parse level geometry
  3. Level_InitScene to create objects from MESHWORLD data
  4. Scene_SpawnBallsAndObjects to place ball and level objects
  5. Level_SelectCameraProfile based on level type

Level-SikeType Race Handlers

Each level type has its own HandleRaceEnd callback:

Address Level Handler
0x420240 Beginner Board_Beginner_HandleRaceEnd
0x420660 Up Board_Up_HandleRaceEnd
0x222630 Master Board_Master_HandleRaceEnd

These handle:

  • Time comparison against stored best times
  • Medal awards (Bronze/Silver/Gold thresholds)
  • Tournament advancement
  • Unlock conditions for mirror mode

Tournament Progression

Board_ctor (0x419030)

Initializes board/level with:

  • LoadRaceData for the specific level
  • Set camera profile
  • Create HUD elements (timer, score display)
  • Initialize physics parameters

Tournament_AdvanceRace (0x427080)

Switch statement handling race progression:

case 0: Warm Up     → levels\arena-WarmUp
case 1: Beginner    → levels\arena-Beginner  
case 2: Intermediate→ levels\arena-Intermediate
case 3: Dizzy       → levels\arena-dizzy
case 4: Expert      → levels\arena-Expert
case 5: Neon        → levels\arena-neon
case 6: Sky         → levels\arena-Sky
case 7: Toob        → levels\arena-Toob
case 8: Odd         → levels\arena-Odd
case 9: Glass       → levels\arena-Glass
case 10: Impossible → levels\arena-impossible
case 11: Master     → levels\arena-Master
default: Mirror races (reverse direction)

Start Position Selection

Scene_SpawnBallsAndObjects (0x41C5B0)

For each player:

  1. Look up "START{player}-{player}" in hash table
  2. In tournament/mirror modes: random between START2-1 and START2-2
  3. If "START-DEBUG" exists: use that (cheat position)
  4. Create Ball struct (0xC60 bytes)
  5. Set defaults: radius=26, max_speed=5, gravity=0.5, speed_factor=1.05
  6. Set position from start point
  7. Append to ball list

Timer System

TimerArray (0x475980-0x475AA0)

Manages multiple countdown timers:

  • TimerArray_Ctor: Initialize timer array
  • TimerArray_RenderTimed: Render and tick timers
  • Timer_Decrement: Advance timer by 1 frame

UITimer (0x448AC0)

On-screen timer display:

  • Shows minutes:seconds:centiseconds
  • Position derived from Scene+0x87C (camera)

ArenaBoard Timer (0x458E60-0x458E90)

Arena tournament timer:

  • ToggleTimer_Init: Set countdown (typically 60-90 seconds)
  • ToggleTimer_Tick: Per-frame countdown
  • ToggleTimer_Cleanup: Reset timer

🔗 Related Documents

Game State Machine & Tournamen

types : gameplay
keywords :

📂 View source on GitHub


Game State Machine & Tournament System

Architecture Overview

Hamsterball's game flow is a menu-driven state machine where each "screen" is a SceneObject added to the active scene. Menu transitions work by deleting the old menu object and creating a new one — there is no explicit state enum. Instead, game mode is tracked via boolean flags in the app object.

Menu Hierarchy (State Machine)

MainMenu (0x42DE50, vtable 0x4D3F30)
  ├── LET'S PLAY! → GameSelectionScreen (0x42E0C0, vtable 0x4D4010)
  │     ├── TOURNAMENT → GameSelectionManager (0x433C00) "1PT"
  │     │     ├── DifficultyMenu → Normal/Frenzied selection
  │     │     ├── TourneyContinueDialog → Resume saved tournament
  │     │     ├── Start → App_StartTourneyRace (0x428A40)
  │     │     └── Result → TourneyMenu (0x450070, vtable 0x4D83F0)
  │     ├── TIME TRIALS → TimeTrialMenu "1PTT"
  │     ├── MIRROR TOURNAMENT → "1PMT" (if unlocked, else "LOCKED")
  │     ├── PARTY GAMES → MPMenu (0x42E1A0) "PARTY"
  │     └── PREVIOUS → back to MainMenu
  ├── HIGH SCORES → "HS"
  ├── OPTIONS → OptionsMenu "OP"
  ├── CREDITS → "CR"
  ├── REGISTER GAME → "RG" (if not registered)
  ├── MINI GAMES → "MG" (if unlocked)
  └── EXIT TO DESKTOP → "EXIT"

Game Mode Flags (App Object)

Offset Type Description
App+0x234 bool is_multiplayer (from menu selection)
App+0x235 bool is_tournament_mode
App+0x236 bool is_mirror_mode (mirror tournament)
App+0x237 bool (cleared on race start)
App+0x23C bool race_active (set to 1 when race starts)
App+0x5D4 bool is_single_player
App+0x5D5 bool (cleared on tourney entry)
App+0x5D7 bool (cleared on race start, set 1 during)
App+0x677 bool (set 1 = default, cleared if MP)
App+0x717 bool (set 1 on race start)
App+0x7B7 bool (set 1 on race start)
App+0x850 bool mirror_tournament_unlocked
App+0x851-0x865 bool[15] Per-race visited flags (sound triggers on first visit)

Tournament System

Tournament Flow

  1. Main Menu → "LET'S PLAY!" → Game Menu (GameSelectionScreen)
  2. Game Menu → "TOURNAMENT" (button code "1PT") → GameSelectionManager
  3. GameSelectionManager checks for DATA\TOURNAMENT.SAV
  4. If save exists: TourneyContinueDialog — Continue or New?
  5. If no save or New: DifficultyMenu — Normal or Frenzied?
  6. Difficulty selected → App_StartTourneyRace (0x428A40)
  7. Race completes → TourneyMenu (0x450070) — shows results
  8. TourneyMenu → "PLAY!" → Tournament_AdvanceRace → next race
  9. After all 14 races: "TOURNAMENT WINNER!" or "LOST TOURNAMENT"

GameSelectionManager (0x433C00)

Central button handler for game mode selection. Receives button codes as strings:

Button Code Action Target Menu
"BACK" Return to MainMenu MainMenu_ctor (0x42DE50)
"LOCKED" Mirror tournament locked OkayDialog with unlock message
"1PT" 1P Tournament DifficultyMenu or TourneyContinueDialog
"1PMT" 1P Mirror Tournament DifficultyMenu (mirror flag set)
"1PP" Practice PracticeMenu_ctor
"1PTT" Time Trials TimeTrialMenu_ctor
"PARTY" Party/Multiplayer MPMenu_ctor (0x42E1A0)

For "1PT" and "1PMT", the manager:

  1. Sets App+0x235 = 1 (tournament flag)
  2. Sets App+0x236 = 1 for mirror, 0 for normal
  3. Clears App+0x5D5, 0x5D7
  4. Sets App+0x5D4 = 1 (single player)
  5. Checks for DATA\TOURNAMENT.SAV file
  6. If save exists AND Game_SetInProgress() returns true → TourneyContinueDialog
  7. Else → DifficultyMenu (fresh start)

DifficultyMenu

Presented when starting a new tournament:

  • NORMAL — Standard time pool
  • FRENZIED — Reduced time pool ("Experts only! At Frenzied Difficulty, you get less time")

App_StartTourneyRace (0x428A40)

Entry point for tournament race initialization:

App_StartTourneyRace(app):
  App_StartRace(app)
  app.is_237 = 0
  app.is_717 = 1
  app.is_7b7 = 1
  app.is_5d7 = 0
  app.is_677 = 1
  
  // Create PlayerProfile (0x98 bytes)
  profile = PlayerProfile_ctor(app, app.is_234)
  app.player_profile = profile  // +0x220
  
  // Load tournament save and show menu
  TourneyMenu_LoadSaveAndShow(profile, "DATA\\TOURNAMENT.SAV")
  
  // Set graphics cull mode based on mirror flag
  if !app.is_mirror:
    gfx.reversed = 0;  cull = default
  else:
    gfx.reversed = 1;  cull = mirror mode
  
  // Clean up old UI objects
  delete app.old_ui_0x90c
  delete app.old_ui_0x910

App_StartMPRace (0x428B20)

Multiplayer race initialization:

App_StartMPRace(app, param_1):
  App_StartRace(app)
  app.is_mp = 1 (0x717, 0x7b7)
  app.is_5d7 = 0
  app.is_677 = 1 (cleared if MP flag set)
  
  // Create PlayerProfile
  profile = PlayerProfile_ctor(app, app.is_234)
  app.player_profile = profile  // +0x220
  profile[2] = param_1
  profile.is_active = 1  // +0x10
  
  // Start tournament advancement
  Tournament_AdvanceRace(profile, 0)
  
  // Clean up old UI objects
  delete app.old_ui_0x90c
  delete app.old_ui_0x910

TourneyMenu (0x450070, vtable 0x4D83F0)

Post-race tournament screen showing:

  • PLAY! — Advance to next race
  • ROLLBACK — Re-do previous race (only if current_race > 1, shows "RXLL" if race <= 1)
  • MAIN MENU — Exit tournament

If tournament is in progress (not warm-up race):

  • Shows rank badge from textures\ranks\%d.jpg
  • Rank computed by comparing accumulated time against 16 thresholds at DAT_004F710C
  • Warm-up race text: "TAKE YOUR TIME ON THE WARM-UP RACE! This easy little race lets you get the hang of various hamsterball tricks without affecting your time pool."

Race visit tracking (per-race "first visit" sound triggers):

Race # App Flag Offset Race
3 App+0x851 Race 3
4 App+0x852 Race 4
5 App+0x853 Race 5
6 App+0x863 Race 6
7 App+0x854 Race 7
8 App+0x855 Race 8
9 App+0x856 Race 9
10 App+0x857 Race 10
11 App+0x864 Race 11
12 App+0x858 Race 12
13 App+0x859 Race 13
14 App+0x865 Race 14

On first visit to a race, this+0x1108 = 1 and a "first visit" sound plays.

PlayerProfile Structure (0x98 bytes)

Created during tournament/race init:

Offset Type Description
+0x00 vtable* PlayerProfile vtable
+0x04 App* Back-pointer to App
+0x08 int Current race number (1-14)
+0x10 bool is_active (set 1 for MP)
+0x90 int Accumulated time for save
+0x95 bool has_rollback_available
+0x96 bool tournament_completed
+0x5E4 float Total accumulated time (for rank calculation)

Tournament Save File

  • Path: DATA\TOURNAMENT.SAV
  • Written via TourneyMenu_WriteSave(profile, path) after each race
  • Loaded via TourneyMenu_LoadSaveAndShow(profile, path) at tournament start
  • Contains: current race number, accumulated times, visited flags

Unlock System

Race unlock: Each race is unlocked by reaching it during a Normal or Frenzied tournament game:

"THIS RACE ISN'T UNLOCKED YET! TO UNLOCK %s RACE, YOU NEED TO REACH IT
 WHILE PLAYING A NORMAL OR FRENZIED TOURNAMENT GAME!"

Arena unlock: Each arena requires finding a secret unlock spot in a specific race:

"THIS ARENA ISN'T UNLOCKED YET! TO UNLOCK %s ARENA, YOU NEED TO FIND THE
 SECRET UNLOCK SPOT IN THE %s RACE DURING A NORMAL OR FRENZIED TOURNAMENT GAME!"

Mirror Tournament unlock: Win a tournament at Normal or Frenzied difficulty:

"THE MIRROR TOURNAMENT ISN'T UNLOCKED YET! TO UNLOCK THE MIRROR TOURNAMENT,
 YOU NEED TO WIN A TOURNAMENT AT NORMAL OR FRENZIED DIFFICULTY!"
  • Tracked by App+0x850 (mirror_tournament_unlocked bool)

Game End States

  • TOURNAMENT WINNER! — Won all 14 races within time pool
  • LOST TOURNAMENT — Time pool depleted
    • "YOU HAVE LOST THE TOURNAMENT. IN ORDER TO RESUME, YOU NEED TO SELECT 'ROLLBACK' ON THE MENU!"
    • ROLLBACK option lets player redo the previous race with their remaining time

Key Address Map

Address Function Description
0x42DE50 MainMenu_ctor Main menu (PLAY/HS/OP/CR/RG/MG/EXIT)
0x42E0C0 GameSelectionScreen_ctor Game menu (Tourney/TT/Mirror/Party/Back)
0x433C00 GameSelectionManager Mode selection button handler
0x428A40 App_StartTourneyRace Tournament race init + save load
0x428B20 App_StartMPRace Multiplayer race init
0x450070 TourneyMenu_ctor Post-race tournament results menu
0x42E1A0 MPMenu_ctor Multiplayer menu

Reimplementation Notes

State Machine (SDL2)

  • Replace menu system with a simple state stack or scene graph
  • Each "menu" is a screen class with Update/Render/HandleInput methods
  • Button codes become enum values or string IDs
  • Replace operator_new(size) + ctor() with modern factory methods
  • The boolean-flag approach is fragile — consider an explicit GameMode enum:
    enum class GameMode { MainMenu, Tournament, TimeTrial, MirrorTournament, 
                          Practice, PartyGames, Multiplayer };
    

Tournament Save

  • Use JSON or simple binary format instead of raw struct dump
  • PlayerProfile is only 0x98 bytes — small enough for JSON
  • Rank thresholds at DAT_004F710C should be extracted as constants

Mirror Mode

  • Mirror flag at App+0x236 reverses cull mode (D3DRS_CULLMODE)
  • Equivalent to: glFrontFace(GL_CW) vs glFrontFace(GL_CCW) in OpenGL
  • Also flips camera/track direction — need to verify if track geometry is mirrored or just rendering

🔗 Related Documents

Global Bonk (Hammer) Mod

types : mods
keywords :

📂 View source on GitHub


Global Bonk (Hammer) Mod

Spawns Bonk the Hammer on any level. Hammers chase ALL ball entities, physically move toward their targets, and smash without head-turning during the smash animation.

Usage

  1. Load GlobalBonk.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnBonk to 1 in CE (or use a hotkey)
  4. Bonk spawns at player 1's position and activates when any ball comes within ~30 units

v2 Fixes (June 2026)

Issue 1: No head turn during smash

Root cause: The old script wrote player 1's position into the target fields (+0x1120-0x1128) every frame, even during smash state. This fought with the game's own target-finding logic and forced the hammer to always face player 1.

Fix: Removed ALL manual target writes. The game's vtable[11] (Bonk_Update at 0x43F930) sets targets itself. During state 2 (smash), positions are frozen — no rotation drift.

Issue 2: Hammer doesn't move (only rotates head)

Root cause: Two compounding bugs:

  • (a) vtable[11] was never called by the script — the old version only manually wrote positions, never invoking the game's update function.
  • (b) The original Bonk is a stationary turretcurrent moves toward start, but start was set to the spawn position, so current never moved toward the ball.

Fix:

  • Call vtable[11] every frame (during player 1's Ball_Update for once-per-frame timing)
  • After the call, if NOT smashing (state ≠ 2), copy target → start (0x11200x1108)
  • This redirects the game's own movement code: current now moves toward the ball at 20%/frame

Issue 3: Only targets player 1

Root cause: The old script filtered everything to [esi+0x18]==0 (player 1 only) and overwrote the game's target with player 1's position. But vtable[11] already iterates the full ball list (board+0x29D4) and finds the nearest ball automatically.

Fix:

  • Removed player-1 filter from proximity check — any ball can activate
  • Do NOT override target positions — let vtable[11] find the nearest ball from ALL balls
  • Activation state set to 0 (find target) so the game finds nearest ball on the very next frame

Object Details

  • Name string: "BONK" (0x4CFA4C)
  • Alloc size: 0x1200 (4608 bytes)
  • Constructor: 0x438850 (ret 0x10 — 4 stack params)
  • Vtable: 0x4D5120
  • Mesh: "levels\level5-bonk" — loaded INTERNALLY by the constructor
  • Update function: vtable[11] at offset 0x2C → 0x43F930

Key Object Fields

Offset Size Description
+0x10D0 4 Board pointer
+0x10D4 12 Current position XYZ (float)
+0x10E0 12 Home position XYZ (float)
+0x10F8 4 CollisionLevel pointer
+0x10FC 1 Idle flag (1=idle, 0=active)
+0x10FD 1 Chasing flag (1=chasing)
+0x1100 4 Timer/decay (init 1000, decays to 0)
+0x1104 4 State machine: 0=find target, 1=approach, 2=smash
+0x1108 12 Start position (where current moves toward)
+0x1120 12 Target position (ball position, set by vtable[11])
+0x1138 4 Speed (starts 0.5, ×1.15/frame until ≥90 → smash)
+0x113C 4 Smash countdown (25 frames)

State Machine (vtable[11] @ 0x43F930)

State 0 (find target): speed -= 10.0
  When speed < 1.0:
    speed = 0.5, state → 1
    Iterate board+0x29D4 ball list, find NEAREST ball
    Set target (0x1120-0x1128) = ball position + radius offset
    Set start (0x1108-0x1110) = current position

State 1 (approach): speed *= 1.15
  Move home → target at dist*0.333/frame
  Move current → start at dist*0.2/frame
  When speed ≥ 90.0: SMASH
    Play BONKBASH sound, state → 2, countdown = 25
    Knockback all balls within 80.0 units

State 2 (smash): countdown -= 1, when 0 → state 0

Rendering (every frame):
  Gfx_RotateY(home.x - current.x, 0, home.z - current.z)
  Gfx_SetPosition(current.x, current.y - timer, current.z)

How v2 Makes the Hammer Chase

The original game's Bonk is stationary — current moves toward start, but start = spawn position, so the hammer never physically relocates. v2 fixes this by redirecting start = target after each vtable[11] call (when not smashing). This makes the game's own movement code move current toward the target ball at 20% of remaining distance per frame (~0.25s to reach 95%).

No External Dependencies

  • Mesh loaded internally by constructor (no Board+0x43xx needed)
  • CollisionLevel created internally
  • Only appends to Board+0x2578 (always init'd)
  • Board+0x436C is saved/restored (slot JIT pattern)

Hook Point

  • Address: 0x405E22 (inside Ball_Update, ESI = current ball)
  • Original: mov eax, [esi+0x0C5C] (6 bytes: 8B 86 5C 0C 00 00)
  • Player 1 filter ([esi+0x18]==0) used only for: vtable[11] call (once per frame), spawn trigger, position save
  • Proximity check: no player filter — any ball can activate idle bonks

🔗 Related Documents

Global Bumper & Impossible Obj

types : docs
keywords :

📂 View source on GitHub


Global Bumper & Impossible Objects — Reverse Engineering Analysis

1. Bumpers in Beginner Race

What Are Bumpers?

Bumpers are bouncy collision objects embedded in MESHWORLD geometry. They are NOT standalone game objects with their own vtable — they are mesh triangles tagged with N:BUMPER%d event names in the MESHWORLD octree (Section 6).

Where Do They Come From?

  • Beginner Race (LevelCascade.MESHWORLD): Contains N:BUMPER1 through N:BUMPER6 ref points
  • Toob Race (Level8.MESHWORLD): Contains N:BUMPER1 through N:BUMPER8
  • The bumper visual mesh comes from the level's own MESHWORLD file
  • The bumper collision event is processed by the board's collision dispatch

How Are Bumpers Created?

Step 1: Board Initialization

LevelBoard_Beginner_ctor (0x004200E0)
  → Board_ctor(this, param_1)
  → Sets vtable = 0x4D1098 (Beginner board)
  → LoadRaceData(this, "CASCADERACE")
  → vtable[0x12] = Scene_SetupLevelCascade (0x004110D0)

Step 2: Scene Setup (vtable[0x12] call)

Scene_SetupLevelCascade (0x004110D0)
  1. operator_new(0x10D0) → allocates MeshWorld
  2. MeshWorld_ctor(mesh, App+0x174, "levels\levelcascade")
     → Parses LevelCascade.MESHWORLD binary
     → Builds octree with all geometry including N:BUMPER%d tagged triangles
  3. Stores mesh at Board+0x8AC (param_1[0x22B])
  4. operator_new(0x10D0) → allocates CollisionLevel
  5. CollisionLevel_ctorWithLevel(colLevel, mesh)
     → Clones collision geometry from mesh
  6. Stores at Board+0x8B0 (param_1[0x22C])
  7. Level_InitScene(board)
     → Iterates Section 1 ref points
     → For each ref, dispatches to CreateLevelObjects/CreateMechanicalObjects/CreateSpeedCylinder
     → N:BUMPER%d refs do NOT match any factory → fall through to CreatePlatformOrStands
     → But the bumper TRIANGLES are already in the octree from MeshWorld_ctor
  8. vtable[0x80]() — post-init callback
  9. Loops 8 times (i=1..8):
     - AthenaString_Format("N:BUMPER%d", i) → generates "N:BUMPER1", "N:BUMPER2", etc.
     - Scene_CollectByNameFilter(mesh, nameFilter, &bumperSlot[i])
       → Searches the MeshWorld octree for triangles tagged with this event name
       → Stores collected triangle groups in Board+0x43E4 + i*0x418 (bumper slot array)
     - Sets Board+0x642C + i*4 = 0 (bumper activation state)

Key insight: Bumpers are NOT created by a factory function. They are pre-baked into the level's MESHWORLD file as tagged collision triangles. The Scene_CollectByNameFilter call gathers them into slot arrays for the collision system.

Step 3: Collision Dispatch (vtable[0x1D])

When the ball collides with any triangle in the level:

Beginner Collision Dispatch (0x004111E0) — vtable[0x1D]
  1. Checks if event name starts with "N:BUMPER"
  2. If yes:
     a. Sound_Play3D — plays bumper bounce sound at ball position
     b. Reads ball velocity from Ball+0xCA4 (velX), +0xCA8 (velY), +0xCAC (velZ)
     c. Scales velocity by _DAT_004CF41C (a float multiplier)
     d. If speed < threshold: Vec3_NormalizeAndScale(&vel, 5.0) — small bounce
     e. If speed > threshold: Vec3_NormalizeAndScale(&vel, 10.0) — big bounce
     f. Writes scaled velocity back to Ball+0xCA4/+0xCA8/+0xCAC
     g. Reads bumper index from event name: _atol(name + 8) → "N:BUMPER3" → index 3
     h. Sets Board+0x6428 + index*4 = 1.0 (0x3F800000) — bumper activation flag
  3. Falls through to DispatchCollisionEvents (0x0040C5D0) for shared event handling

Bumper Data Structure

Offset Type Description
Board+0x43E4 + i*0x418 struct Bumper slot[i] (collected triangle groups)
Board+0x6428 + i*4 float Bumper activation state (0=inactive, 1=just hit)
Board+0x642C + i*4 int Bumper state flag

Why Bumpers Are Hard to Clone Globally

Bumpers are embedded collision triangles, not standalone objects. To spawn them globally:

  1. You must load the Level8.MESHWORLD (or LevelCascade.MESHWORLD) file which contains the bumper geometry
  2. Create a Stands object from that mesh (clones the octree including collision triangles)
  3. Position it at the player
  4. Register it in the board's collision lists (Board+0x10EC and Board+0x8B0→+0x18)

The collision dispatch (vtable[0x1D]) automatically handles "N:BUMPER" events regardless of which board type you're on, because ALL boards inherit from the same base collision dispatch that calls DispatchCollisionEvents.


2. Impossible Race Objects

Overview

Impossible Race (LevelImpossible.MESHWORLD) contains 5 types of mechanical objects, each loaded from a separate sub-MESHWORLD file:

Object Mesh File String Address Board Slot Alloc Size Ctor Address RET N
LOOPER LevelImpossible-Looper 0x4D2158 +0x436C 0x1500 0x435800 0x14 (5 params)
GEAR LevelImpossible-Gear 0x4D213C +0x4370 0x1514 0x437590 0x20 (8 params)
BIGGEAR LevelImpossible-BigGear 0x4D211C +0x4374 0x1514 0x437590 0x20 (8 params)
ROTATOR LevelImpossible-Rotator 0x4D20FC +0x4378 0x1508 0x435940 0x14 (5 params)
PENDULUM LevelImpossible-Pendulum 0x4D20DC +0x437C 0x1504 0x436A20 0x18 (6 params)

How They Are Created

Step 1: Board_Impossible_ctor (0x00424C20)

Board_ctor(this, param_1);
this->vtable = 0x4D21C0;  // Impossible board
this->name = "Board (Impossible)";
this->race_name = "IMPOSSIBLE RACE";
LoadRaceData(this, "IMPOSSIBLERACE");

// Pre-load 5 sub-meshes into board slots:
this->mesh_Looper   = MeshWorld_ctor(new(0x10D0), App+0x174, "Levels\LevelImpossible-Looper");   // +0x436C
this->mesh_Gear     = MeshWorld_ctor(new(0x10D0), App+0x174, "Levels\LevelImpossible-Gear");     // +0x4370
this->mesh_BigGear  = MeshWorld_ctor(new(0x10D0), App+0x174, "Levels\LevelImpossible-BigGear");  // +0x4374
this->mesh_Rotator  = MeshWorld_ctor(new(0x10D0), App+0x174, "Levels\LevelImpossible-Rotator");  // +0x4378
this->mesh_Pendulum = MeshWorld_ctor(new(0x10D0), App+0x174, "Levels\LevelImpossible-Pendulum"); // +0x437C

Step 2: LevelImpossible_InitScene (0x00417F20) — vtable[0x12]

mesh = MeshWorld_ctor(new(0x10D0), App+0x174, "levels\levelimpossible");
board->sceneMesh = mesh;        // +0x8AC
board->collisionMesh = CollisionLevel_ctorWithLevel(new(0x10D0), mesh); // +0x8B0
Level_InitScene(board);         // Creates dynamic objects from MESHWORLD refs
board->vtable[0x80]();         // Post-init

Step 3: Scene_CreateDynamicObjects (0x0040C430)

Iterates all Section 1 ref points in the MESHWORLD octree. For each ref, calls the board's vtable[0x84]:

vtable[0x84](refName, &outObj, &outCol, refParams)

This dispatches to:

  • CreateLevelObjects (0x004121D0): BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT, GLUEBIE
  • CreateMechanicalObjects (0x00417FE0): LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
  • CreateSpeedCylinder (0x004117B0): LIFTER, SPEEDCYLINDER, TIMEBUTTON
  • Scene_CreateObject_Gear (0x00418760): GEAR (arena variant)
  • CreatePlatformOrStands (0x004133E0): Fallback for anything else

Step 4: CreateMechanicalObjects (0x00417FE0) — Detailed

For each ref name, uses __strnicmp to match:

LOOPER (strnicmp "LOOPER", 6):

obj = operator_new(0x1500);
Looper_ctor(obj, board, posX, posY, posZ, board->mesh_Looper);  // +0x436C
collisionLevel = obj->CollisionLevel;  // +0x10D4
AthenaList_Append(board+0x2578, obj);  // general objects

GEAR (strnicmp "GEAR", 4):

obj = operator_new(0x1514);
Gear_ctor(obj, board, posX, posY, posZ, dirX, dirY, dirZ, board->mesh_Gear);  // +0x4370
AthenaList_Append(board+0x2578, obj);

BIGGEAR (strnicmp "BIGGEAR", 7):

obj = operator_new(0x1514);
Gear_ctor(obj, board, posX, posY, posZ, dirX, dirY, dirZ, board->mesh_BigGear);  // +0x4374
obj->scale = 0.5;       // +0x10F4 = 0x3F000000
if (strstr(name, "TOUCH")) obj->touchFlag = 1;  // +0x1510
AthenaList_Append(board+0x2578, obj);

ROTATOR (strnicmp "ROTATOR", 7):

obj = operator_new(0x1508);
Rotator_ctor(obj, board, posX, posY, posZ, board->mesh_Rotator);  // +0x4378
obj->spinDir = 1.0;     // +0x10E8 = 0x3F800000
if (RNG_Rand(0, 2, 0) == 0) obj->spinDir = -1.0;  // 0xBF800000 — 50% chance reverse
AthenaList_Append(board+0x2578, obj);

PENDULUM (strnicmp "PENDULUM", 8):

obj = operator_new(0x1504);
Pendulum_ctor(obj, board, posX, posY, posZ, phase, board->mesh_Pendulum);  // +0x437C
collisionLevel = obj->CollisionLevel;  // +0x10D4
AthenaList_Append(board+0x2578, obj);

Step 5: Constructor Internals

All 5 ctors follow the same pattern:

// 1. Call Stands_ctor(this, mesh)
//    → SpriteAnim_Ctor, sets vtable=Level_DeletingDtor, Timer_Init
//    → Clones SpatialTree from mesh (geometry + collision)
//    → AthenaList_Init on +0x18, +0x488, +0x8A0, +0xCB8

// 2. Set object-specific vtable:
//    Looper  → vtable = 0x4D54B8 (PopCylinder_DeletingDtor)
//    Gear    → vtable = 0x4D5AD0 (Gear_Vec3List_DeletingDtor)
//    Rotator → vtable = 0x4D5518 (Lifter_DeletingDtor)
//    Pendulum→ vtable = 0x4D57D0 (Pendulum_DeletingDtor2)

// 3. AthenaList_Init on object-specific list (+0x10E8/+0x10F0/+0x10F8)

// 4. Set board pointer: obj+0x10D0 = board

// 5. Set position: obj+0x10D8/+0x10DC/+0x10E0 = posX/posY/posZ

// 6. Create CollisionLevel:
//    operator_new(0x10D0) → CollisionLevel_ctorWithLevel(col, this)
//    obj->CollisionLevel = col  (+0x10D4)
//    col->field_434 = obj->field_434  (difficulty flag propagation)
//    col->field_431 = 0  (collision enabled)

Impossible Race Collision Dispatch (0x00418360)

When the ball touches any collision triangle:

if (ball->is_active) {  // ball+0x2E8 (param_2[0x1DA] in Ghidra int* indexing = 0x768)
    // N:BOUNCE — bounce physics (same as Impossible Gear bumpers)
    if (strnicmp(event, "N:BOUNCE", 8) == 0) {
        // Scale velocity, normalize, apply bounce force
    }
    // N:ONROTATOR — score tracking
    if (strnicmp(event, "N:ONROTATOR", 11) == 0) {
        ScoreObject_SetScore(board->scoreObj, ball);
    }
    // N:ONGEAR — catapult-style object attachment
    if (strnicmp(event, "N:ONGEAR", 8) == 0) {
        Catapult_AddObjectConditional(board->scoreObj, ball);
    }
    // E:HELPINERTIA — increase ball inertia (0x40200000 = 2.5)
    if (stricmp(event, "E:HELPINERTIA") == 0) {
        ball->inertia = 0x40200000;  // +0x2A4 (param_2[0xA9])
    }
    // E:UNHELPINERTIA — decrease ball inertia (0x40A00000 = 5.0)
    if (stricmp(event, "E:UNHELPINERTIA") == 0) {
        ball->inertia = 0x40A00000;
    }
}
// Always calls shared handler:
DispatchCollisionEvents(board, ball, collision);

DispatchCollisionEvents (0x0040C5D0) — Shared Event Handler

Processes ALL common collision events:

  • N:SECRET → Rotator_MarkTriggered
  • N:UNLOCKSECRET → CheckArenaUnlock
  • E:NODIZZY<TIME>N</TIME> → Ball_DizzyImmunity (no-dizzy timer)
  • E:SAFESWITCH → copy switch data to ball+0xC2C
  • E:LIMIT → track arena completions
  • E:JUMP → play sound + SetForce(0.1, 1) + impact=10
  • E:ACTION(ONCE/SCORE) → score tracking
  • E:TRAJECTORY(X,Y,Z) → set ball trajectory
  • N:WATER → water flag + timer=10
  • N:TARPIT → tar sound + tar flag
  • N:GOAL → finish race
  • DROPIN → sound + score + 200
  • PIPEBONK → random sound + score + 100
  • POPOUT → sound + score + 100
  • N:MOUSETRAP → deflect + rotator collision

3. Global Spawn Pattern

Both CEA scripts use the same proven pattern as the SpeedCylinder spawn script:

Hook Point

Ball_Update @ 0x00405E22 (original: mov eax, [esi+0x0C5C])
→ jmp SpawnCode
  • esi = Ball pointer
  • ball+0x14 = Board pointer
  • ball+0x18 = state (0 = active racing)
  • ball+0x164/+0x168/+0x16C = position X/Y/Z

Spawn Flow

  1. Save player position every frame (when ball+0x18 == 0)
  2. Check spawn flag (set via CE address list)
  3. If flag set: pushad → spawn → popad
  4. Spawn process:
    a. Get board from ball+0x14
    b. Get App+0x174 (D3D device) from board+0x878
    c. operator_new(0x10D0)MeshWorld_ctor(mesh, D3D, "filename")
    d. operator_new(objSize)Object_ctor(obj, board, posX, posY, posZ, mesh)
    e. Set position at obj+0x10D8/+0x10DC/+0x10E0
    f. AthenaList_Append(board+0x2578, obj) — general objects
    g. AthenaList_Append(board+0x10EC, collisionLevel) — collision registration
    h. AthenaList_Append(board+0x8B0→+0x18, collisionLevel) — spatial tree
    i. Timer_Initvtable[0x58] (CallUpdate) → vtable[0x54] (CallRender)
    j. Track in array for board-change cleanup

Key Addresses

Address Function
0x004BA57B operator_new
0x00461510 MeshWorld_ctor (RET 0x8)
0x00465080 CollisionLevel_ctorWithLevel (RET 0x4)
0x00462850 Stands_ctor (RET 0x4)
0x00453810 AthenaList_Append
0x00453210 AthenaList_Init (RET 0x4)
0x00457AD0 Timer_Init
0x00457A40 Timer_Cleanup
0x0040B090 Level_InitScene

String Constants

Address String
0x4CFCA4 "levels\level8"
0x4D2158 "Levels\LevelImpossible-Looper"
0x4D213C "Levels\LevelImpossible-Gear"
0x4D211C "Levels\LevelImpossible-BigGear"
0x4D20FC "Levels\LevelImpossible-Rotator"
0x4D20DC "Levels\LevelImpossible-Pendulum"

Ctor Calling Conventions (verified via RET N)

Ctor RET N Stack Params Signature
Looper_ctor (0x435800) 0x14 5 (this, board, posX, posY, posZ, mesh)
Gear_ctor (0x437590) 0x20 8 (this, board, posX, posY, posZ, dirX, dirY, dirZ, mesh)
Rotator_ctor (0x435940) 0x14 5 (this, board, posX, posY, posZ, mesh)
Pendulum_ctor (0x436A20) 0x18 6 (this, board, posX, posY, posZ, phase, mesh)
Stands_ctor (0x462850) 0x4 1 (this, mesh)
MeshWorld_ctor (0x461510) 0x8 2 (this, D3D, filename)

🔗 Related Documents

Global Catapults (CEA)

types : mods
keywords :

📂 View source on GitHub


Global Catapults (CEA)

Spawns functional catapults on any level/arena. Ball is launched via the game's native collision system (E:CATAPULTBOTTOM), not proximity detection.

Usage

  1. Enable the script in Cheat Engine.
  2. Set SpawnCatapult to 1 in the address list.
  3. Enter any race or arena.
  4. A catapult spawns at the ball's position (20 units below).
  5. Roll over the catapult's E:CATAPULTBOTTOM surface — the ball gets launched.

What Changed from v1

Issue v1 (Original) v2 (Fixed)
Board+0x43B8 crash Uninitialized AthenaList on non-Tower levels → _realloc(garbage) crash Checks vtable vs 0x4D875C, calls AthenaList_Init(0x00453210) if needed
Board+0x584C crash Same uninitialized list crash Same fix
Proximity launch Checks XZ distance < 9.0 and manually calls Catapult_Launch Removed entirelycollision system handles launch via E:CATAPULTBOTTOM
float_9 9.0 proximity constant Removed (unused)

How Launch Works (Original Game)

  1. Ball touches E:CATAPULTBOTTOM collision triangle on the catapult's CollisionLevel mesh
  2. Collision dispatch fires:
    • Race mode: TowerCollisionEvents (0x0040DCD0) iterates Board+0x43B8
    • Arena mode: MasterCollisionEvents (0x00412850) iterates Board+0x584C
  3. For each catapult in the list, checks catapult+0x10D4 == *collision_entry
  4. On match: sets catapult+0x10EC = ball_ptr, calls Catapult_Launch(catapult)
  5. Catapult_Launch sets +0x10F0 = 1 (active), +0x10F4 = 50 (launch timer frames)
  6. Catapult_Update (0x0043E600) applies matrix transform each frame, physically launching the ball

Board AthenaList Initialization

Board_ctor (0x00419030) initializes these AthenaLists:

0x08B8, 0x0CD4, 0x10EC, 0x1518, 0x1930, 0x1D48,
0x2160, 0x2578, 0x29D4, 0x2DEC, 0x3204, 0x362C

But Board+0x43B8 and Board+0x584C are only initialized by level-specific ctors:

  • LevelBoard_Tower_ctor (0x0041E340) → inits +0x43B8, +0x584C
  • BoardLevel_Master_Ctor (0x004206D0) → inits +0x584C

On all other levels these offsets contain garbage from malloc. The fix checks the vtable pointer (first 4 bytes) against 0x4D875C (the AthenaList vtable set by AthenaList_Init). If it doesn't match, the list is uninitialized and AthenaList_Init is called before appending.

Catapult Struct Layout (0x1108 bytes)

Offset Type Description
+0x0000 void* vtable (0x4D4F98 after Catapult_ctor)
+0x10D0 void* parent Board pointer
+0x10D4 void* CollisionLevel pointer (matched in collision dispatch)
+0x10D8 float position X
+0x10DC float position Y
+0x10E0 float position Z
+0x10E4 float rotation
+0x10E8 float launch direction Y (-1.0 = upward)
+0x10EC void* ball pointer (set by collision dispatch on launch)
+0x10F0 byte triggered flag (1 = launch active)
+0x10F4 int launch timer (50 frames)
+0x10F8 AthenaList tracked objects (0x418 bytes)
+0x1100 byte active state (1 = active)
+0x1104 float launch power (17.0)

🔗 Related Documents

Global Chomper Mod

types : mods
keywords :

📂 View source on GitHub


Global Chomper Mod

Spawns Tower Race chompers (purple thing in the pit) on any level with a hotkey.

Usage

  1. Load GlobalChomper.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnChomper to 1 in CE (or use a hotkey)
  4. Chomper spawns at player 1's position
  5. Ball touching chomper takes 25.0 damage (E:BITE event) + chomp sound

How It Works

  • Hooks at Ball_Update (0x405E22)
  • On first spawn: loads Meshes\Chomper mesh via MeshWorld_ctor → stores at board+0x4390
  • Creates CollisionLevel from mesh via CollisionLevel_ctorWithLevel (0x465080)
  • Loads mesh data via Level_LoadMeshes (0x465200)
  • Registers collision with scene manager at 0x4F7360
  • E:BITE events are baked into the chomper mesh's collision triangles
  • When ball touches chomper → E:BITE → board+0x43A0 = 25.0 (bite damage)
  • Game loop reads board+0x43A0 → applies damage to ball → "sounds\chomp" plays
  • Registered to board+0x2578 (general), board+0xCD4, scene spatial tree

Object Details

  • Type: MeshWorld + CollisionLevel (NOT a game object with vtable)
  • Mesh: "Meshes\Chomper" (VA 0x4D094C)
  • Sound: "sounds\chomp" (VA 0x4D2D98)
  • Collision Event: E:BITE → 25.0 damage (0x41C80000)
  • No per-frame update needed — collision events fire automatically

How E:BITE Works

The chomper mesh's collision triangles have E:BITE event names embedded in them.
When the ball intersects these triangles, the game's collision system fires E:BITE:

// In Level_HandleCollision (0x40DCD0):
if (stricmp(eventName, "E:BITE") == 0) {
    board+0x43A0 = 25.0;   // bite damage
    board+0x43A8 = 0;      // reset counter
}

The damage value is read by the game loop at 0x4023C1 and applied to the ball.

Full Analysis

See docs/MACE_WINDMILL_CHOMPER_SYSTEM.md for complete reverse-engineering documentation.


🔗 Related Documents

Global Drawbridge Spawner

types : mods
keywords :

📂 View source on GitHub


Global Drawbridge Spawner

Spawns Tower Race drawbridges at Player 1's position in any level via hotkey.

Usage

  1. Enable script in Cheat Engine
  2. Press hotkey (set SpawnDrawbridge to 1) to spawn a drawbridge at player position
  3. Drawbridges animate (open/close) automatically via vtable[0x2C] update

Root Cause of Original Crash

board+0x4378 is a REUSED SLOT across levels:

Level board+0x4378 contains
Tower (Level4) Levels\Level4-Mace mesh (set by LevelBoard_Tower_ctor)
Beginner (LevelCascade) 0 (unused)
Other races varies — may contain other mesh data

The original script wrote the drawbridge mesh to board+0x4378 permanently and never restored it. In Beginner Race the slot was 0 (unused) so it worked. In Tower Race and other races, the slot already contained another level's mesh, causing:

  • Wrong mesh used for drawbridge (Mace mesh instead of Drawbridge mesh)
  • Board slot corrupted for original objects → crash when level objects try to use the slot

Fix: JIT Mesh Injection

Save board+0x4378 → inject drawbridge mesh → call constructor → restore board+0x4378.

This is the same pattern as the universal ref loader (see skill hamsterball-dll-modding).

Functions Traced

Address Name Description
0x4396F0 Spinner_Level_ctor __thiscall, RET 0x14 (5 params). Reads board+0x4378 for mesh.
0x461510 MeshWorld_ctor __thiscall, RET 0x8. Loads MESHWORLD file.
0x462850 Stands_ctor Clones SpatialTree from mesh source.
0x453810 AthenaList_Append __thiscall, RET 4. Adds to list.
0x439870 Impossible_Level_Update vtable[0x2C]. Drawbridge animation: timer, rotation, sound.
0x4BA57B operator_new __cdecl. Allocates memory.
0x40E250 CreateSawblade Game factory. Creates BONK, TOWER, SAWBLADE, BRIDGE, JUDGE, BELL.
0x41E340 LevelBoard_Tower_ctor Tower board init. Loads meshes into board+0x43xx slots.

Board Slot Layout (Tower Race)

Offset Mesh Set by
board+0x436C Levels\Level4-Catapult LevelBoard_Tower_ctor
board+0x4370 Levels\Level4-Drawbridge LevelBoard_Tower_ctor
board+0x4374 Meshes\YellowLink LevelBoard_Tower_ctor
board+0x4378 Levels\Level4-Mace LevelBoard_Tower_ctor
board+0x437C Levels\Level4-Windmill LevelBoard_Tower_ctor
board+0x4390 Meshes\Chomper LevelBoard_Tower_ctor
board+0x43B4 Levels\Level4-Turret LevelBoard_Tower_ctor

Object Registration

Drawbridges are registered in:

  • board+0x2578 — active objects list (update + render)
  • board+0xCD4 — collision objects list
  • board+0x10EC — collision level list
  • board+0x8B0+0x18 — collision dispatch list
  • board+0x8AC→+0x480→+0x1C — render list (post-alpha)

Object Structure (Spinner_Level / Drawbridge)

Offset Type Description
+0x000 int* vtable (0x4D51E0)
+0x10D0 int board pointer
+0x10D4 float position X
+0x10D8 float position Y
+0x10DC float position Z
+0x10E0 float rotation angle
+0x10E4 int state (0=initialized)
+0x10E8 int unknown (0)
+0x10F0 int timer countdown (100)
+0x10F4 void* CollisionLevel pointer
+0x10F8 float rotation direction (1.0 or -1.0)

🔗 Related Documents

Global Expert Objects Mod

types : mods
keywords :

📂 View source on GitHub


Global Expert Objects Mod

Spawns all 6 expert race objects on any level with a hotkey.

Objects Spawned

  1. BONK (Hammer) — The hammer that chases you (ctor 0x438850, alloc 0x1200)
  2. FAN — The fanstorm (ctor 0x438C20, alloc 0x1188)
  3. SAWBLADE — Saw blade (ctor 0x434660, alloc 0x111C)
  4. BRIDGE — Breakable bridge (ctor 0x4396F0, alloc 0x10FC)
  5. JUDGE — Judge object (ctor 0x43A150, alloc 0x1100)
  6. BELL — Bell to ring (ctor 0x434D70, alloc 0x10E8)

Usage

  1. Load GlobalExpert.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnExpert to 1 in CE (or use a hotkey to set it)
  4. All 6 objects spawn at the player's position, spread out

Mesh Loading

Only BRIDGE needs a mesh: Levels\Level5-Bridge loaded via MeshWorld_ctor (cached on first spawn).

All other objects (BONK, FAN, SAWBLADE, JUDGE, BELL) create their geometry internally via Stands_ctor(0x461740) using the D3D device — no external mesh needed.

AthenaList Initialization

On non-Expert levels, three lists are initialized on first spawn:

  • Board+0x4380 (bridge list 1)
  • Board+0x4798 (bridge list 2)
  • Board+0x4BBC (judge list)

Uses vtable check cmp dword [ecx], 0x004D875C to avoid double-init.

Post-Spawn Board Field Writes

  • BONK → Board+0x436C (for E:CALLHAMMER event)
  • SAWBLADE → Board+0x4370 (for E:ALERTSAW1/ACTIVATESAW1 events)
  • BELL → Board+0x4FD4 (for bell ring event)

Collision Events

Expert Race collision handler at 0x40E6A0 handles:

  • E:CALLHAMMER, E:HAMMERCHASE — activate hammer chase
  • E:ALERTSAW1/2, E:ACTIVATESAW1/2 — activate saw blades
  • E:ALERTJUDGES — cycle judges
  • E:SCORE — scoring event
  • E:JUMP — jump pad

On non-Expert levels, objects have physical collision but special events won't fire.

Hook Point

  • Address: 0x405E22 (inside Ball_Update)
  • Original: mov eax, [esi+0x0C5C] (6 bytes: 8B 86 5C 0C 00 00)

🔗 Related Documents

Global FunBall Spawner

types : mods
keywords :

📂 View source on GitHub


Global FunBall Spawner

Spawns a real FunBall ball entity at Player 1's position in any level.

Usage

Set SpawnObject to 1 to spawn a FunBall at the player's position.

How the FunBall Works

Mesh Loading

The "Meshes\FunBall" mesh (string at 0x4D3474) is loaded by App_ResourceLoader (TimerDisplay, 0x4298C0) via vtable[0x4C] into App+0x26C as a MeshNode (0x18 bytes).

Ball Mesh Array

The game stores all ball meshes in an array at board+0x244, indexed by ball+0x754 (mesh type index):

Index Offset Mesh Path
0 App+0x244 Meshes\Sphere
1 App+0x248 Meshes\SphereBreak1
2 App+0x24C Meshes\SphereBreak2
3 App+0x250 Meshes\Hamster-Waiting
4 App+0x254 Meshes\Hamster-trot1
5 App+0x258 Meshes\Hamster-trot2
6 App+0x25C Meshes\Hamster-trot3
7 App+0x260 Meshes\RBGlare
8 App+0x264 Meshes\Sphere+Tar
9 App+0x268 Meshes\8Ball
10 App+0x26C Meshes\FunBall
11 App+0x270 Meshes\Bell
12 App+0x274 Meshes\Dizzy

Ball Construction

  • Ball_ctor (0x40AFE0): alloc 0xC98, __thiscall(this, board)
    • Calls Ball_ctor2 which initializes all fields including ball+0x754 = 0 (Sphere)
    • Sets vtable to 0x4CF3A0
  • After construction, set ball+0x754 = 10 to use the FunBall mesh

Rendering

Ball_Render (0x402DE0) and Ball_RenderShadow (0x401B00) read board+0x244[ball+0x754] to select the mesh for rendering. The render function calls mesh->vtable[0x07](matrix, 0) to render.

Registration

Balls are registered in two lists:

  • board+0x29D4 — ball list (for updates and rendering)
  • board+0x2DEC — collision list (for physics)

Ball Key Fields

Offset Type Description
+0x000 int* vtable (0x4CF3A0)
+0x010 int board pointer
+0x014 int scene pointer
+0x164 float position X
+0x168 float position Y
+0x16C float position Z
+0x281 byte unused_init_flag (DEAD: set by ctor, never read)
+0x284 float radius (26.0)
+0x2F9 byte is_falling (0=not falling)
+0x2FC float fall_timer (1.0)
+0x754 int32 mesh type index (10=FunBall)
+0xC74 int32 AI chase target (0=none)
+0xC80 byte flag (0)

🔗 Related Documents

Global Judge (Hammy Judge) Mod

types : mods
keywords :

📂 View source on GitHub


Global Judge (Hammy Judge) Mod

Spawns Expert Race Hammy Judges on any level with a hotkey.

Usage

  1. Load GlobalJudge.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnJudge to 1 in CE (or use a hotkey)
  4. Judge spawns at player 1's position and animates

How It Works

  • Hooks at Ball_Update (0x405E22)
  • Allocates 0x1100 bytes, calls Judge_Ctor (0x43A150)
  • Constructor internally calls Level_ctor which loads "meshes\hammyjudge" mesh
  • Sets vtable to 0x4D52B8
  • Sets position at obj+0x10D4 (X), +0x10D8 (Y), +0x10DC (Z)
  • Registered to board+0x4BBC (judge list), board+0x2578 (general), board+0xCD4
  • Per-frame: calls vtable[11] (Judge_Update @ 0x434B60) for animation

Object Details

  • Alloc size: 0x1100 (4352 bytes)
  • Constructor: Judge_Ctor (0x43A150) — thiscall(obj, board) ret 4
  • Vtable: 0x4D52B8
  • Mesh: "meshes\hammyjudge" (VA 0x4D0AA8) — loaded internally by Level_ctor
  • Update: vtable[11] @ 0x434B60 — trig oscillation (same pattern as Pendulum)
  • Render: vtable[18] @ 0x43A270
  • No JIT mesh injection needed — Level_ctor loads the mesh internally

Collision Events

Event Action
E:ALERTJUDGES Activates all judges in board+0x4BBC list
E:BELL Rings bell sound
E:SCORE Awards points

Full Analysis

See docs/WINDMILL_JUDGE_SYSTEM.md for complete reverse-engineering documentation.


🔗 Related Documents

Global Lifter (DropLift) Spawn

types : mods
keywords :

📂 View source on GitHub


Global Lifter (DropLift) Spawner

Spawns a Lifter at Player 1's position in any level. The lifter rises when the ball approaches — replicating the Up Race lifter with E:DROPLIFT behavior globally.

Files

  • GlobalLifterDropLift.CEA — CE AutoAssembler script (pure CEA, no Lua)

Installation

  1. Copy to your Cheat Engine scripts folder
  2. In CE: File → Load → select GlobalLifterDropLift.CEA
  3. Enable the script

Usage

  1. Add the address SpawnLifter to your CE address list
  2. Enter a race or arena (any level works)
  3. Set SpawnLifter to 1 — a Lifter spawns at Player 1's current position
  4. The flag auto-resets to 0 after spawning
  5. Set to 1 again to spawn another

The lifter automatically rises when the ball gets within 100.0 units.

How It Works

Lifter Creation Chain

The original game creates lifters through this chain:

  1. Board constructor loads Levels\Level6-Lifter mesh into App+0x5C8 (global lifter mesh slot)
  2. CreateLevelObjects (0x4121D0) dispatches to TryCreateLifter (0x40EC40)
  3. TryCreateLifter checks if object name == "LIFTER" (strnicmp, 6 chars)
  4. If match: operator_new(0x10FC)Lifter_ctor(0x434E60) → register in board+0x2578

Lifter_ctor (0x434E60)

__thiscall: ECX=this, push board, push x, push y, push z (ret 0x10)

  1. Gets lifter mesh from board→[board+0x878]→[App+0x5C8]
  2. Calls Stands_ctor(0x462850) — clones SpatialTree from the lifter mesh for rendering
  3. Sets vtable = 0x4D5390 (Lifter vtable)
  4. Copies position to +0x10D8 (x, y, z as 3 floats)
  5. Creates CollisionLevel (0x10D0 bytes) via CollisionLevel_ctorWithLevel(0x465080)
  6. Stores collision mesh at +0x10D4
  7. Sets state +0x10E4 = 3 (WAITING)
  8. Sets +0x10F0 = -1 (no current collision target)

State Machine (vtable[0x2C] = 0x434F60)

State Name Behavior
3 WAITING Initial state, lifter at bottom, doing nothing
0 RISING Height increases (+0x10E8 accumulates to 95.0 max)
1 DESCENDING Height decreases back to 0, plays sound every 20 frames
2 DROP Quick descent (not used in normal operation)

Key fields:

  • +0x10D8/+0x10DC/+0x10E0 — position (x, y, z)
  • +0x10E4 — state (0-3)
  • +0x10E8 — current height offset (0=bottom, 95.0=top)
  • +0x10EC — sound timer (counts down from 20)
  • +0x10F0 — last height (for collision detection)
  • +0x10F4 — descent/wait timer
  • +0x10F8 — rise speed accumulator

DropLift Trigger

When the ball touches an E:DROPLIFT collision surface in the level mesh:

  1. Collision handler at 0x0040F06C checks for "E:DROPLIFT" event
  2. Gets lifter pointer from board+0x436C
  3. Calls Lifter_TriggerDrop(0x435170)__thiscall: ECX=lifter
  4. Lifter_TriggerDrop only fires if state == 3 (WAITING)
  5. Sets state = 0 (RISING), rise_speed = 0.25
  6. Plays sound at lifter position via Sound_Play3D

The NOP Patch (Hamsterball.exe+EC74)

The user's existing patch at 0x0040EC74 NOPs a JNE instruction in TryCreateLifter:

  • Original: JNE 0x0040ED01 (skip if name != "LIFTER")
  • Patched: NOP × 6 (always create lifter, regardless of name)

This makes the Lifter factory accept ANY mesh object name, causing lifters to be created for all objects in the odd race. The CEA script takes a different approach — it directly creates the lifter without going through the factory.

Verified Addresses (Ghidra/r2, 2026-06-25)

Address Function Convention
0x40EC40 TryCreateLifter ECX=board, stack=[name, out1, out2, pos], RET 0x10
0x434E60 Lifter_ctor ECX=this, stack=[board, x, y, z], RET 0x10
0x434F60 Lifter_Update (vtable[0x2C]) ECX=this
0x435170 Lifter_TriggerDrop ECX=lifter
0x462850 Stands_ctor ECX=this, push source_mesh
0x465080 CollisionLevel_ctorWithLevel ECX=new, push source_mesh
0x4BA57B operator_new push size, RET
0x453810 AthenaList_Append ECX=list, push item, RET 0x4
0x461510 MeshWorld_ctor ECX=this, push d3d, push path, RET 0x8
0x459860 Sound_Play3D push pos_vec, push vol, ECX=sound_obj
0x4D5390 Lifter vtable
0x4D3308 "Levels\Level6-Lifter" string
0x4CFB90 "E:DROPLIFT" string
0x4CFB1C "LIFTER" string

🔗 Related Documents

Global Lifters Mod

types : mods
keywords :

📂 View source on GitHub


Global Lifters Mod

Spawns Up Race lifters (Rotators) on any level with a hotkey.

Usage

  1. Load GlobalLifters.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnLifter to 1 in CE (or use a hotkey to toggle it)
  4. The next time Ball_Update runs (every frame), a lifter spawns at the player's position

How It Works

  • Hooks Ball_Update at 0x00405E22 (inside the player physics update)
  • When SpawnLifter == 1:
    • Saves player position from ball fields (+0x164/+0x168/+0x16C)
    • Creates a temporary MeshWorld from "levels\levelup-lifter" (the Up Race lifter mesh)
    • Stores it at Board+0x4784 (the Up Race lifter mesh field)
    • Calls CreateUpLevelObjects (0x4117B0) to spawn a Rotator at the player position
    • Registers the Rotator in 4 lists:
      • Board+0xCD4 (timer/cleanup list)
      • Board+0x8AC+0x480+0x1C (SceneObject render list)
      • Board+0x10EC (timer list)
      • Board+0x8B0+0x18 (CollisionLevel collision list)
    • Calls SceneObject_CallUpdate (vtable[0x58]) and SceneObject_CallRender (vtable[0x54])

v2 Fix — Dizzy Race Crash

Crash address: 0001:0001C5F0 (near-null access violation)

Root cause: Stack corruption from an unmatched push ebx before the SceneObject_CallRender call.

SceneObject_CallRender (0x45DF90) is a __fastcall(ecx) tail-call — it takes zero stack parameters and does not clean up any stack. The push ebx before the call left ESP off by 4 bytes. When add esp, 68 executed, it only cleaned 68 of 72 bytes on the stack. popad then read from shifted positions, giving ESI the value of EBP instead of the ball pointer. The original instruction mov eax, [esi+0x0C5C] then accessed [EBP + 0x0C5C].

On most levels, EBP happened to point to valid memory, so the corrupted read silently succeeded (wrong value, no crash). On Dizzy Race, EBP was a near-null pointer (0x0001B994), producing the access violation at 0x0001B994 + 0x0C5C = 0x0001C5F0.

Fix: Removed the push ebx line. Stack is now perfectly balanced — add esp, 68 exactly matches sub esp, 68, and popad restores all registers correctly.


🔗 Related Documents

Global Mace (Pendulum) Mod

types : mods
keywords :

📂 View source on GitHub


Global Mace (Pendulum) Mod

Spawns Tower Race swinging maces on any level with a hotkey.

Usage

  1. Load GlobalMace.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnMace to 1 in CE (or use a hotkey on that address)
  4. Mace spawns at player 1's position (slightly above for hang-down swing)
  5. Each hotkey press spawns another mace (up to 16)

How It Works

  • Hooks at Ball_Update (0x405E22) — same pattern as SpeedCylinder/Bonk
  • On first spawn: loads Levels\Level4-Mace mesh via MeshWorld_ctor → stores at board+0x4378
  • On Tower Race, mesh is already loaded by LevelBoard_Tower_ctor — reuses it
  • Allocates 0x110C bytes, calls CascadeStands_Ctor (0x438750)
  • Sets position at obj+0x10D8 (X), +0x10DC (Y), +0x10E0 (Z)
  • Sets swing amplitude (+0x10E8 = 80.0), active flag (+0x10F4 = 1), timer (+0x10F8 = 50)
  • Registered to board+0x2578 (general), board+0x5000 (mace list), board+0xCD4
  • Per-frame: calls vtable[11] (Pendulum_Update @ 0x43F3C0) for swing animation

Object Details

  • Alloc size: 0x110C (4364 bytes)
  • Constructor: CascadeStands_Ctor (0x438750) — thiscall(obj, board, mesh) ret 8
  • Vtable: 0x4D50C0
  • Mesh: "Levels\Level4-Mace" (VA 0x4D0974)
  • Update: vtable[11] @ 0x43F3C0 — swing animation with sin/cos oscillation
  • Render: vtable[18] @ 0x45E0E0 — shared base render

Key Fields

Offset Type Value Description
+0x10D8 float player X Current position X
+0x10DC float player Y + 20 Current position Y (elevated)
+0x10E0 float player Z Current position Z
+0x10E8 float 80.0 Swing amplitude (radius)
+0x10F4 int 1 Active flag
+0x10F8 int 50 Timer

Collision

  • N:MACE — ball touching mace gets bounced (calls ball->vtable[8])
  • E:MACETRIGGER — activates mace swing (sets +0x10F0=1)
  • Both handled in Level_HandleCollision (0x40DCD0) via board+0x5000 mace list

Notes

  • Position offset is +0x10D8 (not +0x10D4 like most other objects)
  • Constructor takes 2 params (board + mesh), unlike Bonk/Sawblade which take position params
  • CascadeStands_Ctor internally calls Stands_ctor + allocates CollisionLevel
  • Swing animation uses amplitude 80.0 and 0.05/frame angle increment (~71.5°/sec at 25fps)

Full Analysis

See docs/MACE_WINDMILL_CHOMPER_SYSTEM.md for complete reverse-engineering documentation of Mace, Windmill, and Chomper systems.


🔗 Related Documents

Global Neon Mod

types : mods
keywords :

📂 View source on GitHub


Global Neon Mod — Spawn Objects + Neon Lighting

Two independent features, toggled via CE address symbols:

  1. Spawn Neon Objects — Spawns 6 neon race objects at ball position
  2. Neon Lighting — Darkens the scene and attaches a point light to the ball

Controls

Symbol Address (CE) Default Description
SpawnNeon alloc'd 0 Set to 1 to spawn neon objects at ball position
NeonLighting alloc'd 1 1 = neon lighting ON (dark scene + ball light), 0 = OFF
NeonAmbient alloc'd 0x000C0C14 Ambient color (0x00RRGGBB). Lower = darker. Set to 0 for pitch black.

Feature 1: Neon Object Spawning

Spawns all 6 neon race objects on any level with a flag.

Objects Spawned

  1. NEONPLATFORM — Disappearing neon floor (alloc 0x10EC, ctor 0x43E110)
  2. DFLOOR1 — Disappearing floor 1 (alloc 0x1104, ctor 0x43E450)
  3. DFLOOR2 — Disappearing floor 2
  4. DFLOOR3 — Disappearing floor 3
  5. DFLOOR4 — Disappearing floor 4
  6. TRODE — Neon electrode/tube (alloc 0x1104, ctor 0x43E450)

Usage

  1. Load GlobalNeon.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnNeon to 1 in CE (or use a hotkey to set it)
  4. All 6 objects spawn at the player's position, spread out in a line

Mesh Loading

All 6 meshes are loaded on first spawn (cached):

  • Levels\LevelDark-NeonPlatform → mesh_neonplatform
  • Levels\LevelDark-DFloor1 → mesh_dfloor1
  • Levels\LevelDark-DFloor2 → mesh_dfloor2
  • Levels\LevelDark-DFloor3 → mesh_dfloor3
  • Levels\LevelDark-DFloor4 → mesh_dfloor4
  • Levels\LevelDark-Trode → mesh_trode

Object Placement

  • NEONPLATFORM: at player position (X, Y, Z)
  • DFLOOR1: Y+20 (above player)
  • DFLOOR2: X+20
  • DFLOOR3: X+40
  • DFLOOR4: X+60
  • TRODE: Z+20

Feature 2: Neon Lighting

Replicates Neon Race's dark atmosphere on any level. The scene background and ambient are darkened, and a D3D point light is attached to the ball position.

How It Works

The hook intercepts Graphics_RenderScene (0x454BC0) per frame:

  1. Darkens ambient — Overrides gfx+0x730 (ambient color) with NeonAmbient value
  2. Disables fog — Sets gfx+0x734 = 0, gfx+0x738 = black
  3. Enables D3D lighting — Calls SetRenderState(D3DRS_LIGHTING, TRUE)
  4. Sets point light at ball position — Reads ball XYZ from gfx+0x854/858/85C (populated by Scene_Render), updates a pre-allocated D3DLIGHT8 structure, and calls SetLight(7, ...) + LightEnable(7, TRUE) on the D3D device

Light Properties

Property Value
Type D3DLIGHT_POINT
Diffuse (0.8, 0.9, 1.0) — cool white-cyan
Specular (0.3, 0.4, 0.5) — faint highlight
Range 500.0
Attenuation0 0.0 (no constant)
Attenuation1 0.02 (linear falloff)
Light Index 7 (unused by game's default lights)

Tuning

  • Darkness: Change NeonAmbient in CE. 0x00000000 = pitch black, 0x00202020 = dim, 0x00404040 = moderate.
  • Light brightness: Edit the light_data diffuse values in the script (offsets 4-12 in the D3DLIGHT8 struct).
  • Light range: Edit offset 64 in light_data (float). 500.0 = covers visible area, 250.0 = tight spotlight.
  • Light falloff: Edit offset 72 (Attenuation1). Higher = shorter range. 0.02 = gradual, 0.05 = steep.

Hook Points

Hook Address Function Original Bytes
1 (spawn) 0x405E22 Ball_Update 8B 86 5C 0C 00 00 (6 bytes)
2 (lighting) 0x454BC0 Graphics_RenderScene 81 EC C0 00 00 00 (6 bytes)

Technical Notes

  • The lighting hook fires once per frame, before Graphics_SetupLights is called by the original function. This means the game's own lighting setup runs afterward, but our ambient/light overrides persist because we write directly to the gfx struct and D3D device state.
  • Ball position (gfx+0x854/858/85C) is written by Scene_Render (0x41A2E0) each frame before Graphics_RenderScene is called, so the light position is always current.
  • Light index 7 is used to avoid conflicts with the game's own light setup (which uses indices 0-3 at most).
  • When NeonLighting is set to 0, the hook disables light 7 and lets the game's original lighting run unmodified.
  • D3DLIGHT8 struct is 104 bytes (not 88 — includes Direction vector between Position and Range). Position is at offset 52 (three floats). The struct is pre-filled in the script with all necessary fields; only Position.x/y/z are updated per-frame.

🔗 Related Documents

Global Sawblade Mod

types : mods
keywords :

📂 View source on GitHub


Global Sawblade Mod

Spawns Expert Race sawblades on any level with a hotkey.

Usage

  1. Load GlobalSaw.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnSaw to 1 in CE (or use a hotkey)
  4. Saw spawns at player 1's position and auto-activates

How It Works

  • Hooks at Ball_Update (0x405E22)
  • On spawn: allocates 0x111C bytes, calls Sawblade_Level_Ctor (0x434660)
  • Constructor loads Meshes\sawblade mesh internally (no pre-loaded mesh needed)
  • Auto-activates: sets +0x110D=0 (clear alert) and +0x1114=1 (activated)
  • Direction: +0x10F8=1 (Z-axis movement)
  • Per-frame: calls vtable[11] (Sawblade_Update @ 0x439BB0) for spin, debris, collision, movement
  • Registered to board+0x2578 (general list) and board+0xCD4

Object Details

  • Alloc size: 0x111C (4380 bytes)
  • Constructor: 0x434660 (Sawblade_Level_Ctor, thiscall, params: board, x, y, z)
  • Vtable: 0x4D5240
  • Mesh: "Meshes\sawblade" (loaded internally by Level_ctor)
  • Update: vtable[11] @ 0x439BB0
  • Render: vtable[18] @ 0x4347E0

Key Fields

Offset Description
+0x10D0 Board pointer
+0x10D4/+0x10D8/+0x10DC Current position XYZ
+0x10E0/+0x10E4/+0x10E8 Home position XYZ
+0x10F0 Rotation angle (RNG 0-360)
+0x10F4 Spin speed (0→25.0)
+0x10F8 Direction (1=Z-axis, 2=X-axis)
+0x10FC Debris spawn counter
+0x110D Alert flag
+0x1110 Spin velocity (500.0 init, decays ×0.95)
+0x1114 Activated flag (0=idle, 1=active)
+0x1118 Movement velocity

No Difficulty Gate

The original factory checks App+0x23C != 0 (Normal/Frenzied only).
This script bypasses that check — saws spawn on ALL difficulty levels.

Full Analysis

See docs/SAW_DRAWBRIDGE_SYSTEM.md for complete reverse-engineering documentation.


🔗 Related Documents

Global Sky Race Object Spawner

types : objects
keywords :

📂 View source on GitHub


Global Sky Race Object Spawner — FUNBALL, PILLAR, MAGNIFYER

Spawns Sky Race objects at Player 1's position in any level via hotkey.

Usage

  1. Set ObjectType (1-3):
    • 1 = FUNBALL (PopCylinder — blue ball that pops up near goal)
    • 2 = PILLAR (Platform — pillar that pops up and blocks path)
    • 3 = MAGNIFYER (CollisionLevel_Spatial — magnifying glass lens effect)
  2. Set SpawnObject to 1 to spawn at player position

Object Details

FUNBALL (PopCylinder)

The blue ball that pops up near the goal in Sky Race.

  • Constructor: PopCylinder_ctor (0x436EE0), alloc 0x10E8
  • RET: 0x14 (5 params: board, X, Y, Z, mesh)
  • vtable: 0x4D58F0
  • Mesh: "levels\level9-popcylinder1" (string at 0x4D0F5C)
  • Stands_ctor → renders automatically
  • Update (vtable[0x0B/+0x2C] = 0x43DED0): if +0x10E4 flag set → position update, then clear flag. SAFE.
  • Fields: +0x10D0=board, +0x10D4=X, +0x10D8=Y, +0x10DC=Z, +0x10E0=CollisionLevel, +0x10E4=1(active)

Note on "Meshes\FunBall": The string "Meshes\FunBall" (0x4D3474) is the ball sphere MESH MODEL loaded by the resource loader (App_ResourceLoader at 0x4298C0) into App+0x26C as a MeshNode (0x18 bytes). This is NOT a level object — it's the ball's sphere model used for rendering. It's loaded alongside Sphere (0x244), 8Ball (0x268), Bell (0x270), and Dizzy (0x274). The visible "funball" object in Sky Race that pops up near the goal is actually a POPCYLINDER created from the MESHWORLD file "levels\level9-popcylinder1".

PILLAR (Platform)

The pillars that suddenly pop up and block your path in Sky Race and Sky Arena.

  • Constructor: Platform_ctor (0x4363F0), alloc 0x10F4
  • RET: 0x14 (5 params: board, X, Y, Z, mesh)
  • vtable: 0x4D56A8
  • Mesh: "levels\level9-popcylinder1" (string at 0x4D0F5C)
  • Stands_ctor → renders automatically
  • Update (vtable[0x0B/+0x2C] = 0x436540): state machine (0=idle, 1=descending, 2=waiting, 3=ascending)
    • Pops up/down using Gfx_SetPosition for vertical movement
    • Timer at +0x10F0, state at +0x10EC. SAFE.
  • Fields: +0x10D0=board, +0x10D4=X, +0x10D8=Y, +0x10DC=Z, +0x10E0=CollisionLevel, +0x10E4=45.0, +0x10E8=-1.0

MAGNIFYER (Magnifying Glass)

The magnifying glass lens effect in Sky Race.

  • Constructor: CollisionLevel_Spatial_Ctor (0x436250), alloc 0x444
  • RET: 0x10 (4 params: board, X, Y, Z) — NO mesh parameter!
  • vtable: 0x4D569C (Pendulum variant)
  • INVISIBLE — creates a collision sphere (radius 90.0), NOT a visible mesh
  • Creates a lens effect that magnifies the ball when it passes through
  • Gate: Only created if difficulty != 0 (App+0x23C)
  • Fields: +0x04=board, +0x08=X, +0x0C=Y, +0x10=Z, +0x24=90.0, +0x28=90.0

Factory Chain

LevelBoard_Sky_ctor (0x41F930):
  LoadRaceData("SKYRACE")
  board+0x436C = MeshNode("meshes\skypillar")
  board+0x4370 = MeshNode("meshes\magnifyingglass")
  board+0x4384 = MeshWorld("levels\level9-popcylinder1")
  board+0x4388 = MeshWorld("levels\level9-popcylinder2")
  board+0x438C = MeshWorld("levels\level9-trapdoor")

Scene_SetupLevel9 (0x410830):
  "PILLAR"     → collected into board+0x4394 (AthenaList) for later popping
  "MAGNIFYER"  → CollisionLevel_Spatial_Ctor → board+0x46AC (difficulty gate)
  "CLOUDSCAPE" → position lookup via AthenaHashTable

Sky Factory (0x410AD0, vtable[0x21]):
  "POPCYLINDER" → Platform_ctor with board+0x4384 mesh
  "TRAPDOOR"    → Rotator_ctor with board+0x438C mesh

CreateLevelObjects (0x4121D0):
  "POPCYLINDER" → PopCylinder_ctor with mesh param

Mesh String Addresses

Address String Usage
0x4D0F5C levels\level9-popcylinder1 FUNBALL mesh (MESHWORLD)
0x4D0F40 levels\level9-popcylinder2 FUNBALL2 mesh (MESHWORLD)
0x4D0F90 meshes\skypillar PILLAR mesh (MeshNode)
0x4D0F78 meshes\magnifyingglass MAGNIFYER mesh (MeshNode)
0x4D0F28 levels\level9-trapdoor TRAPDOOR mesh (MESHWORLD)
0x4D3474 Meshes\FunBall Ball sphere model (MeshNode, App+0x26C)

Registration Pattern (same as SpeedCylinder script)

  1. Create MeshWorld via MeshWorld_ctor (alloc 0x10D0)
  2. Store mesh in board slot board+0x4788
  3. Call factory: CreateSpeedCylinder (0x4117B0) for FUNBALL
  4. Register in board+0xCD4 (post-alpha render list)
  5. Register in board+0x10EC (collision list)
  6. Call vtable[0x16] (update) and vtable[0x15] (render) on new object

🔗 Related Documents

Global Toob Race Object Spawne

types : mods
keywords :

📂 View source on GitHub


Global Toob Race Object Spawner — SAW, SAW2, FALLOUT1, SPINNY, BLOCKDAWGS

Spawns Toob Race objects at Player 1's position in any level.

Usage

  1. Set ObjectType (1-6):
    • 1 = SPINNY (rotating platform)
    • 2 = SAW (big saw blade)
    • 3 = SAW2 (small saw blade)
    • 4 = FALLOUT1 (fallout floor)
    • 5 = BLOCKDAWG1 (block dog enemy 1)
    • 6 = BLOCKDAWG2 (block dog enemy 2)
  2. Set SpawnObject to 1 to spawn at player position

How Each Object Is Created and Works

Factory Function (0x40FB28)

All Toob Race mechanical objects are created by a single unlabeled factory function at 0x40FB28. It uses __strnicmp to match ref names from the MESHWORLD file:

Ref Name Constructor Alloc RET Mesh Mesh Slot Path
SPINNY Rotator_ctor (0x435940) 0x1508 0x14 Level8-Spinny board+0x436C
SAW Stands_CtorCollision (0x43B780) 0x1110 0x18 Level8-Saw board+0x4370 SAWPATH
SAW2 Stands_CtorSpeedCylinder (0x43BE20) 0x1118 0x18 Level8-Saw board+0x4370 SMALLSAWPATH
FALLOUT1 Stands_CtorCollisionV2 (0x43BBC0) 0x10E8 0x14 Level8-Fallout board+0x4374
BLOCKDAWG1 Blockdawg_ctor (0x43C310) 0x1154 0x18 Level8-Blockdawg1 board+0x4378 DAWGPATH1
BLOCKDAWG2 Blockdawg_ctor (0x43C310) 0x1154 0x18 Level8-Blockdawg1 board+0x4378 DAWGPATH2

SPINNY (Rotator_ctor)

  • Constructor: Rotator_ctor(this, board, X, Y, Z, mesh) — 5 stack params, RET 0x14
  • Mesh: Levels\Level8-Spinny (string at 0x4D0E38)
  • vtable: 0x4D5518 (Lifter vtable)
  • CollisionLevel: +0x10D4
  • Position: +0x10D8 (X), +0x10DC (Y), +0x10E0 (Z)
  • Update (vtable[0x0B/+0x2C]): Catapult_Update (0x43E600) — rotates platform, transforms balls riding on it
  • No path needed — safe for global spawn

SAW (Stands_CtorCollision)

  • Constructor: Stands_CtorCollision(this, board, X, Y, Z, mesh, pathId) — 6 stack params, RET 0x18
  • Mesh: Levels\Level8-Saw (string at 0x4D0E24)
  • vtable: 0x4D5578 (Button vtable)
  • pathId: stored at +0x10D8
  • Update (0x43B8E0):
    • Reads +0x110C (byte flag). If 0 → skips entire update (safe, no path access)
    • If +0x110C != 0: reads +0x10F4 (state=2), uses +0x10D8 (pathId) → crashes if 0
  • CRASH FIX: Set +0x110C = 0 after construction → update does nothing, object renders but stays static

SAW2 (Stands_CtorSpeedCylinder)

  • Constructor: Stands_CtorSpeedCylinder(this, board, X, Y, Z, mesh, pathId) — 6 params, RET 0x18
  • Calls Stands_CtorCollision first, then overrides:
    • vtable → 0x4D5CA0 (Button variant)
    • +0x10F0 = 0xC2C80000 (float)
    • +0x1110 = 1 (flag)
    • +0x1114 = 0x3F800000 (1.0)
  • Update (0x43BEB0): Same +0x110C check → if 0, skips (safe)
  • CRASH FIX: Set +0x110C = 0 after construction

FALLOUT1 (Stands_CtorCollisionV2)

  • Constructor: Stands_CtorCollisionV2(this, board, X, Y, Z, mesh) — 5 params, RET 0x14
  • Mesh: Levels\Level8-Fallout (string at 0x4D0E0C)
  • vtable: 0x4D55D8 (SpeedCylinder vtable)
  • Update (0x43BD10): Reads +0x10D8 (pathId pointer), if null → skips path code gracefully
  • No crash fix needed — handles null pathId automatically

BLOCKDAWG1/2 (Blockdawg_ctor)

  • Constructor: Blockdawg_ctor(this, board, X, Y, Z, mesh, pathId) — 6 params, RET 0x18
  • Mesh: Levels\Level8-Blockdawg1 (string at 0x4D0DF0, same for both)
  • vtable: 0x4D5638 (ArenaObject vtable)
  • pathId: stored at +0x10F0
  • Update (0x43C4E0, ArenaObject_Update):
    • If +0x1150 == 0 (awake): follows path via Path_GetPosition(+0x10F0)crashes if 0
    • If +0x1150 != 0 (sleeping): plays wake-up sound + creates particle ring → safe
  • CRASH FIX: Set +0x1150 = 1 (sleeping mode)
  • BLOCKDAWG2 difference: Also sets +0x1152 = 1 (is_blockdawg2 flag, set by factory)

Rendering

All constructors call Stands_ctor(this, mesh) which clones the SpatialTree from the mesh. The mesh is added to the board's SpatialTree automatically. The board's render pipeline renders it through normal opaque/translucent passes — no manual render list registration needed.

This is why the user's Judge script (using Gear_Level_ctorLevel_ctor → empty mesh) showed nothing: Level_ctor doesn't clone a SpatialTree, so the board has no mesh to render. The Toob objects use Stands_ctor which properly sets up the mesh.

Registration

Factory code adds each object to board+0x2578 (active objects list) via AthenaList_Append (0x453810). The board's update loop iterates this list and calls vtable[0x0B] (+0x2C) for each object each frame.

String Table (0x4CFC80+)

Address String Purpose
0x4CFCB4 DAWGPATH3 Path for BLOCKDAWG3
0x4CFCC0 BLOCKDAWG3 Ref name (unused in standard Toob)
0x4CFCCC DAWGPATH2 Path for BLOCKDAWG2
0x4CFCD8 BLOCKDAWG2 Ref name
0x4CFCE4 DAWGPATH1 Path for BLOCKDAWG1
0x4CFCF0 BLOCKDAWG1 Ref name
0x4CFCFC FALLOUT1 Ref name
0x4CFD08 SMALLSAWPATH Path for SAW2
0x4CFD18 SAW2 Ref name
0x4CFD20 SAWPATH Path for SAW
0x4CFD28 SAW Ref name
0x4CFD2C SPINNY Ref name

🔗 Related Documents

Global Trapdoor Mod

types : mods
keywords :

📂 View source on GitHub


Global Trapdoor Mod

Spawns Tower Race trapdoors on any level with a hotkey.

Usage

  1. Load GlobalTrapdoor.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnTrapdoor to 1 in CE (or use a hotkey on that address)
  4. The trapdoor spawns at the player's position (slightly above)

How It Works

  • Hooks at Ball_Update (0x405E22) — same as Global Lifters
  • No mesh loading needed — trapdoor meshes are globally pre-loaded at App level
  • Initializes AthenaList at Board+0x47D0 (trapdoor list) if not already initialized
  • Allocates 0x10F8 bytes, calls GlassStands_Ctor(alloc, Board) @ 0x438290
  • Sets position at obj+0x10E0/+0x10E4/+0x10E8
  • Appends to:
    • Board+0x2578 (general objects list)
    • Board+0x47D0 (trapdoor list — for N:TRAPDOOR collision)
    • Board+0xCD4 (sub-object 1: Stands/Trapdoor2 mesh)
    • Board+0x10EC (sub-object 2: TipperVisual/Trapdoor2 collision)
    • Board+0x8AC→+0x480→+0x1C (primary collision level)
    • Board+0x8B0→+0x18 (secondary collision level)

Mesh Dependency

NONE! All trapdoor meshes are globally pre-loaded by the resource loader (0x4298C0):

  • App+0x594 = MeshWorld("Levels\Level4-Trapdoor1")
  • App+0x598 = MeshWorld("Levels\Level4-Trapdoor2")
  • App+0x59C = CollisionLevel(App+0x594)
  • App+0x5A0 = CollisionLevel(App+0x598)

Collision

Handled in TowerCollisionEvents (0x40DCD0):

  • "N:TRAPDOOR"Trapdoor_Activate(obj) @ 0x438410 — sets obj+0x10F4=10, plays sound
  • "E:OPENSESAME"Trapdoor_Open(obj) @ 0x4344D0 — opens first trapdoor in drawbridge list

Sub-objects at obj+0x10D8 and obj+0x10DC are appended to Board collision level lists
for physical ball-vs-trapdoor collision.

Addresses

Symbol Address
GlassStands_Ctor 0x438290
operator_new 0x4BA57B
AthenaList_Init 0x453210
AthenaList_Append 0x453810
App+0x594 (Trapdoor1 mesh) global
App+0x598 (Trapdoor2 mesh) global
App+0x59C (Trapdoor1 collision) global
App+0x5A0 (Trapdoor2 collision) global
Board+0x47D0 (trapdoor list)

🔗 Related Documents

Global Variables

types : objects
keywords :

📂 View source on GitHub


Hamsterball Global Variables

This document catalogs every global variable in the Hamsterball.exe binary,
derived from Ghidra analysis. Variables are organized by category.

Memory Layout

Section Range Description
.text 0x00401000 - 0x004CEFFF Code (executable instructions)
.rdata 0x004CF000 - 0x004F6FFF Read-only data (strings, vtables, IAT)
.data 0x004F7000 - 0x00536AF3 Mutable global variables
.data1 0x00537000 - 0x00537FFF Additional data
.rsrc 0x00538000 - 0x0058FFFF Resources (icons, cursors, DLL)

1. Game Engine Globals

These are the primary game state variables used by the Hamsterball engine.

g_App

  • Address: 0x004FD680
  • Type: App struct (2328 bytes / 0x918 bytes)
  • Description: The central application object. Contains all global game state including
    the D3D device, window handle, scene pointer, player data, input state, difficulty settings,
    tournament data, and rendering context. This is the single most important global in the game.
    Passed to App_Initialize_Full(), App_Run(), and App_Shutdown() from WinMain.
  • Used in:
    • WinMain+0x0A (DATA) — App_Initialize_Full(&g_App, ...)
    • WinMain+0x14 (DATA) — App_Run((int*)&g_App)
    • WinMain+0x1E (DATA) — App_Shutdown((int*)&g_App)
    • FUN_004ce4e0 (DATA) — LoadOrSaveConfig(&g_App.dwVtable)
    • Unwind@004ce3e6+0x1A (DATA) — exception handler cleanup

g_renderIndex

  • Address: 0x005341CC
  • Type: uint32 (4 bytes, signed integer)
  • Description: Render frame counter / sprite animation index. Incremented once per
    rendered object during Scene_RenderFrame. Used to index sprite animation ranges via
    SpriteAnim_SetRange(). Reset to 0 at the start of each frame when the scene is active.
  • Used in:
    • Scene_RenderFrame+0x3F (WRITE) — g_renderIndex = 0; (reset per frame)
    • Scene_RenderFrame+0x181 (READ) — SpriteAnim_SetRange(param_1, g_renderIndex);
    • Scene_RenderFrame+0x18E (READ_WRITE) — g_renderIndex = g_renderIndex + 1;

MeshWorld_vtable

  • Address: 0x004D9CDC (.rdata)
  • Type: pointer (4 bytes, pointer to vtable array)
  • Description: Virtual function table for the MeshWorld class. Assigned to all MeshWorld
    instances during construction (*(undefined***)this = &MeshWorld_vtable;). Contains
    virtual destructor and other MeshWorld virtual methods.
  • Used in:
    • MeshWorld_ctor+0x1A (DATA) — *this = &MeshWorld_vtable;
    • MeshObject_dtor+0x1F (DATA) — vtable lookup during destruction

PTR_OBJ_VTABLE

  • Address: 0x004F7360 (.data)
  • Type: pointer (4 bytes, function pointer table)
  • Description: Pointer to the base engine object vtable (at 0x004D8F88 in .rdata).
    Initialized at startup by FUN_004ce500. Used extensively (193 xrefs) for:
    • RNG operations (RNG_Rand(&PTR_OBJ_VTABLE, ...)) — contains the random number generator state
    • Object construction/destruction (sets vtable on new objects)
    • Scene camera setup, ball spawning, collision setup
    • Board level constructors, rumble board setup
    • Sound channel initialization
  • Initialized by: FUN_004ce500PTR_OBJ_VTABLE = &PTR_LAB_004d8f88;
  • Used in (193 locations, key ones):
    • Ball_ctor2+0x126RNG_Rand(&PTR_OBJ_VTABLE, 3, '\0') for random ball properties
    • Ball_Update+0xFF — per-frame ball physics
    • Ball_Shatter+0x21F — ball splitting mechanic
    • Scene_SetCamera+0x1EA — camera setup
    • Scene_SpawnBallsAndObjects+0x174 — level object spawning
    • Scene_UpdateBallsAndState+0x3E4 — main game logic update
    • Scene_ComputeArenaLighting+0x202 — arena lighting
    • Board_Master_Update+0x42 — master board update
    • CreateBumper+0x7F3, HandleArenaCollisionEvents+0x3C0, DispatchCollisionEvents+0x7AC — object creation
    • ArenaBoard_Update+0x331 — arena mode update
    • Sound_InitChannels+0x90audio system init
    • LoadingScreenGadget_Ctor+0x2E2 — loading screen
    • ScoreDisplay_SetTime+0x09 — score display
    • Many level dtor functions (LevelBoard_Dizzy_dtor, LevelBoard_Tower_dtor, etc.)

PTR_PTR_004f7188

  • Address: 0x004F7188 (.data)
  • Type: pointer (4 bytes, pointer to math lookup table)
  • Description: Pointer to the engine's trigonometric lookup table (at 0x004D8E5C in .rdata).
    Used as the first argument to Math_Atan2Angle(), Wave_Sin(), and Wave_Cos() for fast
    angle and wave computations. Contains pre-computed sine/cosine/atan2 tables for
    performance (avoids FPU transcendental calls). 121 xrefs across physics, rendering, and camera.
  • Initialized by: FUN_004ce4f0PTR_PTR_004f7188 = &PTR_LAB_004d8e5c;
  • Used in (121 locations, key ones):
    • Ball_ApplyForceV2+0x12FMath_Atan2Angle(&PTR_PTR_004f7188, x, z, 0, 0) for movement direction
    • Ball_ApplyForceWithMultipliers+0x12F — same with force multipliers
    • Ball_FallUpdate+0x44F — falling physics
    • Ball_Update+0x1F95 — main ball physics tick
    • Scene_SetCamera+0x111Wave_Sin(&PTR_PTR_004f7188, ...) for camera orbit
    • Scene_RenderWithCamera+0xBF — render camera transform
    • Scene_ComputeLighting+0xA5 — lighting calculation
    • Sprite_DrawRotatedQuad+0x152 — sprite rotation
    • Gear_AdvanceAlongPath+0x49A — gear path following
    • FlagWaver_UpdateVertices+0x48 — flag wave effect

PTR_PTR_004f7448

  • Address: 0x004F7448 (.data)
  • Type: pointer (4 bytes, pointer to string format table)
  • Description: Pointer to a level/texture name format string table. Used by
    AthenaString_Format(0x4f7448, &DAT_004d03f8) to generate texture filenames for levels.
    Initialized to &PTR_SoundBuffer_ScalarDtor_004d9124 by FUN_004ce510 (static init).
    99 xrefs — primarily in UI/menu constructors and render functions for texture loading.
  • Initialized by: FUN_004ce510PTR_PTR_004f7448 = &PTR_SoundBuffer_ScalarDtor_004d9124;
  • Used in (99 locations, key ones):
    • TimeTrialMenu_ctor+0x6EAthenaString_Format(0x4f7448, ...) for level textures
    • HighScoreMenu_Render+0x291 — high score display textures
    • ArenaBoard_Render+0x218 — arena rendering
    • DifficultyMenu_Render+0x252 — difficulty selection
    • RaceGoalReached_Tick+0x297 — race completion
    • Scene_SetupLevel10+0xBB — Master level setup
    • Scene_SetupLevelCascade+0xCA — Beginner Race setup
    • TourneyMenu_ctor+0x26F — tournament menu construction
    • MPMenu_ctor+0x356 — multiplayer menu
    • Options_Menu_dtor+0x1C8 — options cleanup
    • Ball_Update+0x1219 — ball rendering

PTR_PTR_004f77c0

  • Address: 0x004F77C0 (.data)
  • Type: pointer (4 bytes, pointer to diagnostic data)
  • Description: Pointer to a diagnostic data structure used by App_BuildDiagnosticReport.
    Initialized to &PTR_LAB_004da74c by FUN_004ce550. Used when building crash/error reports.
    Contains system information for the diagnostic report.
  • Initialized by: FUN_004ce550PTR_PTR_004f77c0 = &PTR_LAB_004da74c;
  • Used in:
    • App_BuildDiagnosticReport+0x4EF (DATA)
    • App_BuildDiagnosticReport+0x56C (DATA)
    • App_BuildDiagnosticReport+0x5E9 (DATA)
    • App_BuildDiagnosticReport+0x666 (DATA)
    • App_BuildDiagnosticReport+0x6E3 (DATA)

s_BACK

  • Address: 0x004D2334 (.rdata)
  • Type: char[5] (string literal "BACK\0", Ghidra labels as undefined1)
  • Description: The string literal "BACK". Used as the label for the 'Back' / 'Previous Menu'
    button across all menus and UI screens. 45 xrefs — appears in every menu constructor and handler.
  • Used in (45 locations, key ones):
    • ArenaMenu_ctor+0x6BD — arena selection back button
    • TourneyMenu_ctor+0x2C2 — tournament menu back button
    • KeyRemapMenu_Ctor+0x1E5 — key remapping back button
    • ConfirmMenu_ctor+0x7A — confirmation dialog back button
    • CreditsScreen_ctor+0x2B7 — credits screen back button
    • PracticeMenu_ctor+0xAF4 — practice level selection back button
    • OptionsMenu_ctor+0x312options menu back button
    • PauseMenu_Ctor+0xD6 — pause menu back button
    • DifficultyMenu_ctor+0x10B — difficulty menu back button
    • PauseMenu_HandleButtonClick+0xB2 — back button click handler
    • OptionsMenu_HandleButtonClick+0x481 — options back click handler
    • QuitRace+0x10B — quit race back button

2. D3D / Graphics Function Pointer Globals

These are function pointers set up at runtime for graphics operations. The D3D thunk system
uses indirect calls through these pointers to support multiple code paths (e.g., MMX vs scalar).

PTR_D3DX_ShaderDispatch0

  • Address: 0x004F7194
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for D3DX shader dispatch (shader profile 0). Set by
    D3DX_DetectShaderProfile() based on CPU capabilities. Called indirectly to execute
    vertex/pixel shader operations.
  • Used in:
    • D3DX_DetectShaderProfile+0x22 (WRITE) — assignment
    • D3DX_DetectShaderProfile+0x51 (WRITE) — assignment
    • D3DX_ShaderDispatch0+0x0 (INDIRECTION) — indirect call
    • D3DX_ShaderDispatch0+0x7 (INDIRECTION) — indirect call

PTR_D3DX_ShaderDispatch1

  • Address: 0x004F71B8
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for D3DX shader dispatch profile 1.
  • Used in: D3DX_ShaderDispatch1 (2 refs)

PTR_D3DX_ShaderDispatch2

  • Address: 0x004F71CC
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for D3DX shader dispatch profile 2.
  • Used in: D3DX_ShaderDispatch2 (2 refs)

PTR_D3DX_ShaderDispatch_noarg

  • Address: 0x004F71FC
  • Type: pointer (4 bytes, function pointer)
  • Description: No-argument variant of D3DX shader dispatch.
  • Used in: shader dispatch functions (2 refs)

PTR_D3DX_ShaderDispatch_noarg2

  • Address: 0x004F71F4
  • Type: pointer (4 bytes, function pointer)
  • Description: Second no-argument variant of D3DX shader dispatch.
  • Used in: shader dispatch functions (2 refs)

PTR_D3DX_ShaderDispatch_noarg4

  • Address: 0x004F721C
  • Type: pointer (4 bytes, function pointer)
  • Description: Fourth no-argument variant of D3DX shader dispatch.
  • Used in: shader dispatch functions (2 refs)

PTR_D3DX_ShaderDispatch_noarg5

  • Address: 0x004F71E8
  • Type: pointer (4 bytes, function pointer)
  • Description: Fifth no-argument variant of D3DX shader dispatch.
  • Used in: shader dispatch functions (2 refs)

PTR_D3DX_ShaderDispatch_2a / 2b / 2c

  • Addresses: 0x004F7208 / 0x004F720C / 0x004F7210
  • Type: pointer (4 bytes each, function pointers)
  • Description: D3DX shader dispatch variants 2a, 2b, 2c. Different parameter configurations
    for the shader execution pipeline.
  • Used in: respective ShaderDispatch_2a/2b/2c functions (2 refs each)

PTR_D3DX_ShaderDispatch_3

  • Address: 0x004F7238
  • Type: pointer (4 bytes, function pointer)
  • Description: D3DX shader dispatch variant 3.
  • Used in: D3DX_ShaderDispatch_3 (2 refs)

PTR_D3DX_ShaderDispatch_4

  • Address: 0x004F7214
  • Type: pointer (4 bytes, function pointer)
  • Description: D3DX shader dispatch variant 4.
  • Used in: D3DX_ShaderDispatch_4 (2 refs)

PTR_D3DX_ShaderDispatch_4b

  • Address: 0x004F71F8
  • Type: pointer (4 bytes, function pointer)
  • Description: D3DX shader dispatch variant 4b (alternative parameter format).
  • Used in: shader dispatch functions (2 refs)

PTR_Graphics_SetRenderState

  • Address: 0x004F719C
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for Graphics_SetRenderState. Called indirectly via
    (*(code*)PTR_Graphics_SetRenderState_004f719c)(); to set D3D render states.
  • Used in: Graphics_SetRenderState (2 refs)

PTR_Graphics_InitShaderDispatch

  • Address: 0x004F71AC
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for graphics shader dispatch initialization.
  • Used in: Graphics_InitShaderDispatch (2 refs)

PTR_Matrix_TransformVec4x3

  • Address: 0x004F7278
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for 4x3 matrix vector transformation. Used by
    D3DX_DetectShaderProfile() to select between CPU-specific implementations.
  • Used in: D3DX_DetectShaderProfile (4 refs — read and indirection)

PTR_D3D_Thunk0 through PTR_D3D_Thunk_C

  • Addresses: 0x004F722C (Thunk0), 0x004F7230 (Thunk4), 0x004F7234 (Thunk5),
    0x004F71D4 (Thunk_6), 0x004F724C (Thunk_7), 0x004F7250 (Thunk_8),
    0x004F7258 (Thunk_9), 0x004F725C (Thunk_A), 0x004F7264 (Thunk_B),
    0x004F7268 (Thunk_C)
  • Type: pointer (4 bytes each, function pointers)
  • Description: D3D device thunk function pointers. Each thunk detects the shader profile
    (D3DX_DetectShaderProfile) then calls through the stored pointer. Used to dispatch D3D
    device operations through the shader-accelerated code path. Each has 1 xref.

PTR_D3DX_Thunk3Param

  • Address: 0x004F7204
  • Type: pointer (4 bytes, function pointer)
  • Description: D3DX thunk for 3-parameter operations.
  • Used in: 1 xref (thunk dispatch)

PTR_D3DThunk_DrawIndexedPrimitiveUP

  • Address: 0x004F723C
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer for DrawIndexedPrimitiveUP D3D device operation.
    Dispatches indexed primitive drawing through the shader pipeline.
  • Used in: 1 xref (thunk dispatch)

PTR_D3DX_BoxFilter2x_Init / PTR_D3DX_BoxFilter2x_Init16

  • Addresses: 0x004FA638 / 0x004FA63C
  • Type: pointer (4 bytes each, function pointers)
  • Description: Function pointers for 2x box filter operations (texture downsampling).
    Set by D3DX_BoxFilter2x_Init() based on MMX availability:
    • No MMX: D3DX_BoxFilter2x_Scalar / D3DX_BoxFilter2x_NoMMX
    • With MMX: D3DX_BoxFilter2x_MMX / D3DX_BoxFilter2x_MMX
  • Used in:
    • D3DX_BoxFilter2x_Init+0x11/0x1D/0x32 (WRITE) — sets the function pointer
    • D3DX_BoxFilter2x_Init16+0x11/0x18/0x27 (WRITE) — sets the 16-bit variant
    • D3DX_BoxFilter2x_Dispatch+0x26 (READ) — calls through pointer
    • D3DX_BoxFilter2x_Dispatch16+0x26 (READ) — calls through pointer

PTR_D3DDevice_Reset (two entries)

  • Addresses: 0x004DB360 / 0x004DB3D0 (.rdata)
  • Type: pointer (4 bytes each)
  • Description: D3D device vtable entries for the Reset method. These are entries
    in the COM vtable for the D3D9 device interface, used by D3D_GetAdapterMode.
  • Used in: D3D_GetAdapterMode (2 refs each)

PTR_COM_QueryInterface

  • Address: 0x004DBEAC (.rdata)
  • Type: pointer (4 bytes)
  • Description: COM QueryInterface vtable entry. Used by COM_QueryInterface() to
    query D3D COM interfaces for specific GUIDs.
  • Used in: COM_QueryInterface (2 refs)

3. Resource Pool & Audio Globals

PTR_PTR_004fa7d8

  • Address: 0x004FA7D8
  • Type: pointer (4 bytes, pointer to vtable array)
  • Description: Pointer to a vtable array for D3D resource pool objects. Used by
    D3DResourcePool_Release() to call virtual destructors on pooled D3D resources.
    Indexed by resource type (puVar2[-0x40]). Also used by the Vorbis decoder.
  • Used in:
    • D3DResourcePool_Release+0x58 (DATA) — (**(code**)((&PTR_PTR_004fa7d8)[type] + 8))(obj);
    • Vorbis_InitDecodeChannel+0x175 (DATA)
    • Vorbis_FreeDecoder+0x97 (DATA)
    • Vorbis_ReadSetupHeader+0x1A6 (DATA)
    • Vorbis_DecodeAudioFrame+0x139 (DATA)

PTR_PTR_004fa7c4 / PTR_PTR_004fa7cc

  • Addresses: 0x004FA7C4 / 0x004FA7CC
  • Type: pointer (4 bytes each)
  • Description: Additional vtable arrays for D3D resource pool objects. Used alongside
    PTR_PTR_004fa7d8 by D3DResourcePool_Release() to dispatch virtual destructors for
    different resource types. Also used by Vorbis decoder and channel freeing.
  • Used in:
    • D3DResourcePool_Release+0x88/+0xB8 (DATA)
    • Vorbis_ReadSetupHeader+0xDD/+0x146 (DATA)
    • Channel_FreePairArray+0x11C/+0x15C (DATA)

4. Game Content / Level Data Globals

PTR_s_WARM-UP_RACE_004f7080

  • Address: 0x004F7080
  • Type: pointer (4 bytes, pointer to string array)
  • Description: Pointer to an array of race level name strings. Indexed by level number
    in TourneyMenu_GetRaceName: return (&PTR_s_WARM_UP_RACE_004f7080)[level_index];
    Contains strings like "WARM-UP RACE", "BEGINNER RACE", etc.
  • Used in: TourneyMenu_GetRaceName+0x03 (1 xref)

PTR_s_TAKE_YOUR_TIME_ON_THE_WARM-UP_RA_004f7148

  • Address: 0x004F7148
  • Type: pointer (4 bytes, pointer to string array)
  • Description: Pointer to an array of level description/tutorial strings. Contains
    helpful text like "TAKE YOUR TIME ON THE WARM-UP RACE" displayed when selecting levels.
  • Used in: 1 xref (level selection display)

PTR_s_HAMSTER_PELLET_004f70c8

  • Address: 0x004F70C8
  • Type: pointer (4 bytes, pointer to string)
  • Description: Pointer to the string "HAMSTER PELLET". Used in tournament/menu rendering
    for displaying hamster pellet-related UI elements (likely score or item display).
  • Used in:
    • TourneyMenu_Render+0xB07 (DATA)
    • ConfirmMenu_Render+0x75E (DATA)
    • TourneyMenu_TickWithRank+0x11F2 (DATA)
    • HighScoreEntry_DeletingDtor+0x69E (DATA)

PTR_Rsrc_DLL_1_409[304949]

  • Address: 0x004D5E88 (.rdata)
  • Type: pointer (4 bytes, pointer to resource data)
  • Description: Pointer to embedded DLL resource data (resource ID 304949). Used by
    OptionsMenu_ApplySettings for applying graphics/display settings. The embedded DLL
    is likely the eSellerate licensing engine or a D3D helper.
  • Used in:
    • OptionsMenu_ApplySettings+0x2B7/+0x30F/+0x496/+0x4EE (DATA)

PTR_RaceGoalReached_Render (two entries)

  • Addresses: 0x004D6C70 / 0x004D6CB8 (.rdata)
  • Type: pointer (4 bytes each)
  • Description: Vtable pointers for the ScoreObject class (race goal objects).
    Assigned in ScoreObject_ctor: *this = &PTR_RaceGoalReached_Render_004d6c70;
    Contains virtual render function for race finish/goal markers.
  • Used in:
    • ScoreObject_ctor+0x?? (1 xref each) — vtable assignment

PTR_Level_LoadCollision

  • Address: 0x004D90C8 (.rdata)
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer to Level_LoadCollision. Stored in a vtable for
    indirect calling during level collision mesh setup.
  • Used in: Level_LoadCollision (1 xref)

PTR_SceneObject_SetVisible

  • Address: 0x004D935C (.rdata)
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer to SceneObject_SetVisible. Stored in a vtable for
    indirect calling when toggling object visibility.
  • Used in: SceneObject_SetVisible (1 xref)

5. CRT / Runtime Globals

These are MSVC C Runtime (CRT) global variables used for CRT initialization,
exception handling, and locale management.

PTR_PTR_004fc664

  • Address: 0x004FC664
  • Type: pointer (4 bytes, pointer to lconv structure)
  • Description: Pointer to the CRT locale conversion (lconv) structure. Used by
    ___free_lconv_num and ___free_lconv_mon to compare locale string pointers before
    freeing — only frees strings that are NOT the static defaults from the lconv struct.
    Also used by CRT_GetLocalePtr.
  • Used in:
    • ___free_lconv_num+0x0B/+0x27 (READ)
    • ___free_lconv_mon+0x10/+0x2D/+0x4A/+0x67/+0x84/+0xA1/+0xBE (READ)
    • CRT_GetLocalePtr+0x00 (READ)

PTR_PTR_004fc6bc

  • Address: 0x004FC6BC
  • Type: pointer (4 bytes)
  • Description: Additional locale structure pointer. Used by ___updatetlocinfo
    (CRT thread locale info update).
  • Used in: ___updatetlocinfo (1 xref)

PTR_s_R6009_-_not_enough_space_for_env_004fc85c

  • Address: 0x004FC85C
  • Type: pointer (4 bytes, pointer to error string)
  • Description: Pointer to the CRT runtime error message string for error R6009
    ("not enough space for environment"). Used by CRT_RuntimeError to display the
    appropriate error message when the runtime encounters an out-of-memory condition.
  • Used in:
    • CRT_RuntimeError+0xDE/+0x11B/+0x141/+0x147/+0x150 (READ/DATA)

PTR_s_(null)_004fc840

  • Address: 0x004FC840
  • Type: pointer (4 bytes, pointer to string)
  • Description: Pointer to the string "(null)". Used by CRT string formatting
    functions as a fallback for NULL string pointers during printf/sprintf operations.
  • Used in: 2 xrefs in CRT string formatting

PTR_ReturnZero_004fce88

  • Address: 0x004FCE88
  • Type: pointer (4 bytes, function pointer)
  • Description: Function pointer to a function that returns zero. Used by
    FPU_WriteMathError as a handler for FPU exception processing. Called as
    iVar1 = (*(code*)PTR_ReturnZero_004fce88)(&local_28);
  • Used in:
    • FPU_WriteMathError+0x11D/+0x1D8/+0x26B (READ)

PTR_terminate_004fc82c

  • Address: 0x004FC82C
  • Type: pointer (4 bytes, function pointer)
  • Description: C++ std::terminate handler function pointer. Used by the CRT
    exception handling system (___InternalCxxFrameHandler) when an unhandled exception occurs.
  • Used in: ___InternalCxxFrameHandler (1 xref)

PTR_CRT_amsg_exit (6 entries)

  • Addresses: 0x004FC810 through 0x004FC824
  • Type: pointer (4 bytes each)
  • Description: CRT abort message exit handlers. Array of function pointers for
    different CRT abort error codes. Called when the CRT needs to terminate with a specific error.
  • Used in: 2 xrefs each (CRT abort dispatch)

PTR_CRT_InitSecurityCookie_004f7004

  • Address: 0x004F7004
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT security cookie initialization function pointer. Part of the
    MSVC stack buffer overrun protection mechanism.
  • Used in: 1 xref (CRT init)

PTR____onexitinit_004f7030

  • Address: 0x004F7030
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT atexit/onexit initialization function pointer.
  • Used in: 1 xref (CRT init)

PTR____endstdio_004f704c

  • Address: 0x004F704C
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT stdio cleanup function pointer. Called during CRT shutdown
    to flush and close standard I/O streams.
  • Used in: 1 xref (CRT shutdown)

PTR_CRT_SetUnhandledExceptionFilter_004f7058

  • Address: 0x004F7058
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT unhandled exception filter setup function pointer.
  • Used in: 1 xref (CRT init)

PTR_CRT_InitFPState_004fc458

  • Address: 0x004FC458
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT floating-point unit state initialization function pointer.
  • Used in: 1 xref (CRT init)

PTR___exit_004fc484

  • Address: 0x004FC484
  • Type: pointer (4 bytes, function pointer)
  • Description: CRT exit function pointer.
  • Used in: 1 xref (CRT shutdown)

s_0123456789ABCDEF_004f780c

  • Address: 0x004F780C
  • Type: char[17] (string literal "0123456789ABCDEF\0")
  • Description: Hexadecimal digit lookup table (uppercase). Used by CRT_FormatInteger
    when format specifier is %X (uppercase hex). Each character's index = its hex value.
  • Used in:
    • CRT_FormatInteger+0x0F (DATA)
    • CRT_FormatInteger+0x55 (DATA)
    • CRT_FormatInteger+0x7A (DATA)

s_0123456789abcdef_004f77f8

  • Address: 0x004F77F8
  • Type: char[17] (string literal "0123456789abcdef\0")
  • Description: Hexadecimal digit lookup table (lowercase). Used by CRT_FormatInteger
    when format specifier is %x (lowercase hex).
  • Used in:
    • CRT_FormatInteger+0x55 (DATA)
    • CRT_FormatInteger+0x7A (DATA)

s__004f76b8

  • Address: 0x004F76B8
  • Type: byte[49] (lookup table)
  • Description: Base32 decoding lookup table. Used by Base32_Decode to map
    Base32 characters to their decoded values: bVar2 = s__004f76b8[*param_2];
    Contains values 0x20 for invalid characters (whitespace). Part of the license key
    validation system (LicenseKey_Validate).
  • Used in:
    • Base32_Decode+0x1E (DATA)
    • Base32_Encode (1 xref)

6. Licensing / eSellerate Globals

These globals support the eSellerate DRM/licensing system embedded in the game.

s_\eSellerateEngine.dll_004f74b0

  • Address: 0x004F74B0
  • Type: char[22] (string literal)
  • Description: Filename string "\eSellerateEngine.dll". Used by
    eSellerate_ExtractDLLNull to locate and extract the eSellerate licensing DLL.
  • Used in: eSellerate_ExtractDLL (1 xref)

s_Software\eSellerate\Affiliates\%_004f74cc

  • Address: 0x004F74CC
  • Type: char[37] (string literal)
  • Description: Registry path template "Software\eSellerate\Affiliates\%s".
    Used for reading/writing eSellerate affiliate data in the Windows registry.
  • Used in: eSellerate_ExtractDLL (1 xref)

s_RegCloseKey / s_RegQueryValueExA / s_RegOpenKeyExA / s_advapi32

  • Addresses: 0x004F74F4 / 0x004F7500 / 0x004F7514 / 0x004F7524
  • Type: char[] (string literals)
  • Description: Windows API function name strings used by eSellerate_ExtractDLL
    to dynamically load registry functions from advapi32.dll via GetProcAddress.
    Strings: "RegCloseKey", "RegQueryValueExA", "RegOpenKeyExA", "advapi32".
  • Used in: eSellerate_ExtractDLL (1 xref each)

s_MVESD_Entry2_004f77d4 / s_eSellerateEngine_004f77e4

  • Addresses: 0x004F77D4 / 0x004F77E4
  • Type: char[] (string literals)
  • Description: eSellerate engine entry point and module name strings.
    "MVESD_Entry2" is the DLL export function name; "eSellerateEngine" is the module name.
  • Used in: 1 xref each (eSellerate init)

PTR_s_http://bugs.raptisoft.com / PTR_s_RaptisoftBugTracker

  • Addresses: 0x004F77C4 / 0x004F77C8
  • Type: pointer (4 bytes each)
  • Description: Pointers to the Raptisoft bug tracker URL string and name.
    Used when building diagnostic/crash reports to include the bug submission URL.
  • Used in: 1 xref each (diagnostic report)

7. RTTI / Exception Handling Globals

vftable @ 0x004e9b44

  • Address: 0x004E9B44 (.rdata)
  • Type: pointer (4 bytes, RTTI vtable)
  • Description: RTTI type info vtable. Used by TypeInfo_Dtor for destroying
    C++ type_info objects. Referenced by multiple RTTI Type Descriptor structures.
  • Used in:
    • TypeInfo_Dtor+0x5 (DATA)
    • RTTI Type Descriptor entries at 0x004F7450, 0x004F7468, 0x004F7488,
      0x004FC414, 0x004FC434, 0x004FC4A0

PTR_RTTI_Type_Descriptor entries

  • Addresses: 0x004EE664, 0x004EE67C, 0x004EE6C4, 0x004EE710 (.rdata)
  • Type: pointer (4 bytes each)
  • Description: RTTI Type Descriptor pointers. Used by the C++ runtime for
    dynamic_cast and typeid operations. Point to TypeDescriptor structures that
    describe class type information.
  • Used in: 1-3 xrefs each (RTTI lookup)

PTR_PTR_004f7820

  • Address: 0x004F7820
  • Type: pointer (4 bytes)
  • Description: Pointer to a CRT string table entry. Initialized to
    &PTR_LAB_004db4dc by FUN_004ce570. Used by AthenaString_AssignCRLF
    for CRLF string assignment operations.
  • Initialized by: FUN_004ce570PTR_PTR_004f7820 = &PTR_LAB_004db4dc;
  • Used in:
    • AthenaString_AssignCRLF+0x?? (1 xref)
    • FUN_004ce570 (WRITE, static init)

8. Miscellaneous Globals

DWORD_004f5b80

  • Address: 0x004F5B80
  • Type: dword (4 bytes, signed integer)
  • Description: Referenced by sub_004001a0 (CRT startup code in the PE header region).
    Likely a CRT initialization flag or security cookie value.
  • Used in: sub_004001a0 (1 xref)

PTR_s_Bogus_message_code_%d_004e42d8

  • Address: 0x004E42D8 (.rdata)
  • Type: pointer (4 bytes)
  • Description: Pointer to format string "Bogus message code %d". Used for
    displaying unknown/invalid Windows message codes in the window procedure.
  • Used in: 1 xref (window message handler)

u_null)_004e9e86

  • Address: 0x004E9E86 (.rdata)
  • Type: wchar16[6] (Unicode string)
  • Description: Unicode string "(null)". Used as a fallback for NULL wide-string
    pointers in CRT wide-character formatting functions.
  • Used in: 1 xref (CRT wide string formatting)

Summary Table

# Name Address Type Size Xrefs Category
1 PTR_OBJ_VTABLE 0x004F7360 pointer 4 193 Engine (RNG/object vtable)
2 PTR_PTR_004f7188 0x004F7188 pointer 4 121 Engine (trig lookup table)
3 PTR_PTR_004f7448 0x004F7448 pointer 4 99 Engine (texture format table)
4 s_BACK 0x004D2334 char[5] 1* 45 UI (back button label)
5 PTR_PTR_004fc664 0x004FC664 pointer 4 11 CRT (locale lconv)
6 PTR_PTR_004f77c0 0x004F77C0 pointer 4 6 Engine (diagnostic data)
7 PTR_D3DX_BoxFilter2x_Init 0x004FA638 pointer 4 6 Graphics (box filter)
8 PTR_PTR_004fa7d8 0x004FA7D8 pointer 4 5 Audio/Graphics (vtable array)
9 PTR_s_R6009 0x004FC85C pointer 4 5 CRT (error string)
10 g_App 0x004FD680 App struct 2328 5 Engine (main app object)
11 PTR_s_HAMSTER_PELLET 0x004F70C8 pointer 4 4 UI (string pointer)
12 PTR_D3DX_ShaderDispatch0 0x004F7194 pointer 4 4 Graphics (shader dispatch)
13 PTR_Matrix_TransformVec4x3 0x004F7278 pointer 4 4 Graphics (matrix ops)
14 PTR_D3DX_BoxFilter2x_Init16 0x004FA63C pointer 4 4 Graphics (box filter 16)
15 s_0123456789ABCDEF 0x004F780C char[17] 17 3 CRT (hex lookup)
16 PTR_PTR_004fa7c4 0x004FA7C4 pointer 4 3 Audio (vtable array)
17 PTR_PTR_004fa7cc 0x004FA7CC pointer 4 3 Audio (vtable array)
18 PTR_ReturnZero 0x004FCE88 pointer 4 3 CRT (FPU error handler)
19 g_renderIndex 0x005341CC uint32 4 3 Engine (render counter)
20 PTR_Graphics_SetRenderState 0x004F719C pointer 4 2 Graphics (render state)
21 PTR_Graphics_InitShaderDispatch 0x004F71AC pointer 4 2 Graphics (shader init)
22 PTR_D3DX_ShaderDispatch1 0x004F71B8 pointer 4 2 Graphics (shader dispatch)
23 PTR_D3DX_ShaderDispatch2 0x004F71CC pointer 4 2 Graphics (shader dispatch)
24 PTR_D3DX_ShaderDispatch_noarg5 0x004F71E8 pointer 4 2 Graphics (shader dispatch)
25 PTR_D3DX_ShaderDispatch_noarg2 0x004F71F4 pointer 4 2 Graphics (shader dispatch)
26 PTR_D3DX_ShaderDispatch_4b 0x004F71F8 pointer 4 2 Graphics (shader dispatch)
27 PTR_D3DX_ShaderDispatch_noarg 0x004F71FC pointer 4 2 Graphics (shader dispatch)
28 PTR_D3DX_ShaderDispatch_2a 0x004F7208 pointer 4 2 Graphics (shader dispatch)
29 PTR_D3DX_ShaderDispatch_2b 0x004F720C pointer 4 2 Graphics (shader dispatch)
30 PTR_D3DX_ShaderDispatch_2c 0x004F7210 pointer 4 2 Graphics (shader dispatch)
31 PTR_D3DX_ShaderDispatch_4 0x004F7214 pointer 4 2 Graphics (shader dispatch)
32 PTR_D3DX_ShaderDispatch_noarg4 0x004F721C pointer 4 2 Graphics (shader dispatch)
33 PTR_D3DX_ShaderDispatch_3 0x004F7238 pointer 4 2 Graphics (shader dispatch)
34 s__004f76b8 0x004F76B8 byte[49] 49 2 Licensing (Base32 table)
35 s_0123456789abcdef 0x004F77F8 char[17] 17 2 CRT (hex lookup)
36 PTR_PTR_004f7820 0x004F7820 pointer 4 2 CRT (string table)
37 PTR_s_(null) 0x004FC840 pointer 4 2 CRT (null string fallback)
38 PTR_s_WARM-UP_RACE 0x004F7080 pointer 4 1 Game (level names)
39 PTR_s_TAKE_YOUR_TIME 0x004F7148 pointer 4 1 Game (level descriptions)
40 PTR_D3D_Thunk_6 0x004F71D4 pointer 4 1 Graphics (D3D thunk)
41 PTR_D3DX_Thunk3Param 0x004F7204 pointer 4 1 Graphics (D3D thunk)
42 PTR_D3D_Thunk0 0x004F722C pointer 4 1 Graphics (D3D thunk)
43 PTR_D3D_Thunk4 0x004F7230 pointer 4 1 Graphics (D3D thunk)
44 PTR_D3D_Thunk5 0x004F7234 pointer 4 1 Graphics (D3D thunk)
45 PTR_D3DThunk_DrawIndexedPrimUP 0x004F723C pointer 4 1 Graphics (D3D thunk)
46 PTR_D3D_Thunk_7 through _C 0x4F724C-4F7268 pointer 4 each 1 each Graphics (D3D thunks)
47 s_\eSellerateEngine.dll 0x004F74B0 char[22] 22 1 Licensing (DLL name)
48 s_Software\eSellerate... 0x004F74CC char[37] 37 1 Licensing (reg path)
49 s_RegCloseKey 0x004F74F4 char[12] 12 1 Licensing (API name)
50 s_RegQueryValueExA 0x004F7500 char[17] 17 1 Licensing (API name)
51 s_RegOpenKeyExA 0x004F7514 char[14] 14 1 Licensing (API name)
52 s_advapi32 0x004F7524 char[9] 9 1 Licensing (DLL name)
53 PTR_s_http://bugs.raptisoft 0x004F77C4 pointer 4 1 Misc (bug tracker URL)
54 PTR_s_RaptisoftBugTracker 0x004F77C8 pointer 4 1 Misc (bug tracker name)
55 s_MVESD_Entry2 0x004F77D4 char[13] 13 1 Licensing (entry point)
56 s_eSellerateEngine 0x004F77E4 char[17] 17 1 Licensing (module name)
57 PTR_PTR_004fc6bc 0x004FC6BC pointer 4 1 CRT (locale update)
58 PTR_terminate 0x004FC82C pointer 4 1 CRT (std::terminate)
59 MeshWorld_vtable 0x004D9CDC pointer 4 2 Engine (MeshWorld vtable)
60 vftable 0x004E9B44 pointer 4 7 RTTI (type info vtable)
61 PTR_Rsrc_DLL_1_409 0x004D5E88 pointer 4 4 Resource (embedded DLL)
62 PTR_RaceGoalReached_Render (x2) 0x4D6C70/4D6CB8 pointer 4 each 1 each Game (ScoreObject vtable)
63 PTR_Level_LoadCollision 0x004D90C8 pointer 4 1 Game (collision func ptr)
64 PTR_SceneObject_SetVisible 0x004D935C pointer 4 1 Game (visibility func ptr)
65 PTR_D3DDevice_Reset (x2) 0x4DB360/4DB3D0 pointer 4 each 2 each Graphics (D3D vtable)
66 PTR_COM_QueryInterface 0x004DBEAC pointer 4 2 Graphics (COM vtable)
67 DWORD_004f5b80 0x004F5B80 dword 4 1 CRT (init flag)
68 PTR_s_Bogus_message_code 0x004E42D8 pointer 4 1 Misc (error string)
69 u_null) 0x004E9E86 wchar16[6] 12 1 CRT (unicode null)
70 PTR_CRT_InitSecurityCookie 0x004F7004 pointer 4 1 CRT (security cookie)
71 PTR____onexitinit 0x004F7030 pointer 4 1 CRT (atexit init)
72 PTR____endstdio 0x004F704C pointer 4 1 CRT (stdio cleanup)
73 PTR_CRT_SetUnhandledExFilter 0x004F7058 pointer 4 1 CRT (exception filter)
74 PTR_CRT_InitFPState 0x004FC458 pointer 4 1 CRT (FPU init)
75 PTR___exit 0x004FC484 pointer 4 1 CRT (exit)
76 PTR_CRT_amsg_exit (x6) 0x4FC810-4FC824 pointer 4 each 1-2 each CRT (abort handlers)
77 PTR_RTTI_Type_Descriptor (x4) 0x4EE664-4EE710 pointer 4 each 1-3 each RTTI (type descriptors)

*s_BACK is labeled as 1 byte by Ghidra but is actually a 5-byte string ("BACK\0").


Excluded from this document

The following categories of global data were excluded as they are not game-specific:

  • TEB (Thread Environment Block): ~70 entries at 0xFFDFF000-0xFFDFFFFF (system TEB fields)
  • IAT (Import Address Table): ~177 entries at 0x004CF000-0x004CF2FF (Windows API import thunks)
  • String constants: ~1666 string literals in .rdata (level names, object names, error messages, etc.)
  • Vtable destructor pointers: ~218 PTR_*_Dtor entries in .rdata (C++ virtual destructor function pointers)
  • FuncInfo/UnwindMapEntry: ~400+ entries (MSVC exception handling tables)
  • Resource data: Rsrc_ entries (icons, cursors, embedded DLL data)
  • TypeDescriptor (RTTI): 6 entries in .data (C++ RTTI type descriptors)

🔗 Related Documents

Global Windmill Mod

types : mods
keywords :

📂 View source on GitHub


Global Windmill Mod

Spawns Tower Race windmills on any level with a hotkey. Creates BOTH a visual mesh AND a collision level — the ball gets spun (N:SWIRL) when it touches the windmill.

Usage

  1. Load GlobalWindmill.CEA in Cheat Engine
  2. Enable the script
  3. Set SpawnWindmill to 1 in CE (or use a hotkey)
  4. Windmill spawns at player 1's position (both visual + collision)

How It Works

This mod creates two objects per spawn:

Part A: Visual Mesh (for rendering)

  • operator_new(0x10D0)Stands_ctor (0x462850) with windmill mesh
  • Sets vtable to 0x4D8FB0 (renderable mesh)
  • Position at obj+0x10D4/+0x10D8/+0x10DC
  • Registered to board+0x2578 (general list) + board+0xCD4 + scene tree

Part B: CollisionLevel (for N:SWIRL ball spinning)

  • operator_new(0x10D0)CollisionLevel_ctorWithLevel (0x465080)
  • Level_LoadMeshes (0x465200) loads collision data
  • Position at coll+0x10D8/+0x10DC/+0x10E0
  • Registered to board+0x2578 + board+0xCD4 + scene tree
  • Registered with scene manager via SceneObject_SetupCallback(0x4F7360, 0x168, 0)
  • N:SWIRL events baked into mesh → fire automatically when ball touches

Mesh Loading

  • On first spawn: loads Levels\Level4-Windmill mesh via MeshWorld_ctor
  • Stores at board+0x437C (same slot as Tower ctor)
  • On Tower Race, mesh is already loaded — reuses it

How N:SWIRL Works

The windmill mesh's collision triangles have N:SWIRL event names embedded in them. When the ball touches them:

Rotator_AddBall(board, ball);  // 0x43B6F0

This applies a rotation matrix to the ball's position and velocity, spinning it around the windmill center. A 10-frame tick counter resets each frame the ball stays on the rotator.

Object Details

Component Alloc Constructor Vtable
Visual 0x10D0 Stands_ctor (0x462850) 0x4D8FB0
Collision 0x10D0 CollisionLevel_ctorWithLevel (0x465080) N/A (collision-only)

Full Analysis

See docs/WINDMILL_JUDGE_SYSTEM.md for complete reverse-engineering documentation.


🔗 Related Documents

Global Wobbly Race Object Spaw

types : objects
keywords :

📂 View source on GitHub


Global Wobbly Race Object Spawner — WAVY1, WOBBLY1-4

Spawns Wobbly Race objects at Player 1's position in any level.

Usage

  1. Set ObjectType (1-5):
    • 1 = WAVY1 (wavy floor — wave-based physics surface)
    • 2 = WOBBLY1 (wobbly platform 1)
    • 3 = WOBBLY2 (wobbly platform 2)
    • 4 = WOBBLY3 (wobbly platform 3)
    • 5 = WOBBLY4 (wobbly platform 4)
  2. Set SpawnObject to 1 to spawn at player position

How Each Object Works

WAVY1 (Wavy Floor)

The actual wavy floor from Wobbly Race — a large physics surface that creates wave-based deformation.

  • Constructor: Stands_CtorWithCollisionLevel (0x43AD40), alloc 0x1AE7C (110,204 bytes — huge!)
  • RET: 0x14 (5 params: board, X, Y, Z, mesh_path_string)
  • Mesh: "Levels\Level7-Wavy1" (string at 0x4CFC54)
  • vtable: 0x4D5458
  • Update (vtable[0x0B/+0x2C] = 0x440390, Scene_UpdateArenaPhysics):
    • Increments counter at +0x10F8 each frame
    • When counter > 2: begins wave physics
    • Uses Wave_Sin for wave motion deformation across multiple segments
    • Calls Mesh_FindClosestCollision for ball collision
    • Transforms ball positions riding on the surface
    • No path access, no external dependencies → safe for global spawn

Key difference from WOBBLY: Takes a mesh path string (not MeshWorld pointer). The constructor internally calls MeshWorld_ctor(this, device, path) to create its own mesh. No need to create a separate MeshWorld.

WOBBLY1-4 (Wobbly Platforms)

Wobbly platforms that create a wave-like motion for balls riding on them.

  • Constructor: GameLevel_ctor (0x4351F0), alloc 0x1524
  • RET: 0x14 (5 params: board, X, Y, Z, mesh_pointer)
  • vtable: 0x4D53F8 (Spinner vtable)
  • Update (vtable[0x0B/+0x2C] = 0x43A700, Stands_Update):
    • Reads wave amplitudes at +0x10F0/+0x10F4
    • Reads position at +0x10D8/+0x10DC/+0x10E0
    • Reads wave scale at +0x1100 (=150.0)
    • Reads max amplitude at +0x10E8/+0x10EC (=10.0)
    • Iterates balls on platform, transforms positions (wave motion)
    • Uses Gfx_ScaleY/Z for deformation
    • No path access, no crash risks → safe for global spawn

Key difference from WAVY1: Takes a MeshWorld pointer (not path string). Must create a MeshWorld first via MeshWorld_ctor, then pass the pointer.

Factory Chain

LevelBoard_Wobbly_ctor (0x41F110):
  LoadRaceData("WOBBLYRACE")
  board+0x436C = MeshWorld("Levels\Level7-Wobbly1")
  board+0x4370 = MeshWorld("Levels\Level7-Wobbly2")
  board+0x4374 = MeshWorld("Levels\Level7-Wobbly3")
  board+0x4378 = MeshWorld("Levels\Level7-Wobbly4")
  board+0x437C = MeshWorld("Levels\Level7-Wobbly5")
  board+0x4380 = MeshWorld("Levels\Level7-Wobbly6")
  board+0x4384 = MeshWorld("Levels\Level7-Wobbly7")

Factory Dispatch (0x40F420, vtable[0x21]/+0x84):
  "WOBBLY1" → GameLevel_ctor, mesh=board+0x436C
  "WOBBLY2" → GameLevel_ctor, mesh=board+0x4370
  "WAVY1"   → Stands_CtorWithCollisionLevel, mesh_path="Levels\Level7-Wavy1"
  "WOBBLY3" → GameLevel_ctor, mesh=board+0x4374
  "WOBBLY4" → GameLevel_ctor, mesh=board+0x4378
  Fallback  → CreatePlatformOrStands (PLATFORM, STANDS)

Object Field Layout

WAVY1 — vtable 0x4D5458

Offset Type Description
+0x000 int* vtable (0x4D5458)
+0x10D0 int board pointer
+0x10D4 void* CollisionLevel pointer
+0x10E0 float position X
+0x10E4 float position Y
+0x10E8 float position Z
+0x10D8 int 0 (unused?)
+0x10F8 int frame counter (increments, wave physics starts at >2)
+0x1104 array Vec3List array (0x32 entries × 0x418 bytes)
+0xDDB4 array Vec3List array (0x32 entries × 0x418 bytes)
+0x1AA64 AthenaList ball list

WOBBLY1-4 — vtable 0x4D53F8

Offset Type Description
+0x000 int* vtable (0x4D53F8)
+0x10D0 int board pointer
+0x10D4 void* CollisionLevel pointer
+0x10D8 float position X
+0x10DC float position Y
+0x10E0 float position Z
+0x10E4 byte flag (1 = active)
+0x10E8 float max amplitude X (10.0)
+0x10EC float max amplitude Z (10.0)
+0x10F0 float wave amplitude X
+0x10F4 float wave amplitude Z
+0x1100 float wave scale (150.0)
+0x1104 byte freeze X flag
+0x1105 byte freeze Z flag
+0x1108 AthenaList ball list
+0x1520 int sound channel

Mesh String Addresses

Address String
0x4D0CF4 Levels\Level7-Wobbly1
0x4D0CDC Levels\Level7-Wobbly2
0x4D0CC4 Levels\Level7-Wobbly3
0x4D0CAC Levels\Level7-Wobbly4
0x4CFC54 Levels\Level7-Wavy1

String Table (0x4CFC40+)

Address String
0x4CFC44 WOBBLY4
0x4CFC4C WOBBLY3
0x4CFC54 Levels\Level7-Wavy1
0x4CFC68 WAVY1
0x4CFC70 WOBBLY2
0x4CFC78 WOBBLY1
0x4CFC80 N:WAVY
0x4CFC88 N:SQUAREWOBBLY

🔗 Related Documents

Gluebie Spawn Mod

types : mods
keywords :

📂 View source on GitHub


Gluebie Spawn Mod

Spawns animated Gluebie (tar blob) objects at Player 1's position on hotkey press.
Works globally in any race or arena.

What It Does

  • Press the CE hotkey to spawn a Gluebie at Player 1's exact position
  • Spawned Gluebies animate (wobble, rotate, scale) like original game Gluebies
  • Balls within ~30 units of a spawned Gluebie get the tar effect:
    • Ball slows down (velocity × 0.85 per frame)
    • Tar sound plays on first contact
    • When ball leaves range: recovers (flags cleared, normal speed restored)
  • Up to 16 Gluebies can be spawned simultaneously
  • Gluebie array clears on level/board change

How It Works

The Bug In v1

The original script used C array indices (0xB3, 0x1DA) as byte offsets in CE assembly.
In C, param_1[0xB3] accesses byte offset 0xB3*4 = 0x2CC, but in x86 assembly
[esi+0xB3] accesses byte offset 0xB3 directly. The correct byte offsets are:

C Index Byte Offset Field
0xB3 0x2CC tar_collision_flag
0x1DA 0x768 physics_enabled

What Was Fixed

  1. Byte offset correction: [esi+0xB3][esi+0x2CC], [esi+0x1DA][esi+0x768]
  2. Tar sound effect: Calls Sound_Play3D (0x459860) on first contact with a Gluebie
    • Gets sound resource via: ball+0x14 → board → +0x878 → scene → +0x484
    • Pushes ball X/Y/Z position as 3 floats + 1.0f (volume)
    • Only plays when ball+0x2CC == 0 (first contact, not already tarred)
  3. Recovery on leaving range: When ball moves away from all Gluebies:
    • Clears ball+0x2CC = 0 (tar_collision_flag)
    • Restores ball+0x768 = 1 (physics_enabled = normal)
    • This lets the ball recover speed after leaving the tar area

Tar Physics (in Ball_Update at 0x4081D9)

; Check physics_enabled flag
0x004081D9: MOV AL, [ESI+0x768]
0x004081DF: TEST AL, AL
0x004081E1: JNZ  0x004081F7      ; if 1 (normal) → speed boost

; TAR PATH (0x768 == 0): multiply blend by 0.85
0x004081E3: FLD  [ESI+0x764]     ; load vel_blend_factor
0x004081E9: FMUL [0x004CF4C0]    ; × 0.85
0x004081EF: FSTP [ESI+0x764]
0x004081F5: JMP  0x0040823D

; NORMAL PATH (0x768 != 0): multiply blend by 1.1
0x004081F7: FLD  [ESI+0x764]
0x004081FD: FCOMP [0x004CF538]   ; compare with 89128.96
0x0040820A: MOV  [ESI+0x764], 0x3C23D70A  ; cap to 0.01
0x0040821A: FMUL [0x004CF4B8]   ; × 1.1 (double)
0x00408233: MOV  [ESI+0x764], 0x3F800000  ; cap to 1.0

Addresses

Address Function Purpose
0x405E22 Ball_Update hook point Per-frame ball physics tick
0x437CB0 Gluebie_ctor Constructs Gluebie object (0x110C bytes)
0x4BA57B operator_new Memory allocation
0x461510 MeshWorld_ctor Loads Level3-Gluebie mesh
0x453810 AthenaList_Append Adds to render/update lists
0x459860 Sound_Play3D Plays 3D positioned sound
0x4D0728 "Level3-Gluebie" string Mesh file name

Ball Struct Offsets

Offset Type Field
+0x014 ptr Board/level pointer
+0x018 int Player index (0 = Player 1)
+0x164 float Ball X position
+0x168 float Ball Y position
+0x16C float Ball Z position
+0x2CC byte tar_collision_flag (1 = tarred)
+0x768 byte physics_enabled (1 = normal, 0 = tar)
+0x764 float vel_blend_factor (decayed by 0.85 in tar)

Gluebie Struct Offsets

Offset Type Field
+0x10D0 ptr Scene pointer
+0x10D4 float Position X
+0x10D8 float Position Y
+0x10DC float Position Z
+0x10F0 float Random rotation (0-360)
+0x10F4 float Direction (1.0 or -1.0)
+0x10F8 float Wobble speed
+0x1108 float Scale factor (1.0)

Constants

Address Type Value Purpose
0x4CF4C0 float32 0.85 Tar friction multiplier
0x4CF4B8 float64 1.1 Normal speed multiplier
0x4CF538 float64 89128.96 Speed cap threshold
0x4CF310 float32 1.0 Max blend factor

🔗 Related Documents

half_size_all

types : tools
keywords :

📂 View source on GitHub


half_size_all

Shrinks the player's ball to half size by inlining Ball_Shrink's physics fields

How It Works

Hooks Scene_SpawnBallsAndObjects at the point where the player ball has just been created and registered. A code cave checks if the ball's player index (ball+0x18) is 0, and if so, writes the same three fields that Ball_Shrink (0x00402200) sets — but without calling the function, so no sound effect plays.

Fields written (identical to Ball_Shrink):

Ball Offset Field Value Effect
+0x284 radius 13.0 (0x41500000) Half visual + collision size
+0x188 physics_scale 2.5 (0x40200000) Half max speed
+0xC4C is_shrunk 1 Shrunk physics state

Only player index 0 is affected. AI balls, split balls, follow balls, and board-init balls remain normal size.

Hook Details

Hook Point Address Original Instruction Catches
Scene_SpawnBallsAndObjects 0x0041C8D7 MOV byte [ESI+0x281], 0 (7 bytes) Player balls (loop, gated on index 0)

The code cave:

  1. CMP dword [ESI+0x18], 0 — is this player 0?
  2. If no → skip to step 4
  3. If yes → write radius=13.0, physics_scale=2.5, unused_init_flag=1 (NOTE: this flag is DEAD code — never read by any function, but we set it anyway to match original behavior)
  4. Execute original MOV byte [ESI+0x281], 0
  5. JMP back to 0x0041C8DE

Files

  • half_size_balls.c — C source code
  • bass.dll — Compiled DLL (PE32 i386)
  • half_size_balls.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll into the game folder
  3. Launch Hamsterball

A Hamsterball_half_size.log file is written next to the EXE showing whether the hook applied.


🔗 Related Documents

half_size_p1

types : mods

📂 View source on GitHub


half_size_p1

Halves Player 1's ball size only (conditional code caves)

Files

  • half_size_p1.c — C source code
  • bass_p1.dll — Compiled DLL (PE32 i386)
  • half_size_p1.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

Hammy Judge System

types : docs
keywords :

📂 View source on GitHub


Hammy Judge System (Expert Race)

Complete reverse-engineering analysis of the Judge ("Hammy Judge") system from the Expert Race in Hamsterball.

Overview

Hammy Judges are floating, spinning score-display objects placed around the Expert Race track. They show time values above their heads and activate when a ball rolls over E:ALERTJUDGES trigger zones.

Binary Addresses

Function Address Purpose
LevelBoard_Expert_ctor 0x41EA40 Expert level constructor — pre-loads judge meshes
CreateSawblade (Arena Factory) 0x40E590 Factory — creates JUDGE objects from level refs
Gear_Level_ctor 0x43A150 Judge constructor
Judge_Reset 0x434C40 Activates judge (called by E:ALERTJUDGES)
Gear_Update (vtable[11]) 0x434B60 Per-frame update (countdown + rotation)
Gear_Render (vtable[18]) 0x43A270 Renders mesh + floating 3D text
ScoreDisplay_SetTime 0x434C80 Sets score text with random modifiers
ExpertCollisionEvents 0x40E6A0 Collision handler (E:ALERTJUDGES, E:SCORE)
Gear_Level_Dtor 0x434B50 Destructor
Gear_DeletingDtor 0x43A250 Deleting destructor

Strings

String VA Purpose
JUDGE 0x4CFA14 Object name (factory lookup, strnicmp 5 chars)
E:ALERTJUDGES 0x4CFA70 Collision event — activates all judges
meshes\hammyjudge 0x4D0AA8 Mesh file for the judge visual
E:SCORE Collision event — sets score on all judges

Object Structure (0x1100 bytes = 4352)

Offset Size Type Description
+0x0000 4 ptr Vtable pointer (0x4D52B8 = Gear vtable)
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Position X
+0x10D8 4 float Position Y
+0x10DC 4 float Position Z
+0x10E0 4 float Scale (RNG 0–10)
+0x10E4 4 float Rotation angle (RNG 0–360, +2.0/frame)
+0x10E8 ~32 char[] Display text buffer ("%1.1f" formatted)
+0x10F0 4 int Text width (for centering)
+0x10F4 1 byte Active flag (1=active/idle, 0=activated/counting down)
+0x10F8 4 int Display value (init 400, decays ×0.8)
+0x10FC 4 int Countdown timer (RNG 100–150 frames)

Vtable (0x4D52B8)

Index Offset Address Function
0 0x00 0x43A250 Gear_DeletingDtor
11 0x2C 0x434B60 Gear_Update (countdown + rotation)
18 0x48 0x43A270 Gear_Render (mesh + 3D text)

Creation Flow

1. Level Constructor — LevelBoard_Expert_ctor (0x41EA40)

AthenaList_Init(board+0x4BBC, 0)           ← initialize judge list (empty)

MeshNode("meshes\hammyjudge") → board+0x4BB0  ← pre-load judge mesh 1
MeshNode("meshes\hammyjudge") → board+0x4BB4  ← pre-load judge mesh 2
MeshNode("meshes\hammyjudge") → board+0x4BB8  ← pre-load judge mesh 3

The Expert level uses Level5.MESHWORLD for its geometry. This mesh file contains N:JUDGE ref points and E:ALERTJUDGES / E:SCORE collision triggers.

2. Factory — CreateSawblade (0x40E590)

When the level parser encounters N:JUDGE refs in the mesh:

if (strnicmp(name, "JUDGE", 5) == 0) {
    obj = operator_new(0x1100);                    // 4352 bytes
    Gear_Level_ctor(obj, board, x, y, z);          // constructor
    AthenaList_Append(board+0x4BBC, obj);           // register to judge list
}

No difficulty gate! Unlike BONK (which checks App+0x23C != 0), JUDGE spawns on ALL difficulty levels.

3. Constructor — Gear_Level_ctor (0x43A150)

void Gear_Level_ctor(this, board, x, y, z) {
    Level_ctor(this, d3d_device);                  // base class init
    this->vtable = &Gear_Vtable;                   // 0x4D52B8
    this->pos = {x, y, z};                         // +0x10D4/+0x10D8/+0x10DC
    this->board = board;                           // +0x10D0
    this->scale = RNG(0, 10);                      // +0x10E0
    this->active = 1;                              // +0x10F4 (starts ACTIVE)
    this->display_value = 400;                     // +0x10F8
    this->countdown = RNG(100, 150);               // +0x10FC
    this->rotation = RNG(0, 360);                  // +0x10E4
    ScoreDisplay_SetTime(this, 0);                 // format display text
}

Activation — E:ALERTJUDGES

When a ball rolls over the E:ALERTJUDGES collision trigger, ExpertCollisionEvents (0x40E6A0) runs:

if (stricmp(event_name, "E:ALERTJUDGES") == 0) {
    // Iterate ALL judges in board+0x4BBC list
    for each judge in board+0x4BBC:
        Judge_Reset(judge);
}

Judge_Reset (0x434C40)

void Judge_Reset(this) {
    this->active = 0;                              // +0x10F4 = 0 (deactivate)
    
    // If not already in general render list, add it
    if (!AthenaList_Contains(board+0x2578, this)) {
        AthenaList_Append(board+0x2578, this);     // make visible
    }
}

This starts the countdown — the judge becomes visible and its display begins decaying.

Update — Gear_Update (vtable[11] @ 0x434B60)

Runs every frame from Scene_Update:

void Gear_Update(this) {
    if (this->active != 0) {                       // +0x10F4
        // Active/idle — just spin
        goto update_rotation;
    }
    
    // Inactive — counting down
    this->countdown--;                             // +0x10FC
    if (this->countdown > 0) {
        goto update_rotation;                      // still waiting
    }
    
    // Countdown finished — decay display
    if (this->display_value == 400) {              // +0x10F8
        // First decay step — play sound/effect at position
        play_effect(board->app->mgr, pos.x, pos.y, pos.z, 1.0);
    }
    
    this->countdown = 0;                           // reset timer
    this->display_value = (int)(this->display_value * 0.8);  // decay
    if (this->display_value < 1) {
        this->display_value = 0;                   // fully decayed
    }
    
update_rotation:
    this->rotation += 2.0;                         // +0x10E4 (always spins)
}

Display Decay Sequence

Starting at 400, each step takes RNG(100–150) frames (~1.7–2.5 seconds at 60fps):

Step Value Time
0 400 Initial (plays sound)
1 320 ~2s
2 256 ~2s
3 204 ~2s
4 163 ~2s
5 131 ~2s
... ... ...
~27 0 Fully decayed (~54s total)

Scoring — E:SCORE event

When a ball hits an E:SCORE trigger:

if (strnicmp(event_name, "E:SCORE", 7) == 0) {
    long value = atol(event_name + 7);  // parse number after "E:SCORE"
    for each judge in board+0x4BBC:
        ScoreDisplay_SetTime(judge, value);
}

ScoreDisplay_SetTime (0x434C80)

void ScoreDisplay_SetTime(this, base_time) {
    float value = base_time + RNG(0, 2);           // add random 0-2
    
    switch (RNG(0, 5)) {
        case 0: value += 0.0; break;               // 20% no change
        case 1: value -= 0.0; break;               // 20% no change (DAT=0.0)
        case 2: value += 1.0; break;               // 20% +1.0
        default: break;                             // 40% no change
    }
    
    // Format as text
    if (value < 0 || value > 10) {
        sprintf(this->display_buffer, "%s", "");   // empty if out of range
    } else {
        sprintf(this->display_buffer, "%1.1f", value);
    }
    
    this->text_width = Font_MeasureText(this->display_buffer) / 2;
}

Rendering — Gear_Render (vtable[18] @ 0x43A270)

void Gear_Render(this) {
    // Apply scale
    Gfx_ScaleX(this->scale + 290.0);               // +0x10E0 + 290
    
    // Position with oscillation (bobbing animation)
    float bob = Wave_Sin(this->rotation) * 0.0;
    float y = this->pos_y - 20.0 - this->countdown + bob;
    Gfx_SetPosition(this->pos_x, y, this->pos_z);
    
    // Draw the hammyjudge mesh
    board->judge_mesh_1->vtable[0x1C]();           // render mesh
    
    // Draw floating 3D score text
    Font_DrawGlyph3D(
        board->app->font,                          // font resource
        this->display_buffer,                       // text ("%1.1f" formatted)
        pos_with_transforms,                        // position
        scale, 0, -1.0, 0, 1.0, 1.0, 0,            // orientation/scale
        ...
    );
}

The judge mesh is drawn at board+0x4BB0's MeshNode render function, which renders the pre-loaded meshes\hammyjudge mesh. The 3D text floats above showing the score value.

Key Constants

Address Value Purpose
0x4D5318 0.8 Display value decay multiplier
0x4CF48C 2.0 Rotation increment per frame
0x4D5C80 290.0 Base scale offset
0x4CF370 20.0 Y position offset (raises judge above ground)
0x4CF9F8 10.0 RNG range for scale
0x4CF310 1.0 Score modifier (add)

Board Fields

Offset Size Description
+0x4BB0 4 Judge mesh 1 (MeshNode)
+0x4BB4 4 Judge mesh 2 (MeshNode)
+0x4BB8 4 Judge mesh 3 (MeshNode)
+0x4BBC ~24 AthenaList — Judge object list
+0x4FC8 4 Internal AthenaList iteration array

ExpertCollisionEvents Events

The Expert board collision handler (0x40E6A0) processes these events:

Event Action
E:CALLHAMMER CreateBonkPopup (Bonk appears)
E:HAMMERCHASE Hammer_ChaseStart (Bonk chases)
E:ALERTSAW1/2 Saw_AlertActivate (Sawblade warning)
E:ACTIVATESAW1/2 Saw_Activate (Sawblade fires)
E:ALERTJUDGES Judge_Reset on all judges (activate countdown)
E:SCORE ScoreDisplay_SetTime on all judges (set score)
E:JUMP Jump boost (Ball_DizzyImmunity +200)
E:BELL Bell_Activate + extra time bonus

Notes

  • The Expert race level uses Level5.MESHWORLD for its geometry
  • Three judge meshes are pre-loaded but the number of judge objects depends on how many N:JUDGE refs exist in the mesh
  • Judges are NOT difficulty-gated (unlike BONK which requires App+0x23C != 0)
  • The judge continuously spins at +2.0 degrees/frame regardless of state
  • The score display uses random ±modifiers to create slight variation between judges
  • When E:ALERTJUDGES fires, all judges activate simultaneously and begin their countdown
  • The display value decays by ×0.8 each step (400→320→256→...) over ~54 seconds total

🔗 Related Documents

hbtestd

types : tools
keywords :

📂 View source on GitHub


hbtestd — Automated Hamsterball Testing Daemon

An MCP server that runs Hamsterball under Wine/Xvfb and exposes tools for screenshots, input, and live runtime telemetry.

What It Does

  • Starts a virtual X display (Xvfb :99)
  • Launches the original Hamsterball.exe under Wine with LIBGL_ALWAYS_SOFTWARE=1
  • Exposes an MCP server with HTTP/SSE transport on http://127.0.0.1:8777/sse
  • Provides tools to:
    • Start / stop / restart the game
    • Capture screenshots on demand
    • Send keyboard input (arrows, Enter, Escape, Space, WASD, etc.)
    • Click at screen coordinates
    • Read runtime telemetry (process stats + internal FPS values)
    • Estimate FPS from frame capture timing

Install

cd /home/evan/hamsterball-re/tools/hbtestd
uv venv
uv pip install -e .

Run

./run_server.sh

Connect from Hermes

Add to ~/.hermes/config.yaml under mcp_servers:

mcp_servers:
  hbtestd:
    transport: sse
    url: http://127.0.0.1:8777/sse

Then restart Hermes or reload MCP servers.

Tools

Tool Description
start_game() Launch Hamsterball
stop_game() Kill game + Wine + Xvfb
restart_game() Clean restart
get_status() PID, CPU, memory, runtime
screenshot() Save PNG and return path
screenshot_base64() Return PNG as base64
send_key(key) Single keypress
hold_key(key, duration_ms) Hold key
send_text(text) Type text
mouse_click(x, y, button) Click at coordinates
get_telemetry() Internal FPS targets & process stats
estimate_fps(samples) Approximate FPS from captures
wait(seconds) Sleep

Environment Variables

Variable Default Description
HBTESTD_GAME_DIR /home/evan/hamsterball-wasm/boxedwine-package/hamsterball Game install directory
HBTESTD_GAME_EXE Hamsterball.exe Game executable name
HBTESTD_DISPLAY :99 Virtual display number
HBTESTD_RESOLUTION 800x600x24 Xvfb screen spec
HBTESTD_PORT 8777 MCP SSE port
HBTESTD_SCREENSHOT_PATH /tmp/hbtestd_screenshot.png Default screenshot file
HBTESTD_LOG_PATH /tmp/hbtestd.log Game stdout/stderr log
HBTESTD_LIBGL_SOFTWARE 1 Force llvmpipe software rendering

Runtime Telemetry

get_telemetry() reads from the Windows PE static addresses running inside the Wine process:

  • g_App at 0x005341E0
  • App+0x16C → target update FPS (default 100)
  • App+0x170 → target render FPS (default 75)

This requires ptrace access to the Wine process. If kernel.yama.ptrace_scope blocks it, you will still get process statistics but not the internal FPS values.

Requirements

  • Wine
  • Xvfb
  • xdotool
  • scrot
  • Python ≥3.10
  • MinGW cross compiler (if you want to build the fps unlock DLL separately)

🔗 Related Documents

Input System

types : input
keywords :

📂 View source on GitHub


Hamsterball Input System — Complete Reverse-Engineering Document

Scope: Original Hamsterball.exe (PE32, i386, Athena engine).
Method: Direct Ghidra decompilation of original binary.
Last Updated: 2026-06-02

Table of Contents

  1. Input Architecture Overview
  2. Core Data Structures
  3. Input Device Lifecycle
  4. Key-to-Action Mapping (Registry)
  5. Runtime Input Processing
  6. options menu / Remap UI](#options-menu--remap-ui)
  7. DirectInput Integration
  8. Function Reference
  9. Offsets Cheat-Sheet
  10. Modding Notes

Input Architecture Overview

The original Hamsterball uses DirectInput 8 for all player input. The top-level owner is App; it creates two related objects:

Member Size Stored At Description
InputDevice (keyboard/mouse) 0x91C App+0x178 Created by App_CreateInputDevice (0x46C050)
InputHandler (4 control slots) 0x438 App+0x180 Created by App_CreateInputHandler (0x46C110)

Ball_GetInputForce (0x46EC30) is the main entry-point that converts raw input state into a 2-D force vector used by the physics engine. It supports three families of input:

Switch Case Mode Source
1 Keyboard DirectInput keyboard device, DIK scan-codes at InputDevice+0x50C..0x518
2 Mouse Cursor position offset from screen centre, clipped by App+0x15A centre-capture flag
4-7 Joystick / Gamepad 4 DirectInput gamepad devices polled into GamepadDevice structs at InputHandler+0x10C/0x110

Core Data Structures

App (top-level singleton)

The global App instance is at DAT_005341E0. Constructor is App_Ctor (0x46DC40).

Offset Type Name Notes
+0x04 void* graphics GraphicsDevice
+0x08 void* window Win32 window handle
+0x0C void* scene Active Scene
+0x10 int scene_type 0=menu, 1=level
+0x54 void* registry RegKey object (HKCU SOFTWARE key)
+0x15A bool mouse_centre_capture If true Ball_GetInputForce recentres the cursor every frame
+0x158 bool fullscreen Full-screen flag
+0x15C int screen_width e.g. 640
+0x160 int screen_height e.g. 480
+0x174 void* graphics_device D3D8 device wrapper
+0x178 InputDevice* input_device Keyboard / mouse DInput device
+0x17C Audio_MusicDevice* music_device BASS audio
+0x180 InputHandler* input_handler 4-slot control bindings + gamepad array
+0x1A0 int player1_mode 1=keyboard, 2=mouse, 4-7=joy
+0x1A4 int player2_mode Same enum for P2
+0x208 char* status_text Debug status string ("Initialize(x)")
+0x224 SimpleMenu* current_menu Active menu object
+0x228 void* results_dialog Race-end results screen
+0x240 HCURSOR blank_cursor Invisible cursor handle
+0x278 Texture* shadow_texture Ball shadow sprite
+0x2A8 float mouse_sensitivity Multiplier for mouse mode
+0x2AC float music_volume 0.0-1.0
+0x2B0 float sfx_volume 0.0-1.0
+0x2B4 bool pause_with_right_button If true RMB pauses the game
+0x2B8 bool registered Full-game flag
+0x2C0 float time_remaining Race timer
+0x2F8 bool is_tournament Tournament mode flag
+0x300 char[32] player1_name Tournament name
+0x320 char[32] player2_name Tournament name
+0x340 float best_time Saved best lap
+0x344 float target_time Target for medals
+0x350 int free_plays Demo counter
+0x354 int play_count Total launches
+0x358 int unlock_flags Secret level unlock bits
+0x3E0 void* high_scores_table High-score data
+0x534 MusicDevice* jukebox Background music
+0xB28 DWORD CONTROL1 Raw key/button DWORD (see registry section)
+0xB2C DWORD CONTROL2 "
+0xB30 DWORD CONTROL3 "
+0xB34 DWORD CONTROL4 "

InputDevice (0x91C bytes)

Allocated with operator_new(0x91C) in App_CreateInputDevice (0x46C050). The constructor at 0x466620 (named SoundDevice_ctor in older labels — it is actually a generic device constructor) sets the DirectInput cooperative level and fills the default keyboard DIK codes.

Offset Type Name Notes
+0x00 vtable* vtable PTR_InputDevice_DeletingDtor
+0x04 AthenaList sub_devices AthenaList of sub-device pointers
+0x41C AthenaList ?? Second list (possibly device caps)
+0x434 void* dinput_device IDirectInputDevice8* for keyboard
+0x508 int device_type See DEVICE_TYPE_KEYBOARD etc. below
+0x50C int key_left DIK scan-code for "left"
+0x510 int key_right DIK scan-code for "right"
+0x514 int key_up DIK scan-code for "forward/up"
+0x518 int key_down DIK scan-code for "backward/down"
+0x51C int key_escape DIK scan-code for escape/pause
+0x520 int key_pause DIK scan-code for pause (second binding)

Note: The reimplementation and some community docs claim 6 directional keys exist; the decompilation of Ball_GetInputForce (0x46EC30) only reads 4 directional keys (+0x50C through +0x518). There is no explicit "brake" or "jump" action read here — jumping is handled exclusively by E:JUMP collision objects.

InputHandler (0x438 bytes)

Allocated with operator_new(0x438) in App_CreateInputHandler (0x46C110). Constructor is at roughly 0x46DFA0 (named FUN_0046dfa0 in Ghidra).

Offset Type Name
+0x00 vtable* vtable
+0x04 App* back-pointer to App
+0x0C int[4] player_device_index
+0x1C int[4] player_input_mode
+0x2C int[4] ??
+0x3C int joy_count
+0x40 GamepadDevice*[4] gamepads
+0x10C int joy0_x
+0x110 int joy0_y

GamepadDevice (layout inferred from Ball_GetInputForce)

Polled by GamepadDevice_PollState during InputDevice_PollAndRelease.

Offset Type Name
+0x130 char button_a
+0x131 char button_b
+0x132 char button_x
+0x133 char button_y
+0x10C int axis_x
+0x110 int axis_y

Input Device Lifecycle

1. Construction — App_Initialize_Full (0x429530)

The full 26-step init sequence creates devices in this order:

Step Function What it does
1 App_Initialize (0x46BB40) Base init, D3D8 device, window
15 App_CreateInputHandler (0x46C110) Alloc 0x438, ctor, store at App+0x180
16 FUN_0046dfc0(slot0, 1) Bind slot 0 to keyboard (mode 1)
17-18 FUN_0046dfa0 / FUN_0046dfc0(slot1, 2) Bind slot 1 to mouse (mode 2)
19-20 FUN_0046dfa0 / FUN_0046dfc0(slot2, 4) Bind slot 2 to joy0 (mode 4)
21-22 FUN_0046dfa0 / FUN_0046dfc0(slot3, 5) Bind slot 3 to joy1 (mode 5)
23 RegKey_Close Close registry handle

The InputHandler constructor reads the registry keys CONTROL1..CONTROL4 and stores the raw DWORDs at App+0xB28..0xB34 (see next section).

2. Per-Frame Polling — InputDevice_PollAndRelease (0x46EBD0)

void InputDevice_PollAndRelease(int self)
{
    // 1. Poll keyboard via DirectInput (256-byte state buffer at +0xC)
    int didev = *(int*)(self + 0x434);
    if (didev != 0) {
        int hr = IDirectInputDevice8_GetDeviceState(didev, 0x100, self + 0xC);
        if (hr < 0) {
            IDirectInputDevice8_Acquire(didev);
            memset(self + 0xC, 0, 0x100);   // 256 bytes = full DI8DEVSTATE size
        }
    }
    // 2. Poll up to 4 gamepads
    int *slot = (int*)(self + 0x424);
    for (int i = 0; i < 4; ++i) {
        if (*slot != 0) GamepadDevice_PollState(*slot);
        slot++;
    }
}
  • The 0x100 bytes starting at InputDevice+0x0C is the standard DirectInput keyboard state array (256 entries, one per scan-code). Each byte uses the high bit (& 0x80) for "key is currently down".
  • InputDevice+0x424 is the start of a 4-element int[] holding gamepad device pointers.

3. Destruction — KeyboardDevice_ScalarDtor (0x46E910)

Called on game exit. Releases the IDirectInputDevice8 pointer and frees the InputDevice heap block.


Key-to-Action Mapping (Registry)

Registry Keys

Under HKCU\Software\Raptisoft\Hamsterball:

Key Type Default Meaning
CONTROL1 DWORD 0x00000063 (99) Binding slot 0 → Keyboard
CONTROL2 DWORD 0x00000064 (100) Binding slot 1 → Mouse
CONTROL3 DWORD varies Binding slot 2 → Joystick / Gamepad
CONTROL4 DWORD varies Binding slot 3 → Joystick / Gamepad
  • CONTROL1 and CONTROL2 are hard-coded to keyboard (99) and mouse (100) by InputHandler ctor.
  • The values written to registry are NOT DIK scan-codes in the obvious way; they are internal mode indicators (99 = kbd, 100 = mouse, other values = joy index).
  • When the user chooses "REMAP KEYBOARD CONTROLS" in the options menu, the values at App+0xB28..0xB38 are updated and later flushed to registry on exit.

Default Keyboard DIK Codes (hard-coded in constructor)

From the InputDevice constructor (0x466620) disassembly, the default scan-codes populated are:

Action DIK Code Hex Description
Left / Turn Left DIK_LEFT 0xCB (203) Arrow left
Right / Turn Right DIK_RIGHT 0xCD (205) Arrow right
Up / Roll Forward DIK_UP 0xC8 (200) Arrow up
Down / Roll Backward DIK_DOWN 0xD0 (208) Arrow down
Escape / Pause DIK_ESCAPE 0x01 (1) Escape key
Pause (alt) DIK_P 0x19 (25) P key

Important: There is no separate "brake" key — releasing the up/down arrows simply stops applying force. There is no "jump" key in the input system; ball jumping is caused by E:JUMP collision events handled in DispatchCollisionEvents (0x40C5D0).

Registry Persistence

Registry read happens once during InputHandler construction. Registry write happens in SoundDevice_dtor (0x4668A0) — the same function also saves Sound Volume. The exact registry path is built by RegKey_SetSoftwarePath (0x472F50):

HKCU\Software\Raptisoft\Hamsterball

Runtime Input Processing

Input_IsKeyDown (0x46E0B0)

uint Input_IsKeyDown(void* self, int key)
{
    int mode = *(int*)(self + 8) - 1;   // 0=kbd, 1=mouse, 3-6=joyN
    switch (mode) {
    case 0: // Keyboard
        if (key == -1) {
            // Check ANY key down (scan whole 256-byte buffer)
            byte* state = (byte*)(*(int*)(*(int*)(self + 4) + 0x434) + 0xC);
            return any_nonzero(state, 256);
        }
        else if (key == 0) {
            return (state[0x45] >> 7);  // ??? special binding
        }
        else if (key == 1) {
            return App_Is2PMode(...);
        }
        else {
            byte* state = (byte*)(*(int*)(*(int*)(self + 4) + 0x434) + 0xC + key);
            return (*state != 0);       // high-bit test
        }
    case 1: // Mouse
        // Returns left/right/middle button state from Win32 mouse struct
        // iVar4 + 0x1C8, 0x1C9, 0x1CA
        return ...
    case 3: case 4: case 5: case 6: // Joystick
        int pad = *(int*)(self + 0x10); // gamepad struct ptr
        if (pad) {
            if (key == -1) return any_button_on_pad(pad);
            return *(char*)(pad + 0x130 + key) != 0;
        }
    }
    return 0;
}
  • self+8 stores the input mode (1 = kbd, 2 = mouse, 4-7 = joy).
  • self+4 is a back-pointer to the App.
  • For the keyboard branch, the actual DI8 state buffer is reached by:
    App->input_device->dinput_device->+0xC (the 256-byte keyboard state array).

Ball_GetInputForce (0x46EC30)

This is the main physics input function — every frame it produces a (forceX, forceY) float pair.

void Ball_GetInputForce(void* this, float* outForce)
{
    float forceX = 0.0f;
    float forceY = 0.0f;
    switch (*(int*)(this + 8)) {      // player input mode
    case 1: // Keyboard
    {
        int input_dev = *(int*)(*(int*)(this + 4) + 0x434); // InputDevice ptr
        byte* kbd = (byte*)(input_dev + 0xC);               // DI8 state
        int left  = *(int*)(input_dev + 0x50C);             // DIK code
        int right = *(int*)(input_dev + 0x510);
        int up    = *(int*)(input_dev + 0x514);
        int down  = *(int*)(input_dev + 0x518);
        if (kbd[left]  & 0x80) forceX = -1.0f;
        if (kbd[right] & 0x80) forceX =  1.0f;
        if (kbd[up]    & 0x80) forceY = -1.0f;   // Note: screen-up is -Y
        if (kbd[down]  & 0x80) forceY =  1.0f;
        break;
    }
    case 2: // Mouse
    {
        POINT pt; GetCursorPos(&pt);
        int cx = App->screen_width  / 2;
        int cy = App->screen_height / 2;
        forceX = (float)(pt.x - cx);
        forceY = (float)(pt.y - cy);
        if (App->mouse_centre_capture)
            SetCursorPos(cx, cy);
        break;
    }
    case 4: case 5: case 6: case 7: // Joy
    {
        int pad = *(int*)(this + 0x10);   // GamepadDevice*
        if (pad != 0) {
            forceX = (float)(*(int*)(pad + 0x10C) / 100);
            forceY = (float)(*(int*)(pad + 0x110) / 100);
            Vec3_NormalizeAndScale(&pt, 1.0f);
            forceY = (float)pt.y;
            forceX = (float)pt.x;
        }
        break;
    }
    }
    float scale = *(float*)(this + 0xC);   // per-ball sensitivity
    outForce[0] = scale * forceX;
    outForce[1] = scale * forceY;
}

Key observations for modders:

  • this+8 = player input mode, this+4 = App*, this+0xC = sensitivity float.
  • The keyboard branch only reads 4 directional keys. There is no jump, no brake, no action button.
  • The Y axis is inverted: -1.0 is "up" on screen (toward smaller Y) because D3D uses a top-down coordinate system for this engine.
  • Mouse mode uses absolute cursor offset from screen centre, not delta motion.

Input_CheckKeyCombo (0x428F10)

Used by menus (and the pause system) to detect "any key pressed in slot" with a 50-frame debounce.

int Input_CheckKeyCombo(void* self, int slot)
{
    if (slot == 2) {   // special escape/pause check
        int dev = *(int*)(*(int*)(self + 0x180) + 0x434); // InputDevice
        byte esc = *(byte*)(dev + 0x51C + 0xC); // escape DIK state
        byte pau = *(byte*)(dev + 0x520 + 0xC); // pause DIK state
        if ((esc & 0x80) || (pau & 0x80)) {
            if (*(int*)(self + 0x560) == 0) {
                *(int*)(self + 0x560) = 0x32;   // 50 frame cooldown
                return 1;
            }
        }
    }
    if (slot < 4) {
        int* combo = (int*)(self + slot*4 + 0x550); // combo state array
        while (slot < 4) {
            if (*(int*)(combo + 4) == 0) {   // cooldown timer == 0?
                int down = Input_IsKeyDown((void*)*combo, -1);
                if (down) {
                    *(int*)(self + slot*4 + 0x560) = 0x32; // set 50-frame timer
                    return 1;
                }
            }
            slot++;
            combo++;
        }
    }
    return 0;
}
  • App+0x550..0x55C = 4 InputCombo objects (one per control slot).
  • App+0x560..0x56C = 4 int cooldown counters (50 frames = ~0.8 s at 60 Hz).

Options Menu / Remap UI

OptionsMenu_ctor (0x442CE0)

The options menu is a UIList-based menu (SimpleMenu -> OptionsMenu). Items are added in this order:

0:  "RESOLUTION: 1024 X 768"    -> "REZ"
1:  "FULLSCREEN: YES"            -> "FS"
2:  "COLOR QUALITY: MEDIUM"      -> "CM"
3:  "SAFE MODE: OFF"             -> "SM"
4:  <spacer 10>
5:  "SOUND VOLUME:"              -> "SV"
6:  "MUSIC VOLUME:"              -> "MV"
7:  <spacer 10>
8:  "REMAP KEYBOARD CONTROLS"    -> "REMAP"
9:  <spacer 10>
10: "MOUSE SENSITIVITY:"         -> "MS"
11: "PAUSE W/RIGHT BUTTON: YES"  -> "PWRB"
12: <spacer 10>
13: "BACK"                       -> "BACK"

Selecting item "REMAP" enters the remap sub-menu. That sub-menu code is not fully decompiled here, but the render function shows the visual binding representation.

OptionsMenu_RenderControls (0x42E840)

void OptionsMenu_RenderControls(void* menu)
{
    int binding = 0;
    int offset = 0xB28;   // App+0xB28 = CONTROL1
    while (offset < 0xB38) {
        Matrix_Scale4x4(...);
        int val = *(int*)(offset + *(int*)(menu + 0x878)); // read CONTROLx
        int duplicate = 0;
        if (val == 99) {
            // Keyboard icon (scale x=0.5)
        } else if (val == 100) {
            // Mouse icon (scale x=0.5)
        } else {
            // Joystick / other — check for duplicates
            int check = 0xB28;
            int idx = 0;
            while (check < 0xB38) {
                if (idx != binding &&
                    *(int*)(check + App) == val) {
                    // Duplicate binding → render in RED
                    Matrix_Scale4x4(..., 1.0f, 0.0f, 0.0f, 1.0f);
                }
                check += 4;
                idx++;
            }
        }
        binding++;
        // Draw "CONTROL%d" label with chosen colour
        AthenaString_SprintfToBuffer(buf, "CONTROL%d");
        UIList_SetColorsByName(menu, ..., buf);
        offset += 4;
    }
}
  • The menu reads App+0xB28..0xB38 (the 4 DWORD control bindings).
  • Value 99 = keyboard (rendered with magenta tint in UIList).
  • Value 100 = mouse (rendered with cyan tint).
  • Any other value = a joystick/gamepad index; if two slots share the same value the second is drawn in red to warn the player.
  • The actual remapping UI (where the user presses a key to rebind) is likely a small tight loop inside the menu update function — it has not been decompiled in this round but its address can be found by xref-ing OptionsMenu_ctor from the vtable handler at UIList item "REMAP".

DirectInput Integration

Enumeration & Cooperative Level

DirectInput8Create (imported at 0x47C7F0 from dinput8.dll) is called inside the InputDevice constructor. The constructor then:

  1. Creates the IDirectInput8 device (DllEntryPoint in decomp — actually DirectInput8Create).
  2. Calls IDirectInputDevice8_SetCooperativeLevel with:
    • Window handle = App->window
    • Flags = DISCL_NONEXCLUSIVE | DISCL_BACKGROUND (or DISCL_EXCLUSIVE | DISCL_FOREGROUND for joystick)
  3. Calls IDirectInputDevice8_SetDataFormat with c_dfDIKeyboard or c_dfDIJoystick.
  4. Acquires the device.

The exact cooperative-level path is visible in the decompilation of InputDevice constructor (0x466620) — the branch at offset 0x46666C checks a flag and either calls with 2 (BACKGROUND) or 3 (FOREGROUND).

Device Acquisition Failure Path

In InputDevice_PollAndRelease (0x46EBD0):

int hr = IDirectInputDevice8_GetDeviceState(didev, 0x100, buffer);
if (hr < 0) {
    IDirectInputDevice8_Acquire(didev);
    memset(buffer, 0, 0x100);   // zero state on failure
}

If the device is lost (Alt-Tab, UAC popup, etc.), the game automatically re-acquires it and returns a zeroed state for that frame — preventing phantom inputs.

Default DirectInput8 DLL Import

Import Address Called From
DirectInput8Create 0x0047C7F0 InputDevice constructor (0x466620)

Function Reference

Input System Functions

Address Name Args Description
0x46EC30 Ball_GetInputForce (void* this, float* outForceXY) Convert player input to 2-D force vector for physics
0x46E0B0 Input_IsKeyDown (void* this, int key) Check if key/button is currently pressed (mode-aware)
0x46EBD0 InputDevice_PollAndRelease (int device) Poll DInput kb + 4 gamepads, re-acquire if lost
0x428F10 Input_CheckKeyCombo (void* app, int slot) 50-frame debounced "any key in slot" check
0x42E840 OptionsMenu_RenderControls (void* menu) Draw control icons coloured by device type
0x442CE0 OptionsMenu_ctor (void* this, int arg1, int arg2) Build options menu including REMAP entry
0x46C050 App_CreateInputDevice (int app) Alloc+construct InputDevice (0x91C) store at App+0x178
0x46C110 App_CreateInputHandler (int app) Alloc+construct InputHandler (0x438) store at App+0x180
0x46DC40 App_Ctor (void* this) Full App init; creates MeshWorld, cursors, COM
0x46E910 KeyboardDevice_ScalarDtor (void* this) Release DInput device, free InputDevice
0x466620 InputDevice_ctor (mis-label "SoundDevice") (void* this, int app) Set coop-level, alloc kb buffer, default DIK codes
0x4692F0 Scene_HandleInput (void* scene) Menu item iteration + input dispatch
0x47C7F0 DirectInput8Create (import) Standard DInput8 creation

Registry Functions

Address Name Description
0x472EC0 RegKey_Ctor Open HKCU\Software\Raptisoft\Hamsterball
0x472F30 RegKey_Close Close key handle
0x473030 RegKey_WriteDWORD Persist a DWORD
0x473100 RegKey_QueryValue Read a value by name
0x473170 RegKey_ReadString Read string (not used for controls)

Offsets Cheat-Sheet

App Struct (global DAT_005341E0)

Offset Size Meaning
+0x178 4 InputDevice*
+0x180 4 InputHandler*
+0x1A0 4 Player 1 input mode (1=kbd, 2=mouse, 4-7=joy)
+0x15A 1 mouse_centre_capture bool
+0x15C 4 screen_width
+0x160 4 screen_height
+0x434 4 InputDevice* (duplicate path)
+0x550 4x4 4 InputCombo* slot objects
+0x560 4x4 4-frame debounce counters
+0xB28 4 CONTROL1 (raw binding DWORD)
+0xB2C 4 CONTROL2
+0xB30 4 CONTROL3
+0xB34 4 CONTROL4

InputDevice (0x91C bytes)

Offset Size Meaning
+0x00 4 vtable
+0x04 0x418 AthenaList (sub-devices)
+0x41C 0x10 Second AthenaList
+0x434 4 IDirectInputDevice8* keyboard
+0x508 4 device_type
+0x50C 4 key_left DIK code
+0x510 4 key_right DIK code
+0x514 4 key_up DIK code
+0x518 4 key_down DIK code
+0x51C 4 key_escape DIK code
+0x520 4 key_pause DIK code
+0x0C 0x100 DirectInput keyboard state buffer (256 bytes)
+0x424 4x4 Gamepad device pointers (4 slots)

InputHandler (0x438 bytes)

Offset Size Meaning
+0x00 4 vtable
+0x04 4 App* back-pointer
+0x0C 4x4 player_device_index[4]
+0x1C 4x4 player_input_mode[4]
+0x40 4x4 GamepadDevice*[4]
+0x10C 4 joy0_x
+0x110 4 joy0_y

Ball (relevant fields for input)

Offset Size Meaning
+0x08 4 Player input mode (copied from App)
+0x0C 4 Sensitivity float (default 1.0)
+0x10 4 GamepadDevice* (for joy modes)

Modding Notes

Changing Default Key Bindings

If you want to patch the default keys (before the user remaps), edit the immediate values written in InputDevice_ctor (0x466620). The four mov dword ptr [reg+N], imm32 instructions near +0x40 through +0x50 set the initial DIK codes.

Adding a New Input Mode

The switch(mode) in Ball_GetInputForce only handles cases 1, 2, and 4-7. If you add case 3 (or re-purpose an unused case), you must also:

  1. Update InputDevice_PollAndRelease to poll the new hardware.
  2. Update Input_IsKeyDown to read the new device state.
  3. Update OptionsMenu_RenderControls to recognise the new mode value (or it will render as a red duplicate).

Disabling the 50-Frame Debounce

Input_CheckKeyCombo stores 0x32 (50 decimal) into App+0x560..0x56C to prevent repeated triggers. Patch the immediate 0x32 at 0x428F1A and 0x428F3A to 0x01 or 0x00 for instant re-triggering.

Mouse-Sensitivity Scaling

Ball_GetInputForce case 2 does not apply App->mouse_sensitivity (App+0x2A8). The raw cursor offset is used directly. To add sensitivity scaling, patch a multiply by *(float*)(App + 0x2A8) before the break; at offset 0x46ECXX.

Registry-Free Operation (Portable Mode)

All four CONTROLx values are read from registry during InputHandler construction. If the registry key does not exist, the constructor falls back to hard-coded defaults (99, 100, joy0, joy1). To make the game fully portable, patch RegKey_QueryValue (0x473100) to always return failure (0) so defaults are always used.

Multi-player Input

The game supports up to 4 local players (Party Race / Rodent Rumble). Each player gets a control slot (0..3) and the InputHandler maps slots to physical devices. The InputDevice+0x424 array holds up to 4 gamepad structs; keyboard and mouse are treated as "virtual" device #0 and #1.


Document compiled from live Ghidra decompilation of Hamsterball.exe. All offsets verified against the PE binary loaded at 0x00400000. For questions or corrections, open an issue in the hamsterball-re repository.


🔗 Related Documents

Jump Mod (Touch)

types : mods

📂 View source on GitHub


Jump Mod (Touch)

Info

  • File: bass.dll (proxy)
  • Controls: Tap the screen to jump
  • Android-safe: Code cave is pure asm, no IAT hooks, no GetTickCount

What it does

Adds a jump mechanic to the ball! Tap the screen (Winlator maps touch to left-click) and the ball gets an upward impulse.

How it works:

  1. Background thread polls left mouse button (touch tap) every 16ms
  2. On tap: runs a raycast straight down from the ball using the game's own Mesh_FindClosestCollision function
  3. If the raycast hits ground within radius × 1.45 → ball is grounded → jump allowed
  4. If airborne → jump denied (no double jumping)
  5. Phase 15 code cave (pure asm): adds upward impulse to ball+0x174 (Y force)

Safety gates (jump only works when):

  • Countdown finished (Scene+0x3A4C = 1)
  • Race not ended (App+0x5D6 = 0)
  • Player not flagged (App+0x5D5 = 0)
  • Ball is on the ground (raycast confirmed)

Winlator Safety

  • Phase 15 code cave is pure FPU asm (FLD/FADD/FSTP/MOV/JMP) — no C function calls mid-function
  • Raycast runs in background thread (safe C context)
  • No IAT hooks, no GetTickCount
  • All pointer accesses guarded by IsBadReadPtr

Build

i686-w64-mingw32-gcc -shared -o bass.dll jump_touch.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy mod bass.dll to game folder
  3. On Android/Wine: set DLL override to native for bass.dll
  4. Tap screen in-game to jump!

🔗 Related Documents

jump_mod v22

types : tools
keywords :

📂 View source on GitHub


jump_mod v22

Press SPACE to jump (Player 1 only, raycast ground detection).

What changed from v20

v22 adds countdown and race-end gating:

  1. Countdown gate: Before allowing a jump, checks Scene+0x3A4C
    (countdown_done flag). This flag is set to 1 by Scene_HandleRaceEnd
    (0x41B130) when all 3 Ready/Set/Go phases complete. If 0, the game
    itself blocks all input in Scene_vmethod31 (0x41AC70) — the jump
    mod now mirrors this behavior.

  2. Race-end gate: Checks ball+0x14C (freeze flag). Set to 1 by
    Scene_HandleRaceEnd at 0x41B40D when the race timer expires
    (player touches the goal). Also checked by the game's Ball_Update
    at 0x4060A1. When set, the ball is frozen and jumping is blocked.

Both gates are checked in the input thread BEFORE running the raycast,
so denied jumps don't waste a raycast call.

What changed from v17

v20 changed ground detection from a fixed epsilon to a slope-aware
threshold (radius × 1.45):

On a slope of angle θ, a straight-down raycast hits at distance r/cos(θ)
from the ball center, not r. Using radius * 1.45 covers slopes up to
45° (cos(45°) ≈ 0.707, r/0.707 ≈ 1.414r). The 1.45 factor gives a small
safety margin beyond the theoretical minimum of √2 ≈ 1.414.

Features

  • Raycast ground detection: Casts a ray straight down from the ball
    position using the game's own Mesh_FindClosestCollision (0x465D90).
    If the hit point is within radius × 1.45 of the ball Y, the ball is
    grounded and can jump.
  • Countdown gating: No jumping during Ready/Set/Go countdown
    (Scene+0x3A4C == 0).
  • Race-end gating: No jumping after touching the goal
    (ball+0x14C == 1).
  • Airborne denial: Can't jump while in the air (raycast misses).
  • Edge detection: SPACE uses rising-edge detection (one jump per keypress).
  • Safety checks: Won't jump if ball pointer not yet captured or
    during fall/respawn.

Hook points

Hook Address Original bytes Purpose
Phase 15 cave 0x407BB4 8B 4C 24 1C 8B 11 (6 bytes) Jump impulse application

The input thread polls the keyboard (DIK_SPACE at KeyboardDevice+0x45)
every 16ms. On rising-edge keypress, it checks countdown/race-end gates,
then runs the raycast. If grounded, sets g_want_jump=1. The Phase 15
cave checks this flag and adds an upward impulse to ball+0x174 (Y force
accumulator) if set.

Files

  • jump_mod_raycast.c — C source code (BASS proxy + raycast + gates)
  • bass.dll — Compiled DLL (MinGW cross-compiled, PE32 i386)
  • jump_mod_v22.zip — Distribution archive

Build

i686-w64-mingw32-gcc -shared -o bass.dll jump_mod_raycast.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

🔗 Related Documents

Key Function Decompilations

types : decompilation
keywords :

📂 View source on GitHub


Hamsterball - Key Function Decompilations

Binary: Hamsterball.exe (MD5: 7d25019366b8d7f55906325bd630d7fe)
3,958 functions found by Ghidra auto-analysis

WinMain (0x004278E0)

Entry point. Creates the global App singleton, initializes, runs game loop, shuts down.

undefined4 WinMain(undefined4 param_1,undefined4 param_2,undefined4 param_3)

{
  App_Initialize_Full(&DAT_004fd680,param_1,param_3);
  App_Run((int *)&DAT_004fd680);
  App_Shutdown((int *)&DAT_004fd680);
  return 0;
}

App_Initialize_Full (0x00429530) - The 26-Step Init Sequence

Called by WinMain. Sets up the entire game in 26 labeled steps.

void __thiscall App_Initialize_Full(void *this,undefined4 param_1,undefined4 param_2)

{
  int iVar1;
  int *piVar2;
  undefined4 uVar3;
  undefined4 *puVar4;
  uint uVar5;
  void *pvVar6;
  void *pvVar7;
  void *pvStack_c;
  undefined1 *puStack_8;
  undefined4 uStack_4;
  
  uStack_4 = 0xffffffff;
  puStack_8 = &LAB_004caeec;
  pvStack_c = ExceptionList;
  ExceptionList = &pvStack_c;
  *(char **)((int)this + 0x208) = "Initialize(1)";
  App_Initialize(this,param_1,param_2);
  *(undefined1 *)(*(int *)((int)this + 0x174) + 0x7d1) = 1;
  pvVar7 = *(void **)((int)this + 4);
  *(char **)((int)this + 0x208) = "Initialize(3)";
  uVar3 = LoadCursorA(pvVar7,"BLANKCURSOR");
  *(undefined4 *)((int)this + 0x240) = uVar3;
  *(char **)((int)this + 0x208) = "Initialize(4)";
  (**(code **)(*(int *)this + 0x8c))(800,600);
  iVar1 = *(int *)((int)this + 0x174);
  *(char **)((int)this + 0x208) = "Initialize(5)";
  if (iVar1 == 0) {
    *(char **)((int)this + 0x208) = "** No Graphics **";
  }
  if (*(int *)(iVar1 + 0x154) == 0) {
    *(char **)((int)this + 0x208) = "** No Graphics Device **";
  }
  if (iVar1 != 0) {
    piVar2 = *(int **)(iVar1 + 0x154);
    if (*(char *)(iVar1 + 0x7d2) == '\0') {
      (**(code **)(*piVar2 + 200))(piVar2,0x16,3);
    }
    else {
      (**(code **)(*piVar2 + 200))(piVar2,0x16,2);
    }
    *(undefined4 *)(iVar1 + 0x708) = 3;
  }
  *(char **)((int)this + 0x208) = "Initialize(6)";
  puVar4 = FUN_00455c50(*(void **)((int)this + 0x174),"shadow.png",'\x01');
  *(undefined4 **)((int)this + 0x278) = puVar4;
  *(int *)((int)this + 0x1cc) = *(int *)((int)this + 0x1cc) + 1;
  *(char **)((int)this + 0x208) = "Initialize(7)";
  puVar4 = FUN_004743f0(*(void **)((int)this + 0x17c),"music\\music.mo3");
  *(undefined4 **)((int)this + 0x534) = puVar4;
  *(undefined4 *)((int)this + 0x53c) = 0;
  *(undefined4 *)((int)this + 0x538) = 0;
  *(char **)((int)this + 0x208) = "Initialize(8)";
  if (puVar4 != (undefined4 *)0x0) {
    FUN_0046a4d0("jukebox.xml");
    *(char **)((int)this + 0x208) = "Initialize(9)";
    puVar4 = FUN_0046a3c0(*(int *)((int)this + 0x534));
    *(undefined4 **)((int)this + 0x53c) = puVar4;
    *(char **)((int)this + 0x208) = "Initialize(10)";
    puVar4 = FUN_0046a3c0(*(int *)((int)this + 0x534));
    *(undefined4 **)((int)this + 0x538) = puVar4;
    *(char **)((int)this + 0x208) = "Initialize(11)";
  }
  *(char **)((int)this + 0x208) = "Initialize(12)";
  FUN_00472ec0(*(int *)((int)this + 0x54));
  *(char **)((int)this + 0x208) = "Initialize(13)";
  uVar3 = FUN_00473170(*(void **)((int)this + 0x54),"PlayCount");
  if ((char)uVar3 == '\0') {
    *(undefined4 *)((int)this + 0x914) = 0x14;
  }
  else {
    uVar5 = FUN_00473080(*(void **)((int)this + 0x54),"PlayCount");
    *(uint *)((int)this + 0x914) = uVar5;
  }
  *(undefined1 *)((int)this + 0x200) = 1;
  *(char **)((int)this + 0x208) = "Initialize(15)";
  puStack_8 = operator_new(0x14);
  if (puStack_8 == (void *)0x0) {
    pvVar6 = (void *)0x0;
  }
  else {
    pvVar6 = (void *)FUN_0046dfa0(puStack_8,*(undefined4 *)((int)this + 0x180));
  }
  *(void **)((int)this + 0x550) = pvVar6;
  *(char **)((int)this + 0x208) = "Initialize(16)";
  FUN_0046dfc0(pvVar6,1);
  *(char **)((int)this + 0x208) = "Initialize(17)";
  puStack_8 = operator_new(0x14);
  if (puStack_8 == (void *)0x0) {
    pvVar6 = (void *)0x0;
  }
  else {
    pvVar6 = (void *)FUN_0046dfa0(puStack_8,*(undefined4 *)((int)this + 0x180));
  }
  *(void **)((int)this + 0x554) = pvVar6;
  *(char **)((int)this + 0x208) = "Initialize(18)";
  FUN_0046dfc0(pvVar6,2);
  *(char **)((int)this + 0x208) = "Initialize(19)";
  puStack_8 = operator_new(0x14);
  if (puStack_8 == (void *)0x0) {
    pvVar6 = (void *)0x0;
  }
  else {
    pvVar6 = (void *)FUN_0046dfa0(puStack_8,*(undefined4 *)((int)this + 0x180));
  }
  *(void **)((int)this + 0x558) = pvVar6;
  *(char **)((int)this + 0x208) = "Initialize(20)";
  FUN_0046dfc0(pvVar6,4);
  *(char **)((int)this + 0x208) = "Initialize(21)";
  puStack_8 = operator_new(0x14);
  if (puStack_8 == (void *)0x0) {
    pvVar6 = (void *)0x0;
  }
  else {
    pvVar6 = (void *)FUN_0046dfa0(puStack_8,*(undefined4 *)((int)this + 0x180));
  }
  *(void **)((int)this + 0x55c) = pvVar6;
  *(char **)((int)this + 0x208) = "Initialize(22)";
  FUN_0046dfc0(pvVar6,5);
  *(char **)((int)this + 0x208) = "Initialize(23)";
  FUN_00472f30(*(int *)((int)this + 0x54));
  *(char **)((int)this + 0x208) = "Initialize(25)";
  (**(code **)(*(int *)this + 0xa0))();
  *(char **)((int)this + 0x208) = "Initialize(26)";
  ExceptionList = pvVar7;
  return;
}

App_Initialize (0x0046bb40)

void __thiscall App_Initialize(void *this,undefined4 param_1,undefined4 param_2)

{
  uint *puVar1;
  undefined4 unaff_retaddr;
  
  *(char **)((int)this + 0x208) = "App::Initialize(1)";
  (**(code **)(*(int *)this + 0x94))(param_2);
  *(undefined4 *)((int)this + 4) = unaff_retaddr;
  *(char **)((int)this + 0x208) = "App::Initialize(2)";
  FUN_00472f50(*(int *)((int)this + 0x54));
  *(char **)((int)this + 0x208) = "App::Initialize(3)";
  (**(code **)(*(int *)this + 0xc))();
  *(char **)((int)this + 0x208) = "App::Initialize(4)";
  (**(code **)(*(int *)this + 0x1c))();
  *(char **)((int)this + 0x208) = "App::Initialize(5)";
  (**(code **)(*(int *)this + 0x18))();
  *(char **)((int)this + 0x208) = "App::Initialize(6)";
  (**(code **)(*(int *)this + 0x30))();
  *(char **)((int)this + 0x208) = "App::Initialize(7)";
  (**(code **)(*(int *)this + 0x3c))();
  *(char **)((int)this + 0x208) = "App::Initialize(8)";
  (**(code **)(*(int *)this + 0x34))();
  *(char **)((int)this + 0x208) = "App::Initialize(9)";
  (**(code **)(*(int *)this + 0x38))();
  *(char **)((int)this + 0x208) = "App::Initialize(10)";
  (**(code **)(*(int *)this + 0x40))();
  *(char **)((int)this + 0x208) = "App::Initialize(11)";
  Graphics_Initialize(*(void **)((int)this + 0x174),*(undefined4 *)((int)this + 8),
                      *(undefined4 *)((int)this + 0x15c),*(undefined4 *)((int)this + 0x160),
                      *(undefined1 *)((int)this + 0x158));
  if (*(char *)(*(int *)((int)this + 0x174) + 0x60) != '\0') {
    MessageBoxA(0,
                "Hamsterball was not able to initialize DirectX!  Hamsterball requires DirectX8.0 or better to run.  To download the latest DirectX, visit http://www.microsoft.com/directx"
                ,"DirectX Initialization Error",0);
    FUN_004bbaeb(0);
  }
  *(char **)((int)this + 0x208) = "App::Initialize(12)";
  if (*(uint **)((int)this + 0x1b4) != (uint *)0x0) {
    puVar1 = FUN_004bacc0(*(uint **)((int)this + 0x1b4),'-');
    if (puVar1 != (uint *)0x0) {
      FUN_00454000(*(void **)((int)this + 0x174),(char *)((int)puVar1 + 1));
    }
  }
  *(char **)((int)this + 0x208) = "App::Initialize(Ok)";
  return;
}

App_Run (0x0046bd80)

void __fastcall App_Run(int *param_1)

{
  char cVar1;
  int iVar2;
  uint uVar3;
  int iVar4;
  int local_78;
  int local_74;
  int local_70;
  undefined1 auStack_6c [28];
  undefined4 local_50 [17];
  void *pvStack_c;
  undefined1 *puStack_8;
  undefined4 local_4;
  
  local_4 = 0xffffffff;
  puStack_8 = &LAB_004cd5f8;
  pvStack_c = ExceptionList;
  ExceptionList = &pvStack_c;
  FUN_00457ad0(local_50);
  local_70 = (int)(1000 / (longlong)param_1[0x5c]);
  local_4 = 0;
  local_74 = 0;
  local_78 = 0;
  iVar2 = GetTickCount();
  param_1[0x59] = iVar2;
  cVar1 = *(char *)((int)param_1 + 0x159);
  while (cVar1 == '\0') {
    iVar4 = 0;
    Sleep(0);
    param_1[0x84] = (int)"Background";
    param_1[0x5a] = (int)(1000 / (longlong)param_1[0x5b]);
    iVar2 = GetTickCount();
    if (DAT_005341e4 < iVar2) {
      if ((char)param_1[0x6b] == '\x01') {
        FUN_004bae43((char *)(param_1 + 0x66),&DAT_004d03f8);
      }
      param_1[0x65] = 0;
      iVar2 = GetTickCount();
      DAT_005341e4 = iVar2 + 1000;
    }
    iVar2 = PeekMessageA(&local_70,0,0,0,1);
    while (iVar2 != 0) {
      if (*(char *)((int)param_1 + 0x159) != '\0') goto LAB_0046bfc3;
      TranslateMessage(auStack_6c);
      DispatchMessageA(&local_70);
      iVar2 = PeekMessageA(&local_74,0,0,0,1);
    }
    if (*(char *)((int)param_1 + 0x159) != '\0') break;
    do {
      uVar3 = GetTickCount();
      if (((int)(uVar3 - param_1[0x59]) < param_1[0x5a] + -5) ||
         (local_78 = local_78 + 1, 9 < local_78)) {
        param_1[0x84] = (int)&DAT_004d9584;
        local_78 = 0;
        if ((uint)(local_70 + -5 + local_74) < uVar3) {
          if (((void *)param_1[0x5d] != (void *)0x0) &&
             ((*(char *)((int)param_1 + 0x15a) != '\0' || ((char)param_1[0x56] == '\0')))) {
            param_1[0x65] = param_1[0x65] + 1;
            FUN_00453b50((void *)param_1[0x5d],(int)local_50);
            (**(code **)(*param_1 + 0x24))();
            (**(code **)(*param_1 + 0x28))();
            (**(code **)(*param_1 + 0x2c))();
            FUN_00455a90((void *)param_1[0x5d],'\x01');
          }
          local_74 = GetTickCount();
        }
        break;
      }
      param_1[99] = param_1[99] + 1;
      param_1[0x84] = (int)"Update";
      FUN_00453b50((void *)param_1[0x5d],(int)local_50);
      (**(code **)(*param_1 + 0x20))();
      iVar2 = param_1[0x59];
      param_1[0x59] = iVar2 + param_1[0x5a];
      if (1000 < (int)(uVar3 - (iVar2 + param_1[0x5a]))) {
        param_1[0x59] = uVar3 - 1000;
      }
      iVar4 = iVar4 + 1;
    } while (iVar4 < 1);
    cVar1 = *(char *)((int)param_1 + 0x159);
  }
LAB_0046bfc3:
  local_4 = 0xffffffff;
  FUN_00457a40(local_50);
  ExceptionList = pvStack_c;
  return;
}

Graphics_Initialize (0x00455380)

/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */

void __thiscall
Graphics_Initialize(void *this,undefined4 param_1,undefined4 param_2,undefined4 param_3,
                   undefined1 param_4)

{
  int *piVar1;
  int iVar2;
  undefined4 uVar3;
  uint uVar4;
  int iVar5;
  uint uVar6;
  undefined4 *puVar7;
  undefined4 *puVar8;
  undefined4 *puVar9;
  undefined1 *puVar10;
  int **ppiVar11;
  undefined4 uStack_17c;
  undefined4 uStack_178;
  int *piStack_174;
  int *piStack_164;
  char *pcStack_160;
  int *piStack_154;
  char *pcStack_150;
  int iStack_14c;
  undefined4 uStack_148;
  int iStack_138;
  int aiStack_134 [2];
  int iStack_12c;
  
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(1)";
  *(undefined4 *)((int)this + 0x6c) = param_1;
  *(undefined4 *)((int)this + 0x70) = param_2;
  *(undefined1 *)((int)this + 0x78) = param_4;
  iVar5 = 0;
  *(undefined4 *)((int)this + 0x7c4) = 0;
  *(undefined4 *)((int)this + 0x74) = param_3;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(2)";
  uStack_148 = 0x4553d8;
  FUN_00453ed0((int)this);
  uStack_148 = 0xdc;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(3)";
  iStack_14c = 0x4553ef;
  iVar2 = Direct3DCreate8();
  *(int *)((int)this + 0x7c) = iVar2;
  if (iVar2 == 0) {
    iStack_14c = 0;
    pcStack_150 = "DirectX Error";
    piStack_154 = (int *)0x4d8d78;
    MessageBoxA();
    *(undefined1 *)((int)this + 0x60) = 1;
    return;
  }
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(4)";
  piStack_154 = *(int **)((int)this + 0x7c);
  iStack_14c = (int)this + 0x158;
  pcStack_150 = (char *)0x0;
  iVar2 = (**(code **)(*piStack_154 + 0x20))();
  if (iVar2 < 0) {
    pcStack_160 = "Failed: GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&mDesktopMode)";
    piStack_164 = (int *)0x0;
    MessageBoxA();
    *(undefined1 *)((int)this + 0x60) = 1;
    return;
  }
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(5)";
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(6)";
  piStack_164 = *(int **)((int)this + 0x7c);
  pcStack_160 = (char *)0x0;
  uVar6 = 0;
  uVar4 = 0;
  piStack_154 = (int *)(**(code **)(*piStack_164 + 0x18))();
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(7)";
  *(undefined4 *)((int)this + 0x17c) = 0x14;
  *(undefined4 *)((int)this + 0x178) = 0x17;
  *(undefined1 *)((int)this + 0x7d3) = 0;
  *(undefined1 *)((int)this + 0x7d4) = 0;
  *(undefined1 *)((int)this + 0x7d5) = 0;
  *(undefined1 *)((int)this + 0x7d6) = 0;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(8)";
  if (0 < (int)piStack_154) {
    do {
      piStack_174 = *(int **)((int)this + 0x7c);
      uStack_178 = 0x4554e7;
      iVar2 = (**(code **)(*piStack_174 + 0x1c))();
      if (-1 < iVar2) {
        if (iStack_138 == 0x280) {
          if (aiStack_134[0] == 0x1e0) {
            *(undefined1 *)((int)this + 0x7d3) = 1;
          }
        }
        else if (iStack_138 == 800) {
          if (aiStack_134[0] == 600) {
            *(undefined1 *)((int)this + 0x7d4) = 1;
          }
        }
        else if (iStack_138 == 0x400) {
          if (aiStack_134[0] == 0x300) {
            *(undefined1 *)((int)this + 0x7d5) = 1;
          }
        }
        else if ((iStack_138 == 0x500) && (aiStack_134[0] == 0x400)) {
          *(undefined1 *)((int)this + 0x7d6) = 1;
        }
        if (iStack_12c == 0x14) {
          if (uVar4 < 1000) {
            *(undefined4 *)((int)this + 0x17c) = 0x14;
            uVar4 = 1000;
          }
        }
        else if (iStack_12c == 0x16) {
          if (uVar4 < 900) {
            *(undefined4 *)((int)this + 0x17c) = 0x16;
            uVar4 = 900;
          }
        }
        else if (iStack_12c == 0x15) {
          if (uVar4 < 800) {
            *(undefined4 *)((int)this + 0x17c) = 0x15;
            uVar4 = 800;
          }
        }
        else if (iStack_12c == 0x17) {
          if (uVar6 < 1000) {
            *(undefined4 *)((int)this + 0x178) = 0x17;
            uVar6 = 1000;
          }
        }
        else if (iStack_12c == 0x18) {
          if (uVar6 < 900) {
            *(undefined4 *)((int)this + 0x178) = 0x18;
            uVar6 = 900;
          }
        }
        else if (iStack_12c == 0x19) {
          if (uVar6 < 800) {
            *(undefined4 *)((int)this + 0x178) = 0x19;
            uVar6 = 800;
          }
        }
        else if ((iStack_12c == 0x1a) && (uVar6 < 700)) {
          *(undefined4 *)((int)this + 0x178) = 0x1a;
          uVar6 = 700;
        }
      }
      iVar5 = iVar5 + 1;
    } while (iVar5 < (int)piStack_154);
  }
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(9)";
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(10)";
  if (*(char *)((int)this + 0x18c) == '\0') {
    uVar3 = *(undefined4 *)((int)this + 0x17c);
  }
  else {
    uVar3 = *(undefined4 *)((int)this + 0x178);
  }
  *(undefined4 *)((int)this + 0x174) = uVar3;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(11)";
  puVar9 = (undefined4 *)((int)this + 0x194);
  puVar7 = puVar9;
  for (iVar2 = 0xd; iVar2 != 0; iVar2 = iVar2 + -1) {
    *puVar7 = 0;
    puVar7 = puVar7 + 1;
  }
  *(undefined4 *)((int)this + 0x1ac) = *(undefined4 *)((int)this + 0x6c);
  *(undefined4 *)((int)this + 0x1b0) = 1;
  *puVar9 = *(undefined4 *)((int)this + 0x70);
  *(undefined4 *)((int)this + 0x1a8) = 1;
  *(undefined4 *)((int)this + 0x1b4) = 1;
  *(undefined4 *)((int)this + 0x198) = *(undefined4 *)((int)this + 0x74);
  *(undefined4 *)((int)this + 0x19c) = *(undefined4 *)((int)this + 0x164);
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(12)";
  piStack_174 = (int *)0x4556e4;
  FUN_00453c90(this,*(undefined4 **)((int)this + 0x164),&piStack_154,
               (undefined1 *)((int)this + 0x180));
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(13)";
  *(int **)((int)this + 0x1b8) = piStack_154;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(14)";
  puVar7 = (undefined4 *)((int)this + 0x1c8);
  puVar8 = puVar7;
  for (iVar2 = 0xd; iVar2 != 0; iVar2 = iVar2 + -1) {
    *puVar8 = 0;
    puVar8 = puVar8 + 1;
  }
  *(undefined4 *)((int)this + 0x1cc) = *(undefined4 *)((int)this + 0x74);
  *(undefined4 *)((int)this + 0x1d0) = *(undefined4 *)((int)this + 0x174);
  *puVar7 = *(undefined4 *)((int)this + 0x70);
  *(undefined4 *)((int)this + 0x1d4) = 1;
  *(undefined4 *)((int)this + 0x1d8) = 0;
  *(undefined4 *)((int)this + 0x1e4) = 0;
  *(undefined4 *)((int)this + 0x1e8) = 1;
  *(undefined4 *)((int)this + 0x1e0) = *(undefined4 *)((int)this + 0x6c);
  *(undefined4 *)((int)this + 0x1dc) = 2;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(15)";
  piStack_174 = (int *)0x45578c;
  FUN_00453c90(this,*(undefined4 **)((int)this + 0x174),&piStack_154,
               (undefined1 *)((int)this + 0x181));
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(16)";
  *(int **)((int)this + 0x1ec) = piStack_154;
  *(undefined4 *)((int)this + 500) = 0;
  *(undefined4 *)((int)this + 0x1f8) = 1;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(17)";
  if (*(char *)((int)this + 0x78) == '\x01') {
    *(undefined4 **)((int)this + 400) = puVar7;
    *(undefined1 *)((int)this + 0x182) = *(undefined1 *)((int)this + 0x181);
  }
  else {
    *(undefined4 **)((int)this + 400) = puVar9;
    *(undefined1 *)((int)this + 0x182) = *(undefined1 *)((int)this + 0x180);
  }
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(18)";
  piVar1 = (int *)((int)this + 0x154);
  piStack_174 = *(int **)((int)this + 0x6c);
  uStack_178 = 1;
  uStack_17c = 0;
  (**(code **)(**(int **)((int)this + 0x7c) + 0x3c))(*(int **)((int)this + 0x7c));
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(19)";
  if (*piVar1 == 0) {
    (**(code **)(**(int **)((int)this + 0x7c) + 0x3c))
              (*(int **)((int)this + 0x7c),0,1,*(undefined4 *)((int)this + 0x6c),0x80,
               *(undefined4 *)((int)this + 400),piVar1);
    if (*piVar1 == 0) {
      iVar2 = (**(code **)(**(int **)((int)this + 0x7c) + 0x3c))
                        (*(int **)((int)this + 0x7c),0,1,*(undefined4 *)((int)this + 0x6c),0x20,
                         *(undefined4 *)((int)this + 400),piVar1);
      if (*piVar1 == 0) {
        FUN_004764f0(iVar2,(char *)aiStack_134,0xff);
        MessageBoxA(0,aiStack_134,"Graphics::Initialize",0);
        *(undefined1 *)((int)this + 0x60) = 1;
        return;
      }
    }
  }
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(20)";
  FUN_0042c810(this);
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(21)";
  (**(code **)(**(int **)((int)this + 0x7c) + 0x34))
            (*(int **)((int)this + 0x7c),0,1,(int)this + 0x80);
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(22)";
  FUN_00454f10(this,0,0);
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(23)";
  FUN_00454ab0(this,10.0,5000.0);
  piStack_164 = (int *)0x3f800000;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(24)";
  pcStack_160 = (char *)0x0;
  piStack_154 = (int *)0x0;
  uStack_17c = 0;
  pcStack_150 = (char *)0x0;
  ppiVar11 = &piStack_164;
  iStack_14c = 0;
  uStack_178 = 0;
  puVar10 = &stack0xfffffe90;
  piStack_174 = (int *)0x0;
  puVar9 = &uStack_17c;
  iVar2 = (int)this + 0x1fc;
  thunk_FUN_0045c01a();
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(25)";
  FUN_00454630(this);
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(26)";
  (**(code **)(*(int *)*piVar1 + 0xfc))
            ((int *)*piVar1,0,0x13,*(float *)((int)this + 0x184) * _DAT_004cf41c - _DAT_004cf48c,
             iVar2,puVar9,puVar10,ppiVar11);
  *(int *)((int)this + 0x7c8) = *(int *)((int)this + 0x7c8) + 1;
  *(int *)((int)this + 0x184) = iVar2;
  *(char **)(*(int *)((int)this + 0x5c) + 0x208) = "Graphics::Initialize(27)";
  return;
}

LoadMeshWorld (0x0045de30)

undefined4 __thiscall LoadMeshWorld(void *this,char *param_1)

{
  code *pcVar1;
  int iVar2;
  undefined4 uVar3;
  void *this_00;
  undefined4 *this_01;
  char local_10c [248];
  void *pvStack_14;
  void *pvStack_c;
  undefined1 *puStack_8;
  undefined4 local_4;
  
  local_4 = 0xffffffff;
  puStack_8 = &LAB_004cceae;
  pvStack_c = ExceptionList;
  ExceptionList = &pvStack_c;
  FUN_004bae43(local_10c,(byte *)"%s.meshworld");
  iVar2 = FUN_004c8ff7((uint)local_10c);
  if (iVar2 == 0) {
    (**(code **)(*(int *)this + 0x38))(local_10c);
    uVar3 = (**(code **)(*(int *)this + 0x3c))(0);
  }
  else {
    this_00 = operator_new(0x488);
    local_4 = 0;
    if (this_00 == (void *)0x0) {
      this_01 = (undefined4 *)0x0;
    }
    else {
      this_01 = FUN_004706e0(this_00,*(undefined4 *)((int)this + 4));
    }
    local_4 = 0xffffffff;
    *(undefined4 **)((int)this + 8) = this_01;
    *(undefined1 *)((int)this + 0xd) = 1;
    uVar3 = FUN_00470930(this_01,param_1,'\0');
    if ((char)uVar3 != '\x01') {
      MessageBoxA(0,param_1,"COULD NOT LOAD",0);
      FUN_004bbaeb(0);
      pcVar1 = (code *)swi(3);
      uVar3 = (*pcVar1)();
      return uVar3;
    }
    *(undefined1 *)((int)this + 0xc) = 1;
    uVar3 = (**(code **)(*(int *)this + 4))();
  }
  ExceptionList = pvStack_14;
  return CONCAT31((int3)((uint)uVar3 >> 8),1);
}

CameraLookAt (0x00413280)

void __fastcall CameraLookAt(int *param_1)

{
  int iVar1;
  int iVar2;
  int iVar3;
  void *pvVar4;
  undefined4 *puVar5;
  int *piVar6;
  undefined4 auStack_20 [3];
  void *pvStack_14;
  void *pvStack_c;
  undefined1 *puStack_8;
  undefined4 local_4;
  
  local_4 = 0xffffffff;
  puStack_8 = &LAB_004c97a6;
  pvStack_c = ExceptionList;
  ExceptionList = &pvStack_c;
  pvVar4 = operator_new(0x10d0);
  local_4 = 0;
  if (pvVar4 == (void *)0x0) {
    puVar5 = (undefined4 *)0x0;
  }
  else {
    puVar5 = FUN_00461510(pvVar4,*(undefined4 *)(param_1[0x21e] + 0x174),
                          "levels\\arena-spawnplatform");
  }
  local_4 = 0xffffffff;
  param_1[0x10e3] = (int)puVar5;
  pvVar4 = operator_new(0x10d0);
  local_4 = 1;
  if (pvVar4 == (void *)0x0) {
    puVar5 = (undefined4 *)0x0;
  }
  else {
    puVar5 = FUN_00461510(pvVar4,*(undefined4 *)(param_1[0x21e] + 0x174),"levels\\arena-stands");
  }
  param_1[0x10e4] = (int)puVar5;
  local_4 = 0xffffffff;
  (**(code **)(*param_1 + 0x90))(param_1[0x10e3]);
  (**(code **)(*param_1 + 0x90))(param_1[0x10e4]);
  FUN_0040b090((int)param_1);
  piVar6 = (int *)FUN_004605e0((void *)param_1[0x22b],auStack_20,"CAMERALOOKAT",(undefined1 *)0x0);
  iVar1 = piVar6[1];
  iVar3 = *piVar6;
  iVar2 = piVar6[2];
  param_1[0x10de] = iVar3;
  param_1[0x10df] = iVar1;
  param_1[0x10e0] = iVar2;
  param_1[0x10db] = iVar3;
  param_1[0x10dc] = iVar1;
  param_1[0x10dd] = iVar2;
  param_1[0xa6f] = 0x42340000;
  param_1[0xa70] = 0x44480000;
  param_1[0x10e1] = 0x44480000;
  param_1[0x10e2] = 1;
  (**(code **)(*param_1 + 0x54))();
  ExceptionList = pvStack_14;
  return;
}

ESellerate_Init (0x00429200)

void __fastcall ESellerate_Init(int param_1)

{
  int iVar1;
  
  if (*(char *)(param_1 + 0x218) == '\0') {
    *(undefined1 *)(param_1 + 0x218) = 1;
    iVar1 = FUN_00473355();
    if (iVar1 == 1) {
      MessageBoxA(0,"Count not install eSellerate Engine!",&DAT_004d2908,0);
    }
  }
  return;
}

App_Shutdown (0x0046ba10)

void __fastcall App_Shutdown(int *param_1)

{
  *(undefined1 *)((int)param_1 + 0x159) = 1;
                    /* WARNING: Could not recover jumptable at 0x0046ba19. Too many branches */
                    /* WARNING: Treating indirect jump as call */
  (**(code **)(*param_1 + 8))();
  return;
}

🔗 Related Documents

Level [[53892486924051|collision system]]

types : physics
keywords :

📂 View source on GitHub


Hamsterball Level Collision System

This document describes the level-side collision classes used by the game’s static mesh raycasts. It is distinct from the ball’s CollisionMesh physics body (see COLLISIONMESH_OBJECT_MODDING.md).

All offsets and addresses are from the original Windows Hamsterball.exe (image base 0x00400000).


Overview

When a level is loaded, the game creates two objects from the .MW file:

Scene Offset Field Type Purpose
+0x8AC source_meshworld_ptr MeshWorld* Render/source level geometry, loaded by MeshWorld_ctor
+0x8B0 collision_level_ptr CollisionLevel* (or Level*) Collision-only copy of the level, used by Mesh_FindClosestCollision

Both pointers are filled by Scene_LoadLevel2 / Scene_LoadLevel3 etc. and finalized by Level_InitScene. The collision pointer (+0x8B0) is the this pointer you hook in Mesh_FindClosestCollision.


Class Hierarchy

SceneObject (base)
  └── Level / MeshWorld
        ├── +0x8   MeshWorld*   inner_mesh_world  (copy of source)
        ├── +0x480 SpatialTree* root_spatial_tree (acceleration structure)
        └── inherited AthenaLists for buffers / objects

CollisionLevel = Level variant produced by CollisionLevel_ctorWithLevel
                 its vtable is at 0x004D9068.

Important distinction

  • MeshWorld contains the render-side vertex buffers, materials, and object lists.
  • CollisionLevel is a separate level instance that mirrors the geometry but is optimized for collision queries (spatial tree, NOCOLLIDE handling, (NOCOLLIDE) tag stripping, etc.).
  • You almost never want to touch the source MeshWorld for physics; raycasts use Scene + 0x8B0.

CollisionLevel / Level Struct Layout

The class is loaded through MeshWorld_ctor and CollisionLevel_ctorWithLevel. alloc size is 0x10d0 (4304) bytes.

Offset Type Field Notes
+0x000 void** vtable 0x004D9068 for CollisionLevel; 0x004D8FB0 for plain Level
+0x004 int ref_count Standard SceneObject refcount
+0x008 MeshWorld* inner_meshworld Points to the internal copy of the render mesh. Collision_TraverseSpatialTree reads this + 0x8+0x2c (mesh buffer list).
+0x00C int owner_id Usually holds the owning App* passed to Level_ctor.
+0x018 AthenaList child_levels List of subdivided child levels (used by Scene_SubdivideRandom).
+0x430 byte has_subdivided Set to 1 if the level was spatially subdivided.
+0x431 byte use_subdivision Subdivision active flag.
+0x434 uint32_t level_index Level number / index.
+0x438 Timer update_timer Standard Timer object.
+0x47C void* owner_app Back-pointer to the App instance.
+0x480 SpatialTree* root_spatial_tree Root of the static spatial acceleration tree (size 0x10d0).
+0x484 byte build_spatial_tree If 1, a spatial tree was built for collision.
+0x488 AthenaList render_contexts RenderContext list.
+0x8A0 AthenaList mesh_buffer_list2 Secondary buffer list.
+0xCB8 AthenaList object_list Level objects such as START1-1, E:JUMP, etc.

Note: Many internal lists are AthenaList (12-byte embedded structure: head index + size + data vector). Do not write raw pointers into them.


The SpatialTree

SpatialTree is the actual acceleration structure used for ray-triangle tests. It is allocated at 0x10d0 bytes and stored in the Level at +0x480.

SpatialTree vtable: 0x004D9038

Slot Address Notes
0 0x004633B0 SpatialTree_DeletingDtor
1 0x004633B0? destructor path
2+ traversal / build slots Collision_TraverseSpatialTree uses child lists

SpatialTree notable fields

Offset Type Field Default Notes
+0x00 void** vtable 0x004D9038
+0x04 void* owner list or Level* Used by CollisionNode_BaseInit
+0x08 void* meshworld Used to reach mesh buffers (+0x2c)
+0x0C float leaf_size 0.1f Cell/target size for tree subdivision
+0x10 int max_depth 6 Max spatial-tree recursion
+0x14 float scale 0.9f Bounding-box scale factor
+0x18+0x1E byte[7] axis_flags all 1 Which axes are active for subdivision

Collision_TraverseSpatialTree recursively walks the child list at +0x18, tests triangles from each MeshBuffer against an AABB, and appends candidate triangles to a temporary collision mesh.


How to Access an Instance

From the global App

The easiest stable pointer is the global App object at 0x004FD680:

App* app   = (App*)0x004FD680;
void* scene = *(void**)((char*)app + 0x878);   // App->active_scene
void* collision_level = *(void**)((char*)scene + 0x8B0);

From the Ball

void* ball  = ...;
void* scene = *(void**)((char*)ball + 0x14);  // Ball->scene
void* collision_level = *(void**)((char*)scene + 0x8B0);

From a Mesh_FindClosestCollision hook

The this pointer in ECX is exactly Scene + 0x8B0:

using Mesh_FindClosestCollision_t =
    Vec3* (__thiscall*)(void* collision_level, Vec3* out,
                         Vec3 origin, Vec3 direction, float max_dist);

Mesh_FindClosestCollision_t Original =
    (Mesh_FindClosestCollision_t)0x00465D90;

Key Functions

Address Name Role
0x00461510 MeshWorld_ctor Loads/references a .MW level and creates the source MeshWorld.
0x00465080 CollisionLevel_ctorWithLevel Creates a collision-only Level from a source MeshWorld. Sets vtable 0x004D9068; calls Level_LoadMeshes.
0x00465860 Level_LoadMeshes Builds the internal MeshWorld copy and collision geometry for a Level.
0x00465D90 Mesh_FindClosestCollision Main raycast API; builds a temporary spatial tree over the level and casts a ray. See How max_dist Actually Works below.
0x00403980 Ball_FindMeshCollision Thin wrapper around Mesh_FindClosestCollision; rarely called by the engine directly.
0x00465EF0 Collision_TraverseSpatialTree Recursive triangle-collection function inside Mesh_FindClosestCollision.
0x00463330 SpatialTree_ctor Initializes an axis-aligned spatial subdivision node.
0x0040D280 Scene_LoadLevel2 Hard-coded loader for Level 2; allocates MeshWorld at +0x8AC and CollisionLevel at +0x8B0.
0x0040B090 Level_InitScene Finishes scene setup after both level instances are created; reads scene + 0x8AC and +0x8B0.
0x00456D80 CollisionMesh_ctor Creates the temporary CollisionMesh used inside a single raycast (not persisted).

Vtables

CollisionLevel / Level vtable: 0x004D9068

Slot Address Logical method
0 0x00465240 Level_DeletingDtor2
1 0x004606D0 (render / strip builder)
2 0x00472770 SceneObject_BuildStrips
3 0x00471750 LoadMesh
4 0x00470440 (render utility)
5 0x0046F3B0 (render utility)
6 0x00470150 SceneObject_RenderFull
7 0x0046F390 (render utility)
8 0x00471830 utility / mesh preprocessor
9 0x0045DFD0 SceneObject_CallRender variant
10 0x00461370 MeshWorld_CollectTrianglesInAABB related
11 0x0044ACB0 SceneObject_CallUpdate variant
12 0x00465100 Level_LoadCollision / clone helper
13 0x00461890 Scene_LoadMeshWorld
14 0x004651D0 Level_dtor helper
15 0x00460DA0 Scene_RenderFrame related
16 0x00461F00 Scene_Subdivide
17 0x00462100 Scene_SubdivideRandom
18 0x00465650 Level_CloneTree related

CollisionMesh (temporary) vtable: 0x004D8E10

This is the temporary collision buffer used inside Mesh_FindClosestCollision, not the level class itself. Listed here because the same name appears in the code path.

Slot Address Method
0 0x00456870 Mesh_DeletingDtor
1 0x004564C0 Ball_AdvancePositionOrCollision
5 0x00456120 CollisionMesh_AddTriangle
8 0x00457A20 helper
19 0x00458200 helper
22 0x0045DF20 traversal helper
23 0x00458A40 Scene_ScalarDtorBase path

Hooking Example: Ground Raycast Using the Collision Level

struct Vec3 { float x, y, z; };

using Mesh_FindClosestCollision_t =
    Vec3* (__thiscall*)(void* collision_level,
                        Vec3* out,
                        Vec3 origin,
                        Vec3 direction,
                        float max_dist);

static auto Mesh_FindClosestCollision =
    (Mesh_FindClosestCollision_t)0x00465D90;

bool IsGroundedAt(float x, float y, float z, void* scene, float radius) {
    void* collision_level = *(void**)((char*)scene + 0x8B0);
    if (!collision_level) return false;

    Vec3 origin = { x, y, z };
    Vec3 down   = { 0.0f, -1.0f, 0.0f };
    Vec3 out    = { x, y - 1000.0f, z };
    float max_dist = radius + 0.5f;

    Mesh_FindClosestCollision(collision_level, &out, origin, down, max_dist);

    float dy = origin.y - out.y;
    return (dy > 0.001f && dy < max_dist);
}

Note: The max_dist check in the return line above (dy < max_dist) is a caller-side filter. The function itself does NOT use max_dist as a distance limit — see below.


How max_dist Actually Works

Verified by full decompilation chain on 2026-06-17. Traced through 5 functions.

max_dist is the collision sphere radius — it controls how wide the AABB broad-phase query is around the ray. It is NOT a distance limit on the ray itself (the ray is always ~994 units long after clamping).

Full call chain

Step 1 — Mesh_FindClosestCollision (0x465D90):

  • Creates a temp CollisionMesh and calls Ball_InitBattleMode which sets:
    • +0xC68 (friction) = 0.555
    • +0xC70 (max_speed) = 1000.0
    • +0xC7C (use_collision_callback) = 1
    • +0xC8C (gravity2 vector) = (0, -1.0, 0) BUT +0xC64 (scale) = 0.0no gravity
  • Scales direction to 99999 units: Vec3_NormalizeAndScale(&direction, 99999.0)
  • Packs max_dist into Vec3(d, d, d)
  • Calls Ball_AdvancePositionOrCollision(mesh, out, origin, &dir_99999, &max_dist_vec, 0.01)

Step 2 — Ball_AdvancePositionOrCollision (0x4564C0):

  • Velocity starts at (0,0,0), adds dir_99999 → velocity = 99999 units
  • Clamps to max_speed=1000: Vec3_NormalizeAndScale(&velocity, 1000.0) → effective ray = 1000 units
  • Friction damping: velocity *= (1.0 - 0.01) + (1.0 - 0.555) * 0.01 = 0.99445 → ~994 units
  • Gravity: 0.01 * scale(0.0) = 0.0no gravity applied
  • use_callback=1 → calls vtable[7] (0x456890): callback(buf, origin, velocity, max_dist_vec, 0.01, &hit_flag)

Step 3 — Collision callback (0x456890):

  • Computes magnitude = sqrt(max_dist² × 3) = max_dist × √3
  • If magnitude < 0.0001: return origin (no collision — max_dist too small)
  • Calls AABB_FromSphere(origin, velocity, max_dist_vec, &min_bounds, &max_bounds)

Step 4 — AABB_FromSphere (0x477330):

  • Computes the axis-aligned bounding box of the swept sphere:
    min = min(origin, origin+velocity) - max_dist - 0.01
    max = max(origin, origin+velocity) + max_dist + 0.01
    
  • max_dist is the sphere radius that expands the AABB on all axes
  • The spatial tree then returns only triangles that intersect this AABB

Step 5 — Back in callback:

  • Normalizes space: divides origin and velocity by max_dist (sphere radius → 1.0)
  • Does sphere-vs-triangle intersection in normalized space
  • Scales hit point back by max_dist to get world-space result

What this means in practice

The AABB is a box from origin to origin + velocity (the ray), expanded by max_dist on all sides. The spatial tree only returns triangles within this box.

max_dist AABB width (perpendicular to ray) Effect
39 39 units Wide box → captures floor 26 units below a horizontal ray
5 5 units Tight box → floor excluded → only nearby walls returned

Your max_dist should be the ball's collision radius (or slightly larger). The game uses radius + 0.5f. Using a large value like 39 causes the AABB to include geometry far off-axis from the ray direction.

The 0.01 parameter

The 0.01 (param_5) is a physics damping/timestep factor:

fVar3 = (1.0 - 0.01) + (1.0 - friction) * 0.01;  // velocity damping
fVar3 = 0.01 * scale;                              // gravity scaling (scale=0 → no gravity)

It is NOT a sphere radius and NOT a distance limit.

Summary

Parameter What it actually does
direction Normalized → 99999 → clamped to 1000 → damped to ~994. This is the ray length.
max_dist Sphere radius for AABB broad-phase. Expands the bounding box perpendicular to the ray. Larger = wider query = more triangles tested. Use ball_radius + 0.5f.
0.01 (internal) Physics damping factor. Not accessible to callers.

Practical implications

  • To detect walls: use max_dist = ball radius (e.g., 10). The AABB will be tight enough to exclude floor geometry when casting horizontally.
  • To detect ground: cast downward with the same max_dist. The AABB will include the floor naturally.
  • To limit search range: check distance(origin, out) after the call. The ray is always ~994 units long.
  • Too-large max_dist: includes off-axis geometry in the AABB, causing false hits (like the floor 26 units below when casting horizontally with max_dist=39).

Modding Notes

  • Do not free or replace Scene + 0x8B0. It owns triangle data and child spatial-tree memory used by the physics tick.
  • The spatial tree is rebuilt per raycast in Mesh_FindClosestCollision. The level pointer stored at +0x8B0 is stable; the heavy work is done inside Collision_TraverseSpatialTree.
  • Scene + 0x8AC is the source MeshWorld used for rendering and object lookup; Scene + 0x8B0 is the collision-only copy. Modifying +0x8AC geometry will not affect physics unless you regenerate +0x8B0.
  • CollisionLevel_PlayBreakSound (0x00435B00) and CollisionLevel_Spatial_Ctor (0x00436250) are arena/break-specific branches; normal race levels use the standard collision path.

Sources

All data verified from live GhidraMCP headless decompilation of Hamsterball.exe:

  • CollisionLevel_ctorWithLevel @ 0x00465080
  • Level_LoadMeshes @ 0x00465860
  • Level_ctor @ 0x00461740
  • MeshWorld_ctor @ 0x00461510
  • Mesh_FindClosestCollision @ 0x00465D90
  • Collision_TraverseSpatialTree @ 0x00465EF0
  • SpatialTree_ctor / SpatialTree_CloneToLevel @ 0x00463330 / 0x00462380
  • Scene_LoadLevel2 @ 0x0040D280
  • Level_InitScene @ 0x0040B090
  • Static disassembly of Hamsterball.exe for field references to +0x8AC and +0x8B0

Document generated: 2026-06-17


🔗 Related Documents

Level Base Color System

types : rendering
keywords :

📂 View source on GitHub


Level Base Color System

Overview

Each race/arena level in Hamsterball has a base color — an RGB triplet hardcoded
in the BoardLevel constructor. This color is NOT stored in the MESHWORLD file. It
controls the tint of:

  1. Timer oval background (timerblot.png quad tinted with the level color)
  2. Timer text (the countdown clock numbers, rendered with showcardgothic72 font)
  3. Arena score display oval (same mechanism, different position/size)

When the timer is running low, the G and B channels are zeroed (leaving only R),
making the timer turn red as a visual warning.

Where the Color Is Set

Each BoardLevel*_ctor function calls Vec3_Init with three float constants, then
writes the result to the board struct:

// Example: LevelBoard_WarmUp_ctor (0x0041CA40)
iVar1 = Vec3_Init(local_20, 0x3f800000, 0, 0x3f800000);  // R=1.0, G=0.0, B=1.0 = Magenta
*(board + 0x1508) = *(iVar1 + 4);   // R
*(board + 0x150C) = *(iVar1 + 8);   // G
*(board + 0x1510) = *(iVar1 + 0xC); // B
*(board + 0x1514) = *(iVar1 + 0x10); // A (always 1.0 from Vec3_Init)
Matrix_Identity(local_20);

Board Struct Fields

Offset Type Description
+0x1508 float Base color R
+0x150C float Base color G
+0x1510 float Base color B
+0x1514 float Base color A (always 1.0)
+0x4340 float Render scale factor (always 1.0, set in Board_ctor at 0x00419030)

Where the Color Is Used

Timer HUD — Scene_RenderTimerHUD (vtable[0x1C] @ 0x0041BFD0)

This is Scene vtable slot 0x1C (offset 0x70 in vtable), called during the overlay
render pass (after opaque + translucent passes).

1P race mode:

// Read level color from board
R = *(float*)(board + 0x1508);
G = *(float*)(board + 0x150C);
B = *(float*)(board + 0x1510);
Scale = *(float*)(board + 0x4340);  // 1.0

// Draw timer oval (timerblot.png quad, tinted with level color)
Scene_CreateObject4f(gfx, &timer_vtable, x, 10.0, 180.0, 105.0,
    &Vec3_dtor, R, G, B, Scale);

// Build color matrix for text
Matrix_Scale4x4(&matrix, 1.0, 1.0, 1.0, Scale);  // Normal: full color

// Time warning thresholds:
if (time < 0x44C) {  // ~1100ms = 1.1 seconds
    Matrix_Scale4x4(&matrix, 1.0, 0, 0, Scale);  // Zero G,B → RED
}
if (time < 600) {  // 600ms = 0.6 seconds
    Matrix_Scale4x4(&matrix, 1.0, 0, 0, Scale);  // Keep RED
}

// Draw timer text (showcardgothic72 font)
UI_DrawTextCentered(font72, formatted_time, ..., R, G, B, Scale);
// Draw decimal text (showcardgothic28 font, smaller)
UI_DrawTextShadow_Wrapper(font28, ".N", ..., R, G, B, Scale);

2P race mode: Same mechanism, draws two timer displays (one per viewport).

Arena Score HUD — Scene_RenderScoreHUD (vtable[0x1B] @ 0x0041B710)

This is Scene vtable slot 0x1B (offset 0x6C in vtable), called during the post-FX
render pass.

// Arena mode score display also uses the level color
R = *(float*)(board + 0x1508);
G = *(float*)(board + 0x150C);
B = *(float*)(board + 0x1510);
Scale = *(float*)(board + 0x4340);

// Draw score oval (timerblot.png quad, tinted with level color)
Scene_CreateObject4f(gfx, &timer_vtable, 460.0, 17.0, 80.0, 46.67,
    &Vec3_dtor, R, G, B, Scale);

// Score text uses WHITE (1,1,1) with level color only on the oval
Matrix_Scale4x4(&matrix, 1.0, 1.0, 1.0, Scale);

Level Name Text — NOT Level-Colored

The level name string (e.g., "WARM-UP RACE") is stored at board+0x29B4 and rendered
in the ScoreHUD with hardcoded white text and a black shadow:

// Background rect: semi-transparent (alpha=0.75)
Matrix_Scale4x4(&matrix, 1.0, 1.0, 0, 0.75);
UI_DrawRectAndReset(gfx);

// Shadow: black (0,0,0,1.0)
// Main text: white (1,1,1,1.0)
Matrix_Scale4x4(&matrix, 1.0, 1.0, 1.0, 1.0);
UI_DrawTextCenteredAbsolute(font28, level_name, x+400, 0x82, 5, 5, ...);

Timer Oval Texture

The oval behind the timer uses timerblot.png (loaded at App+0x390 by
App_ResourceLoader at 0x004298C0). This is a white/neutral texture that gets
tinted by the level color via D3D8 MODULATE blending (texture × vertex diffuse color).

Related textures also loaded but used elsewhere:

  • blueblot.png (App+0x354) — alternative blot, possibly for 2P indicator
  • blueblot2.png (App+0x358) — another blot variant
  • bluecircle.png (App+0x35C) — circle overlay

Color Table — Race Levels

Level MESHWORLD Ctor Address R G B RGB255 Color
Warm-Up Level1 0x0041CA40 1.0 0.0 1.0 (255,0,255) Magenta/Pink
Intermediate Level2 0x0041CB20 0.0 0.0 1.0 (0,0,255) Blue
Dizzy Level3 0x0041D060 0.0 1.0 0.0 (0,255,0) Green
Tower Level4 0x0041E340 1.0 0.75 0.0 (255,191,0) Orange
Expert Level5 0x0041EA40 1.0 0.0 0.0 (255,0,0) Red
Odd Level6 0x0041ED80 1.0 0.5 0.0 (255,128,0) Orange
Wobbly Level7 0x0041F110 0.62 0.84 0.30 (158,214,77) Yellow-Green
Toob Level8 0x0041F4B0 0.5 0.5 1.0 (128,128,255) Light Blue
Sky Level9 0x0041F930 0.0 0.5 1.0 (0,128,255) Sky Blue
Beginner LevelCascade 0x004200E0 1.0 0.75 0.25 (255,191,64) Gold
Up LevelUp 0x00420390 1.0 0.0 1.0 (255,0,255) Magenta/Pink
Master Level10 0x004206D0 0.5 0.5 0.5 (128,128,128) Gray
Neon LevelDark 0x00424440 1.0 1.0 0.0 (255,255,0) Yellow
Impossible LevelImpossible 0x00424C20 1.0 0.0 0.0 (255,0,0) Red

Color Table — Arena Levels

Arena levels use the same mechanism (same struct offset, same Vec3_Init pattern).
Each ArenaBoard constructor sets the color identically to its race counterpart.

Arena Ctor Address R G B RGB255 Color
Warmup Arena 0x004224A0 1.0 0.0 1.0 (255,0,255) Magenta/Pink
Beginner Arena 0x00422550 1.0 0.75 0.25 (255,191,64) Gold
Intermediate Arena 0x004226E0 0.0 0.0 1.0 (0,0,255) Blue
Dizzy Arena 0x00422790 0.0 1.0 0.0 (0,255,0) Green
Tower Arena 0x004228C0 1.0 0.75 0.0 (255,191,0) Orange
Expert Arena 0x00423060 1.0 0.0 0.0 (255,0,0) Red
Odd Arena 0x00423220 1.0 0.5 0.0 (255,128,0) Orange
Toob Arena 0x004234E0 0.5 0.5 1.0 (128,128,255) Light Blue
Wobbly Arena 0x00423690 0.62 0.84 0.30 (158,214,77) Yellow-Green
Sky Arena 0x00423BF0 0.0 0.5 1.0 (0,128,255) Sky Blue
Master Arena 0x00424380 0.5 0.5 0.5 (128,128,128) Gray
Neon Arena 0x00424860 1.0 1.0 0.0 (255,255,0) Yellow

Floor Checker Textures (Separate System)

The floor checker/brick textures are a separate color system — they are NOT
controlled by board+0x1508. Instead, each level's MESHWORLD file references textures
by name, and the game loads them all at startup into App struct slots:

App Offset Texture File Used By
+0x2C8 pinkchecker.bmp Warm-Up, Up
+0x2CC bluechecker.bmp Intermediate
+0x2D0 bluebrick.png Intermediate (brick variant)
+0x2D4 greenchecker.bmp Dizzy
+0x2D8 greenbrick.png Dizzy (brick variant)
+0x2DC yelllowchecker.png Neon
+0x2E0 greyoutlinechecker.png Master
+0x2E4 redchecker.bmp Tower, Expert, Impossible
+0x2E8 redbrick.png Tower/Expert (brick variant)
+0x2EC orangechecker.bmp Odd, Beginner
+0x2F0 orangebrick.png Odd/Beginner (brick variant)
+0x2F4 brightgreenchecker.bmp Wobbly
+0x2F8 brightgreenbrick.png Wobbly (brick variant)
+0x2FC toobchecker.png Toob
+0x300 toobbrick.png Toob (brick variant)
+0x304 skychecker.png Sky
+0x308 purplechecker.bmp (unused?)
+0x30C purplebrick.png (unused?)
+0x310 brownbrick.png (unused?)
+0x314 blackchecker.png (unused?)

The MESHWORLD material section (*BITMAP entries in the ASCII format, or texture
name strings in the binary format) references these textures by filename. The floor
color comes from the texture itself, not from the board struct.

MESHWORLD Colors (Section 4)

The meshworld binary format DOES contain color data in Section 4:

Offset Type Description
MW+0x45C float[3] Background color (R,G,B) — skybox/clear color
MW+0x468 float[3] Ambient light color (R,G,B) — scene-wide ambient

These are read by Scene_LoadMeshWorld (0x00461890) via __read(file, MW+0x45C, 0x18)
(24 bytes = 6 floats). They control:

  • Background color: The D3D8 clear color / skybox tint
  • Ambient light: The minimum illumination for all lit surfaces

These are completely independent from the board+0x1508 timer oval color.

Race Selection Menu Colors (Separate System)

The race selection menu (Practice/Time Trial) uses separate hardcoded colors
in PracticeMenu_ctor (0x0042EA30). Each UIList_AddItem call is preceded by a
Matrix_Scale4x4 that sets the text color for that item. These colors are NOT
the same as board+0x1508 — they are independently defined and generally brighter
(more pastel) versions of the level colors.

Level Menu RGB255 Board RGB255 Same?
Warm-Up (255,191,255) (255,0,255) No
Beginner (255,191,64) (255,191,64) Yes
Intermediate (191,191,255) (0,0,255) No
Dizzy (191,255,191) (0,255,0) No
Tower (255,229,115) (255,191,0) No
Up (191,115,255) (255,0,255) No
Neon (255,255,0) (255,255,0) Yes
Expert (255,64,64) (255,0,0) No
Odd (255,191,0) (255,128,0) No
Toob (191,191,255) (128,128,255) No
Wobbly (156,240,74) (158,214,77) No
Glass (255,191,255) (255,0,255) No
Sky (64,191,255) (0,128,255) No
Master (217,184,69) (128,128,128) No
Impossible (255,0,0) (255,0,0) Yes

Conclusion: Changing board+0x1508 would NOT affect the race selection menu text
colors. Those are separate hardcoded values in PracticeMenu_ctor at 0x0042EA30.
Locked levels all use gray (0x3f266666 = ~0.65) for all channels.

The menu also loads level preview images (practice-level1.png through
practice-impossible.png) stored as Sprite objects at menu+0xCDC through +0xD14.

Summary

Color Source Where Set What It Controls
Board ctor Vec3_Init board+0x1508/0x150C/0x1510 Timer oval tint, timer text tint, arena score oval tint
PracticeMenu_ctor Matrix_Scale4x4 Per-item in UIList Race selection menu text color (NOT same as board color)
MESHWORLD Section 4 MW+0x45C (bg) / MW+0x468 (ambient) Background clear color, ambient scene lighting
MESHWORLD *BITMAP Per-material texture reference Floor checker/brick texture (the actual pixel colors)
MESHWORLD *MATERIAL_DIFFUSE Per-material diffuse color Mesh surface diffuse color
MESHWORLD *MATERIAL_AMBIENT Per-material ambient color Mesh surface ambient color

The timer oval color and level text color are hardcoded per-level in the EXE
they cannot be changed by editing the MESHWORLD file. To change them, you must
either patch the EXE's Vec3_Init call arguments or use a DLL mod to overwrite
board+0x1508/0x150C/0x1510 at runtime after the board constructor runs. The race
selection menu text colors are a separate set of hardcoded values in
PracticeMenu_ctor — changing board+0x1508 will NOT change the menu text.

Key Function Addresses

Function Address Description
Scene_RenderTimerHUD 0x0041BFD0 Draws timer oval + timer text (race mode)
Scene_RenderScoreHUD 0x0041B710 Draws arena score oval + level name
Scene_CreateObject4f 0x00418870 Creates tinted quad (used for timer oval)
Gfx_DrawQuadRandomColor 0x0045D450 Low-level quad renderer with color
Board_ctor 0x00419030 Base board ctor (sets scale=1.0)
App_ResourceLoader 0x004298C0 Loads timerblot.png and all textures
Vec3_Init (inline) Initializes 4-float vector from 3 components

🔗 Related Documents

Level Colors Mod

types : mods
keywords :

📂 View source on GitHub


Level Colors Mod

Changes the per-level base colors (timer oval, timer text, and race selection menu text) based on a colors.txt config file.

Installation

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod bass.dll into the game folder
  3. Launch Hamsterball — the mod auto-creates colors.txt on first run

Config File

The mod reads colors.txt next to bass.dll. Edit it at runtime — changes apply within 2 seconds.

Format

; LevelName=RRGGBB (hex RGB, like HTML colors)
; Lines starting with ; or # are comments
;
; Prefixes:
;   LevelName=RRGGBB       — applies to BOTH timer and menu
;   board:LevelName=RRGGBB — only timer oval/text during gameplay
;   menu:LevelName=RRGGBB  — only race selection menu text

WarmUp=FF00FF
Beginner=FFBF40
Intermediate=0000FF
Dizzy=00FF00
Tower=FFBF00
Up=FF00FF
Neon=FFFF00
Expert=FF0000
Odd=FF8000
Toob=8080FF
Wobbly=9ED64D
Glass=FF00FF
Sky=0080FF
Master=808080
Impossible=FF0000

Level Names

Name Level
WarmUp Warm-Up Race
Beginner Beginner Race
Intermediate Intermediate Race
Dizzy Dizzy Race
Tower Tower Race
Up Up Race
Neon Neon Race
Expert Expert Race
Odd odd race
Toob Toob Race
Wobbly Wobbly Race
Glass Glass Race
Sky Sky Race
Master Master Race
Impossible Impossible Race

What It Changes

Board Colors (timer oval + timer text)

During gameplay, the timer oval background (timerblot.png) and the countdown numbers are tinted with each level's color. The mod writes to board+0x1508 (R), +0x150C (G), +0x1510 (B) at runtime via a background thread polling every 100ms.

When the timer is running low, the game's own code zeros the G and B channels, making the timer turn red as a warning — this behavior is preserved.

Menu Colors (race selection text)

In the Practice/Time Trial race selection menu, each level name is displayed in its own color. The mod patches the hardcoded float constants in PracticeMenu_ctor (0x0042EA30) using VirtualProtect to make the .text section writable, then overwrites the PUSH immediate operands.

These patches are applied once at startup and persist for the session.

How It Works

The mod uses two independent mechanisms:

  1. Board colors: Background thread polls the active board struct and writes RGB floats. The game sets these once in the BoardLevel constructor via Vec3_Init, so the mod must re-apply them continuously.

  2. Menu colors: Code patching. Each Matrix_Scale4x4 call in PracticeMenu_ctor has four PUSH instructions with float immediates (A, B, G, R in cdecl reverse order). The mod patches the 4-byte float operands directly in the .text section.

Build

i686-w64-mingw32-gcc -shared -o bass.dll level_colors.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Technical Details

Board Color Patch Points

Offset Field Type
board+0x1508 R float
board+0x150C G float
board+0x1510 B float
board+0x1514 A float (always 1.0)
board+0x4340 Scale float (always 1.0)

Menu Color Patch Addresses

Each level has three patch points (R, G, B float operands in PUSH instructions):

Level R addr G addr B addr
WarmUp 0x0042EE94 0x0042EE8F 0x0042EE8A
Beginner 0x0042EED3 0x0042EECE 0x0042EEC9
Intermediate 0x0042EF0D 0x0042EF08 0x0042EF03
Dizzy 0x0042EF60 0x0042EF5B 0x0042EF56
Tower 0x0042EFDA 0x0042EFD5 0x0042EFD0
Up 0x0042F054 0x0042F04F 0x0042F04A
Neon 0x0042F0D4 0x0042F0CF 0x0042F0CD
Expert 0x0042F14E 0x0042F149 0x0042F144
Odd 0x0042F1C5 0x0042F1C0 0x0042F1BE
Toob 0x0042F248 0x0042F243 0x0042F23E
Wobbly 0x0042F2C2 0x0042F2BD 0x0042F2B8
Glass 0x0042F33C 0x0042F337 0x0042F332
Sky 0x0042F3BF 0x0042F3BA 0x0042F3B5
Master 0x0042F439 0x0042F434 0x0042F42F
Impossible 0x0042F4AD 0x0042F4AB 0x0042F4A9

See docs/rendering/LEVEL_COLOR_SYSTEM.md for the full RE analysis.


🔗 Related Documents

Level Naming and Counts (Verif

types : skill
keywords :

📂 View source on GitHub


Level Naming and Counts (Verified 2026-06-18)

Critical Facts

  • 15 race tracks (single-player tournament progression)
  • 17 arena MESHWORLD files (verified by file enumeration 2026-06-18: Arena-Beginner, Arena-Dizzy, Arena-Expert, Arena-Glass, Arena-Impossible, Arena-Intermediate, Arena-Master, Arena-Neon, Arena-Odd, Arena-Sky, Arena-SpawnPlatform, Arena-Stands, Arena-Toob, Arena-Tower, Arena-Up, Arena-WarmUp, Arena-Wobbly)
  • 86 total MESHWORLD files across all categories (65 race-track files + 17 arenas + 4 utility objects)
  • Internal file names and XML tags do NOT match display names
  • LevelCascade = Beginner Race (tournament position #2, NOT Intermediate)
  • BEGINNERRACE XML tag = Warm-up Race (the first race)
  • CASCADERACE XML tag = Beginner Race (the second race)

MESHWORLD File Breakdown by Category

Race Track Files (65 total)

Tier Main File Variant Files Count
Level1 Level1 1
Level2 Level2 Bridge 2
Level3 Level3 Gluebie, Swirl, Tipper, WaterWheel 5
Level4 Level4 Catapult, Drawbridge, Mace, Trapdoor1, Trapdoor2, Turret, Windmill 8
Level5 Level5 Bonk, Bridge 3
Level6 Level6 Lifter 2
Level7 Level7 Wavy1, Wobbly1–Wobbly8 10
Level8 Level8 BlockDawg1, BlockDawg2, Fallout, Saw, Spinny 6
Level9 Level9 PopCylinder1, PopCylinder2, TrapDoor 4
Level10 Level10 2PBridge, Bridge1, Bridge2 4
LevelUp LevelUp Button, Lifter, SpeedCylinder 4
LevelDark LevelDark DFloor1–4, FlickRing, NeonPlatform, Trode 8
LevelImpossible LevelImpossible BigGear, Gear, Looper, Pendulum, Rotator 6
LevelGlass LevelGlass 1
LevelCascade LevelCascade 1

Variant files (e.g. Level7-Wobbly1..8) are modular track pieces assembled into
the full level, not standalone playable tracks. The 15 main tier files are the
playable race tracks.

Arena Files (17 total)

Arena-SpawnPlatform and Arena-Stands are structural (spawn area + spectator
stands), not standalone playable arenas. The 15 playable arenas correspond 1:1
to the 15 race tracks for multiplayer Rumble mode.

Utility Object Files (4 total)

MouseTrap, PopupSign, Secret, Secret-Unlock — game objects, not playable levels.

Common Confusions

"LevelCascade" is Beginner Race — NOT Intermediate, NOT Dizzy

The internal file LevelCascade.MESHWORLD is used for Beginner Race (tournament
position #2). The name "Cascade" comes from the game's internal XML tag CASCADERACE.
It has nothing to do with Intermediate Race or Dizzy Race.

Do NOT let users (or yourself) talk you into changing this. In June 2026 testing,
the user saw unexpected geometry when LevelCascade was swapped with Level3 and
concluded "cascade is another name for dizzy." This was actually caused by stale
.cached files (see below), not a misidentification. The XML race data confirms:
CASCADERACE = Beginner Race = position #2, DIZZYRACE = Dizzy = position #4.

Level2.MESHWORLD is NOT a playable race track. It's not in the tournament order
table. Do not use Level2 for level swaps — use LevelCascade for Beginner Race.

.cached Files Override MESHWORLD Swaps (CRITICAL PITFALL)

The game generates binary .cached files (e.g., level1.cached, level2.cached,
Level2-Bridge.cached) that store pre-processed level data. When a .cached file
exists alongside a .MESHWORLD file, the game loads the cache instead of
re-reading the .MESHWORLD file.

This means swapping .MESHWORLD files has NO effect if .cached files are present.
The game will silently load the old cached data, making you think the swap failed or
that you have the wrong file.

Fix: Delete ALL .cached files from the game's Levels/ directory before testing
any MESHWORLD swap. This forces the game to re-read and re-process the .MESHWORLD
files. The game will regenerate new .cached files on next load with the correct
swapped data.

# Remove all .cached files from a drop-in levels folder
rm -f levels/*.cached

When distributing a drop-in levels folder: Never include .cached files — they
contain stale data from the original level layout and will override your swaps.

XML Tags Are Off By One

XML Tag Display Name Level #
BEGINNERRACE Warm-up Race 1
CASCADERACE Beginner Race 2
INTERMEDIATERACE Intermediate Race 3

The XML tags appear to be "shifted" — BEGINNERRACE is the easiest race (Warm-up),
not what the display name "Beginner Race" would suggest.

Level File Numbers Don't Match Tournament Order (VERIFIED June 2026)

The MESHWORLD filename number does NOT correspond to the menu/tournament position.
The mapping was verified by reading each BoardLevel constructor's vtable[+0x48] slot,
which points to the Scene_SetupLevelN function that loads the actual levels\levelN file.

Key corrections (June 2026):

  • LevelCascade = Beginner Race (position #2), NOT Dizzy. The "Cascade" name comes from the CASCADERACE XML tag.
  • Level2 = Intermediate Race (position #3), NOT Beginner. This was previously mislabeled across many docs.
  • Level3 = Dizzy Race (position #4), NOT Intermediate.
  • LevelBoard_Odd_ctor (0x0041ED80) → vtable[+0x48]=Scene_SetupLevel6 → loads levels\level6 (Level6=Odd, NOT Sky)
  • LevelBoard_Sky_ctor (0x0041F930) → vtable[+0x48]=Scene_SetupLevel9 → loads levels\level9 (Level9=Sky, NOT Odd)

File numbers are internal asset IDs, not menu positions.

Level File Tournament # Display Name
Level1 1 Warm-up Race
LevelCascade 2 Beginner Race
Level2 3 Intermediate Race
Level3 4 Dizzy Race
Level4 5 Tower Race
LevelUp 6 Up Race
LevelDark 7 Neon Race
Level5 8 Expert Race
Level6 9 odd race
Level8 10 Toob Race
Level7 11 Wobbly Race
LevelGlass 12 Glass Race
Level9 13 Sky Race
Level10 14 Master Race
LevelImpossible 15 Impossible Race

Tier Colors

# Race Color RGB
1 Warm-up Race Pink (0.99, 0.63, 1.0)
2 Beginner Race Blue (0.42, 0.62, 0.91)
3 Intermediate Race Green (0.59, 0.91, 0.64)
6 Up Race Red (0.81, 0.0, 0.0)
7 Neon Race Orange (1.0, 0.49, 0.0)

Doc Correction Checklist

When level counts or names are found wrong in docs, check these files:

  1. docs/LEVEL_REFERENCE.md — canonical source of truth
  2. docs/MESHWORLD_OBJECT_TYPES.md — level file tables
  3. docs/KEY_FINDINGS.md — arena init function count, level setup table
  4. docs/RUMBLEBOARD_SYSTEM.md — arena architecture tree, race table
  5. docs/FUNCTION_MAP.md — arena init function table
  6. docs/XML_DATA_FORMATS.md — race data + jukebox tables
  7. docs/LEVEL_LOCKED_OBJECTS.md — BoardLevel section headers
  8. docs/STRUCTS_AND_TYPES.md — arena path count comment
  9. docs/TODO.md — arena function count in completed items

Race Data (from RaceData.xml)

XML Tag Time Pool Par Weasel Gold Silver Bronze CAM
BEGINNERRACE 60 47.0 6.6 15.0 10.3 7.6 2.57
CASCADERACE 50 25.0 15.9 17.5 24.5 30.3 0.64
INTERMEDIATERACE 45 35.0 23.0 26.5 35.2 46.7 0.0
DIZZYRACE 40 35.0 37.2 41.4 48.0 58.8 0.88
TOWERRACE 35 35.0 36.5 40.0 47.8 59.2 0.03
UPRACE 30 25.0 29.7 32.0 35.1 40.8 0.33
NEONRACE 30 25.0 37.7 46.0 55.3 65.8 0.0
EXPERTRACE 30 20 34.0 39.5 48.0 61.2 0.0
ODDRACE 30 20 44.6 48.0 61.8 80.7 1.28
TOOBRACE 25 20 42.3 45.2 53.5 60.6 0.13
WOBBLYRACE 25 20 37.0 44.0 52.1 63.8 0.0
GLASSRACE 25 10 36.0 43.5 52.1 65.0 0.71
SKYRACE 25 5 40 46.0 53.5 60.0 0.44
MASTERRACE 55 2 65 73.4 88.8 112.4 0.0
IMPOSSIBLERACE 50 2 44 60.0 80.3 100.4 0.5

🔗 Related Documents

Level Object Factory

types : objects
keywords :

📂 View source on GitHub


Level Object Factory - Complete Reference

Overview

Hamsterball creates level objects through a layered factory system:

  1. CreateLevelObjects (0x4121D0) - Main dispatcher, matches mesh names via strnicmp
  2. CreateMechanicalObjects (0x417FE0) - Mechanical hazard factory
  3. CreateExpertLevelObjects (0x40E250) - Multi-factory for arena obstacles
  4. CreatePlatformOrStands (0x4133E0) - Platform/stands factory (16 xrefs)
  5. Scene_CreateDynamicObjects (0x40C430) - Dynamic object scanner

All created objects are appended to Scene+0x2578 (AthenaList, the master object list).

CreateLevelObjects Factory Map (0x4121D0)

Mesh Name Object Created Struct Size Scene Offset Notes
BRIDGE Scene+0x436C (bridge mesh) - 0x437C Checks (NOCOLLIDE) suffix; stores pos at +0x437C/80/84
TIPPER Tipper 0x1104 +0x2578 + TipperVisual (0x10D0), attached via TipperVisual_Attach
BONK Bonk 0x1200 +0x540C Arena bonk popup; stored at Scene+0x540C
BBRIDGE1 BreakBridge 0x1100 +0x5418 Breakable bridge 1; Scene+0x5418
BBRIDGE2 BreakBridge 0x1100 +0x541C Breakable bridge 2; Scene+0x541C
POPCYLINDER PopCylinder 0x10E8 +0x5428 Pop-up cylinder; also appended to Scene+0x5428 list
BLOCKDAWG1 Blockdawg 0x1154 +0x2578 Searches for "DAWGPATH1" nav path
BLOCKDAWG2 Blockdawg 0x1154 +0x2578 Searches for "DAWGPATH2" nav path; flag+0x1152=1
CATAPULT Catapult 0x1108 +0x584C Scene+0x584C; flag+0x440=1
GLUEBIE Gluebie 0x110C +0x6080 Sticky trap; appended to Scene+0x6080 list

Additional Level Object Factories

Address Factory Creates Notes
0x40BCA0 CreateBadBall Enemy ball Only in tournament/demo mode
0x40BF50 CreateMouseTrap Mouse trap Only in tournament/demo mode
0x40BAA0 CreateSecretObjects Secret collectibles Hidden items
0x40C0F0 Scene_CreateFlags Finish flags Race completion markers
0x40C270 Scene_CreateSigns Direction signs Arrow signposts
0x40C430 Scene_CreateDynamicObjects Dynamic objects Moving platforms, etc.
0x40C5D0 DispatchCollisionEvents No-dizzy pickup 27 xrefs — common collectible
0x40E250 CreateExpertLevelObjects BONK/TOWER/SAWBLADE/BRIDGE/JUDGE/BELL Multi-factory for arena
0x40FA20 CreateBumper Bumper Standard collision bumper
0x410D00 NeonCollisionEvents Level boundary Invisible wall/limit
0x4117B0 CreateUpLevelObjects Speed boost Acceleration cylinder
0x412850 HandleArenaCollisionEvents Spinning obstacle Rotating hazard
0x4133E0 CreatePlatformOrStands Platform/Stands 16 xrefs - most common factory
0x413CE0 CreateBumper2 Bumper variant Second bumper type
0x4143D0 CreateSpinny Spinny obstacle Rotating hazard
0x414A20 CreateLifter Lifter platform Elevator/lift
0x415460 CreateWobbly1 Wobbly bridge Swaying bridge
0x4173B0 CreateFlickRing Flick ring Launch ring
0x417FE0 CreateMechanicalObjects MECH objects Mechanical hazard factory
0x418760 Scene_CreateObject_Gear Gear object Gear mechanism
0x418870 Scene_CreateObject4f Generic object 25 xrefs - versatile factory
0x438B30 CreateBonkPopup Bonk popup Score popup effect

CreateExpertLevelObjects Arena Multi-Factory (0x40E250)

Called for arena levels. Creates different object types based on mesh name:

Name Pattern Object Type Created By
BONK_xxx Bonk Bonk_ctor
TOWER_xxx Tower Tower_ctor
SAWBLADE_xxx Sawblade Sawblade_ctor
BRIDGE_xxx Bridge Bridge_ctor (arena)
JUDGE_xxx Judge Judge_ctor
BELL_xxx Bell Bell_ctor

Scene Object List Offsets

Offset List Description
0x2578 AthenaList Master object list (all objects)
0x540C Bonk* Bonk popup pointer
0x5410 BreakBridge* Bridge 1 mesh
0x5414 BreakBridge* Bridge 2 mesh
0x5418 BreakBridge* Active bridge 1
0x541C BreakBridge* Active bridge 2
0x5420 PopCylinder* Pop cylinder mesh
0x5428 AthenaList Pop cylinder list
0x5840 Blockdawg* Blockdawg1 path
0x5844 Blockdawg* Blockdawg2 path
0x584C AthenaList Catapult list
0x607C Gluebie* Gluebie mesh
0x6080 AthenaList Gluebie list
0x878 App* Application pointer
0x8AC LevelMesh* Level mesh data
0x8AC LevelMesh Mesh object for FindObjectByName

Object Creation Pattern

All object factories follow this pattern:

void* obj = operator_new(STRUCT_SIZE);  // Allocate
Object_ctor(obj, scene, mesh, ...);      // Initialize
obj->position = mesh_position;           // Copy position from MESHWORLD data
AthenaList_Append(&scene->objects, obj); // Add to master list
scene->specific_list = obj;              // Store in type-specific pointer

🔗 Related Documents

Level Object System

types : objects
keywords :

📂 View source on GitHub


Level Object System

Interactive objects in race levels and ArenaBoard arenas. Created by the
level parser and triggered by collision events.

Level Object Factory Hierarchy

Level (base, 0x10D0 bytes)
├── Stands (inherits Level)
│   └── Catapult (0x10D0 base + extra)
├── Sawblade_Level (0x111C bytes)
├── TowerLevel (0x1188 bytes)
├── Spinner_Level / Bridge (0x10FC bytes)
├── Gear_Level / Judge (0x1100 bytes)
├── Tipper_Level / Bell (0x10E8 bytes)
├── Bonk / Hammer (0x1200 bytes)
├── Trapdoor
└── CollisionLevel (0x10D0 bytes)

Level Base Structure (0x10D0 bytes minimum)

Offset Type Field
+0x000 vtable* Virtual function table
+0x434 int Type identifier
+0x480 MeshWorld* Mesh world pointer
+0x10D0 int Parent board pointer

Catapult System

Catapult_ctor (0x437E10)

Inherits from Stands (which inherits from Level).
Creates a CollisionLevel for the catapult mesh.

Offset Type Default Field
+0x10D0 int param_1 App pointer
+0x10D4 CollisionLevel* new(0x10D0) Collision mesh
+0x10D8 Vec3 (0,0,0) Launch direction
+0x10E4 int 0 State
+0x10E8 float -1.0 Gravity multiplier
+0x10F0 byte 0 is_active
+0x10F4 int Launch timer (set to 50 on launch)
+0x10F8 byte 0 Launch state flag
+0x1100 byte 0 Complete flag
+0x1104 float 16.5 Launch velocity (0x41880000)

Catapult_Launch (0x434290)

Triggered by "E:CATAPULTBOTTOM" collision event (if ball cooldown < 1):

  1. Set is_active = 1 (offset 0x10F0)
  2. Set launch_timer = 50 ticks (0x32)

The catapult then updates in Catapult_Update (0x43E600) each frame,
animating the launch arc and applying velocity to the ball.

Catapult_AddObjectConditional (0x43E9C0)

Conditionally adds an object to the catapult's launch target list.
Only adds if the object meets certain criteria (checked internally).

Catapult_Render (0x43EA70)

Renders the catapult mesh with the current animation state.

Trapdoor System

Trapdoor_Open (0x4344D0)

Triggered by "E:OPENSESAME" collision event.
Opens the first trapdoor in the door list.

Trapdoor_Activate (0x438410)

Triggered by "N:TRAPDOOR" collision event.
Activates all doors matching the collider's ID.

ScoreDisplay System

The arena scoring display, created by App_CreateScoreDisplay (0x46CB00).

Ctor Address Variant
0x448590 ScoreDisplay_CtorA
0x4485C0 ScoreDisplay_CtorB
0x4485F0 ScoreDisplay_CtorC

ScoreDisplay_SetTime (0x434C80)

Called from "E:SCORE<time>" collision events.
Parses the time value from the suffix string and updates the display.

HighScore System

HighScoreEntry (0x42B470)

Individual high score entry display. Created by App_CreateHighScoreEntry
(0x446A60) after a race completes.

HighScoreMenu (0x42B290)

High scores viewer, accessible from Main Menu "HIGH SCORES" button (cmd="HS").

CheckPurchaseOrHighScore (0x40A420)

Gatekeeper function — checks if the game is registered before showing
high scores. Also may check for purchase/unlock status.

Damage System

From TowerCollisionEvents (0x40DCD0):

"E:BITE" Collision Event

When the ball hits a "BITE" object:

  • Sets damage_amount = 25.0 (at App+0x43A0)
  • Sets damage_timer (at App+0x43A8)

This causes the ball to take damage, likely displayed as a visual effect
and potentially affecting ball speed/handling temporarily.

Jump Pad System

From ExpertCollisionEvents (0x40E6A0):

"E:JUMP" Collision Event

When the ball hits a jump pad:

  1. ball.cooldown = 10 (jump cooldown ticks)
  2. ball.vert_vel = 0.008 (upward velocity, 0x3B03126F)
  3. ball.vert_vel_on = 1 (enable vertical velocity)
  4. Play jump pad sound

Collision Level Relationship

Each arena hazard that needs collision detection creates its own
CollisionLevel (0x10D0 bytes) via:

CollisionLevel *cl = CollisionLevel_ctorWithLevel(new(0x10D0), parent_level);

The CollisionLevel wraps a MeshWorld with collision data for ray-casting
against that specific hazard's geometry. Multiple CollisionLevels can exist
simultaneously — the Catapult creates one, the ArenaBoard creates one for
the main arena, and some levels have additional sub-levels.

Related Functions

Address Name Purpose
0x437E10 Catapult_ctor Launch pad constructor
0x434290 Catapult_Launch Activate catapult
0x43E600 Catapult_Update Per-frame catapult animation
0x43E9C0 Catapult_AddObjectConditional Add launch target
0x43EA70 Catapult_Render Render catapult
0x4344D0 Trapdoor_Open Open trapdoor (OPENSESAME)
0x438410 Trapdoor_Activate Activate trapdoor by ID
0x434C80 ScoreDisplay_SetTime Set arena score time
0x46CB00 App_CreateScoreDisplay Create score display widget
0x446A60 App_CreateHighScoreEntry Create high score entry
0x40A420 CheckPurchaseOrHighScore Purchase/registration gate
0x40E250 CreateExpertLevelObjects Master arena factory

🔗 Related Documents

Level Reference

types : gameplay
keywords :

📂 View source on GitHub


Hamsterball — Level Reference

The authoritative list of all 15 race tracks and 15 arenas in Hamsterball.

Key Facts

  • 15 race tracks (single-player tournament progression)
  • 15 arenas (1 per race track, used in multiplayer Rumble mode)
  • Internal file names and XML tags do NOT match display names
  • LevelCascade = Beginner Race (NOT Intermediate, NOT Level 3)
  • BEGINNERRACE XML tag = Warm-up Race (the very first race)
  • CASCADERACE XML tag = Beginner Race (the second race)

Race Tracks (in tournament order)

# Display Name XML Tag Level File Tier Color Board Ctor
1 Warm-up Race BEGINNERRACE Level1.MESHWORLD Pink LevelBoard_WarmUp_ctor (0x41CA40)
2 Beginner Race CASCADERACE LevelCascade.MESHWORLD Blue LevelBoard_Beginner_ctor (0x4200E0)
3 Intermediate Race INTERMEDIATERACE Level2.MESHWORLD Green LevelBoard_Intermediate_ctor
4 Dizzy Race DIZZYRACE Level3.MESHWORLD LevelBoard_Dizzy_ctor
5 Tower Race TOWERRACE Level4.MESHWORLD LevelBoard_Tower_ctor
6 Up Race UPRACE LevelUp.MESHWORLD Red LevelBoard_Up_ctor (0x420390)
7 Neon Race NEONRACE LevelDark.MESHWORLD Orange Board_NeonRace_ctor
8 Expert Race EXPERTRACE Level5.MESHWORLD LevelBoard_Expert_ctor
9 odd race ODDRACE Level6.MESHWORLD LevelBoard_Odd_ctor
10 Toob Race TOOBRACE Level8.MESHWORLD LevelBoard_Toob_ctor (0x41F4B0)
11 Wobbly Race WOBBLYRACE Level7.MESHWORLD LevelBoard_Wobbly_ctor
12 Glass Race GLASSRACE LevelGlass.MESHWORLD Board_Glass_ctor
13 Sky Race SKYRACE Level9.MESHWORLD LevelBoard_Sky_ctor
14 Master Race MASTERRACE Level10.MESHWORLD BoardLevel_Master_Ctor
15 Impossible Race IMPOSSIBLERACE LevelImpossible.MESHWORLD Board_Impossible_ctor

Arenas (in tournament order)

# Display Name Arena File Init Function
1 Warm-up Arena Arena-WarmUp.MESHWORLD ArenaLevel_WarmUp_Init (0x413C20)
2 Beginner Arena Arena-Beginner.MESHWORLD ArenaBoard_Beginner_Init (0x413CE0)
3 Intermediate Arena Arena-Intermediate.MESHWORLD ArenaLevel_Intermediate_Init (0x414180)
4 Dizzy Arena Arena-Dizzy.MESHWORLD ArenaLevel_Dizzy_Init (0x414240)
5 Tower Arena Arena-Tower.MESHWORLD ArenaLevel_Tower_Init (0x4144B0)
6 Up Arena Arena-Up.MESHWORLD ArenaLevel_Up_Init (0x414960)
7 Neon Arena Arena-Neon.MESHWORLD ArenaLevel_Neon_Init (0x416F40)
8 Expert Arena Arena-Expert.MESHWORLD ArenaLevel_Expert_Init (0x414B10)
9 Odd Arena Arena-Odd.MESHWORLD ArenaLevel_Odd_Init (0x414CE0)
10 Toob Arena Arena-Toob.MESHWORLD ArenaLevel_Toob_Init (0x414F00)
11 Wobbly Arena Arena-Wobbly.MESHWORLD ArenaLevel_Wobbly_Init (0x4153A0)
12 Glass Arena Arena-Glass.MESHWORLD ArenaLevel_Glass_Init (0x417DF0)
13 Sky Arena Arena-Sky.MESHWORLD ArenaLevel_Sky_Init (0x4158C0)
14 Master Arena Arena-Master.MESHWORLD ArenaLevel_Master_Init (0x416080)
15 Impossible Arena Arena-Impossible.MESHWORLD ArenaLevel_Impossible_Init (0x418540)

Non-playable arena files: Arena-SpawnPlatform.MESHWORLD (spawn platform),
Arena-Stands.MESHWORLD (audience stands) — not counted in the 15.

Common Confusions

"LevelCascade" is Beginner Race, NOT Intermediate

The internal file LevelCascade.MESHWORLD is used for Beginner Race (tournament
position #2). The name "Cascade" comes from the game's internal XML tag CASCADERACE.
It has nothing to do with Intermediate Race.

XML Tags Are Off By One

XML Tag Display Name Level #
BEGINNERRACE Warm-up Race 1
CASCADERACE Beginner Race 2
INTERMEDIATERACE Intermediate Race 3

The XML tags appear to be "shifted" — BEGINNERRACE is the easiest race (Warm-up),
not what we'd call "Beginner" in the display.

Level File Numbers Don't Match Tournament Order

Level File Tournament Position Display Name
Level1 1 Warm-up Race
LevelCascade 2 Beginner Race
Level2 3 Intermediate Race
Level3 4 Dizzy Race
Level4 5 Tower Race
LevelUp 6 Up Race
LevelDark 7 Neon Race
Level5 8 Expert Race
Level6 9 odd race
Level8 10 Toob Race
Level7 11 Wobbly Race
LevelGlass 12 Glass Race
Level9 13 Sky Race
Level10 14 Master Race
LevelImpossible 15 Impossible Race

Sub-Levels (Object Prefabs)

These are not standalone race tracks — they are 3D object meshes loaded by race levels:

File Purpose Used By
Level2-Bridge Bridge section Intermediate Race
Level3-Gluebie Glue trap object Dizzy Race
Level3-Swirl Swirl vortex Dizzy Race / Dizzy Arena
Level3-Tipper Tipping platform Dizzy Race
Level3-WaterWheel Water wheel Dizzy Race
Level4-Catapult Catapult launcher Tower Race
Level4-Drawbridge Drawbridge Tower Race
Level4-Mace Swinging mace Tower Race
Level4-Trapdoor1/2 Trapdoor variants Tower Race
Level4-Turret Turret Tower Race
Level4-Windmill Windmill Tower Race
Level5-Bridge Collapsible bridge Expert Race
Level6-Lifter Lifting platform odd race
Level7-Wobbly1-8 Wobbly platforms Wobbly Race
Level8-BlockDawg1/2 Block-dawg obstacle Toob Race
Level8-Fallout Falling obstacle Toob Race
Level8-Saw Sawblade obstacle Toob Race
Level8-Spinny Spinning obstacle Toob Race
Level9-PopCylinder1/2 Pop cylinder Sky Race
Level9-TrapDoor Trapdoor Sky Race
Level10-Bridge1/2 Bridge variants Master Race
Level10-2PBridge 2-player bridge Master Race
LevelDark-DFloor1-4 Dark floor sections Neon Race
LevelDark-FlickRing Flickering ring Neon Race
LevelDark-NeonPlatform Neon platform Neon Race
LevelDark-Trode Electrode Neon Race
LevelImpossible-BigGear Big gear Impossible Race
LevelImpossible-Gear Gear Impossible Race
LevelImpossible-Looper Looper Impossible Race
LevelImpossible-Pendulum Pendulum Impossible Race
LevelImpossible-Rotator Rotator Impossible Race
LevelUp-Button Button trigger Up Race
LevelUp-Lifter Lifting platform Up Race
LevelUp-SpeedCylinder Speed boost Up Race
Secret Secret area Various
Secret-Unlock Unlock spot Various
PopupSign Popup sign object Various
MouseTrap Mousetrap object Various

🔗 Related Documents

Level Rendering System Documen

types : rendering
keywords :

📂 View source on GitHub


Level Rendering System Documentation

Overview

The Level rendering system handles the visual pipeline for each frame. Three vtable
functions handle different stages: background/sky, geometry+dynamic objects, and
transparent overlays. Ball_Update also calls into rendering for score popups.

Key Addresses

Function Address Purpose
Level_UpdateAndRender 0x40B600 Main level render: opaque pass, waypoint, visible objects, shadows
Level_RenderObjects 0x40B570 Transparent object pass (vtable[0xC])
Level_RenderDynamicObjects 0x40B420 Sky/dome, water ripples, dynamic effects
Scene_SpawnBallsAndObjects 0x41C5B0 Level startup: create balls, pickups, signs, traps
Scene_CheckPath 0x457EC0 Ring topology path connectivity check
SpatialTree_ctor 0x463330 Collision acceleration tree constructor
Ball_RenderShadow (varies) Shadow rendering per ball

Scene Struct Offsets (Level Rendering)

Ball Lists

+0x29D4  ball_list_1 (AthenaList<Ball*>, player 1)
+0x29D8  ball_list_1_count
+0x29DC  ball_list_1_iterator
+0x2DE0  ball_list_1_array (Ball** ptr)
+0x3204  ball_list_2 (AthenaList<Ball*>, player 2)
+0x3208  ball_list_2_count
+0x320C  ball_list_2_iterator
+0x3610  ball_list_2_array (Ball** ptr)

Object/Render Lists

+0x3A48  visible_object_list (AthenaList, rebuilt each frame by UpdateAndRender)
+0x878   app_ptr (App* back-pointer)
+0x8AC   scene_manager (contains level mesh, D3D device)
+0x8B0   sky_dome_mesh (rendered when is_skydome_enabled)
+0x2160  ripple_list (AthenaList of water ripples)
+0x2164  ripple_count
+0x2168  ripple_iterator
+0x256C  ripple_array_ptr
+0x3F18  flag_waver_system (FlagWaver renderer)
+0x3A44  is_skydome_enabled (bool: false=level mesh bg, true=sky dome)
+0x3AFC  dynamic_object (vtable[8] callback after ripples)
+0x361C  waypoint_display_obj (arrow/marker for next checkpoint)
+0x070C  alpha_blend_state (0=opaque pass, 1=alpha pass)
+0x07C8  render_pass_counter (incremented each state change)

App Offsets (accessed via app_ptr)

app+0x154  = d3d_device_ptr (for SetRenderState)
app+0x178  = sound_system_ptr (contains camera info)
app+0x220  = current_game_mode (mode struct: +0x08=mode_type, +0x11=race_active, etc.)
app+0x234  = race_paused (bool)
app+0x237  = is_demo_version (bool)
app+0x23C  = tournament_active (int, non-zero = tournament)
app+0x850   = player_count (updated from ball list size)
app+0x910   = waypoint_list_ptr

Rendering Pipeline (per frame)

Scene_Render (0x41A2E0):
├── For each player (1 or 2):
│   ├── Graphics_SetViewport(player_rect)
│   ├── Scene_SetCamera(scene, ball, true)
│   ├── vtable[0x60] Level_RenderDynamicObjects  ← sky/dome + water ripples
│   ├── vtable[0x64] Level_UpdateAndRender         ← main geometry pass
│   │   ├── Clear visible_object_list
│   │   ├── Append all player balls to visible list
│   │   ├── SetRenderState(ALPHA_BLEND, FALSE)  → opaque pass
│   │   ├── For each ball: ball->vtable[0x1C]()  → RenderOpaque
│   │   ├── SetRenderState(ALPHA_BLEND, TRUE)   → alpha pass
│   │   ├── If race active: WaypointList update + render arrow
│   │   ├── For each visible obj: obj->vtable[0x08]()  → Render
│   │   └── If shadows: Ball_RenderShadow per ball
│   └── vtable[0x68] Level_RenderObjects           ← transparent overlay
│       ├── BeginFrame(device, 0)
│       ├── scene_manager->vtable[0x4C]()  → level geometry
│       └── For each visible obj: obj->vtable[0x0C]()  → RenderTransparent
└── Final:
    ├── Graphics_SetViewport(full_screen)
    ├── vtable[0x6C] Scene_RenderOverlay  ← HUD/score
    └── vtable[0x70] Scene_RenderPostEffects  ← fade/transitions

Level_StartBalls (Scene_SpawnBallsAndObjects, 0x41C5B0)

Ball Spawn Process

1. Set player count in app->sound_system at +0x850
2. For each entry in level_start_list (+0xD8B):
   a. Lookup "START%d-%d" in hash table for spawn position (Vec3)
   b. If demo version: also check "START%d-%d" alternate position
   c. If single player AND special level (types 5,0xB,0xC,0xE):
      Random choose between "START2-1" and "START2-2"
   d. If "START-DEBUG" exists in hash table, use that position
   e. Ball_ctor2(0xC60 bytes) → init physics defaults
   f. Ball_SetTrajectory → set trajectory from level data
   g. Set: player_index, gravity_scale=0.5, radius=26.0,
           max_speed=5.0, is_falling=false, field_0x769=true
   h. AthenaList_Append to ball_list (+0xA75)
   i. Store ball pointer in level_start_entry+0x10
3. Scan object list for "SAFESPOT"/"SAFEPOS" entries → append to safe_list
4. If demo or tournament: CreateBadBall, CreateMouseTrap
5. CreateSecretObjects, Scene_CreateFlags, Scene_CreateSigns
6. Scene_CreateDynamicObjects

Ball Consts at Spawn

  • Ball struct size: 0xC60 (3168 bytes)
  • gravity_scale: 0.5 (1.0 = normal, 0.5 = half)
  • field_0x27C: 0x3DCCCCCD = 0.1f (ball weight/mass?)
  • radius: 26.0 (game units)
  • field_0x1A0: 0x3F866666 ≈ 1.05f (collision radius scale?)
  • max_speed: 5.0 (game units/tick)

Scene_CheckPath (0x457EC0)

Simple ring topology pathfinder for the game's 360-cell (0x167) circular grid.

Scene_CheckPath(from, to):
  iterations = 0
  forward = from, backward = to
  while iterations < 0x167 (359):
    if forward == to: return 1  (reachable forward)
    if backward == to: return -1 (reachable backward)
    forward = (forward + 1) % 0x167
    backward = (backward - 1 + 0x167) % 0x167
    iterations++
  return 0  (unreachable - shouldn't happen)

Used in Ball_Update for grid-based collision/track snapping.
The 360-cell grid matches angular positions (0-359 degrees).

SpatialTree (Collision Acceleration)

Function Address Purpose
SpatialTree_ctor 0x463330 Create tree from object list
SpatialTree_SetDefaults 0x4632F0 Reset tree parameters
SpatialTree_Free 0x4632E0 Free tree memory
SpatialTree_DeletingDtor 0x4633B0 Destructor
SpatialTree_ForEach 0x463880 Iterate leaves
SpatialTree_CloneToLevel 0x462380 Clone for collision testing
SpatialTree_ComputeOffset 0x466480 Compute spatial offset
Collision_TraverseSpatialTree 0x465EF0 Main collision query

SpatialTree is an AABB tree used for O(log N) collision detection.
Ball_Update creates one per frame: SpatialTree_ctor(mesh_list), then
Collision_TraverseSpatialTree to find collision candidates.


🔗 Related Documents

Level-Locked Objects & Spawn M

types : objects
keywords :

📂 View source on GitHub


Level-Locked Objects & Spawn Methods

Hamsterball's object factory (CreateLevelObjects at 0x4121D0) is the central dispatcher that reads named objects from the MESHWORLD file and creates game objects. However, several object types are gated behind two independent mechanisms:

  1. Tournament mode gate (app+0x23C != 0) — prevents objects from spawning in non-tournament/non-arena modes
  2. Scene mesh pointer dependency — object constructors reference pre-loaded mesh pointers that only exist if the specific BoardLevel*_ctor loaded the sub-mesh file

This document covers which objects are locked, why, and how to spawn them anyway.


Object Categories

Category 1: Self-Loading Objects (spawn anywhere)

These objects load their own MeshWorld from a hardcoded file path inside their constructor. They create their own CollisionLevel internally. The only barrier is the app+0x23C tournament flag gate.

Object Factory Match Constructor Address Loads File Struct Size Gate
Bonk "BONK" Bonk_ctor 0x438850 levels\level5-bonk 0x1200 app+0x23C != 0
Bumper "N:BUMPER%d" CreateBumper 0x40FA20 levels\level8 0x10D0 None
Bumper2 "N:BUMPER%d" CreateBumper2 0x413CE0 levels\arena-beginner 0x10D0 None
Bridge "BRIDGE" (inline in factory) 0x4121D0 Uses scene+0x436C/0x4370 None
BreakBridge1 "BBRIDGE1" BreakBridge_ctor 0x436D70 Uses scene+0x5410 0x1100 None
BreakBridge2 "BBRIDGE2" BreakBridge_ctor 0x436D70 Uses scene+0x5414 0x1100 None
PopCylinder "POPCYLINDER" PopCylinder_ctor 0x436EE0 Uses scene+0x5420 0x10E8 None

Category 2: Scene-Dependent Objects (level-locked)

These constructors receive a pre-loaded mesh pointer as a parameter. That pointer only exists if the specific level constructor loaded the sub-mesh file. Without the pointer, calling the constructor will crash.

Object Factory Match Constructor Address Needs Scene Offset Mesh File Loaded By Gate
Tipper "TIPPER" Tipper_ctor 0x437960 +0x4394 (mesh), +0x4398 (visual) Levels\Level3-Tipper LevelBoard_Dizzy_ctor (0x41D060) app+0x23C != 0
Gluebie "GLUEBIE" Gluebie_ctor 0x437CB0 +0x607C (mesh) Levels\Level3-Gluebie LevelBoard_Dizzy_ctor (0x41D060) app+0x23C != 0
BlockDawg1 "BLOCKDAWG1" Blockdawg_ctor 0x43C310 +0x5840 (mesh) + "DAWGPATH1" named object (dawg sub-mesh) Level ctors app+0x23C != 0
BlockDawg2 "BLOCKDAWG2" Blockdawg_ctor 0x43C310 +0x5844 (mesh) + "DAWGPATH2" named object (dawg sub-mesh) Level ctors app+0x23C != 0
Catapult "CATAPULT" Catapult_ctor 0x437E10 +0x5848 (mesh) (catapult sub-mesh) Level ctors None

Spawning Self-Contained Objects (No MESHWORLD Edit Needed)

BadBalls (8-balls / AI opponents)

BadBalls are spawned by CreateBadBall which is called from Scene_SpawnBallsAndObjects. They do NOT go through CreateLevelObjects — they scan ALL MESHWORLD section 3 objects for names starting with "BADBALL".

CreateBadBall — Address: 0x0040BCA0

  • Convention: __fastcall (MSVC), first arg = Scene* (param_1)

  • Parameters: param_1 = Scene*

  • MESHWORLD iteration: Walks scene+0x8AC → +0x480 → +0xCA0 object array, checks __strnicmp(name, "BADBALL", 7)

  • MESHWORLD tag format (in object name string):

    BADBALL<CHASE>25.0</CHASE><HOME>5.0</HOME><SIZE>2.0</SIZE>
    
    Tag Default Offset in Ball Description
    <CHASE>val</CHASE> 25.0 ball+0xC6C AI chase activation distance (piVar5[0x31b])
    <HOME>val</HOME> 5.0 ball+0xC70 AI patrol/home radius (piVar5[0x31c])
    <SIZE>val</SIZE> 1.0 ball+0x188 Base radius → 3.0 (piVar5[0x62]=0x40400000=3.0f, piVar5[0xa1]=size_val, piVar5[0x9f]=0, piVar5+0x313 flag=1)
    <SPINDISTANCE>val</SPINDISTANCE> (none) ball+0xC7C AI spin orbit distance (piVar5[799])
  • Per-ball spawn sequence (from decompilation):

    1. operator_new(0xC98) — allocate 8-ball (larger than player's 0xC60)
    2. Ball_ctor(this, scene) — initialize ball with scene reference
    3. vtable[1]() — call Ball_Init (2nd virtual)
    4. Position from MESHWORLD object: ball+0x164 = obj.x + radius, ball+0x168 = obj.y + ball.radius, ball+0x16C = obj.z + radius
    5. Clear ball+0x281 = 0 (dead flag, never read)
    6. Copy same position to home: ball+0xC60/0xC64/0xC68 = obj.xyz (spawn/return position)
    7. Parse <CHASE>, <HOME>, <SIZE>, <SPINDISTANCE> tags from name string via MWParser_ReadTag
    8. AthenaList_Append(scene+0x29D4, ball) — add to bad_balls list
    9. AthenaList_Append(scene+0x2DEC, ball) — add to all_balls list
  • ⚠️ MESHWORLD dependency: CreateBadBall reads position AND tags from MESHWORLD section 3 object entries. You CANNOT call it directly with a custom position — it iterates the object array internally.

Spawning a BadBall without MESHWORLD (manual spawn)

You can bypass CreateBadBall entirely and construct a BadBall manually by replicating what it does. Here is a code-level pseudocode for spawning at an arbitrary position:

// Manual BadBall spawn — no MESHWORLD needed
// Call from a hook or patched function with access to Scene* (param_1)

void* scene = (void*)param_1;  // Scene pointer

// 1. Allocate
void* ball = operator_new(0xC98);

// 2. Construct
Ball_ctor(ball, scene);  // 0x4087A0 (base Ball_ctor, NOT Ball_Split_ctor)

// 3. Init (vtable[1])
((void(__thiscall*)(void*))(*(void***)ball)[1])(ball);

// 4. Set position (ball+0x164/168/16C = display position)
*(float*)(ball + 0x164) = spawn_x;
*(float*)(ball + 0x168) = spawn_y;
*(float*)(ball + 0x16C) = spawn_z;

// 5. Set home position (ball+0xC60/C64/C68 = return-to position)
*(float*)(ball + 0xC60) = spawn_x;
*(float*)(ball + 0xC64) = spawn_y;
*(float*)(ball + 0xC68) = spawn_z;

// 6. Set AI flag
*(byte*)(ball + 0x281) = 0;  // dead flag (never read by any function)

// 7. Set chase distance (optional, default 25.0)
*(float*)(ball + 0xC6C) = 25.0f;

// 8. Set home radius (optional, default 5.0)
*(float*)(ball + 0xC70) = 5.0f;

// 9. Set size (optional, default 1.0 = normal)
// To make giant: *(float*)(ball + 0x188) = 3.0f; *(byte*)(ball + 0xC4C) = 1;

// 10. Register in scene lists
AthenaList_Append(scene + 0x29D4, ball);  // bad_balls
AthenaList_Append(scene + 0x2DEC, ball);  // all_balls

Key addresses for manual spawn:

Step Function/Offset Address
Allocate operator_new(0xC98) runtime
Ball_ctor Ball_ctor(ball, scene) 0x40AFE0
Ball_ctor2 Ball_ctor2(ball, scene) (base init only) 0x4039E0
Ball_Split_ctor Ball_Split_ctor(ball, scene) (8-ball variant) 0x408D10
Ball_Init vtable[1]() via vtable
Display pos ball+0x164/0x168/0x16C
Home pos ball+0xC60/0xC64/0xC68
AI flag clear ball+0x281 DEAD: never read
Chase dist ball+0xC6C
Home radius ball+0xC70
Add to bad_balls AthenaList_Append(scene+0x29D4, ball)
Add to all_balls AthenaList_Append(scene+0x2DEC, ball)

Alternatively, for split-ball 8-balls (from Ball_Shatter at 0x408D70):

  • Uses Ball_Split_ctor at 0x408D10 (which calls Ball_ctor2 then sets ball+0xC60=5)

  • Allocates 0xC64 bytes (slightly smaller than CreateBadBall's 0xC98)

  • Sets ball+0x31D = 1 (is_8ball), ball+0xC60 = 0x41200000 (30.0f, split timer)

  • Copies collision direction from parent at ball+0xCA4/0xCA8/0xCAC

  • Sets split ID: 1, 2, or 4 (ball+0x324)

  • To add BadBalls via MESHWORLD: Add objects of type 1 in section 3 with names like BADBALL<CHASE>30</CHASE><SIZE>3</SIZE>. They will be spawned automatically on level load when app+0x23C != 0 (tournament mode).

MouseTraps

MouseTraps are spawned by CreateMouseTrap based on MESHWORLD object names starting with "N:MOUSETRAP".

CreateMouseTrap — Address: 0x0040BF50

  • Convention: __fastcall (param_1 = Scene*)
  • MESHWORLD iteration: Same pattern as CreateBadBall — walks scene+0x8AC → +0x480 → +0xCA0 object array, checks __stricmp(name, "MOUSETRAP") (case-insensitive)
  • Alloc size: 0x10F8 bytes per MouseTrap
  • Constructor: TipperVisual_Level_Ctor(this, scene) at 0x437880
  • Position copied from object: obj+0x10DC/0x10E0/0x10E4 = obj.x/y/z
  • Floor offset: obj+0x10F4 = _DAT_004CF44C - obj.rot_y (constant minus Y rotation for floor height)
  • Post-init: Calls vtable[0x2C]() (virtual function at offset 0xB0 in vtable, likely Activate)
  • Scene storage:
    • AthenaList_Append(scene+0xCD4, obj) — mouse_trap_list
    • AthenaList_Append(scene+0x1930, obj) — physics_objects
    • AthenaList_Append(scene+0x8AC→0x480→0x1C, obj) — level_objects
    • AthenaList_Append(scene+0x8B0+0x18, obj+0x10D4) — some sub-component
  • Gate: None (not gated by app+0x23C)
  • Scene mesh dependency: The constructor calls TipperVisual_Level_Ctor(scene) — reads scene+0x4398 (Tipper CollisionLevel). Must be loaded by BoardLevel3 ctor.

Manual spawn:

void* mem = operator_new(0x10F8);
void* trap = TipperVisual_Level_Ctor(mem, scene);
*(float*)(trap + 0x10DC) = obj.x;  // position
*(float*)(trap + 0x10E0) = obj.y;
*(float*)(trap + 0x10E4) = obj.z;
*(float*)(trap + 0x10F4) = _DAT_004CF44C - obj.rot_y;  // floor height
((void(__thiscall*)(void*))(*(void***)(trap))[0x0B])(trap);  // Activate
AthenaList_Append(scene + 0xCD4, trap);   // mouse_trap_list
AthenaList_Append(scene + 0x1930, trap);  // physics_objects

Sawblades (and Arena Object Sub-Dispatcher)

⚠️ CreateExpertLevelObjects is actually a multi-factory, not just a sawblade creator. It handles 6 different arena objects based on the name prefix. It is called from CreateLevelObjects for each section-3 object whose name doesn't match any other prefix.

CreateExpertLevelObjects — Address: 0x0040E250

  • Convention: __thiscall (ECX = Scene*, param_1 = name string, param_2/3 = output ptrs, param_4 = transform)
  • Called from: Scene_HandleCollisions / CreateLevelObjects
  • Gate: All sub-objects with app+0x23C != 0 check: BONK, TIP, SAWBLADE

Sub-dispatcher branches:

Prefix Object Type Alloc Size Constructor Notes
BONK Bonk hammer 0x1200 Bonk_ctor at 0x438850 Reads position from param_4+4/8/C; gate: app+0x23C != 0
TIP Tower (swinging arm) 0x1188 TowerLevel_Ctor Checks SLOW, SUPER, UP suffixes; gate: app+0x23C != 0
SAWBLADE Spinning saw 0x111C Sawblade_Level_Ctor Suffix 1scene+0x4370, suffix 2scene+0x4374; gate: app+0x23C != 0
BRIDGE Spinner platform 0x10FC Spinner_Level_ctor Suffix 1scene+0x4380, suffix 2scene+0x4798, suffix NEGobj+0x10F8 = -1.0f
JUDGE Gear 0x1100 Gear_Level_ctor Appended to scene+0x4BBC
BELL Bell (Tipper variant) 0x10E8 Tipper_Level_Ctor Appended to scene+0x2578; stored at scene+0x4FD4

All created objects get appended to scene+0x2578 (active objects list).

SAWBLADE details:

  • SAWBLADE1 → stored at scene+0x4370, calls Sawblade_SetBreakSound(1)
  • SAWBLADE2 → stored at scene+0x4374, calls Sawblade_SetBreakSound(2)
  • Triggered by collision events E:ALERTSAW1 / E:ACTIVATESAW1

TIP details:

  • TIPSLOWobj+0x10EC = 1
  • TIPSUPERobj+0x10ED = 1
  • TIPUP → calls Sound_InitChannels(obj, 1)

BRIDGE spinner details:

  • BRIDGE1 → appended to scene+0x4380
  • BRIDGE2 → appended to scene+0x4798
  • BRIDGENEG → sets spin direction to -1.0f at obj+0x10F8

Bonks (Hammer popups)

Bonk_ctor — Address: 0x00438850

  • Convention: __thiscall (ECX = this, allocated buffer)
  • Parameters: this (void*), scene (int), x (float), param_3 (undefined4), param_4 (undefined4)
  • Self-loads: MeshWorld_ctor(this, graphics, "levels\\level5-bonk")
  • Creates own collision: CollisionLevel_ctorWithLevel(collision, this)
  • Size: 0x1200 bytes
  • Scene storage: Result stored at scene+0x540C
  • Gate: app+0x23C != 0

This is the easiest level-locked object to unlock. Since Bonk_ctor loads its own mesh file (levels\level5-bonk), it has no scene mesh pointer dependency. The ONLY thing preventing Bonks from appearing in any level is the app+0x23C check at 0x4121D0.

To add Bonks:

  1. Add object named "BONK_01" (or any BONK-prefixed name) in MESHWORLD section 3
  2. Ensure app+0x23C != 0 OR patch the gate

Bumpers (⚠️ Level-replacement hazard)

CreateBumper — Address: 0x0040FA20

  • Convention: __fastcall (param_1 = Scene* as int*)
  • ⚠️ CRITICAL GOTCHA: This does NOT just spawn bumper objects. It loads the entire levels\level8 MESHWORLD file as a new MeshWorld, creates a CollisionLevel from it, calls Level_InitScene, then scans for 8 N:BUMPER%d named objects. This replaces the level's scene data — the current level's meshes get overwritten at scene+0x22B (MeshWorld) and scene+0x22C (CollisionLevel). Calling CreateBumper on non-Level8 levels will crash or corrupt the scene because it overwrites the active level's collision/geometry.
  • Self-loads: MeshWorld_ctor(buf, graphics, "levels\\level8") → stored at param_1[0x22B]
  • CollisionLevel: CollisionLevel_ctorWithLevel(buf2, meshWorld) → stored at param_1[0x22C]
  • Init: Level_InitScene(param_1) — reinitializes scene with new level data
  • 8× bumpers: Scene_CollectByNameFilter("N:BUMPER%d", ...) at stride 0x106 (0x10E3 base)
  • Post-init: (*param_1)[0x80]() — calls vtable offset 0x80 (virtual function, likely level-specific setup)
  • Size: 0x10D0 bytes per allocation
  • Gate: None

CreateBumper2 — Address: 0x00413CE0

  • Same pattern but loads levels\arena-beginner — also replaces scene data
  • Size: 0x10D0 bytes
  • Gate: None

Bottom line: Both CreateBumper variants are level-scoped constructors, not standalone object spawners. They work correctly only when called from within a BoardLevel8 or Arena board constructor that expects this level-swap behavior. Calling them from another level context will corrupt the scene.

Ball Splits (8-ball arena mechanic)

Ball_Shatter — Address: 0x00408D70

  • Convention: __thiscall (ECX = Ball*)
  • Prologue: Standard SEH (PUSH -1; MOV EAX, FS:[0]; PUSH handler)
  • Parameters: this (Ball* — the parent ball), param_1 (struct with target position table)
  • Called from: FollowBall_Update (0x43ECC0) at two call sites (0x43F722, 0x43FE36)
  • NOT called from E:JUMP. The E:JUMP collision handler (in collision_events.c) does something different: plays a 3D sound, applies upward force, and adds +200 score. See docs/collision_events.
  • Behavior: Marks the parent ball for despawn (+0x2E8=1), then creates 3 new Ball_Split objects (0xC64 bytes each) via Ball_Split_ctor (0x408D10). Each split ball:
    • ball+0x31D = 1 (is_8ball flag, activates AI behavior)
    • ball+0xC6 = 0x41200000 (10.0f — split ball size, NOT 30.0f)
    • Split IDs: 1, 2, 4 (bitmask, assigned by iteration index)
    • Trajectory copied from parent's +0x2AC–0x2B8
    • Position from parent or lookup table via param_1
    • Added to scene+0x3204 (ball list)
  • Guard conditions:
    • +0x324 == 0 (only non-8ball-type balls can split)
    • +0x744 == 0 (only split once — split_count must be zero)

Ball Init Battle Mode

Ball_InitBattleMode — Address: 0x00456CD0

  • Convention: __thiscall (ECX = Ball*)
  • Prologue: SUB ESP, 0C; PUSH ESI; LEA EAX, [ECX+0xC80]; ...
  • Sets ball to arena/battle physics:
Offset Value Description
+0xC60 3 battle_mode = 3
+0xC68 0.55 friction
+0xC6C 1.0 bounciness
+0xC70 1000.0 max_speed
+0xC74 25.0 chase_distance
+0xC80 0.0 gravity_vec[0]
+0xC84 -1.0 gravity_vec[1]
+0xC88 0.0 gravity_vec[2]

Additional Spawn Functions (Not in CreateLevelObjects)

These functions are called from Scene_SpawnBallsAndObjects (0x41C5B0) AFTER the main level objects are created. They don't go through the object factory dispatcher.

CreateSecretObjects

Address: Unknown (not yet decompiled)

  • Called from: Scene_SpawnBallsAndObjects at 0x41C5B0
  • Purpose: Creates hidden collectible objects (secret items/bonuses)
  • Likely scans: MESHWORLD for objects with SECRET or similar prefix
  • Gate: Unknown — may be gated by app+0x23C

Scene_CreateFlags

Address: Unknown (not yet decompiled)

  • Called from: Scene_SpawnBallsAndObjects at 0x41C5B0
  • Purpose: Creates checkpoint/finish-line flag objects
  • Likely scans: MESHWORLD for FLAG or FINISH named objects

Scene_CreateSigns

Address: Unknown (not yet decompiled)

  • Called from: Scene_SpawnBallsAndObjects at 0x41C5B0
  • Purpose: Creates directional arrow sign objects
  • Likely scans: MESHWORLD for SIGN or ARROW named objects

Scene_CreateDynamicObjects

Address: Unknown (not yet decompiled)

  • Called from: Scene_SpawnBallsAndObjects at 0x41C5B0
  • Purpose: Creates moving/animated objects (platforms, elevators, etc.)
  • Likely scans: MESHWORLD for DYNAMIC or MOVING named objects

The app+0x23C Tournament Gate

The single variable that gates most level-locked objects. Located at App+0x23C (App singleton at 0x4FD680, so absolute address 0x4FD8B4).

Objects gated by app+0x23C != 0:

  • TIPPER (at 0x4121D0+0x3A — JNZ after CMP)
  • BONK (at 0x4121D0+0xAA — JNZ after CMP)
  • BLOCKDAWG1 (at 0x4121D0+0x1E2 — JNZ after CMP)
  • BLOCKDAWG2 (at 0x4121D0+0x235 — JNZ after CMP)
  • GLUEBIE (inverted: at 0x4121D0+0x2F0 — JZ after CMP, skips if 0x23C == 0)
  • CreateBadBall (spawned from Scene_SpawnBallsAndObjects 0x41C5B0)

How to bypass:

  1. Memory patch: Write 1 to address 0x4FD8B4 at runtime
  2. Binary patch: In CreateLevelObjects at 0x4121D0, patch each conditional jump after the app+0x23C comparison:
    • Find the CMP [addr], 0 + JNZ/JZ sequences
    • Replace JNZ with JMP (unconditional) to always take the branch
    • For GLUEBIE's inverted check, replace JZ with JMP (or NOP out the skip)

Scene Mesh Dependency Reference

For scene-dependent objects, here is exactly what each level constructor loads and where it stores the mesh pointers:

LevelBoard_Dizzy_ctor (0x41D060) — Dizzy Race

Loads the most sub-meshes of any level:

Scene Offset Mesh File Used By
+0x436C Levels\Level3-Tipper (MeshWorld) BRIDGE collision mesh, TIPPER visual
+0x4370 (CollisionLevel from Tipper) Collision against tipper geometry
+0x4374 Levels\Level3-Gluebie (MeshWorld) GLUEBIE visual
+0x4BA8 Levels\Level3-WaterWheel (MeshWorld) Level3-specific water wheel
+0xBAC (CollisionLevel from WaterWheel) Water wheel collision
+0x4BC4 Levels\Level3-Swirl (MeshWorld) Level3-specific swirl
+0xBC8 (CollisionLevel from Swirl) Swirl collision

LevelBoard_Expert_ctor (0x41EA40)

Scene Offset Mesh File Used By
+0x4378 Levels\Level5-Bridge (MeshWorld) BRIDGE collision mesh
+0x437C (CollisionLevel from Bridge) Bridge collision

LevelBoard_Beginner_ctor (0x4200E0) — Beginner Race (Arena, internal name: Cascade)

Scene Offset Content Used By
+0x436C Vec3List array (0x418 bytes, 8 items) Bumper creation data

LevelBoard_WarmUp_ctor (0x41CA40) — Warm-Up

No sub-meshes loaded. All scene mesh pointer offsets (+0x4394, +0x5840, +0x607C, etc.) are NULL/zero from the base Board_ctor.


How to Add Level-Locked Objects to Any Level

For self-loading objects (BONK, Bumper, BadBall):

Step 1: Patch app+0x23C to 1 (for BONK and BadBall) or just add MESHWORLD names (for Bumper — no gate).

Step 2: Add named objects to the level's MESHWORLD file section 3:

  • For BONK: add objects named "BONK_01", "BONK_02", etc. with their position
  • For BadBall: add type-1 objects named "BADBALL<CHASE>25</CHASE><SIZE>2</SIZE>"
  • For Bumper: add objects named "N:BUMPER1", "N:BUMPER2", etc.

For scene-dependent objects (TIPPER, GLUEBIE, BLOCKDAWG, CATAPULT):

Step 1: Patch app+0x23C to 1

Step 2: Patch the level's BoardLevel*_ctor to load the required sub-mesh files and store the MeshWorld/CollisionLevel pointers at the correct scene offsets. For example, to add TIPPER support to Level 1:

  • Patch LevelBoard_WarmUp_ctor at 0x41CA40 to add:
    MeshWorld_ctor(buf, graphics, "Levels\\Level3-Tipper")
    store result → scene+0x4394
    CollisionLevel_ctorWithLevel(buf2, tipper_mesh)
    store result → scene+0x4398
    
  • This requires injecting code (e.g., at the end of the constructor before the return) that allocates, constructs, and stores the mesh pointers.

Step 3: Add TIPPER named objects to Level 1's MESHWORLD section 3 (e.g., "TIPPER_A").

Alternative: Reimplementation approach

In a reimplementation, bypass both gates entirely:

  1. Remove all app+0x23C != 0 checks from CreateLevelObjects
  2. Make all constructors self-loading (like Bonk_ctor does) — call MeshWorld_ctor with the appropriate file path inside each constructor
  3. Load sub-meshes on demand rather than only in level constructors

Function Address Quick Reference

Function Address Purpose
CreateLevelObjects 0x4121D0 Main factory dispatcher — matches MESHWORLD names to constructors
CreateExpertLevelObjects 0x40E250 Multi-factory: BONK, TIP, SAWBLADE, BRIDGE, JUDGE, BELL sub-dispatcher
Scene_SpawnBallsAndObjects 0x41C5B0 Ball spawning on level load
CreateBadBall 0x40BCA0 Spawn 8-ball AI opponent from MESHWORLD BADBALL tag
CreateMouseTrap 0x40BF50 Spawn mouse trap from MESHWORLD MOUSETRAP objects
Ball_Shatter 0x408D70 Arena 8-ball split: replaces parent ball with 3 AI split balls
Ball_InitBattleMode 0x456CD0 Set ball to battle/arena physics
Bonk_ctor 0x438850 Bonk constructor (self-loads levels\level5-bonk)
CreateBumper 0x40FA20 Bumper factory (self-loads levels\level8) — ⚠️ REPLACES scene mesh
CreateBumper2 0x413CE0 Arena bumper factory (self-loads levels\arena-beginner)
TowerLevel_Ctor (in 0x40E250) Tower/swinging arm constructor (TIP prefix)
Sawblade_Level_Ctor (in 0x40E250) Sawblade constructor (SAWBLADE prefix)
Spinner_Level_ctor (in 0x40E250) Spinner platform constructor (BRIDGE prefix)
Gear_Level_ctor (in 0x40E250) Gear constructor (JUDGE prefix)
Tipper_Level_Ctor (in 0x40E250) Bell/Tipper variant constructor (BELL prefix)
TipperVisual_Level_Ctor 0x437880 Mouse trap visual constructor
Tipper_ctor 0x437960 Tipper constructor (needs scene+0x4394)
Gluebie_ctor 0x437CB0 Gluebie constructor (needs scene+0x607C)
Catapult_ctor 0x437E10 Catapult constructor (needs scene+0x5848)
Blockdawg_ctor 0x43C310 BlockDawg constructor (needs scene+0x5840 + path obj)
BreakBridge_ctor 0x436D70 Breakable bridge constructor
PopCylinder_ctor 0x436EE0 Pop cylinder constructor
CreateSecretObjects Unknown Hidden collectibles (called from Scene_SpawnBallsAndObjects)
Scene_CreateFlags Unknown Finish line flags (called from Scene_SpawnBallsAndObjects)
Scene_CreateSigns Unknown Directional signs (called from Scene_SpawnBallsAndObjects)
Scene_CreateDynamicObjects Unknown Moving platforms, etc. (called from Scene_SpawnBallsAndObjects)
Level_FindObjectByName 0x460530 Find MESHWORLD object by name
ExpertCollisionEvents 0x40E6A0 Arena event dispatcher (HAMMERCHASE, ALERTSAW, etc.)
operator_new 0x4BA57B Game's CRT allocator (__cdecl, size on stack)
App singleton 0x4FD680 Global App struct
app+0x23C 0x4FD8B4 Tournament mode flag

Generated from Ghidra 12.0 decompilation. See docs/MODDING_FUNCTION_REFERENCE.md for full function details and docs/OBJECT_CATALOG.md for object listing.


🔗 Related Documents

Mace, Windmill & Chomper Syste

types : docs
keywords :

📂 View source on GitHub


Mace, Windmill & Chomper System (Tower Race)

Complete reverse-engineering analysis of the Mace (Pendulum), Windmill, and Chomper systems in the Tower Race.


Overview

The Tower Race features three notable mechanical objects:

  1. MACE (Pendulum) — Swinging mace on a chain that bounces balls
  2. WINDMILL — Rotating collision level (pushes balls away)
  3. CHOMPER — Visual-only mesh, NOT a separate game object. The actual object is the TURRET.

All three are created inline in Scene_LoadLevel4 (0x40D6D0), not via a standalone factory function.


MACE (Pendulum / Swinging Mace)

Binary Addresses

Function Address Calling Convention Purpose
CascadeStands_Ctor 0x438750 thiscall(obj, board, mesh) ret 8 Constructor — calls Stands_ctor + allocates CollisionLevel
Pendulum_Update 0x43F3C0 thiscall(obj) vtable[11] — swing animation + collision
Pendulum_Render 0x45E0E0 thiscall(obj) vtable[18] — shared base render
Pendulum_scalar_dtor 0x438830 thiscall(obj) vtable[0] — destructor
Stands_ctor 0x462850 thiscall(obj, mesh) ret 4 Base class constructor — sets vtable 0x4D8FB0
SceneObject_SpawnWithSound 0x4536A0 Spawns scene object with sound
Level_FindObjectByName 0x4C7677 cdecl ret 12 Finds mesh ref by name in loaded level

Object Structure (0x110C bytes = 4364)

Offset Size Type Description
+0x0000 4 ptr Vtable (0x4D50C0)
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Home position X (from mesh)
+0x10D8 4 float Current position X
+0x10DC 4 float Current position Y
+0x10E0 4 float Current position Z
+0x10E8 4 float Swing amplitude (80.0) — oscillation radius
+0x10EC 4 Unused
+0x10F0 4 int Activation flag (0=idle, 1=triggered by E:MACETRIGGER)
+0x10F4 4 int Active flag (1 = active)
+0x10F8 4 int Timer (init 50)
+0x10FC 4 ptr CollisionLevel pointer (allocated by CascadeStands_Ctor)
+0x1100+ CollisionLevel data (0x10D0 bytes)

Vtable (0x4D50C0)

Index Offset Address Function
0 0x00 0x438830 scalar_dtor
11 0x2C 0x43F3C0 Pendulum_Update (swing + collision)
18 0x48 0x45E0E0 Render (shared base)

Creation Flow

Inline in Scene_LoadLevel4 (0x40D6D0):

// 1. Find "MACE" mesh ref in loaded level
mesh_ref = Level_FindObjectByName("MACE");
if (mesh_ref == NULL) skip;

// 2. Allocate
obj = operator_new(0x110C);

// 3. Construct
// CascadeStands_Ctor(this=obj, board, *(board+0x4378))
//   → Stands_ctor(obj, mesh) → sets vtable = 0x4D50C0
//   → operator_new(0x10D0) → CollisionLevel_ctorWithLevel(coll, obj)
mesh_ptr = *(board + 0x4378);   // pre-loaded Level4-Mace mesh
CascadeStands_Ctor(obj, board, mesh_ptr);  // thiscall, ret 8

// 4. Copy position from mesh ref to obj+0x10D8/+0x10DC/+0x10E0
obj->pos = mesh_ref->pos;

// 5. Register to lists
AthenaList_Append(board+0x2578, obj);   // general objects
AthenaList_Append(board+0x5000, obj);   // mace list (for E:MACETRIGGER)

// 6. Set fields
obj->swing_amplitude = 80.0;  // +0x10E8
obj->active = 1;               // +0x10F4
obj->timer = 50;              // +0x10F8

Update Function (vtable[11] @ 0x43F3C0)

void Pendulum_Update(this) {
    // Base update (collision tree check)
    call 0x4605E0;  // Level_Update base
    call 0x457AD0;  // Timer_Init
    call 0x457C60;  // Timer_Cleanup
    
    call 0x40A0B0;  // scene operation
    
    // Load position for collision
    FLD [this+0x10D8]  // pos X
    FLD [this+0x10DC]  // pos Y
    FLD [this+0x10E0]  // pos Z
    
    call 0x457A40;  // timer operation
    
    // Swing animation
    FLD [this+0x10E8]          // load amplitude (80.0)
    MOV [this+0x10E8], 0.05    // temporarily set to 0.05 (angle increment)
    FLD [this+0x10E8]          // load increment
    
    // Trig calculation (sin/cos for oscillation)
    FLD [this+0x10E0]          // pos Z
    FLD [this+0x10DC]          // pos Y
    call 0x459860;             // trig function
    
    MOV [this+0x10E8], 80.0    // restore amplitude
    
    call 0x43F559;             // collision check sub-function
    RET;
}

The pendulum oscillates around its position. The 80.0 amplitude defines the swing radius, and 0.05 is the angle increment per frame (about 2.86°/frame ≈ 71.5°/sec at 25fps).

Collision Events

Handled in Level_HandleCollision (0x40DCD0):

// E:MACETRIGGER — activates all maces in board+0x5000
if (stricmp(eventName, "E:MACETRIGGER") == 0) {
    for each mace in board+0x5000:
        mace->activation_flag = 1;  // +0x10F0
}

// N:MACE — ball touches mace, bounce away
if (stricmp(eventName, "N:MACE") == 0) {
    for each mace in board+0x5000:
        if (mace->collides_with_ball && boundary_check)
            ball->vtable[8]();  // bounce/split
}

Mesh Dependency

Pre-loaded by LevelBoard_Tower_ctor (0x41E340):

board+0x4378 = MeshWorld_ctor("Levels\\Level4-Mace");  // VA 0x4D0974

For global spawn: load mesh via MeshWorld_ctor with JIT mesh injection at board+0x4378.


WINDMILL

Binary Addresses

Function Address Purpose
CollisionLevel_ctorWithLevel 0x465080 Creates collision level from mesh
Level_LoadMeshes 0x465200 Loads mesh vertex/index data
SceneObject_SetupCallback 0x45DD60 Registers collision with scene manager (stdcall, ret 8)

Object Structure

The Windmill is NOT a standalone game object. It creates:

  1. CollisionLevel (0x10D0 bytes) — collision-only, no vtable/update/render
  2. Trapdoor (0x10F8 bytes) — separate object via GlassStands_Ctor

Creation Flow

// 1. Find "WINDMILL" mesh ref
mesh_ref = Level_FindObjectByName("WINDMILL");
if (mesh_ref == NULL) skip;

// 2. Get mesh from board+0x437C (pre-loaded by Tower ctor)
mesh_ptr = *(board + 0x437C);  // Level4-Windmill

// 3. Create CollisionLevel
coll = operator_new(0x10D0);
CollisionLevel_ctorWithLevel(coll, mesh_ptr);  // thiscall, ret 4

// 4. Load mesh data
Level_LoadMeshes(coll);

// 5. Copy position from mesh ref
coll->pos = mesh_ref->pos;

// 6. Register with scene manager
SceneObject_SetupCallback(0x4F7360, 0x168, 0);  // global scene + callback type

// 7. Store angle at board+0x438C
board->windmill_angle = (float)result;

// 8. Create Trapdoor
trapdoor_mesh = Level_FindObjectByName("TRAPDOOR");
trapdoor = operator_new(0x10F8);
GlassStands_Ctor(trapdoor, mesh_ptr);

Why Windmill Can't Be Spawned Globally

The Windmill is a CollisionLevel — a collision shape without:

  • A game object vtable
  • An update function (vtable[11])
  • A render function (vtable[18])

The visual windmill mesh is part of the level's static geometry (rendered by the scene's spatial tree). The CollisionLevel only provides collision detection that pushes balls away when they touch it.

To spawn a windmill globally, you would need to:

  1. Create the CollisionLevel (pushes balls)
  2. Also spawn a visual mesh at the same position
  3. Register both with the scene manager

This is significantly more complex than spawning a Mace.


CHOMPER (Purple Thing in the Pit)

The Chomper is the purple thing sitting in the pit near the goal in the Tower Race. It DOES interact with the ball — when the ball touches it, the E:BITE collision event fires, dealing 25.0 damage.

What It Actually Is

The Chomper is a MeshWorld + CollisionLevel (not a game object with vtable). It's rendered as static geometry and has collision via a CollisionLevel. The E:BITE events are baked into the mesh's collision triangles — they fire automatically wherever the chomper is placed.

Binary Addresses

Symbol Address Purpose
"Meshes\Chomper" 0x4D094C Mesh file path string
"sounds\chomp" 0x4D2D98 Chomp sound file path
"E:BITE" 0x4CFECC (→25.0 at 0x41C80000) Collision event on chomper triangles

Creation Flow (Tower Constructor @ 0x41E340)

// 1. Load Chomper mesh
board+0x4390 = MeshWorld_ctor("Meshes\\Chomper");  // VA 0x4D094C

// 2. Allocate CollisionLevel
coll = operator_new(0x10D0);
CollisionLevel_ctorWithLevel(coll, board+0x4390);  // creates collision from mesh

// 3. Register with scene manager (0x4F7360) for collision detection
SceneObject_SetupCallback(0x4F7360, 0x168, 0);

Collision (E:BITE)

Handled in Level_HandleCollision (0x40DCD0):

// E:BITE — ball touches chomper
if (stricmp(eventName, "E:BITE") == 0) {
    board+0x43A0 = 25.0;   // bite damage (0x41C80000)
    board+0x43A8 = 0;      // reset bite counter
}

The damage value at board+0x43A0 is read by the game loop at 0x4023C1, which applies it to the ball (splits/respawns). The "chomp" sound plays on contact.

Position in Tower Level

From Level4.MESHWORLD: Chomper mesh ref at position (5080.9, -2659.6, -3410.8) — in the pit near the goal.

Spawnable: YES

The Chomper CAN be spawned globally using the same CollisionLevel pattern as the Windmill:

  1. Load Meshes\Chomper mesh via MeshWorld_ctor
  2. Create CollisionLevel from mesh via CollisionLevel_ctorWithLevel
  3. Register with scene manager
  4. E:BITE events are baked into the mesh → automatically work at any position!

TURRET (separate object, not Chomper)

The Turret is a separate game object that uses Level4-Turret mesh:

board+0x43B4 = MeshWorld_ctor("Levels\\Level4-Turret");
// Turret creation in Scene_LoadLevel4:
obj = operator_new(0x10D0);
Stands_ctor(obj, mesh);  // sets vtable = 0x4D8FB0

Mesh Pre-loading (Tower Constructor)

board+0x436C = MeshWorld_ctor("Levels\\Level4-Catapult");
board+0x4370 = MeshWorld_ctor("Levels\\Level4-Drawbridge");
board+0x4374 = MeshNode_ctor("Meshes\\YellowLink");
board+0x4378 = MeshWorld_ctor("Levels\\Level4-Mace");      // Mace mesh
board+0x437C = MeshWorld_ctor("Levels\\Level4-Windmill");  // Windmill mesh
board+0x4390 = MeshWorld_ctor("Meshes\\Chomper");           // Chomper mesh + collision
board+0x43B4 = MeshWorld_ctor("Levels\\Level4-Turret");     // Turret mesh

Global Spawn: Mace

The Mace (Pendulum) is the most suitable for global spawning because it:

  • Has its own vtable with update (swing) and render functions
  • Has a standalone constructor that can be called with any mesh
  • Has ball collision via N:MACE event
  • Has activation via E:MACETRIGGER

JIT Mesh Injection Pattern

  1. Load Levels\Level4-Mace mesh via MeshWorld_ctor → cache at board+0x4370
  2. On spawn:
    • Save old board+0x4378 value
    • Write Mace mesh to board+0x4378
    • operator_new(0x110C)
    • CascadeStands_Ctor(obj, board, mesh) — reads from board+0x4378
    • Restore board+0x4378
    • Set position at obj+0x10D8/+0x10DC/+0x10E0
    • Set obj+0x10E8 = 80.0 (swing amplitude)
    • Set obj+0x10F4 = 1 (active)
    • Set obj+0x10F8 = 50 (timer)
    • AthenaList_Append(board+0x2578, obj) (general list)
    • AthenaList_Append(board+0x5000, obj) (mace list)
  3. Per-frame: call vtable[11] (Pendulum_Update) for swing animation

Key Differences from Other Mods

  • Alloc size: 0x110C (larger than Bonk's 0x1200 or Sawblade's 0x111C)
  • Position offset: +0x10D8 (not +0x10D4 like most objects)
  • Constructor: Takes 2 params (board + mesh), not just board
  • Two lists: General (board+0x2578) AND mace list (board+0x5000)
  • JIT mesh injection: Required because CascadeStands_Ctor reads from board+0x4378

🔗 Related Documents

MESH Binary Format Specificati

types : meshworld
keywords :

📂 View source on GitHub


MESH Binary Format Specification

Overview

The .MESH format is Hamsterball's custom binary 3D model format. It is NOT a standard DirectX .X file — it's a proprietary format containing submeshes with materials, vertex buffers, index buffers, and animation data.

File Structure

[MESH File]
├── Header (version, name)
├── SubMesh Array (N submeshes)
│   ├── Submesh[0]: vertex buffer + material + textures
│   ├── Submesh[1]: ...
│   └── Submesh[N-1]: ...
├── Animation Data (M submesh animations)
├── Object Array (K objects with transforms)
└── Footer (transform data, index arrays)

Parsed Format (from decompiled MeshWorld loader at 0x4629E0)

Section 1: SubMeshes (Materials + Vertex Data)

[4 bytes] submesh_count (iStack_578)

FOR each submesh (i = 0..submesh_count-1):
  [4 bytes]     vertex_data_size (uStack_598)
  [vertex_data_size bytes] raw_vertex_buffer (allocated, read in one block)
  [4 bytes]     field_1 (position X or similar)
  [4 bytes]     field_2 (position Y)
  [4 bytes]     field_3 (position Z)
  [4 bytes]     field_4
  [4 bytes]     field_5
  [4 bytes]     field_6
  [4 bytes]     has_material_flag (puVar5+10, checked as char)
  
  IF has_material_flag != 0:
    [4 bytes]   diffuse_r (uStack_594)
    [4 bytes]   diffuse_g (uStack_590)  
    [4 bytes]   diffuse_b (iStack_59c)
    [4 bytes]   diffuse_a (fStack_5a4) → puVar5[0x13]
    [4 bytes]   ambient_r → puVar5[0x10]
    [4 bytes]   ambient_g → puVar5[0x11]
    [4 bytes]   ambient_b → puVar5[0x12]
    has_custom_color = (diffuse_a != _DAT_004CF3C8)  // != default
    
    [4 bytes]   emissive_r → puVar5[0xC]
    [4 bytes]   emissive_g → puVar5[0xD]
    [4 bytes]   emissive_b → puVar5[0xE]
    [4 bytes]   emissive_a → puVar5[0xF]
    
    [4 bytes]   specular_r → puVar5[0x14]
    [4 bytes]   specular_g → puVar5[0x15]
    [4 bytes]   specular_b → puVar5[0x16]
    [4 bytes]   specular_a → puVar5[0x17]
    
    [4 bytes]   unknown_r → puVar5[0x18]
    [4 bytes]   unknown_g → puVar5[0x19]
    [4 bytes]   unknown_b → puVar5[0x1A]
    [4 bytes]   unknown_a → puVar5[0x1B]
    
    [4 bytes]   specular_power → puVar5[0x1C]
    [4 bytes]   has_texture_flag (iStack_554, bool stored at +0x79)
    [4 bytes]   texture_name_length (iStack_55C)
    
    IF texture_name_length == 1:
      [4 bytes]   filename_length (puStack_584)
      [filename_length bytes] texture_filename (acStack_50c)
      texture = Graphics_LoadTexture(graphics, filename, 1)
      puVar5[0x1D] = texture
  ENDIF
  
  Store submesh in meshworld+0x894 list
END FOR

Section 2: Animation Data

[4 bytes]     animation_count (iStack_57C)

FOR each animation (i = 0..animation_count-1):
  [4 bytes]   animation_data_size (uStack_598)
  [animation_data_size bytes] raw animation buffer
  [4 bytes]   keyframe_count (iStack_59C)
  
  FOR each keyframe (j = 0..keyframe_count-1):
    [4 bytes]   keyframe_time (fStack_5A4)
    [... keyframe data ...]  (processed by FUN_004685E0 / FUN_00468510)
  END FOR
  
  FUN_00469090(animation, 0) // Finalize animation
  Store in meshworld+0xCAC list
END FOR

Section 3: Objects (Transforms)

[4 bytes]     object_count (iStack_574)

FOR each object (i = 0..object_count-1):
  [4 bytes]   object_type (iStack_550, checked for 0)
  
  IF object_type == 0:
    piVar8[0x34] = 3  // Set render mode
    
    [4 bytes]   position_x (iStack_5A0)
    [4 bytes]   position_y (uStack_58C) 
    [4 bytes]   position_z (uStack_580)
    vtable[4](x, y, z)  // SetPosition
    
    [4 bytes]   rotation_param1
    [4 bytes]   rotation_param2 (uStack_598)
    [4 bytes]   rotation_param3 (uStack_58C)
    vtable[8](param1, param2, param3)  // SetRotation
    
    [4 bytes]   scale_x (puStack_588)
    [4 bytes]   scale_y (uStack_568)
    [4 bytes]   scale_z (uStack_56C)
    Matrix_Scale4x4(scale_x, scale_y, scale_z, 1.0)
    
    // Apply matrix transforms
    piVar8[0x25..0x28] = transform matrix row 0
    piVar8[0x2F..0x32] = scale matrix (0.8, 0.8, 0.8, 1.0) default
    piVar8[0x2A..0x2D] = zero matrix
    
    Store in meshworld+0x478 list
  ENDIF
  
  uStack_598++
END FOR

Section 4: Footer Data

[4 bytes]   meshworld+0x454 value
[4 bytes]   meshworld+0x458 value
[4 bytes]   meshworld+0x45C value
[4 bytes]   meshworld+0x468 value
[4 bytes]   meshworld+0x46C value
[4 bytes]   meshworld+0x470 value

[4 bytes]   index_count (meshworld+0x438)
meshworld+0x43C = 0
[index_count * 32 bytes] index data (read as single block, stride=0x20)

meshworld+0x10F = this  // Back-pointer
meshworld+0x131 = 1     // Initialized flag

// Timer init
timer = Timer_Init(0x44 bytes)
meshworld+0x10D = timer

// Custom vtable callback
vtable[0x34](filehandle, meshworld)

__close(filehandle)

Vertex Buffer Layout (Inferred from Data)

Based on binary analysis of Sphere.MESH:

  • Position: 3 floats (X, Y, Z)
  • Normal: 3 floats (NX, NY, NZ)
  • TexCoord: 2 floats (U, V)
  • Total: 8 floats = 32 bytes per vertex

Sphere.MESH vertex 0: pos=(25.0, 0.0, 0.0) norm=(0.999, -0.01, 0.054) uv=(0.75, 0.5)
This confirms a sphere of radius 25 units with UV wrapping.

Submesh Entry Structure (0x7C = 124 bytes)

Offset Size Type Description
+0x00 4 void* Vertex buffer pointer
+0x04 4 float Field 1 (position X?)
+0x08 4 float Field 2 (position Y?)
+0x0C 4 float Emissive R
+0x10 4 float Ambient R
+0x14 4 float Ambient G
+0x18 4 float Ambient B
+0x1C 4 float Specular Power
+0x20 4 float Field (init 1.0)
+0x24 4 float Field (init 1.0)
+0x28 4 float Field (init 1.0)
+0x2C 4 float Diffuse A
+0x30 4 float Diffuse R
+0x34 4 float Diffuse G
+0x38 4 float Diffuse B
+0x3C 4 float Emissive G
+0x40 4 float Emissive B
+0x44 4 float Emissive A
+0x48 4 float Specular R
+0x4C 4 float Specular G
+0x50 4 float Specular B
+0x54 4 float Specular A
+0x58 4 float Unknown R
+0x5C 4 float Unknown G
+0x60 4 float Unknown B
+0x64 4 float Unknown A
+0x68 4 float Specular Power
+0x6C 1 bool Has texture
+0x70 4 void* Texture pointer
+0x74 1 bool Has custom color

Known Mesh Files

File Size Submeshes Vertices Name Texture
Sphere.MESH 10181 6 59 Sphere01 HamsterBall.png
8Ball.MESH 7675 44 42 Sphere01 8Ball.png
Sawblade.MESH 6723 1 60 Box01 sawblade.png
Fanblades.MESH 3864 - - REF:FAN -
Hamster-Waiting.MESH 18492 - - Hamster -

Reimplementation Notes (OpenGL/Assimp)

Loading Strategy

struct MeshSubmesh {
    std::vector<float> vertex_buffer;  // pos(3)+normal(3)+uv(2) per vertex
    float position[3];
    struct { float r, g, b, a; } diffuse, ambient, emissive, specular;
    float specular_power;
    bool has_texture;
    Texture* texture;
};

struct MeshObject {
    float position[3];
    float rotation[3];
    float scale[3];
    float transform_matrix[4][4];
};

struct MeshFile {
    std::vector<MeshSubmesh> submeshes;
    std::vector<Animation> animations;
    std::vector<MeshObject> objects;
    
    static MeshFile Load(const std::string& filename);
};

Conversion Roadmap

  1. Write a mesh2gltf converter tool (Python/C++)
  2. Parse .MESH binary → extract submeshes + vertex data
  3. Generate GLTF2 meshes with materials
  4. For animations: export as glTF2 animation channels
  5. Load in engine via Assimp or tinygltf

🔗 Related Documents

Mesh Object Gallery

types : analysis

📂 View source on GitHub


Hamsterball Mesh Object Gallery

# Name Vertices Faces Texture Preview
1 8Ball 42 0 8Ball.png 8Ball
2 Chomper 6 0  Chomper
3 Eye 32 0 PurpleEye.png Eye
4 FunBall 42 0 FunBall.png FunBall
5 GlassBonus 26 0 tenbonus.png GlassBonus
6 Hamster-Trot1 122 0 Hamster.jpg Hamster-Trot1
7 Hamster-Trot2 112 0 Hamster.jpg Hamster-Trot2
8 Hamster-Trot3 117 0 Hamster.jpg Hamster-Trot3
9 Hamster-Waiting 122 0 Hamster.jpg Hamster-Waiting
10 Hamster 119 0 Hamster.jpg Hamster
11 Mouse 243 0 Hamm.png Mouse
12 RBGlare 18 0 Glare.png RBGlare
13 Sawblade 60 0 sawblade.png Sawblade
14 Sphere+Tar 19 0 TarSplotch.png Sphere+Tar
15 Sphere 59 0 HamsterBall.png Sphere
16 SphereBreak1 38 0 HamsterBall.png SphereBreak1
17 SphereBreak2 26 0 HamsterBall.png SphereBreak2
18 TarBubble 19 0 TarBlot.png TarBubble

🔗 Related Documents

MESHWORLD Binary Format

types : meshworld

📂 View source on GitHub


MESHWORLD Binary Format — Definitive Specification

Source: Official Raptisoft 3DS Max exporter (MeshWorldExport.cpp by John Raptis)
Reference: reference/raptisoft-exporter/MeshWorldExport/

Vertex Structure (32 bytes)

typedef struct Vertex {
    float mX, mY, mZ;          // Position (12 bytes)
    float mNormalX, mNormalY, mNormalZ;  // Normal (12 bytes)
    float mTextureU, mTextureV;  // UV coords (8 bytes)
} Vertex;  // Total: 32 bytes

Binary File Layout

SECTION 1: Ref Points (game objects like START, FLAG, N:GOAL, etc.)
  [int]     point_count
  FOR each point:
    [int,len][string]  name (length-prefixed, NUL-terminated)
    [float]   position.x      // NOTE: x,z,y order (Max→D3D swap)
    [float]   position.z
    [float]   position.y
    [float]   rotation.x
    [float]   rotation.z
    [float]   rotation.y
    [int]     has_material    // 0=no material, 1=has material
    IF has_material:
      [float×4]  Ambient  (r,g,b,a)
      [float×4]  Diffuse  (r,g,b,a)
      [float×4]  Specular (r,g,b,a)
      [float×4]  Emissive (r,g,b,a)
      [float]    Power (shininess)
      [int]      has_reflection
      [int]      has_texture (0 or 1)
      IF has_texture:
        [int,len][string] texture_filename

SECTION 2: Splines (BallPath objects)
  [int]     spline_count
  FOR each spline:
    [int,len][string]  name
    [int]     point_count
    FOR each point:
      [float]   x        // NOTE: x,z,y order
      [float]   z
      [float]   y

SECTION 3: Lights
  [int]     light_count
  FOR each light:
    [int]     type (0=DISTANTLIGHT)
    IF DISTANTLIGHT:
      [float]   position.x,z,y
      [float]   lookat.x,z,y
      [float]   color.r,g,b

SECTION 4: Background/Ambient Colors
  [float×3] background_color (r,g,b)
  [float×3] ambient_color (r,g,b)

SECTION 5: Global Vertex Buffer
  [int]     vertex_count
  [Vertex×vertex_count] vertex_list  // 32 bytes each

SECTION 6: Octree Mesh Dump (recursive)
  [Cube]    bounding_box     // 6 floats: corner1(x,y,z) + corner2(x,y,z)
  IF subdivided:
    [int]     submesh_count   // >0 means has children
    FOR each submesh:
      [recursive SECTION 6]
  ELSE (leaf node):
    [int]     0               // 0 = no submeshes, leaf flag
    [int]     geom_count      // Number of geometry objects
    FOR each geom:
      [int,len][string]  name  // "" unless N: or E: prefix or NOCOLLIDE
      [float×4]  Ambient  (r,g,b,a)
      [float×4]  Diffuse  (r,g,b,a)
      [float×4]  Specular (r,g,b,a)
      [float×4]  Emissive (r,g,b,a)
      [float]    Power
      [int]      has_reflection
      [int]      has_texture (0 or 1)
      IF has_texture:
        [int,len][string] texture_filename
      [int]      strip_count
      FOR each strip:
        [int]    triangle_count   // # triangles in this strip
        [int]    vertex_ref_offset // mSecondaryVertexListReference
                                       // (offset into global vertex buffer)

Key Insights from Exporter Source

  1. X/Z/Y swap: All positions written as x,z,y (3DS Max Z-up → D3D Y-up)
  2. Octree structure: Mesh is recursively subdivided into cubes for visibility culling
  3. Global vertex buffer: All vertices in a single buffer, strips reference into it
  4. Triangle strips: Not raw indices — each strip stores triangle_count + vertex_ref_offset
    into the global vertex buffer (WorldOptimize adjusts offsets)
  5. Geometry names: Only written if they have N: or E: prefix or contain NOCOLLIDE
  6. Materials stored per-geom: Ambient/Diffuse/Specular/Emissive + Power + texture + reflection
  7. 16384 triangle cap: From Carsillas' notes — game crashes if level exceeds 2^14 triangles

String Format

[int]     length (including NUL terminator)
[length bytes]  string data (NUL-terminated, may be empty string with length=1)

Cube Structure (24 bytes)

[float] corner1.x
[float] corner1.y  
[float] corner1.z
[float] corner2.x
[float] corner2.y
[float] corner2.z

Reading Order for Level Rendering

  1. Skip ref points (Section 1) — already parsed by our existing code
  2. Skip splines (Section 2)
  3. Skip lights (Section 3)
  4. Read background + ambient colors (Section 4)
  5. Read global vertex buffer (Section 5) — THIS is the geometry to render
  6. Walk octree (Section 6) — each leaf has geoms with materials + strip references into vertex buffer

Original Code References

  • Format writer: MeshWorldExport.cpp:DoExport() line 186
  • Vertex struct: Math.h line 18-30
  • Octree dump: WorldMesh.cpp:Dump() line 259
  • String write: MeshWorldExport.cpp:WriteString() line 147 (length-prefixed)

🔗 Related Documents

MESHWORLD File Format (CONFIRM

types : meshworld
keywords :

📂 View source on GitHub


MESHWORLD File Format (CONFIRMED)

Overview

Binary file format used for Hamsterball levels. Contains 3D geometry data
followed by object definitions with positions, transforms, and materials.

File Structure

[Geometry Section]
  - Binary mesh data (vertices, faces, normals, UVs)
  - Bounding box data
  - May be empty (count=0 in Arena-SpawnPlatform)

[Object Section]
  - uint32: total object count
  - Sequential object definitions (variable length per type)

Geometry Section

Starts at offset 0. The section contains:

  • First uint32 may be 0 (Arena-SpawnPlatform) or a vertex count
  • Followed by floats for bounding box and vertex data
  • Variable-length mesh geometry data

Object Section Format

Each object definition has this structure:

[uint32: type_string_length]
[type_string: variable bytes, null-padded to length]
[object data: variable size depending on type]
  - Position: 3 floats (x, y, z)
  - Rotation/scale: 4 values (Quaternion or Euler+scale)
  - Material properties (for complex types)
  - Texture reference (for complex types):
    [uint32: texture_string_length]  
    [texture_string: null-terminated filename]
  - Face/vertex indices (for complex types)

Object Type Sizes (CONFIRMED from binary analysis)

Simple Objects (28 bytes after type string)

START positions (START1-1, START2-1, START2-2)

[float: x_position]
[float: y_position]  
[float: z_position]
[uint32: 0]           # rotation x (0)
[uint32: 0]           # rotation y (0)
[float: 0x80000000]   # rotation z (-0.0 or direction flag)
[uint32: 0]           # padding/flags

SAFESPOT (28 bytes after type string)

Same structure as START.

Complex Objects (variable size)

FLAG (FLAG02, FLAG04, FLAG06, FLAG07)

  • 104 bytes after type string
  • Then texture reference string (e.g., "BlackCheckerFlag.png")
  • Then 0 bytes before next object
  • Contains: position(12) + rotation(16) + scale/matrix(32) + color(16) + size_param(4) + flags(8) + face indices

PLATFORM / N:SINKPLATFORM

  • 76 bytes after type string (transform + color + size)
  • Then texture reference string
  • Then mesh face index data (pairs of index + value)
  • Size parameter: 35.0 for standard platforms

E:NODIZZY

  • Contains embedded XML property for timer
  • Followed by transform matrix (16 floats = 4x4 identity-like)
  • Then position data (3 floats for center, repeated as 4x3 matrix)
  • Closing: uint32(0), uint32(0), uint32(0), uint32(1), uint32(2), uint32(global_count)

Material/Transform Data (for complex objects)

Offset  Size  Description
0x00    32    Rotation/scale matrix (8 floats, identity = all 1.0)
0x20    12    Diffuse color RGB (3 floats, e.g., 0.45, 0.45, 0.45)
0x2C    4     Alpha/opacity (float, e.g., 0.5)
0x30    12    Ambient/specular (3 floats, often 0, 0, 0)
0x3C    4     Ambient alpha (float, often 1.0)
0x40    4     Platform size parameter (float, e.g., 35.0)
0x44    4     Flag 1 (uint32, often 1)
0x48    4     Flag 2 (uint32, often 1)
[then texture string reference]
[then face index data]

File Trailer (CONFIRMED from Arena-SpawnPlatform)

After all objects:

00 00 00 00  # position zeros
00 00 00 00  
00 00 00 00  
01 00 00 00  # uint32 = 1
02 00 00 00  # uint32 = 2 (same as object count)
1E 00 00 00  # uint32 = 30 (total object/geometry count?)

Arena-SpawnPlatform Example Parse

  • File size: 1,476 bytes
  • Geometry: mostly zeros (minimal geometry)
  • Object count: 2 (N:SINKPLATFORM + E:NODIZZY)
  • N:SINKPLATFORM: position at identity, diffuse color 0.45,0.45,0.45, alpha 0.5, size 35.0, texture "PinkChecker.bmp"
  • E:NODIZZY: identity transform, scale 0.5

Level1 Example Parse

  • Object count: 30 objects
  • Types including: START1-1, FLAG04, FLAG07, SAFESPOT (x8), START2-1, START2-2, FLAG02, FLAG06, SAFESPOT (x8+), PLATFORM, CAMERALOOKAT, etc.

🔗 Related Documents

MESHWORLD File Format & Object

types : meshworld
keywords :

📂 View source on GitHub


MESHWORLD File Format & Object System — Definitive Reference

Verified against: Binary .MESHWORLD files, official Raptisoft exporter source (MeshWorldExport.cpp, WorldMesh.cpp, NodeEnumerator.cpp), and Ghidra decompilations of Hamsterball.exe.

1. Two File Formats

Hamsterball uses two mesh file formats:

1.1 .MESHWORLD (Binary) — Level/world files

  • Written by the official Raptisoft 3DS Max exporter plugin (MeshWorldExport.cpp)
  • Loaded by the game engine via binary file I/O (__read calls)
  • Contains: ref points, splines, lights, colors, vertex buffer, octree mesh
  • File extension: .MESHWORLD (e.g., Level1.MESHWORLD, Arena-WarmUp.MESHWORLD)
  • 37 level files + 15 arena files + sub-level files (bridges, etc.)

1.2 .ASE (Text) — Individual mesh objects

  • 3DS Max ASCII Scene Export format
  • Parsed by MeshWorld_Parse (0x470930) — line-by-line text parser
  • Keywords: *MATERIAL, *GEOMOBJECT, *MESH_VERTEX, *MESH_FACE, etc.
  • Used for loading individual mesh objects (not entire levels)
  • Material struct: 0x50 bytes per material (ambient/diffuse/specular/shine/texture)

2. Binary .MESHWORLD Format (6 Sections)

Section 1: Ref Points (Game Objects)

[int32]   point_count
FOR each point:
  [int32,len][string]  name          (length-prefixed, NUL-terminated)
  [float]  position.x                 (NOTE: x,z,y order — Max Z-up → engine Y-up)
  [float]  position.z
  [float]  position.y
  [float]  rotation.x                 (yaw)
  [float]  rotation.z
  [float]  rotation.y                 (roll)
  [int32]  has_material               (0=no material, 1=has material)
  IF has_material:
    [float×4]  Ambient   (r,g,b,a)
    [float×4]  Diffuse   (r,g,b,a)
    [float×4]  Specular  (r,g,b,a)
    [float×4]  Emissive   (r,g,b,a)
    [float]    Power                (shininess)
    [int32]    has_reflection
    [int32]    has_texture          (0 or 1)
    IF has_texture:
      [int32,len][string] texture_filename

Ref points are the game logic objects — spawn points, triggers, factory objects.

Section 2: Splines (Path Objects)

[int32]   spline_count
FOR each spline:
  [int32,len][string]  name
  [int32]   point_count
  FOR each point:
    [float]  x        (x,z,y order)
    [float]  z
    [float]  y

Splines are used for BlockDawg patrol paths (DAWGPATH1, DAWGPATH2, DAWGPATH3).

Section 3: Lights

[int32]   light_count
FOR each light:
  [int32]   type                    (0 = DISTANTLIGHT)
  IF type == 0:
    [float×3] position (x,z,y)
    [float×3] lookat   (x,z,y)
    [float×3] color     (r,g,b)

Section 4: Background & Ambient Colors

[float×3] background_color (r,g,b)
[float×3] ambient_color    (r,g,b)

Section 5: Global Vertex Buffer

[int32]     vertex_count
[Vertex×N]  vertex_list     (32 bytes per vertex)

Vertex struct (32 bytes):

struct Vertex {
    float mX, mY, mZ;              // Position (12 bytes)
    float mNormalX, mNormalY, mNormalZ;  // Normal (12 bytes)
    float mTextureU, mTextureV;    // UV coords (8 bytes)
};

Section 6: Octree Mesh Dump (Recursive)

[Cube]  bounding_box          // 6 floats: corner1(x,y,z) + corner2(x,y,z)
[int32] submesh_count
IF submesh_count > 0:
  FOR each submesh:
    [recursive Section 6]       // child cube
ELSE (leaf node):
  [int32] geom_count           // number of geometry objects
  FOR each geom:
    [int32,len][string] name  // "" unless name[1]==':' OR contains "NOCOLLIDE"
    [float×4] Ambient
    [float×4] Diffuse
    [float×4] Specular
    [float×4] Emissive
    [float]   Power
    [int32]   has_reflection
    [int32]   has_texture
    IF has_texture:
      [int32,len][string] texture_filename
    [int32]   strip_count
    FOR each strip:
      [int32] triangle_count
      [int32] vertex_ref_offset   // offset into global vertex buffer

Key exporter rule (WorldMesh.cpp:291): A geom name is only written if:

  • name[1] == ':' (any X: prefix), OR
  • The name contains "NOCOLLIDE"
    Otherwise, an empty string "" is written (unnamed geometry).

3. Object Name Prefix System

3.1 Ref Point Names (Section 1) — Game Logic Objects

Parsed into a hash table during scene construction. Looked up by name during Scene_SpawnBallsAndObjects (0x41C5B0) and CreateLevelObjects (0x4121D0).

Category Examples Handler
Spawn points START1-1, START2-1, START2-2, START-DEBUG Scene_SpawnBallsAndObjects — ball placement
Checkpoints SAFESPOT, SAFEPOS, SAFESPOT(A) Appended to scene->safespots list
Race flags FLAG02FLAG18 Scene_CreateFlags — checkpoint flags
Enemy balls BADBALL <CHASE>100</CHASE><HOME>400</HOME> CreateBadBall — XML-parameterized AI ball
Factory objects BRIDGE, TIPPER, BONK, BBRIDGE1, BBRIDGE2, POPCYLINDER, BLOCKDAWG1/2, CATAPULT, GLUEBIE CreateLevelObjects (0x4121D0) — prefix-dispatched
Arena objects BONK, TIP, SAWBLADE, BRIDGE, JUDGE, BELL CreateExpertLevelObjects (0x40E250) — arena sub-factory
Bumpers BUMPER1BUMPER8 CreateBumper (0x40FA20) — loads levels\level8
Camera CAMERALOOKAT Camera target point (present in all 15 race levels)
Secrets SECRET, SECRETUNLOCK, N:SECRET CreateSecretObjects — hidden collectibles
Signs SIGN-TARPIT Scene_CreateSigns
Splines DAWGPATH1, DAWGPATH2, DAWGPATH3 Level_FindObjectByName → BlockDawg patrol path
Misc refs FAN, FAN(UP), FAN(SUPER)(UP), GEAR01GEAR24, BIGGEAR01BIGGEAR14, TURRET, DRAWBRIDGE, etc. Various level-specific handlers

3.2 Octree Geom Names (Section 6) — Collision & Render Objects

Parsed by the binary MESHWORLD loader (function at ~0x461680) into MeshBuffer objects. The loader checks geom name prefixes and sets render classification flags on the MeshBuffer struct:

// Binary MESHWORLD loader (~0x461680) — checks ALL prefixes:
if (name[1] == ':')                 obj->has_named_prefix = 1;     // +0x85C
if (strnicmp(name, "O:", 2) == 0)   obj->is_translucent = 1;       // +0x862
if (strnicmp(name, "T:", 2) == 0)   obj->is_decal = 1;             // +0x85F
if (strnicmp(name, "N:GLASS", 7)==0)obj->is_alpha_test = 1;        // +0x860
if (strnicmp(name, "E:", 2) == 0)   obj->no_render = 1;            // +0x863
if (strstr(name, "(NOSHADOW)"))     obj->no_shadow = 1;            // +0x85E

// Text/.COL loader (Level_LoadCollision 0x465260) — checks N: and E: only:
if (strnicmp(name, "N:", 2) == 0)   buf->interactive = 1;          // +0x85D
if (strnicmp(name, "E:", 2) == 0) { buf->interactive = 1; buf->no_render = 1; }  // +0x85D, +0x863

These flags control how Scene_RenderAllObjects (0x45E0E0) classifies each geom into render passes:

  • Opaque pass (default): AlphaBlend OFF, AlphaTest OFF
  • Translucent pass (+0x862): AlphaBlend ON — objects behind transparent parts are still visible. This is why the ball renders correctly behind O: tubes.
  • Alpha test pass (+0x860): Sorted into translucent bucket with per-pixel alpha cutoff
  • Decal pass (+0x85F): Stencil-based, with depth bias
  • Skip (+0x863): Not rendered at all (invisible collision zones)
  • No shadow (+0x85E): Excluded from shadow rendering pass

Verified by user testing: Changing O: to A: in a level file causes the ball to stop rendering behind tubes, confirming O: directly sets the translucent render flag (+0x862).

Prefix Count Engine Flag Offset Render Pass Behavior
N: 278 interactive +0x85D Opaque Named collision, triggers event handler on hit (set by text loader)
E: 581 no_render +0x863 Skip (invisible) Invisible collision zone, triggers event on hit
O: 60 is_translucent +0x862 Translucent (alpha blend) See-through render — ball visible behind transparent parts (tubes, saws)
T: 175 is_decal +0x85F Decal (stencil + depth bias) Texture overlays: arrows, warnings, bullseyes
N:GLASS is_alpha_test +0x860 Alpha test (sorted into translucent) Alpha-tested transparency (glass platforms)
(NOSHADOW) no_shadow +0x85E Excluded from shadow pass Designer tag in name, checked via strstr
S: 831 (none specific) Opaque (standard) Standard collision + standard render. (NOSHADOW) suffix in name sets +0x85E.
(none) (none) Opaque (standard) Standard level geometry (walls, floors)

3.3 Complete N: Object Catalog (44 unique types)

Name Triangles Texture Files Behavior
N:BOUNCE(NOSHADOW) 38 1 Bouncy surface (no shadow)
N:BRIDGE 244 1 Bridge segment (movable)
N:BUMP 100 1 Bump/perturbation surface
N:BUMPER1N:BUMPER8 24-156 2-4 Arena bumpers
N:DROPIN 2 1 Pipe drop-in trigger
N:EXTRATIME 8 FiveBonus.png 1 Extra time collectible
N:GLASS 32 2 Glass platform (breakable?)
N:GOAL 8 GreyOutlineChecker.png 15 Race finish line
N:JUMPFIRST / N:JUMPSECOND 2 1 Jump pad (sequential)
N:LOOPER(NOSHADOW) 54 RedChecker.bmp 1 Loop-the-loop section
N:MOUSETRAP 2-6 TrapTop/TrapSpring/TrapMetal.jpg 1 Mouse trap obstacle
N:NEONPLATFORM 18 1 Neon-lit platform
N:NOCONTROL 2 GreenChecker.bmp 1 Disable ball input zone
N:ONGEAR(NOSHADOW) 16 1 Gear surface (rides on gear)
N:ONPENDULUM 26 RedChecker.bmp 1 Pendulum surface
N:ONROTATOR 8 RedChecker.bmp 1 Rotator surface
N:SAWTEETH 4 1 Saw teeth hazard
N:SECRET / N:SECRET(NOSHADOW) 6-12 GreyOutlineChecker.png 1 Hidden collectible
N:SINKPLATFORM 10 PinkChecker.bmp 1 Sinking platform
N:SPEEDCYLINDER 17 SpeedCylinder.png 1 Speed boost cylinder
N:SPINNY 62 1 Spinning platform
N:SQUAREWOBBLY / (NOSHADOW) 2-336 BrightGreenChecker.bmp 1-8 Wobbly square platform
N:SWIRL 158 1 Swirl/vortex section
N:TARPIT 8 2 Tar pit (slows ball)
N:TENBONUS1 / N:TENBONUS2 2 1 +10 bonus collectible
N:TRAPDOOR 2 YelllowChecker.png 2 Trapdoor (opens on trigger)
N:UNLOCKSECRET 2 LockTile.png 12 Secret unlock trigger
N:WATERWHEEL(NOSHADOW) 144 1 Water wheel obstacle
N:WAVY 168 BrightGreenChecker.bmp 1 Wavy platform
N:WHEELEMBED 6 1 Embedded wheel section

3.4 Complete E: Event Catalog (86 unique types)

Name Behavior
E:LIMIT Arena boundary / fall-off zone (323 instances — most common)
E:DROPIN Pipe drop-in sound + score (28 instances, 12 files)
E:POPOUT Pipe pop-out sound + score (30 instances, 11 files)
E:PIPEBONK Pipe collision sound (random of 3) (22 instances, 4 files)
E:SAFESWITCH(A)(H) Track which checkpoint/safe zone was hit
E:NODIZZY<TIME>N</TIME> Anti-dizzy zone with duration (50-600)
E:JUMP Jump pad: upward velocity + sound + score
E:CATAPULTBOTTOM Launch catapult
E:OPENSESAME Open first trapdoor
E:BITE Damage: 25.0
E:MACETRIGGER Activate all maces
E:CALLHAMMER Spawn hammer chase (arena MP)
E:HAMMERCHASE Start hammer chase sequence
E:ALERTSAW1/2 Pre-activate saw blade (warning)
E:ACTIVATESAW1/2 Full activate saw blade
E:ALERTJUDGES Reset all judge objects
E:SCORE1/5/7/9 Set score display time
E:BELL Extra time +5s + "EXTRA TIME:" popup
E:GRAVITY<TYPE>X/Z/NORMAL</TYPE> Change gravity direction
E:TRAJECTORY<X>..</X><Y>..</Y><Z>..</Z> Set ball trajectory
E:ACTION<ONCE>TRUE</ONCE><SCORE>N</SCORE> Score action (one-time or repeatable)
E:LAUNCH Launch ball
E:SHRINK Shrink ball
E:SWALLOW Swallow/absorb ball
E:TRAPPOP Trapdoor pop
E:VACPOPOUT Vacuum pop-out
E:ZOOP Speed boost
E:HEATON / E:HEATOFF Heat effect on/off
E:LIGHTSON / E:LIGHTSOFF Lights on/off
E:HELPINERTIA / E:UNHELPINERTIA Assist inertia on/off
E:PEGS / E:NOPEGS Toggle pegs
E:GROWSOUND Grow sound effect
E:BRANCH(A) / E:BRANCH(B) Branch path selection
E:DROPLIFT Drop lift
E:LIMITPIPE1/2 Pipe limit zones
E:LIMITX / E:LIMITZ Axis-specific limit zones

4. How Objects Interact with the EXE

4.1 Loading Pipeline

1. App selects level → MeshWorld_ctor(gfx) creates MeshWorld struct (0x488 bytes)
2. Level_LoadCollision (0x465260) reads binary .MESHWORLD:
   a. Reads header (24 bytes into MeshWorld+0x45C)
   b. Reads sublevel_count
   c. If single-level: reads objects with named collision faces
   d. If multi-level: creates sub-levels recursively
3. Each named collision object → CreateMeshBuffer (0x874 bytes)
   - Name stored at +0x864 (char*)
   - N: prefix → +0x85D = 1 (interactive)
   - E: prefix → +0x85D = 1, +0x863 = 1 (interactive + no_render)
4. Scene construction:
   a. Scene_SpawnBallsAndObjects (0x41C5B0) — reads ref points by name
   b. CreateLevelObjects (0x4121D0) — factory dispatches by ref point name prefix
   c. CreateExpertLevelObjects (0x40E250) — arena object sub-factory
   d. CreateBumper (0x40FA20) — bumper loader
   e. CreateMouseTrap (0x40BF50) — mouse trap spawner
5. Per-frame: Ball_AdvancePositionOrCollision checks CollisionLevel
   → Mesh_FindClosestCollision (0x465D90) raycasts against named faces
   → On hit: TowerCollisionEvents or ExpertCollisionEvents dispatches by name

4.2 Collision Dispatch Chain

Ball physics update
  → Mesh_FindClosestCollision (0x465D90) — raycast against CollisionLevel
    → If hit named collision object:
      → TowerCollisionEvents (0x40DCD0) [Tower board]
         OR ExpertCollisionEvents (0x40E6A0) [arenas]
        → __stricmp / __strnicmp on object name (at collider+0x864)
        → Dispatch to specific handler (Catapult_Launch, Trapdoor_Open, etc.)
        → Always ends with: DispatchCollisionEvents (0x40C5D0) — base event handler

4.3 DispatchCollisionEvents — The Base Event Handler (0x40C5D0)

Handles ALL events not consumed by level/arena-specific handlers. Uses __strnicmp/__stricmp on the event name string:

  • N:SECRETRotator_MarkTriggered
  • N:UNLOCKSECRETCheckArenaUnlock
  • E:NODIZZY<TIME>N</TIME> → Parse XML tags, Ball_DizzyImmunity
  • E:SAFESWITCH(data) → Copy parenthesized data to ball state
  • E:LIMIT → Arena fall-off: increment other players' completion counts
  • E:BREAK → Call ball->vtable[0x20]() (bounce)
  • E:JUMP → Sound + upward velocity + score
  • E:ACTION<ONCE>..<SCORE>..</SCORE> → Parse XML, award points
  • E:TRAJECTORY(X,Y,Z) → Parse XML tags, set ball trajectory
  • N:NOCONTROL → Disable input for 10 frames
  • N:WATER → Set water flag + 10-frame counter
  • N:TARPIT → Slow ball + tar sound
  • E:DROPIN → Drop-in sound + score (distance-gated)
  • E:PIPEBONK → Random pipe bonk sound (1 of 3)
  • E:POPOUT → Pop-out sound + score
  • N:GOAL → Finish race! Play music, store time
  • N:MOUSETRAP → Deflect ball, track rotator collision

4.4 Factory Dispatch (CreateLevelObjects 0x4121D0)

Ref point names are matched via __strnicmp to instantiate game objects:

Prefix Match Length Constructor Size Scene Offset
BRIDGE 6 (configures existing mesh) +0x436C
TIPPER 6 Tipper_ctor + TipperVisual_ctor 0x1104 + 0x10D0 +0x2578
BONK 4 Bonk_ctor 0x1200 +0x2578, +0x540C
BBRIDGE1 8 BreakBridge_ctor 0x1100 +0x5418
BBRIDGE2 8 BreakBridge_ctor 0x1100 +0x541C
POPCYLINDER 11 PopCylinder_ctor 0x10E8 +0x5428
BLOCKDAWG1 10 Blockdawg_ctor 0x1154 +0x2578
BLOCKDAWG2 10 Blockdawg_ctor 0x1154 +0x2578
CATAPULT 8 Catapult_ctor 0x1108 +0x584C
GLUEBIE 7 Gluebie_ctor 0x110C +0x6080

4.5 Arena Sub-Factory (CreateExpertLevelObjects 0x40E250)

Despite the name, this handles 6 arena object types:

Prefix Match Length Constructor Size Scene Offset
BONK 4 Bonk_ctor 0x1200 +0x436C
TIP 3 TowerLevel_Ctor 0x1188 +0x2578
SAWBLADE 8 Sawblade_Level_Ctor 0x111C +0x4370/4374
BRIDGE 6 Spinner_Level_ctor 0x10FC +0x4380/4798
JUDGE 5 Gear_Level_ctor 0x1100 +0x4BBC
BELL 4 Tipper_Level_Ctor 0x10E8 +0x4FD4

Modifiers via strstr: SLOW, SUPER, UP (TowerLevel), 1/2 (Sawblade/Spinner), NEG (Spinner direction).

5. BADBALL — Parameterized Enemy Ball

BADBALL ref points use XML-style tags for parameters:

BADBALL <CHASE>100</CHASE><HOME>400</HOME><SIZE>18</SIZE><SPINDISTANCE>25</SPINDISTANCE>
Tag Meaning Example
<CHASE> Chase distance (when to start pursuing) 100-300
<HOME> Home distance (return-to-home radius) 100-1000
<SIZE> Ball radius 18
<SPINDISTANCE> Spin activation distance 25-45

6. Render Pipeline Integration

Objects are classified into render buckets by flags on SceneObject:

  • +0x863: has_bounding_sphere (skip if set)
  • +0x862: is_translucent (alpha blend)
  • +0x85F: is_decal (stencil-based)
  • +0x860: is_alpha_test (sorted into translucent)
  • +0x861: has_per_object_alpha

Scene_RenderAllObjects (0x45E0E0) — 3-pass render:

  1. Opaque pass: AlphaBlend OFF, AlphaTest OFF
  2. Translucent pass: AlphaBlend ON, AlphaTest OFF
  3. Decal pass: Stencil + depth bias

Render bucket classification is driven by name prefix flags set during MESHWORLD loading (see §3.2 above), NOT by material properties. The binary loader at ~0x461680 checks each prefix and sets the corresponding flag byte on the MeshBuffer struct before rendering.

Flag Offset Set by Render effect
has_named_prefix +0x85C name[1]==':' Name was preserved in file
no_shadow +0x85E (NOSHADOW) substring Excluded from shadow pass
is_decal +0x85F T: prefix Stencil + depth bias render pass
is_alpha_test +0x860 N:GLASS prefix Sorted into translucent with alpha cutoff
is_translucent +0x862 O: prefix Alpha blend ON — see-through rendering
no_render +0x863 E: prefix Skipped entirely (invisible)

7. String Format Convention

All strings in .MESHWORLD files are length-prefixed:

[int32]   length (including NUL terminator)
[length bytes]  string data (NUL-terminated)

Empty string = length 1, single \x00 byte.

References

  • Official exporter: reference/raptisoft-exporter/MeshWorldExport/
  • Binary loader: Level_LoadCollision at 0x465260 (Ghidra decompilation)
  • ASE parser: MeshWorld_Parse at 0x470930 (Ghidra decompilation)
  • Collision dispatch: TowerCollisionEvents at 0x40DCD0, ExpertCollisionEvents at 0x40E6A0
  • Base event handler: DispatchCollisionEvents at 0x40C5D0
  • Object factory: CreateLevelObjects at 0x4121D0, CreateExpertLevelObjects at 0x40E250

🔗 Related Documents

MESHWORLD Format

types : meshworld

📂 View source on GitHub


MESHWORLD Format — Deep Specification

Overview

The .MESHWORLD format is the core level/scene description format for Hamsterball. It contains:

  1. 3D mesh geometry (vertices, faces, normals, UVs)
  2. Object placement definitions with positions, rotations, and properties
  3. Collision geometry references
  4. Event trigger volumes
  5. Visual decoration and lighting data

Complete Object Type Taxonomy

Object types follow a PREFIX:NAME(MODIFIER) pattern:

Prefix System

Prefix Category Description
START Player Start Spawn positions for 1P and 2P
FLAG Checkpoint Race progress checkpoints
SAFESPOT Safety Landing/recovery spot
CAMERALOOKAT Camera Camera target position
CAMERALOCUS Camera Camera focus point
E: Event (enter) Triggered when ball enters volume
N: Named Object Interactive game object with collision
O: Object Static/moving geometry objects
S: Structural Non-shadow decorative geometry
T: Texture/Decal Non-colliding visual elements
W: World World boundary

Modifiers (appended in parentheses)

Modifier Effect
(NOCOLLIDE) No collision with ball
(NOSHADOW) Does not cast/receive shadows
(WANTZ) Uses Z-buffer for depth test
<TIME>NNN</TIME> XML-embedded parameter (duration in seconds)

E: Events (Ball-Enter Triggers)

Event Description
E:ACTION Generic action trigger
E:ACTIVATESAW1/2 Activate sawblade 1 or 2
E:ALERTJUDGES Notify judge objects
E:ALERTSAW1/2 Alert/activate saw 1 or 2
E:BELL Ring bell
E:BITE Chomper bite
E:BRANCH(A)/(B) Branch path A or B
E:CALLHAMMER Summon hammer
E:CATAPULTBOTTOM Catapult launch zone
E:DROPIN/01 Drop ball into area
E:DROPLIFT Drop lift platform
E:GRAVITY Change gravity
E:GROWSOUND Play grow sound
E:HAMMERCHASE Start hammer chase
E:HEATOFF/HEATON Heat hazard toggle
E:HELPINERTIA/UNHELPINERTIA Assist/stop inertia
E:JUMP Jump pad
E:LAUNCH Launch ball
E:LIGHTSOFF/LIGHTSON Neon lights toggle
E:LIMIT/LIMITX/LIMITZ/LIMITPIPE1/2 Boundary limits
E:MACETRIGGER Trigger mace swing
E:NODIZZY Disable dizzy effect (with <TIME>)
E:NOPEGS Remove pegs
E:OPENSESAME Open door/gate
E:PEGS Add pegs
E:PIPEBONK Pipe collision (random sound from 3)
E:PIPERANDOM Random pipe effect
E:POPOUT/01 Pop ball out of area
E:SAFESWITCH(A-Z) Safe zone switch (multiple variants)
E:SCORE1/5/7/9 Score bonus (1/5/7/9 points)
E:SHRINK Shrink ball
E:SWALLOW Swallow/absorb ball
E:TRAJECTORY Ball trajectory change
E:TRAPPOP Trapdoor popup
E:VACPOPOUT Vacuum popout
E:ZOOP Quick movement effect

N: Named Objects (Interactive)

Object Description
N:BOUNCE(NOSHADOW) Bounce pad
N:BRIDGE Destructible/movable bridge
N:BUMP Bump obstacle
N:BUMPER1-8 Bumper variants (1-8)
N:DROPIN Drop-in zone
N:EXTRATIME Time bonus pickup
N:GLASS Glass/breakable platform
N:GOAL Race goal finish line
N:JUMPFIRST/JUMPSECOND Jump sequence triggers
N:MOUSETRAP Mousetrap hazard
N:NEONPLATFORM Neon-lighted platform
N:NOCONTROL Disable player control
N:ONGEAR(NOSHADOW) Ride on gear/rotator
N:ONPENDULUM Ride on pendulum
N:ONROTATOR Ride on rotating platform
N:SAWTEETH Saw blade teeth
N:SECRET/SECRET(NOSHADOW) Secret unlock spot
N:SINKPLATFORM Sinking platform
N:SPEEDCYLINDER Speed boost cylinder
N:SPINNY Spinning obstacle
N:SQUAREWOBBLY Square wobbling platform
N:SWIRL Swirl/vortex
N:TARPIT Tar pit (slow zone)
N:TENBONUS1/2 10-point bonus pickup
N:TRAPDOOR Trapdoor (falls when stepped on)
N:UNLOCKSECRET Unlock secret content
N:WATERWHEEL(NOSHADOW) Water wheel mechanism
N:WAVY Wavy water surface
N:WHEELEMBED Embedded wheel/axle
N:LOOPER(NOSHADOW) Loop-de-loop section

O: Object Types (Static/Moving Geometry)

Object Description
O:EXITTOOB/01-06 Tube exit points
O:HANDLE Handle/grab point
O:SAW Sawblade assembly
O:TOOB Tube/tunnel section
O:TOOBY01-06 Tube variants

S: Structural Types (Non-Shadow Decorative)

Object Description
S:AXLE(NOSHADOW) Axle/rotation pivot
S:BRICK/BRICKS(NOSHADOW) Brick walls
S:BUGFIXPLANE(NOSHADOW) Invisible collision fix plane
S:EnlargePipe(NOSHADOW) Pipe widening section
S:PIPERIM(NOSHADOW) Pipe rim geometry
S:PIPING(NOSHADOW) Pipe sections
S:Pipes(NOSHADOW) Multiple pipe geometry
S:RAILS(NOSHADOW) Rail tracks
S:Rim(NOSHADOW) Rim/border geometry
S:ShrinkPipe(NOSHADOW) Pipe narrowing section
S:TUBE(NOSHADOW) Tube geometry
S:WALLS(NOSHADOW) Wall sections

T: Texture/Decal Types (Visual Only, No Collision)

Type Description
T:ARROW/01(NOCOLLIDE) Direction arrows on floor
T:ARROWCURVE(NOCOLLIDE) Curved direction arrows
T:BULLSEYE(NOCOLLIDE)(WANTZ) Target bullseye pattern
T:FLOORLIGHTS Floor lighting patterns
T:GOALAREA(NOCOLLIDE) Goal zone visual overlay
T:GOSPOT(NOCOLLIDE) Go spot indicator
T:GoalImage/GoalOverlay(NOCOLLIDE) Goal visual effects
T:JUMPARROW(NOCOLLIDE) Jump pad arrow indicator
T:LIGHT/01/LIGHTS Light sources
T:Lighties Small light decorations
T:NEON/08-13 Neon light strips
T:NEONARROW(NOCOLLIDE) Neon direction arrows
T:NEONRING/01/02 Neon ring lights
T:NEONSTRIP Neon strip light
T:RAMPARROW(NOCOLLIDE) Ramp direction arrow
T:SIDEARROW(NOCOLLIDE) Side wall direction arrow
T:SPEEDARROW Speed boost arrow
T:START/STARTER/STARTSPOT(NOCOLLIDE) Start position indicators
T:STARTSTOP(NOCOLLIDE) Start/stop markers
T:TUBETOP/01/02/03 Tube top openings
T:TunnelArrow(NOCOLLIDE) Tunnel direction arrow
T:WARN/WARNING/Warning(NOCOLLIDE) Warning signs
T:WARNINGDECAL(NOCOLLIDE) Warning decal texture
T:YELLOWARROW(NOCOLLIDE) Yellow direction arrow
T:ZIGZAG(NOCOLLIDE) Zigzag pattern

Start Position Format

Type Description
START1-1 Player 1 start
START2-1 Player 2 start (2-player mode)
START2-2 Player 2 alternate start

Checkpoint System

Type Description
FLAG02 Checkpoint 2
FLAG04 Checkpoint 4
FLAG06 Checkpoint 6
FLAG07 Checkpoint 7

Known Level Files

Race Levels (15 levels — one per tournament tier)

Note: Internal file names do NOT match display names. The XML race
tags also differ from display names (e.g. BEGINNERRACE = Warm-up Race,
CASCADERACE = Beginner Race). Always cross-reference with RaceData.xml.

# Display Name XML Tag Level File Tier Color
1 Warm-up Race BEGINNERRACE Level1.MESHWORLD Pink
2 Beginner Race CASCADERACE LevelCascade.MESHWORLD Blue
3 Intermediate Race INTERMEDIATERACE Level2.MESHWORLD Green
4 Dizzy Race DIZZYRACE Level3.MESHWORLD
5 Tower Race TOWERRACE Level4.MESHWORLD
6 Up Race UPRACE LevelUp.MESHWORLD Red
7 Neon Race NEONRACE LevelDark.MESHWORLD Orange
8 Expert Race EXPERTRACE Level5.MESHWORLD
9 odd race ODDRACE Level6.MESHWORLD
10 Toob Race TOOBRACE Level8.MESHWORLD
11 Wobbly Race WOBBLYRACE Level7.MESHWORLD
12 Glass Race GLASSRACE LevelGlass.MESHWORLD
13 Sky Race SKYRACE Level9.MESHWORLD
14 Master Race MASTERRACE Level10.MESHWORLD
15 Impossible Race IMPOSSIBLERACE LevelImpossible.MESHWORLD

Arena Levels (15 arenas — one per race level)

# Display Name Arena File
1 Warm-up Arena Arena-WarmUp.MESHWORLD
2 Beginner Arena Arena-Beginner.MESHWORLD
3 Intermediate Arena Arena-Intermediate.MESHWORLD
4 Dizzy Arena Arena-Dizzy.MESHWORLD
5 Tower Arena Arena-Tower.MESHWORLD
6 Up Arena Arena-Up.MESHWORLD
7 Neon Arena Arena-Neon.MESHWORLD
8 Expert Arena Arena-Expert.MESHWORLD
9 Odd Arena Arena-Odd.MESHWORLD
10 Toob Arena Arena-Toob.MESHWORLD
11 Wobbly Arena Arena-Wobbly.MESHWORLD
12 Glass Arena Arena-Glass.MESHWORLD
13 Sky Arena Arena-Sky.MESHWORLD
14 Master Arena Arena-Master.MESHWORLD
15 Impossible Arena Arena-Impossible.MESHWORLD

Non-playable arena files: Arena-SpawnPlatform.MESHWORLD (spawn platform),
Arena-Stands.MESHWORLD (audience stands) — not counted in the 15.

Sub-Levels (Object Prefabs)

File Purpose
Level3-Gluebie.MESHWORLD Glue trap object
Level3-Swirl.MESHWORLD Swirl vortex
Level3-Tipper.MESHWORLD Tipping platform
Level3-WaterWheel.MESHWORLD Water wheel
Level4-Catapult.MESHWORLD Catapult launcher
Level4-Drawbridge.MESHWORLD Drawbridge
Level4-Mace.MESHWORLD Swinging mace
Level4-Trapdoor1/2.MESHWORLD Trapdoor variants
Level4-Turret.MESHWORLD Turret
Level4-Windmill.MESHWORLD Windmill
Level5-Bridge.MESHWORLD Collapsible bridge
Level2-Bridge.MESHWORLD Bridge section
Level10-Bridge1/2.MESHWORLD Bridge variants (Neon race)
Level7-Wobbly5.MESHWORLD Wobbly platform
Level9-PopCylinder1/2.MESHWORLD Pop cylinder
LevelUp-Lifter.MESHWORLD Lifting platform
LevelUp-SpeedCylinder.MESHWORLD Speed boost
LevelUp-Button.MESHWORLD Button trigger
LevelImpossible-Rotator.MESHWORLD Rotating obstacle
Secret.MESHWORLD Secret area
Secret-Unlock.MESHWORLD Unlock spot
PopupSign.MESHWORLD Popup sign object
MouseTrap.MESHWORLD Mousetrap object
Level6-Lifter.MESHWORLD Level 6 lifter

Reimplementation Notes

Level Loading

The MESHWORLD format is complex and tightly coupled to the Athena engine. For reimplementation:

  1. Write a meshworld2gltf converter that extracts geometry + objects
  2. Parse the binary header, iterate objects, extract type strings + positions
  3. Store object definitions as a JSON sidecar file alongside the GLTF
  4. Load the geometry in-engine, spawn objects from the JSON definitions
  5. Each object type maps to a game entity class in the new engine

🔗 Related Documents

MESHWORLD Parser Decompilation

types : meshworld
keywords :

📂 View source on GitHub


MESHWORLD Parser Decompilation

Function at 0x00470930, called from LoadMeshWorld (0x0045de30)

LoadMeshWorld (0x0045de30)

undefined4 __thiscall LoadMeshWorld(void *this, char *param_1)
{
  // Formats filename with %s.meshworld
  // If file exists: creates MeshWorld obj (0x488 bytes), calls constructor at 0x004706e0
  // Then calls Parse at 0x00470930
  // If load fails: displays "COULD NOT LOAD" message box and exits
}

MeshWorld Constructor (0x004706e0)

  • vtable at PTR_FUN_004d9cdc
  • Object is 0x488 bytes
  • Initializes material array, fog colors, default params

MeshWorld::Parse (0x00470930) - Full Decompilation

/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */

uint __thiscall FUN_00470930(void *this,char *param_1,char param_2)

{
  char cVar1;
  float fVar2;
  float fVar3;
  float fVar4;
  bool bVar5;
  bool bVar6;
  bool bVar7;
  undefined4 *puVar8;
  uint uVar9;
  byte *pbVar10;
  uint *_Str;
  long lVar11;
  long *plVar12;
  char *pcVar13;
  uint *puVar14;
  float *pfVar15;
  long lVar16;
  undefined4 *puVar17;
  undefined4 *puVar18;
  undefined4 uVar19;
  int iVar20;
  int iVar21;
  int unaff_EBP;
  short sVar22;
  short sVar23;
  long *plVar24;
  bool bVar25;
  double dVar26;
  double dVar27;
  int local_5b8;
  undefined4 *local_5b0;
  undefined4 *local_5a8;
  void *local_5a4;
  void *local_5a0;
  int local_59c;
  undefined4 local_594;
  undefined4 uStack_590;
  undefined4 uStack_58c;
  undefined4 uStack_588;
  undefined4 uStack_584;
  undefined4 uStack_580;
  undefined4 uStack_57c;
  undefined4 local_578;
  undefined4 uStack_574;
  undefined4 uStack_570;
  float fStack_554;
  float fStack_550;
  float fStack_54c;
  float local_548;
  float local_544;
  float local_540;
  float local_53c;
  float local_538;
  float local_534;
  float local_530;
  float local_52c;
  undefined1 local_528 [4];
  char local_524 [4];
  undefined1 local_520 [252];
  undefined4 auStack_424 [262];
  void *local_c;
  undefined1 *puStack_8;
  uint local_4;
  
  local_4 = 0xffffffff;
  puStack_8 = &LAB_004cd8b0;
  local_c = ExceptionList;
  ExceptionList = &local_c;
  FUN_0047d670(&local_594);
  local_4 = 0;
  puVar8 = operator_new(0x874);
  local_4._0_1_ = 1;
  if (puVar8 == (undefined4 *)0x0) {
    local_5a8 = (undefined4 *)0x0;
  }
  else {
    local_5a8 = FUN_00458970(puVar8);
  }
  local_4 = (uint)local_4._1_3_ << 8;
  iVar20 = -(int)param_1;
  do {
    cVar1 = *param_1;
    param_1[(int)(local_524 + iVar20)] = cVar1;
    param_1 = param_1 + 1;
  } while (cVar1 != '\0');
  puVar8 = (undefined4 *)(local_528 + 3);
  do {
    puVar17 = puVar8;
    puVar8 = (undefined4 *)((int)puVar17 + 1);
  } while (*(char *)((int)puVar17 + 1) != '\0');
  *(undefined4 *)((int)puVar17 + 1) = DAT_004d9e58;
  *(undefined1 *)((int)puVar17 + 5) = DAT_004d9e5c;
  bVar7 = FUN_0047d7c0(&local_594,local_524);
  if (bVar7) {
    bVar7 = false;
    bVar5 = false;
    bVar6 = false;
    pbVar10 = FUN_0047d6b0(&local_594,'\x01');
    while (pbVar10 != (byte *)0x0) {
      _Str = (uint *)FUN_004bc0d1(pbVar10,&DAT_004d9e54);
      iVar20 = 0x10;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_COUNT";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        *(long *)((int)this + 0x24) = lVar11;
        lVar11 = _atol((char *)_Str);
        plVar12 = operator_new(lVar11 * 0x50 + 4);
        local_4._0_1_ = 2;
        if (plVar12 == (long *)0x0) {
          plVar24 = (long *)0x0;
        }
        else {
          plVar24 = plVar12 + 1;
          *plVar12 = lVar11;
          _eh_vector_constructor_iterator_(plVar24,0x50,lVar11,FUN_00457fa0,FUN_00457fd0);
        }
        local_4 = (uint)local_4._1_3_ << 8;
        *(long **)((int)this + 0x28) = plVar24;
      }
      iVar20 = 10;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        local_5b8 = _atol((char *)_Str);
        bVar7 = true;
        bVar5 = true;
        bVar6 = true;
      }
      iVar20 = 0x12;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_AMBIENT";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if ((bVar25) && (bVar7)) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        iVar20 = (short)local_5b8 * 0x50;
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 0x14 + *(int *)((int)this + 0x28)) = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 0x18 + *(int *)((int)this + 0x28)) = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x1c + *(int *)((int)this + 0x28)) = (float)dVar26;
        *(undefined4 *)(iVar20 + 0x20 + *(int *)((int)this + 0x28)) = 0x3f800000;
        bVar7 = false;
      }
      iVar20 = 0x12;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_DIFFUSE";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if ((bVar25) && (bVar5)) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        iVar20 = (short)local_5b8 * 0x50;
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 4 + *(int *)((int)this + 0x28)) = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 8 + *(int *)((int)this + 0x28)) = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0xc + *(int *)((int)this + 0x28)) = (float)dVar26;
        *(undefined4 *)(iVar20 + 0x10 + *(int *)((int)this + 0x28)) = 0x3f800000;
        bVar5 = false;
      }
      iVar20 = 0xd;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MAP_REFLECT";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        *(undefined1 *)((short)local_5b8 * 0x50 + 0x4d + *(int *)((int)this + 0x28)) = 1;
      }
      iVar20 = 0x13;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_SPECULAR";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if ((bVar25) && (bVar6)) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        iVar20 = (short)local_5b8 * 0x50;
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 0x24 + *(int *)((int)this + 0x28)) = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        *(float *)(iVar20 + 0x28 + *(int *)((int)this + 0x28)) = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x2c + *(int *)((int)this + 0x28)) = (float)dVar26;
        *(undefined4 *)(iVar20 + 0x30 + *(int *)((int)this + 0x28)) = 0x3f800000;
        bVar6 = false;
      }
      iVar20 = 0xe;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_REF";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        local_5a8[1] = lVar11;
      }
      iVar20 = 0x18;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_SHINESTRENGTH";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        iVar20 = (short)local_5b8 * 0x50;
        pfVar15 = (float *)(iVar20 + 0x24 + *(int *)((int)this + 0x28));
        dVar26 = _atof((char *)_Str);
        *pfVar15 = (float)dVar26 * *pfVar15;
        pfVar15 = (float *)(iVar20 + 0x28 + *(int *)((int)this + 0x28));
        dVar26 = _atof((char *)_Str);
        *pfVar15 = (float)dVar26 * *pfVar15;
        pfVar15 = (float *)(iVar20 + 0x2c + *(int *)((int)this + 0x28));
        dVar26 = _atof((char *)_Str);
        *pfVar15 = (float)dVar26 * *pfVar15;
        pfVar15 = (float *)(iVar20 + 0x30 + *(int *)((int)this + 0x28));
        dVar26 = _atof((char *)_Str);
        *pfVar15 = (float)dVar26 * *pfVar15;
      }
      iVar20 = 0x10;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_SHINE";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        *(float *)((short)local_5b8 * 0x50 + 0x44 + *(int *)((int)this + 0x28)) =
             (float)dVar26 * _DAT_004cf454;
      }
      iVar20 = 0x17;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MATERIAL_TRANSPARENCY";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        iVar20 = (short)local_5b8 * 0x50;
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x10 + *(int *)((int)this + 0x28)) = _DAT_004cf310 - (float)dVar26;
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x20 + *(int *)((int)this + 0x28)) = _DAT_004cf310 - (float)dVar26;
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x40 + *(int *)((int)this + 0x28)) = _DAT_004cf310 - (float)dVar26;
        dVar26 = _atof((char *)_Str);
        *(float *)(iVar20 + 0x30 + *(int *)((int)this + 0x28)) = _DAT_004cf310 - (float)dVar26;
        dVar26 = _atof((char *)_Str);
        if ((double)_DAT_004cf310 - dVar26 != _DAT_004cf3c8) {
          *(undefined1 *)(iVar20 + 0x4c + *(int *)((int)this + 0x28)) = 1;
        }
      }
      iVar20 = 8;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*BITMAP";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        uVar9 = FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        _Str = (uint *)(uVar9 + 1);
        puVar14 = FUN_004bacc0(_Str,'\"');
        if (puVar14 != (uint *)0x0) {
          *(undefined1 *)puVar14 = 0;
        }
        puVar8 = FUN_00455c50(*(void **)((int)this + 4),(char *)_Str,'\x01');
        *(undefined4 **)((short)local_5b8 * 0x50 + 0x48 + *(int *)((int)this + 0x28)) = puVar8;
      }
      iVar20 = 0xc;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*GEOMOBJECT";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        puVar8 = operator_new(0x874);
        local_4._0_1_ = 3;
        if (puVar8 == (undefined4 *)0x0) {
          local_5a8 = (undefined4 *)0x0;
        }
        else {
          local_5a8 = FUN_00458970(puVar8);
        }
        local_4 = (uint)local_4._1_3_ << 8;
        FUN_00453280((int)(local_5a8 + 3));
        *(undefined1 *)(local_5a8 + 0x217) = 0;
        FUN_00453780((void *)((int)this + 0x2c),(int)local_5a8);
      }
      iVar20 = 0xb;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*NODE_NAME";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        uVar9 = FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        _Str = (uint *)(uVar9 + 1);
        puVar14 = FUN_004bacc0(_Str,'\"');
        *(undefined1 *)puVar14 = 0;
      }
      iVar20 = 8;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*TM_POS";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        local_5a8[0x21a] = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        local_5a8[0x21c] = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        local_5a8[0x21b] = (float)dVar26;
      }
      iVar20 = 0x10;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_NUMVERTEX";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        local_5a0 = operator_new(lVar11 << 5);
        if (0 < lVar11) {
          puVar8 = (undefined4 *)((int)local_5a0 + 0x10);
          do {
            puVar8[-1] = 0;
            *puVar8 = 0;
            puVar8[1] = 0;
            puVar8[-4] = 0;
            puVar8[-3] = 0;
            puVar8[-2] = 0;
            puVar8[2] = 0;
            puVar8[3] = 0;
            puVar8 = puVar8 + 8;
            lVar11 = lVar11 + -1;
          } while (lVar11 != 0);
        }
      }
      iVar20 = 0x11;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_NUMTVERTEX";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        local_5a4 = operator_new(lVar11 * 8);
        iVar20 = 0;
        if (0 < lVar11) {
          do {
            *(undefined4 *)((int)local_5a4 + iVar20 * 8) = 0;
            *(undefined4 *)((int)local_5a4 + iVar20 * 8 + 4) = 0;
            iVar20 = iVar20 + 1;
          } while (iVar20 < lVar11);
        }
      }
      iVar20 = 0xd;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_VERTEX";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        fVar2 = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        fVar3 = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        fVar4 = (float)dVar26;
        pfVar15 = (float *)((int)local_5a0 + lVar11 * 0x20);
        pfVar15[1] = fVar4;
        *pfVar15 = fVar2;
        pfVar15[2] = fVar3;
        if (fVar2 < *(float *)((int)this + 0x45c)) {
          *(float *)((int)this + 0x45c) = fVar2;
        }
        if (fVar4 < *(float *)((int)this + 0x460)) {
          *(float *)((int)this + 0x460) = fVar4;
        }
        if (fVar3 < *(float *)((int)this + 0x464)) {
          *(float *)((int)this + 0x464) = fVar3;
        }
        if (*(float *)((int)this + 0x468) < fVar2) {
          *(float *)((int)this + 0x468) = fVar2;
        }
        if (*(float *)((int)this + 0x46c) < fVar4) {
          *(float *)((int)this + 0x46c) = fVar4;
        }
        if (*(float *)((int)this + 0x470) < fVar3) {
          *(float *)((int)this + 0x470) = fVar3;
        }
      }
      iVar20 = 0xf;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_NUMFACES";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        local_5b0 = operator_new(lVar11 * 0x60);
        puVar8 = local_5b0;
        for (uVar9 = (uint)(lVar11 * 0x60) >> 2; uVar9 != 0; uVar9 = uVar9 - 1) {
          *puVar8 = 0;
          puVar8 = puVar8 + 1;
        }
        for (iVar20 = 0; iVar20 != 0; iVar20 = iVar20 + -1) {
          *(undefined1 *)puVar8 = 0;
          puVar8 = (undefined4 *)((int)puVar8 + 1);
        }
      }
      iVar20 = 0xc;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_TVERT";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar27 = _atof((char *)_Str);
        *(float *)((int)local_5a4 + lVar11 * 8) = (float)dVar26;
        *(float *)((int)local_5a4 + lVar11 * 8 + 4) = (float)dVar27;
      }
      iVar20 = 0xb;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_FACE";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar16 = _atol(pcVar13);
        puVar17 = (undefined4 *)(lVar16 * 0x20 + (int)local_5a0);
        puVar8 = local_5b0 + lVar11 * 0x18;
        *puVar8 = *puVar17;
        puVar8[1] = puVar17[1];
        puVar8[2] = puVar17[2];
        puVar8[6] = puVar17[6];
        puVar8[7] = puVar17[7];
        FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        iVar20 = lVar11 * 0x20;
        puVar8[0x10] = *(undefined4 *)(iVar20 + (int)local_5a0);
        puVar8[0x11] = *(undefined4 *)((int)local_5a0 + iVar20 + 4);
        puVar8[0x12] = *(undefined4 *)((int)local_5a0 + iVar20 + 8);
        puVar8[0x16] = *(undefined4 *)((int)local_5a0 + iVar20 + 0x18);
        puVar8[0x17] = *(undefined4 *)((int)local_5a0 + iVar20 + 0x1c);
        FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol((char *)_Str);
        iVar20 = lVar11 * 0x20;
        puVar8[8] = *(undefined4 *)(iVar20 + (int)local_5a0);
        puVar8[9] = *(undefined4 *)((int)local_5a0 + iVar20 + 4);
        puVar8[10] = *(undefined4 *)((int)local_5a0 + iVar20 + 8);
        puVar8[0xe] = *(undefined4 *)((int)local_5a0 + iVar20 + 0x18);
        puVar8[0xf] = *(undefined4 *)((int)local_5a0 + iVar20 + 0x1c);
        FUN_00453780(local_5a8 + 3,(int)puVar8);
      }
      iVar20 = 0xc;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_TFACE";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar16 = _atol(pcVar13);
        local_5b0[lVar11 * 0x18 + 6] = *(undefined4 *)((int)local_5a4 + lVar16 * 8);
        local_5b0[lVar11 * 0x18 + 7] =
             (float)_DAT_004cf3c8 - *(float *)((int)local_5a4 + lVar16 * 8 + 4);
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar16 = _atol(pcVar13);
        local_5b0[lVar11 * 0x18 + 0x16] = *(undefined4 *)((int)local_5a4 + lVar16 * 8);
        local_5b0[lVar11 * 0x18 + 0x17] =
             (float)_DAT_004cf3c8 - *(float *)((int)local_5a4 + lVar16 * 8 + 4);
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar16 = _atol((char *)_Str);
        local_5b0[lVar11 * 0x18 + 0xe] = *(undefined4 *)((int)local_5a4 + lVar16 * 8);
        local_5b0[lVar11 * 0x18 + 0xf] =
             (float)_DAT_004cf3c8 - *(float *)((int)local_5a4 + lVar16 * 8 + 4);
      }
      iVar20 = 0x11;
      bVar25 = true;
      puVar14 = _Str;
      pcVar13 = "*MESH_FACENORMAL";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*puVar14 == *pcVar13;
        puVar14 = (uint *)((int)puVar14 + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        lVar11 = _atol(pcVar13);
        local_59c = 0;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        local_548 = (float)dVar26;
        dVar26 = _atof(pcVar13);
        local_53c = (float)dVar26;
        dVar26 = _atof(pcVar13);
        local_530 = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        local_544 = (float)dVar26;
        dVar26 = _atof(pcVar13);
        local_538 = (float)dVar26;
        dVar26 = _atof(pcVar13);
        local_52c = (float)dVar26;
        _Str = (uint *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof((char *)_Str);
        local_540 = (float)dVar26;
        dVar26 = _atof((char *)_Str);
        local_534 = (float)dVar26;
        dVar26 = _atof((char *)_Str);
        local_528 = (undefined1  [4])(float)dVar26;
        thunk_FUN_00459b24();
        thunk_FUN_00459b24();
        thunk_FUN_00459b24();
        iVar20 = lVar11 * 0x60 + unaff_EBP;
        *(undefined4 *)(iVar20 + 0xc) = uStack_590;
        *(undefined4 *)(iVar20 + 0x2c) = uStack_584;
        *(undefined4 *)(iVar20 + 0x4c) = local_578;
        *(undefined4 *)(iVar20 + 0x10) = uStack_588;
        *(undefined4 *)(iVar20 + 0x30) = uStack_57c;
        *(undefined4 *)(iVar20 + 0x50) = uStack_570;
        *(undefined4 *)(iVar20 + 0x14) = uStack_58c;
        *(undefined4 *)(iVar20 + 0x34) = uStack_580;
        *(undefined4 *)(iVar20 + 0x54) = uStack_574;
      }
      iVar20 = 0x13;
      bVar25 = true;
      pcVar13 = "*MESH_VERTEXNORMAL";
      do {
        if (iVar20 == 0) break;
        iVar20 = iVar20 + -1;
        bVar25 = (char)*_Str == *pcVar13;
        _Str = (uint *)((int)_Str + 1);
        pcVar13 = pcVar13 + 1;
      } while (bVar25);
      if (bVar25) {
        FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        sVar22 = 0;
        sVar23 = (short)local_59c;
        if (sVar23 != 0) {
          if (sVar23 == 1) {
            sVar22 = 2;
          }
          else if (sVar23 == 2) {
            sVar22 = 1;
          }
        }
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        fStack_554 = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        fStack_550 = (float)dVar26;
        pcVar13 = (char *)FUN_004bc0d1((byte *)0x0,&DAT_004d9e54);
        dVar26 = _atof(pcVar13);
        fStack_54c = (float)dVar26;
        thunk_FUN_00459b24();
        iVar20 = ((short)local_5a0 * 3 + (int)sVar22) * 0x20 + local_5b8;
        *(undefined4 *)(iVar20 + 0xc) = uStack_58c;
        *(undefined4 *)(iVar20 + 0x10) = uStack_584;
        local_5a4 = (void *)(local_59c + 1);
        *(undefined4 *)(iVar20 + 0x14) = uStack_588;
      }
      pbVar10 = FUN_0047d6b0(&local_594,'\x01');
    }
    _free(local_5a0);
    _free(local_5a4);
    if (param_2 == '\x01') {
      FUN_00470680((int)this);
    }
    else {
      *(undefined4 *)((int)this + 0x34) = 0;
      if (*(int *)((int)this + 0x30) < 1) {
        iVar20 = 0;
      }
      else {
        iVar20 = **(int **)((int)this + 0x438);
        *(undefined4 *)((int)this + 0x34) = 1;
      }
      while (iVar20 != 0) {
        puVar8 = (undefined4 *)0x0;
        FUN_00453210(auStack_424,0);
        local_4 = CONCAT31(local_4._1_3_,4);
        FUN_00453280((int)auStack_424);
        *(undefined4 *)(iVar20 + 0x14) = 0;
        if (0 < *(int *)(iVar20 + 0x10)) {
          puVar8 = (undefined4 *)**(undefined4 **)(iVar20 + 0x418);
          *(undefined4 *)(iVar20 + 0x14) = 1;
        }
        while (puVar8 != (undefined4 *)0x0) {
          puVar18 = operator_new(0x60);
          puVar17 = puVar18;
          for (iVar21 = 0x18; iVar21 != 0; iVar21 = iVar21 + -1) {
            *puVar17 = *puVar8;
            puVar8 = puVar8 + 1;
            puVar17 = puVar17 + 1;
          }
          FUN_00453780(auStack_424,(int)puVar18);
          iVar21 = *(int *)(iVar20 + 0x14);
          if (*(int *)(iVar20 + 0x10) <= iVar21) break;
          puVar8 = *(undefined4 **)(*(int *)(iVar20 + 0x418) + iVar21 * 4);
          *(int *)(iVar20 + 0x14) = iVar21 + 1;
        }
        FUN_00453280(iVar20 + 0xc);
        FUN_00453820((void *)(iVar20 + 0xc),(int)auStack_424);
        local_4 = local_4 & 0xffffff00;
        FUN_00453250(auStack_424);
        iVar21 = *(int *)((int)this + 0x34);
        if (*(int *)((int)this + 0x30) <= iVar21) break;
        iVar20 = *(int *)(*(int *)((int)this + 0x438) + iVar21 * 4);
        *(int *)((int)this + 0x34) = iVar21 + 1;
      }
    }
    _free(local_5b0);
    local_4 = 0xffffffff;
    uVar19 = FUN_0047d680(&local_594);
    uVar9 = CONCAT31((int3)((uint)uVar19 >> 8),1);
  }
  else {
    local_4 = 0xffffffff;
    uVar9 = FUN_0047d680(&local_594);
    uVar9 = uVar9 & 0xffffff00;
  }
  ExceptionList = local_c;
  return uVar9;
}

🔗 Related Documents

Modding Function Reference

types : modding
keywords :

📂 View source on GitHub


Hamsterball Modding Function Reference

A comprehensive reference of every useful function for modders, extracted from Ghidra decompilation of Hamsterball.exe (PE32, i386, Athena engine).
Last updated: 2026-06-13
Total functions in binary: ~3,800


Table of Contents

  1. Game Loop & Frame Control
  2. App & System Initialization
  3. Ball Physics & Movement
  4. input system](#4-input-system)
  5. camera system](#5-camera-system)
  6. Scene & Object Management
  7. Object Spawning & Creation
  8. collision event system](#8-collision-event-system)
  9. Rendering & Graphics Pipeline
  10. Audio & Music System
  11. ui & menu system](#11-ui--menu-system)
  12. Save, Registry & Progression
  13. Arena Scoring & Timer
  14. Level-Specific Functions
  15. Utility & Math Functions
  16. Vtables & Calling Conventions
  17. Quick Reference: Most-Used Offsets

1. Game Loop & Frame Control

App_Run

  • Address: 0x0046BD80
  • Convention: __fastcall (ECX = param_1)
  • Parameters: param_1 (int*): Pointer to App instance (g_App at 0x4FD680)
  • Description: Main game loop. Runs until App+0x159 (quit flag) is set. Every frame: Win32 message pump → App.Update (vtable 0x20) → App.Render (vtable 0x28) → Graphics_PresentOrEnd.
  • Modding use: Hook vtable slot 0x20 (Update) or 0x28 (Render) to inject custom per-frame logic.
  • Key code pattern:
    while (*(char*)(app + 0x159) == 0) {
        PeekMessageA(...);
        app->vtable[0x20]();  // Update
        app->vtable[0x24]();  // PreRender
        app->vtable[0x28]();  // Render
        app->vtable[0x2C]();  // PostRender / HUD
    }
    

Scene_Update

  • Address: 0x00419C00
  • Convention: __thiscall (ECX = this)
  • Parameters: this (void*): Scene* instance
  • Description: Central game tick. Execution order:
    1. Increment frame_counter (this+0x0D88 / this+0x3620)
    2. Demo timer check (if demo_timer_active)
    3. ESC key → spawn GameOverMenu if not suppressed
    4. Ball position propagation (if ball_positions_dirty)
    5. Gear path following (single-gear camera mode)
    6. Rumble board timer ticks
    7. Camera shake decay (shake_magnitude += 10/frame toward 0)
    8. SceneObject update+render loop
    9. Physics pipeline (4 vtable calls, see below)
    10. Level object update (Scene_LevelObjUpdate)
  • Key offsets:
    • this+0x022E (int index 0x8B) = scene_objects (AthenaList)
    • this+0x0A6C (int index 0x29B) = ball_positions_dirty
    • this+0x0A75 (int index 0x29D) = ball_list (AthenaList<Ball*>)
    • this+0x0D88 (int index 0x362) = frame_counter
  • Modding use: Hook object vtable slot 4 (Update) for per-object behavior.

2. App & System Initialization

App_Initialize_Full

  • Address: 0x00429530
  • Convention: __thiscall (ECX = this)
  • Parameters: this (App*), param_1, param_2 (both unused — WinMain params)
  • Description: Full 26-step game init sequence. Each step writes "Initialize(N)" to App+0x208:
    1. App_Initialize (base init)
    2. Set Graphics+0x7D1 = 1
    3. Load "BLANKCURSOR"App+0x240
    4. vtable[0x8C](800, 600) — set display mode
    5. Configure D3D render states (16 vs 32-bit depth)
    6. Load shadow.pngApp+0x278
    7. Load music\music.mo3App+0x534
      8–11. Parse jukebox.xml, create music channels
    8. Registry_ReadPlayCount
      13–14. Create 4 InputDevice instances (players 1–4)
      15–22. Configure input devices with mode flags
    9. RegKey_Close
    10. vtable[0xA0]() — show title screen / main menu
    11. Done
  • Modding use: Hook after step 24 to inject custom startup code.

WinMain

  • Address: 0x004278E0
  • Description: Entry point. Calls App_Initialize_FullApp_RunApp_Shutdown.

App_ShowMainMenu

  • Address: 0x004280E0
  • Description: Allocates 0xCDC bytes, calls MainMenu_ctor, stores at App+0x224, adds to scene via Scene_AddObject.

App_SaveAllConfig

  • Address: 0x004284C0
  • Description: Writes all game settings to Windows registry:
    • Display settings (App_WriteDisplaySettings)
    • Mouse sensitivity (App+0x84C)
    • Mirror tournament (App+0x850)
    • All race/arena unlock flags (0x8510x868)
    • Best times binary blob (0x86C, 80 bytes)
    • Medal status (0x8BC, 80 bytes)
    • 2P controller mappings (0xB280xB34)

App_LoadOrSaveConfig

  • Address: 0x004279F0
  • Description: Called on game exit. Decides whether to save or load config based on internal state.

3. Ball Physics & Movement

Ball_Update

  • Address: 0x00405E00
  • Convention: __fastcall (ECX = param_1)
  • Parameters: param_1 (int*): Ball* instance
  • Description: Per-frame ball update. Returns early if ball+0x324 (dead/eliminated). Clears per-frame flags, runs AI if ball+0x31D (is_8ball) or scene+0x237 (battle mode), then calls physics (Ball_AdvancePositionOrCollision). After physics: proximity checks, trail recording, respawn state machine.
  • Key offsets:
    • ball+0x324 = dead/eliminated flag
    • ball+0x31D = is_8ball / AI enable flag
    • ball+0xC60..0xC94 = AI params (home_pos, chase_distance, npc_flag)

Ball_ctor / Ball_ctor2

  • Addresses: Ball_ctor0x40AFE0, Ball_ctor2 = 0x4039E0
  • Description:
    • Ball_ctor: allocate 0xC98 bytes (8ball) or 0xC60 (player), call GameObject_ctor, set vtable to 0x4CF3A0
    • Ball_ctor2: set physics defaults — gravity 0.5, radius 26.0, max_speed 5.0
    • Player ball: operator_new(0xC60)Ball_ctor2(this, scene)vtable[4]()Ball_SetTrajectory

Ball_GetInputForce

  • Address: 0x46EC30
  • Parameters: this (InputDevice*), output (float*) → [force_x, force_y]
  • Description: Converts input device state to 2D force vector. Reads DIK codes at InputDevice+0x50C/0x510/0x514/0x518.
  • Key offsets: this+0x08 = input mode

Ball_AdvancePositionOrCollision

  • Address: 0x4564C0
  • Parameters: this (Ball*), out_pos, cur_pos, input_vel, collision_flags, dt
  • Description: Core physics step. Velocity integration → damp → gravity → TestSphereVsLevel collision → bounce → trail.
  • Key physics globals:
    • 0x4CF3F0 = damping constant (0.95)
    • 0x4CF3E8 = ice friction factor (6.0)
    • 0x4CF374 = force multiplier on ice (0.2)
    • 0x4CF380 = force multiplier after first frame (0.25)

Ball_SetTargetPos

  • Address: 0x00402030
  • Parameters: this (Ball*), x, y, z (floats)
  • Description: Sets ball+0x758/75C/760 — camera orbit center point (smoothed display position).

Ball_Shrink

  • Address: 0x402200
  • Description: odd race E:SHRINK collision handler. Sets ball+0xC4C = 1 (is_shrunk), ball+0x284 = 13.0 (shrunk radius), plays fall sound.

Ball_FallUpdate

  • Address: 0x408830
  • Description: Physics update while falling. Decrements fall_timer (ball+0x80C). When timer expires, calls Ball_FindClosestRespawnPoint.

Ball_FindClosestRespawnPoint

  • Address: 0x405190
  • Description: Scans scene+0x546 (SAFESPOT/SAFEPOS list) for nearest valid respawn position. Writes new position to ball+0x164..16C.

Ball_Shatter

  • Address: 0x408D70
  • Convention: __thiscall (ECX = Ball* — the parent ball being replaced)
  • Called from: FollowBall_Update (0x43ECC0) — NOT from E:JUMP or any collision handler
  • Description: Arena mechanic that replaces the parent ball with 3 AI-controlled split balls. The parent ball is marked for despawn (+0x2E8 = 1), then 3 new Ball_Split objects are created via Ball_Split_ctor (0x408D10). Each split ball gets:
    • Allocation: 0xC64 bytes
    • ball+0x31D = 1 (is_8ball flag, activates AI)
    • ball+0xC6 = 10.0 (0x41200000 — split ball size)
    • Split IDs: 1, 2, 4 (bitmask, one per iteration)
    • Trajectory from parent's +0x2AC–0x2B8
    • Added to scene+0x3204 (ball list)
  • Guard conditions: +0x324 == 0 (not already an 8-ball) AND +0x744 == 0 (hasn't split yet)

Ball_InitBattleMode

  • Address: 0x456CD0
  • Description: Converts ball to battle mode. Sets:
    • ball+0xC60 = 3 (battle flag)
    • ball+0xC68 = 0.55 (high friction)
    • ball+0xC6C = 1.0 (bouncy)
    • ball+0xC70 = 1000.0 (max speed — 200x normal!)

4. Input System

Input_IsKeyDown

  • Address: 0x46E0B0
  • Parameters: key (int) — DIK scancode
  • Returns: 1 if held, 0 if not

App_CreateInputDevice

  • Address: 0x0046C050
  • Returns: InputDevice* (0x14 bytes)
  • Key offsets: +0x00 vtable, +0x04 App*, +0x08 input mode, +0x10 joystick ptr

App_CreateInputHandler

  • Address: 0x0046C110
  • Returns: InputHandler* — aggregates 4 InputDevice instances

InputDevice_SetType

  • Address: 0x46DFC0
  • Parameters: this (InputDevice*), mode (int) — 1=keyboard, 2=mouse, 4-7=joystick

5. Camera System

Scene_SetCamera

  • Address: 0x00419FA0
  • Convention: __thiscall (ECX = this)
  • Parameters: this (Scene*), param_1 (Ball*), param_2 (char) — if true, apply path-following spring
  • Description: Camera positioning every frame. 5 modes:
    1. Default Follow: Orbit around ball+0x758 + scene+0x434C offset
    2. Path Rail: If scene+0x3F1C != 0 AND param_2 != 0 → call Path_GetPosition(scene+0x3F20, &out, scene+0x3F24)
    3. Camera Shake: If ball+0x744: random ±50 per axis
    4. Snap: If scene+0x3F2C > 0: force camera to ball physics pos, decrement
    5. Orbit: Apply scene+0x29BC (angle) and scene+0x29C0 (distance)
  • Key offsets:
    • scene+0x3F1C = path_follow_mode
    • scene+0x3F20 = path_object
    • scene+0x3F24 = path_position
    • scene+0x3F2C = camera_snap_frames
    • scene+0x29BC = camera_orbit_angle
    • scene+0x29C0 = camera_distance (max 700)
    • scene+0x434C = camera_offset (Vec3)

6. Scene & Object Management

Scene_AddObject

  • Address: 0x00469990
  • Parameters: this (Scene*), obj (Gadget*)
  • Description: Appends to scene+0x2578 (active object list). Objects receive Update (vtable[4]) and Render (vtable[0]) per frame.

Scene_SpawnBallsAndObjects

  • Address: 0x0041C5B0
  • Description: Level startup factory. Creates all game objects:
    1. Ball creation loop — lookup "START%d-%d" in hash table, Ball_ctor2, set properties
    2. Safe spot scan — "SAFESPOT"/"SAFEPOS"scene+0x546
    3. Tournament/demo: CreateBadBall, CreateMouseTrap
    4. Decoration: CreateSecretObjects, Scene_CreateFlags, Scene_CreateSigns, Scene_CreateDynamicObjects
  • Ball defaults: radius=26.0, max_speed=5.0, gravity_scale=0.5

Board_ctor

  • Address: 0x00419030
  • Description: Creates Board (~0x5400 bytes, inherits Gadget). Sets up scene lists, ball list, D3D device, level slots.

Scene_CleanupScene

  • Address: 0x419740
  • Description: Destructor helper. Cleans all AthenaLists in reverse order: collision_list → ripple_list → ball_list → scene_object_list → etc.

7. Object Spawning & Creation

CreateLevelObjects (Object Factory)

  • Address: 0x004121D0
  • Convention: __thiscall (ECX = this)
  • Parameters: this (Scene*), name (char*), param_2 (matrix), param_3 (matrix), param_4 (index)
  • Description: Main object factory. Matches prefixes via __strnicmp:
    • BRIDGEscene+0x436C
    • TIPPERTipper_ctor(0x1104) + visual
    • BONKBonk_ctor(0x1200)scene+0x540C
    • BBRIDGE1/BBRIDGE2BreakBridge_ctor(0x1100)
    • POPCYLINDERPopCylinder_ctor(0x10E8)
    • BLOCKDAWG1/BLOCKDAWG2Blockdawg_ctor(0x1154)
    • CATAPULTCatapult_ctor(0x1108)scene+0x584C
    • GLUEBIEGluebie_ctor(0x110C)scene+0x6080
    • SAFESPOT/SAFEPOS → spawn positions
    • BADBALL → 8-ball AI parameters

CreateBadBall

  • Address: 0x0040BCA0
  • Description: Scans meshworld for "BADBALL". For each:
    • Ball_ctor(new(0xC98), scene) — note: larger than player ball!
    • Parse tags: <CHASE>, <HOME>, <SIZE>, <SPINDISTANCE>
    • Append to scene+0x29D4 (8ball list) and scene+0x2DEC (active ball list)

CreateBumper

  • Address: 0x0040FA20
  • Description: Creates 8 bumpers for Level 8. Loads levels\level8, calls Scene_CollectByNameFilter("N:BUMPER%d").

CreateBumper2

  • Address: 0x00413CE0
  • Description: Arena variant — 4 bumpers for beginner arena.

CreateMouseTrap

  • Address: 0x0040BF50
  • Description: Scans meshworld for "MOUSETRAP". Creates TipperVisual objects appended to collision and render lists.

CreateBonkPopup

  • Address: 0x00438B30
  • Description: Hammer hit visual feedback. Plays 3D sound, calls vtable[0x88]("BONKPOPUP").

Hammer_ChaseStart

  • Address: 0x00438BB0
  • Description: Starts hammer AI chase. Sets chase flags, copies position/target, chase speed = 0.5.

8. Collision Event System

DispatchCollisionEvents / DispatchCollisionEvents

  • Address: 0x0040C5D0
  • Convention: __thiscall (ECX = this)
  • Parameters: this (Level*), ball (Ball*), collider (CollisionObject*)
  • Description: Universal collision event dispatcher. Event name at collider[1]+0x864.
Event Condition Action
N:SECRET Rotator_MarkTriggered
N:UNLOCKSECRET CheckArenaUnlock(scene)
E:NODIZZY<TIME>N</TIME> Ball_DizzyImmunity(ball, duration)
E:SAFESWITCH(data) Copy data to ball+0xC2C
E:LIMIT Clear ball+0x1DA, set ball+0x2E9=1
E:BREAK ball->vtable[0x20]() bounce callback
E:JUMP impactCounter < 1 Sound, force 0.025, freeze 10 frames, Ball_DizzyImmunity(+200)
E:ACTION<ONCE>TRUE</ONCE><SCORE>N</SCORE> Check duplicate at ball+0xCB, award score
E:TRAJECTORY<X>..</X> Set collision direction ball+0xCA4/CA8/CAC
N:NOCONTROL ball+0x202 = 10 (disable input 10 frames)
N:WATER ball+0x2D5 = 1, ball+0xB6 = 10
N:TARPIT first time Play tar sound, ball+0xB3 = 1
DROPIN dist > threshold Sound, dropinCounter = 50, Ball_DizzyImmunity(+200)
PIPEBONK counter < 1 Random sound, counter = 10, Ball_DizzyImmunity(+100)
POPOUT counter < 1 Sound, counter = 50, Ball_DizzyImmunity(+100)
N:GOAL !finished && active Set goalReached=1, play music, mark finished
N:MOUSETRAP Randomize RNG, deflect direction × trap speed

TowerCollisionEvents

  • Address: 0x40DCD0
  • Description: Level-specific events, then delegates to DispatchCollisionEvents:
    • E:CATAPULTBOTTOM → launch catapult
    • E:OPENSESAME → open all trapdoors
    • N:TRAPDOOR → activate trapdoor
    • E:BITEscene+0x43A0 = 25.0
    • E:MACETRIGGER → activate maces

ExpertCollisionEvents

  • Address: 0x40E6A0
  • Description: Arena-specific events (see docs/ARENA_SCORING.md for full table):
    • E:CALLHAMMERCreateBonkPopup
    • E:HAMMERCHASEHammer_ChaseStart
    • E:ALERTSAW1/2Saw_AlertActivate
    • E:ACTIVATESAW1/2Saw_Activate
    • E:ALERTJUDGES → reset all judges
    • E:SCORE<number> → parse time, ScoreDisplay_SetTime
    • E:BELLBell_Activate, +500 time, "EXTRA TIME:" popup
    • E:JUMP → jump pad

9. Rendering & Graphics Pipeline

Scene_Render

  • Address: 0x0041A2E0
  • Convention: __thiscall
  • Description: Main render dispatch. Per viewport:
    1. Graphics_SetViewport
    2. Scene_SetCamera
    3. vtable[0x60] — Background (sky/dome)
    4. vtable[0x64] — Opaque geometry
    5. vtable[0x68] — Transparent objects
    6. vtable[0x70] — Overlay (HUD)
    7. vtable[0x6C] — PostEffects (fade/transition)

Scene_RenderAllObjects

  • Address: 0x0045E0E0
  • Description: Iterates scene+0x22E (AthenaList), calls obj->vtable[0]() (Render) for each.

Ball_Render

  • Address: 0x402DE0
  • Description: D3D8 ball rendering. Sets up render state, applies world transform from ball+0xC88 (4×4 matrix), draws sphere mesh with texture.

Ball_RenderShadow

  • Address: 0x401920
  • Description: Draws shadow quad beneath ball. Uses shadowTexture (App+0x278), alpha blend at ball position with Y offset.

Ball_CreateTrailParticles

  • Address: 0x401DD0
  • Description: Creates trail of particles behind moving ball. Appends to scene+0x3B00 (trail_particles_ptr).

Level_UpdateAndRender

  • Address: 0x40B600 (vtable[0x64])
  • Description: Two-pass rendering: opaque then alpha. Updates waypoint arrow, renders visible objects.

Level_RenderObjects

  • Address: 0x40B570 (vtable[0x68])
  • Description: Transparent pass. Glass, water, effects.

Level_RenderDynamicObjects

  • Address: 0x40B420 (vtable[0x60])
  • Description: Sky/dome + water ripples + dynamic object callback.

Graphics_Initialize

  • Address: 0x455380
  • Description: Creates D3D8 device, enumerates display modes, sets up render targets.

Gfx_SetAlphaBlendState

  • Address: 0x425FE0
  • Description: Toggles D3DRS_ALPHABLENDENABLE.

Gfx_SetCullMode

  • Address: 0x427940
  • Description: Sets D3DRS_CULLMODE.

10. Audio & Music System

Audio_PlayMusic

  • Address: inferred from MusicPlayer_ctor usage
  • Description: Plays BASS music stream. Music handle at App+0x534.

Audio_PlayMusicAtSpeed

  • Address: inferred
  • Parameters: musicHandle, trackName (char*), speed (float)
  • Description: Plays music at modified tempo. Used by MusicPlayer_ctor for "Main Theme - No Intro" at 2.0× speed.

MusicPlayer_ctor

  • Address: 0x426030
  • Parameters: this, App*, bool skipIntro
  • Description: Creates music player gadget. If skipIntro==false: plays "Main Theme". If true: plays "Main Theme - No Intro" at 2.0×.

Sound_Play3D

  • Address: inferred from multiple call sites
  • Parameters: soundHandle, x, y, z (floats)
  • Description: Plays positional 3D audio effect at world coordinates.

Level_ReadSoundVolume

  • Address: 0x466570
  • Description: Reads "Sound Volume" from registry, defaults to 1.0.

SoundDevice_dtor

  • Address: 0x4668A0
  • Description: Writes current volume back to registry before cleanup.

11. UI & Menu System

MainMenu_ctor

  • Address: 0x42DE50
  • Description: Creates main menu gadget (~0xCDC bytes). Buttons: Play, Practice, Options, Quit.

PauseMenu_Ctor

  • Address: 0x42E4B0
  • Description: Creates pause menu overlay. Triggered by ESC during gameplay.

OptionsMenu_RenderControls

  • Address: 0x42E840 / 0x42E910
  • Description: Renders control remapping UI. Reads/writes App+0xB280xB34 (2P controller mappings).

PracticeMenu_ctor

  • Address: 0x42EA30
  • Description: Practice mode menu — level select without tournament constraints.

TimeTrialMenu_ctor

  • Address: 0x42F810
  • Description: Time trial menu — race against ghost data.

ArenaMenu_ctor

  • Address: 0x42FC40
  • Description: Arena mode menu — multiplayer level select.

PartyMenu_ctor

  • Address: 0x42FC10
  • Description: Party mode menu.

DifficultyMenu_ctor

  • Address: 0x42E220
  • Description: Difficulty selection menu (affects AI speed/timing).

TourneyMenu_GetRaceName

  • Address: 0x4264A0
  • Returns: char* — current race level name string

UI_DrawTextCentered

  • Address: 0x409C60
  • Description: Draws centered text at screen position.

UI_DrawTextCenteredAbsolute

  • Address: 0x4013A0
  • Description: Draws centered text with absolute pixel coordinates.

UI_DrawTextShadow

  • Address: 0x4012C0
  • Description: Draws text with drop shadow effect.

UI_DrawTextShadow_Wrapper

  • Address: 0x409B90
  • Description: Wrapper for shadow text with additional parameters.

Font_DrawCentered

  • Address: 0x42C870
  • Description: Renders centered string using game font.

12. Save, Registry & Progression

TourneyMenu_WriteSave

  • Address: 0x4264B0
  • Description: Writes DATA\TOURNAMENT.SAV (~151 bytes):
    • Profile+0x08 (4 bytes) — current_race
    • Profile+0x14 (4 bytes)
    • Profile+0x18 (60 bytes) — race_time_array[15]
    • Profile+0x54 (60 bytes) — race_time_array_2[15]
    • Profile+0x90 (4 bytes) — accumulated_time
    • Profile+0x94 (1 byte) — difficulty
    • Profile+0x95 (1 byte) — rollback flag
    • App+0x236 (1 byte) — mirror mode
    • App+0x23C (4 bytes) — race active
    • App+0x5E8 (4 bytes) — total time
    • App+0x5E4 (4 bytes) — ranking time
    • App+0x5F4 (4 bytes)

TourneyMenu_LoadSaveAndShow

  • Address: 0x4265A0
  • Description: Reads DATA\TOURNAMENT.SAV with same field order, then creates TourneyMenu.

LoadConfig

  • Address: 0x42AE80
  • Description: Loads display settings from registry on startup.

SaveConfig

  • Address: 0x42B6E0
  • Description: Saves display settings to registry.

CheckPurchaseOrHighScore

  • Address: 0x40A420
  • Description: Shareware nag screen. If not registered:
    • Creates ConfirmMenu with "BUY HAMSTERBALL..." text
    • Or HighScoreEntry if game in progress
    • Registered version: skips dialog, allows save

CheckArenaUnlock

  • Address: 0x40ABA0
  • Description: Checks if arena levels should be unlocked based on race completion progress.

LoadRaceData

  • Address: 0x40A120
  • Description: Parses racedata.xml for medal thresholds:
    • TIME → target time
    • GOLD/SILVER/BRONZE → medal cutoffs (stored as 9 - value)
    • WEASEL → weasel time threshold
    • Reads per-level data based on level name parameter

13. Arena Scoring & Timer

Full documentation: See docs/ARENA_SCORING.md

ArenaBoard_Update

  • Address: 0x421FE0
  • Description: Per-frame arena update. Checks timer expiration, computes winner, handles tie-breaker.

ArenaBoard_Render

  • Address: 0x421910
  • Description: Draws countdown timer, 4-player HUD, tie-breaker overlay.

Rotator_AddBall

  • Address: 0x43B6F0
  • Signature: __thiscall Rotator_AddBall(Scene* scene, Ball* ball)
  • Description: Registers ball on rotator's ball-tracking AthenaList (at scene+0x10F0). Allocates 8-byte entry [ball_ptr, tick_counter=10]. If ball already in list, resets tick to 10. Called from collision handlers on N:ONROTATOR (Impossible), N:SPINNY (Toob), N:SWIRL (Dizzy arena). Tick counter decremented each frame by Catapult_Update (0x43E600); resets to 10 on every frame of continued contact (10-frame grace period after leaving). Formerly misnamed ScoreObject_SetScore — has nothing to do with scoring.

ScoreObject_ctor

  • Address: 0x44BE80
  • Parameters: this, App*, playerData*, label (char*)
  • Size: 0x30 bytes
  • Description: Creates a SceneObject with vtable PTR_RaceGoalReached_Render (0x4D6C70). Used for race goal rendering and as the container type for rotator ball tracking.

ScoreDisplay_SetTime

  • Address: 0x434C80
  • Description: Sets displayed timer string with randomized decimal variation.

14. Level-Specific Functions

Each level has a custom BoardLevel subclass with constructor and destructor:

WarmUp (Level 1)

  • Ctor: LevelBoard_WarmUp_ctor @ 0x41CA40
  • Dtor: LevelBoard_WarmUp_dtor @ 0x41CB10

Intermediate (Level 2)

  • Ctor: LevelBoard_Intermediate_ctor @ 0x41CB20
  • Dtor: LevelBoard_Intermediate_dtor @ 0x41CC80

Dizzy (Level 4)

  • Ctor: LevelBoard_Dizzy_ctor @ 0x41D060 (note: named Level3 in code)
  • Dtor: LevelBoard_Dizzy_dtor @ 0x41D450

Tower (Level 5)

  • Ctor: LevelBoard_Tower_ctor @ 0x41E340
  • Dtor: LevelBoard_Tower_dtor @ 0x41E640

Expert (Level 8)

  • Ctor: LevelBoard_Expert_ctor @ 0x41EA40
  • Dtor: LevelBoard_Expert_dtor @ 0x41EC90

Odd (Level 9)

  • Ctor: LevelBoard_Odd_ctor @ 0x41ED80
  • Dtor: LevelBoard_Odd_dtor @ 0x41EE70

Wobbly (Level 12)

  • Ctor: LevelBoard_Wobbly_ctor @ 0x41F110
  • Dtor: BoardLevel12_Wobbly_dtor @ 0x41F3C0

Toob

  • Ctor: LevelBoard_Toob_ctor @ 0x41F4B0
  • Dtor: BoardLevel_Toob_dtor @ 0x41F720

Sky

  • Ctor: LevelBoard_Sky_ctor @ 0x41F930
  • Dtor: BoardLevel_Sky_Dtor @ 0x41FBC0

Beginner

  • Ctor: LevelBoard_Beginner_ctor @ 0x4200E0
  • Dtor: BoardLevel_Beginner_Dtor @ 0x4201D0
  • HandleRaceEnd: Board_Beginner_HandleRaceEnd @ 0x420240

Up

  • Ctor: LevelBoard_Up_ctor @ 0x420390
  • Dtor: BoardLevel_Up_Dtor @ 0x420550

Arena Level Constructors (via TourneyMenu_CreateBoard)

ID Arena Ctor Name Size
1 WarmUp ArenaBoard_WarmUp_ctor 0x47E0
2 Beginner ArenaBoard_Beginner_ctor 0x5850
3 Intermediate ArenaBoard_Intermediate_ctor 0x47E0
4 Dizzy ArenaBoard_Dizzy_ctor 0x47E4
5 Tower ArenaBoard_Tower_ctor 0x501C
6 UpArena ArenaBoard_Up_ctor 0x47E4
7 NeonArena ArenaBoard_Neon_ctor 0x47E8
8 ExpertArena ArenaBoard_Expert_ctor 0x4BFC
9 OddArena ArenaBoard_Odd_ctor 0x47E0
10 ToobArena ArenaBoard_Toob_ctor 0x5C6C
11 WobblyArena ArenaBoard_Wobbly_ctor 0x47E4
12 Glass BoardLevel_Glass_ctor 0x47E0
13 SkyArena ArenaBoard_Sky_ctor 0x4CFC
14 WarmupArena ArenaBoard_Master_ctor 0x47E0
15 Impossible ArenaBoard_Impossible_ctor 0x47E4

TourneyMenu_CreateBoard

  • Address: 0x426780
  • Description: Giant switch statement (cases 1–15) that allocates and constructs the correct ArenaBoard subclass for the selected arena level.

15. Utility & Math Functions

Vec3 Operations

Function Address Description
Vec3_Copy 0x401010 Copy vector
Vec3_Init 0x401040 Initialize to zero
Vec3_dtor 0x401070 Destructor (no-op)
Vec3_Scale 0x4016C0 Multiply by scalar
Vec3_DivideByScalar 0x401890 Component-wise divide
Vec3_AddTwo 0x4018C0 Add two vectors
Vec3_AddInPlace 0x4018F0 a += b
Vec3_Length 0x401A60 Euclidean length
Vec3_NormalizeAndScale 0x401AA0 Normalize then scale
Vec3_Distance 0x401D20 Distance between two vectors

Matrix Operations

Function Address Description
Matrix_TransformVec3 0x401D60 4×4 matrix × vector
Matrix_TransformPoint2D 0x45C273 2D point transform

Math Utilities

Function Address Description
Gfx_PackColorRGB 0x401100 Pack R,G,B into DWORD
RNG_Rand inferred Random number generator
Wave_Sin / Wave_Cos inferred Sine/cosine wave functions
SQRT inferred Square root
ABS inferred Absolute value

16. Vtables & Calling Conventions

Ball Vtable (0x4CF3A0)

Slot Offset Function Purpose
0 0x00 Ball_dtor Destructor
1 0x04 Ball_Init Post-constructor setup
4 0x10 Ball_Update Per-frame physics tick
5 0x14 ApplyForceWithMultipliers Force with scale
7 0x1C CollisionHandler Custom collision response
8 0x20 BounceCallback Called on E:BREAK events
30 0x78 PreSplitCallback Before Ball_Shatter
32 0x80 PostFactoryInit After bumper/sawblade creation

Scene/Board Vtable (0x4D0260)

Slot Offset Function Purpose
0 0x00 Scene_ctor Constructor
4 0x10 Scene_Update Main game tick
8 0x20 Scene_RunTick Single tick (Update+Render)
10 0x28 Scene_Render Full render dispatch
12 0x30 Scene_LoadLevel Level loading
16 0x40 Scene_CleanupScene Cleanup
18 0x48 Scene_StartRace Race countdown start
19 0x4C Scene_HandleRaceEnd Check finish
20 0x50 Scene_UpdateBallsAndState Ball physics
22 0x58 Scene_ProcessRaceEnd Countdown timer
24 0x60 RenderBackground Sky/dome
25 0x64 RenderOpaque Opaque geometry
26 0x68 RenderTransparent Glass/effects
28 0x70 RenderOverlay HUD
29 0x74 RenderPostEffects Fade/transition
33 0x84 CreateDynamicObject Level-specific factory override

App Vtable (0x4CE400)

Slot Offset Function Purpose
0 0x00 App_ScalarDtor Destructor
2 0x08 App_Shutdown Cleanup on exit
8 0x20 Update Game logic (Scene_Update)
9 0x24 PreRender Camera setup
10 0x28 Render Draw scene
11 0x2C PostRender HUD / menus
35 0x8C SetDisplayMode Resolution change
40 0xA0 ShowMainMenu Title screen

Calling Conventions

  • __thiscall: ECX = this pointer. Used for all object methods.
  • __fastcall: ECX = first arg, EDX = second arg. Used for inner loops.
  • Standard cdecl: Stack-based args. Used for utility functions.

Quick Reference: Most-Used Offsets

Ball Struct (0xC98 bytes for 8ball, 0xC60 for player, vtable 0x4CF3A0)

Offset Type Name Description
+0x00 void** vtable 0x4CF3A0
+0x04 void* scene Parent Scene*
+0x14 int32 player_index -1=AI, 0=P1, 1=P2
+0x18 char[0x14] rumble_timer1 ArenaBoard timer sub-object
+0x60 float[3] position Physics position (X, Y, Z)
+0x150 float accumulated_time Delta-time accumulator
+0x158 float[3] prev_pos Previous frame position
+0x164 float[3] display_pos Smoothed display position
+0x170 float[3] velocity Current velocity
+0x17C float[3] acceleration Cleared each frame
+0x188 float max_speed Hard velocity cap (default 5.0)
+0x18C float speed_scale Global speed multiplier (default 1.0)
+0x1A4 void* collision_mesh CollisionMesh ptr
+0x1A8 float[3] gravity_vec Gravity direction vector
+0x1C8 float render_alpha Render context alpha (0.75)
+0x20C float[4] color RGBA tint (default 1,1,1,1)
+0x254 uint8 uses_alpha True if color_a != 1.0
+0x260 uint8 boost_hit_flag Set on boost pad contact
+0x278 float gravity_scale Gravity multiplier (default 0.1)
+0x281 uint8 unused_init_flag DEAD: set to 1 in ctor, never read
+0x284 float radius Collision + render size (default 26.0)
+0x2A4 float spin_rate Angular spin factor (5.0)
+0x2BC float[3] force Accumulated input force
+0x2CC uint8 force_disable 1 = skip Ball_ApplyForce
+0x2DC float[3] checkpoint Last safe position
+0x2E8 uint8 splitting Set during Ball_Shatter
+0x2F0 uint32 force_count Forces applied this frame
+0x2F9 uint8 frozen Stuck on surface
+0x2FC uint32 freeze_timer Countdown while frozen
+0x310 uint8 state_active General active flag
+0x318 float split_timer Countdown for split balls (30.0)
+0x31D uint8 is_8ball AI enable flag
+0x324 uint8 dead Eliminated / in-tube
+0x748 int32 gravity_plane 0=flat, 1=tilted, 2=vertical
+0x758 float[3] camera_target Camera orbit center
+0x76C float[3] camera_actual Camera position (written by Scene_SetCamera)
+0xC28 char** display_string Floating text above ball
+0xC3C uint8 teleport_active Teleport in progress
+0xC40 float[3] teleport_dest Destination coordinates
+0xC4C uint8 is_shrunk odd race shrunk state (E:SHRINK=1, E:GROW=0)
+0xC60 int32 battle_mode 3=battle, 5=split
+0xC68 float friction 0.55 in battle mode
+0xC6C float bounciness 1.0 in battle mode
+0xC70 float max_speed_battle 1000.0 in battle mode
+0xC74 float chase_distance AI chase radius (25.0 battle)
+0xC80 float[5] gravity_vec_battle Battle gravity (0,-1.0,0,...)
+0xCA0 float speed_scale_split 0.01 for split balls
+0xCA4 float[3] collision_dir Last collision direction
+0xC88 float[16] world_matrix 4×4 transform for rendering

Scene Struct (~0x5400 bytes, vtable 0x4D0260)

Offset Type Name Description
+0x0000 void** vtable 0x4D0260
+0x0874 byte is_skydome 0=skybox, 1=skydome
+0x0878 App* scene_manager D3D device / App back-pointer
+0x087C void* viewport_obj D3D viewport
+0x08B8 AthenaList scene_object_list All scene objects
+0x08BC int scene_object_count Number of objects
+0x0CC4 SceneObject** scene_object_array Direct pointer array
+0x1518 AthenaList collision_list Collision surfaces
+0x2160 AthenaList ripple_list Water ripples
+0x29B0 byte ball_positions_dirty Need propagate this frame
+0x29B8 int shake_magnitude Camera shake (-800→0)
+0x29BC float camera_orbit_angle Y-axis rotation
+0x29C0 float camera_distance Orbit distance (max 700)
+0x29D0 Ball* current_ball_ptr Camera-tracked ball
+0x29D4 AthenaList ball_list_1 Player 1 balls
+0x29D8 int ball_list_1_count P1 ball count
+0x2DE0 Ball** ball_list_1_array P1 Ball pointer array
+0x3204 AthenaList ball_list_2 Player 2 balls
+0x3208 int ball_list_2_count P2 ball count
+0x3610 Ball** ball_list_2_array P2 Ball pointer array
+0x361C SceneObject* waypoint_arrow Next checkpoint arrow
+0x3620 int frame_counter Total frames
+0x362C AthenaList player_list Player viewport list
+0x3630 int player_count 0=none, 1=SP, 2=split
+0x3A38 Ball** player_ball_array Indexed by player (0–3)
+0x3A48 AthenaList visible_object_list Render bucket
+0x3A4C byte shake_active Camera shake active
+0x3AFC void* dynamic_object Post-update callback obj
+0x3F1C byte path_follow_mode 1=camera rides spline rails
+0x3F20 void* path_object Spline Path*
+0x3F24 float path_position Parametric t (0.0–1.0)
+0x3F2C int camera_snap_frames Snap countdown
+0x434C float[3] camera_offset Added to ball pos
+0x4358 byte demo_timer_active Demo countdown running
+0x435C int demo_countdown Frames remaining
+0x436C void* hammer_obj Arena hammer
+0x4370 void* saw1_obj Saw blade 1
+0x4374 void* saw2_obj Saw blade 2
+0x43A0 float damage_amount From E:BITE
+0x43A8 int damage_timer Damage countdown
+0x43B8 void* catapult_list Catapult objects
+0x47D0 void* door_list Trapdoor list
+0x4BBC void* judge_list Judge/score displays
+0x4FD4 void* bell_obj Bell (extra time)

App Struct (singleton at 0x4FD680, vtable 0x4CE400)

Offset Type Name Description
+0x000 void** vtable 0x4CE400
+0x004 HINSTANCE hInstance WinMain param
+0x054 RegKey* registryKey Registry handle
+0x05C int targetFPS Target frame rate (30)
+0x159 bool quitFlag 1 = exit loop
+0x15A bool activeFlag Window focused
+0x158 bool minimizedFlag Window minimized
+0x156 bool updateDisabled Pause all updates
+0x15C int width Window width (800)
+0x160 int height Window height (600)
+0x174 Graphics* graphics D3D8 engine
+0x17C AudioSystem* audioSystem BASS audio
+0x180 InputHandler* inputHandler DirectInput8
+0x184 void* gameUpdateObj Passed to tick
+0x1B4 char* versionString ProductVersion
+0x1CC int loadedCount Objects loaded
+0x200 bool initialized 1 after init
+0x208 char* initStep "Initialize(1)".."(26)"
+0x224 void* mainMenuObj MainMenu instance
+0x228 void* resultsScreen Race results
+0x234 bool is_demo Demo mode
+0x237 bool is_2player Multiplayer
+0x238 bool rightButtonPause Right-click pause
+0x23C int is_tournament Tournament/arena active
+0x278 Texture* shadowTexture shadow.png
+0x534 HMUSIC musicHandle BASS music
+0x538 HCHANNEL musicChannel1 BASS channel 1
+0x53C HCHANNEL musicChannel2 BASS channel 2
+0x550 void* gameMode1 1-player mode
+0x554 void* gameMode2 2-player mode
+0x558 void* gameMode3 4-player mode
+0x55C void* gameMode4 Tournament mode
+0x5D8 bool p1_active Player 1 active
+0x5E8 int p1_current_time P1 time/score
+0x5EC int p1_extra_time P1 bonus time
+0x60C int p1_race_index P1 race slot
+0x678 bool p2_active Player 2 active
+0x688 int p2_current_time P2 time/score
+0x84C float mouseSensitivity 0.0–1.0
+0x850 bool mirrorMode Tournament mirror
+0x851+0x868 bool[24] unlock_flags Race/arena unlocks
+0x86C uint8[0x50] bestTimes Per-level best times
+0x8BC uint8[0x50] medals Per-level medals
+0x914 int playCount Total launches
+0xB28 DWORD p2Controller1 DI device index
+0xB2C DWORD p2Controller2 DI device index
+0xB30 DWORD p2Controller3 DI device index
+0xB34 DWORD p2Controller4 DI device index

Document Info

  • Generated from: Ghidra 12.0 decompilation + GhidraMCP REST API
  • Game version: Hamsterball.exe (PE32, i386)
  • Total functions in binary: ~3,800
  • Functions documented: 120+
  • Decompilation sources: analysis/ghidra/decompilations/
  • Struct headers: analysis/ghidra/structs/*.h
  • Last updated: 2026-06-13

Cross-References

Topic Document
Arena scoring internals docs/ARENA_SCORING.md
Arena hazards docs/ARENA_HAZARD_SYSTEM.md
ball object deep dive docs/BALL_OBJECT.md
App singleton docs/APP_OBJECT.md
Scene system docs/SCENE_SYSTEM_DECOMP.md
scene object modding docs/SCENE_OBJECT_MODDING.md
Collision events docs/COLLISION_EVENT_SYSTEM.md
input system docs/DIRECTINPUT_SYSTEM.md
Save/registry docs/SAVE_REGISTRY_SYSTEM.md
rendering pipeline docs/RENDERING_PIPELINE.md
camera system docs/CAMERA_SYSTEM.md
8-ball AI docs/8BALL_AI_SYSTEM.md
UI menus docs/UI_MENU_SYSTEM.md
meshworld format docs/MESHWORLD_FORMAT.md
Key decompilations docs/KEY_DECOMPILATIONS.md
ArenaBoard system docs/RUMBLEBOARD_SYSTEM.md
Game state lifecycle docs/GAME_STATE_RACE_LIFECYCLE.md

All offsets verified against raw Ghidra decompiled C. For questions, check the decompilation files in analysis/ghidra/decompilations/.


🔗 Related Documents

Mods Catalog

types : mods
keywords :

📂 View source on GitHub


Hamsterball Mods Catalog

Mod Description Files
universal-ref-loader v3: Loads any ref type into any level via vtable[33] hook. JIT mesh injection from disk, clone-on-return for static-mesh objects, difficulty gate bypass, board slot safety checks universal-ref-loader/
global-lifters Spawns Up Race lifters (Rotators) on any level with a hotkey global-lifters/
global-drawbridge Spawns Tower Race drawbridges on any level with a hotkey global-drawbridge/
global-trapdoor Spawns Tower Race trapdoors on any level with a hotkey global-trapdoor/
global-neon Spawns all 6 neon race objects on any level + neon lighting mode (dark scene with ball-emitted point light). Toggle via NeonLighting/NeonAmbient symbols global-neon/
global-expert Spawns all 6 expert race objects (Bonk/Hammer, Fan, Sawblade, Bridge, Judge, Bell) on any level with a hotkey global-expert/
global-bonk Spawns Bonk the Hammer from Expert Race on any level — standalone, constructor loads its own mesh internally global-bonk/
wall-bumpers All walls act as pinball bumpers — velocity-reversal detection amplifies wall bounces. F8 toggle, F9 force cycle wall_bumpers/
ball-break Press X to shatter your ball and respawn at nearest checkpoint. Calls the game's own Ball_Shatter function ball_break/
half-size-all Shrinks player ball to half size by inlining Ball_Shrink's physics fields (radius=13.0, physics_scale=2.5). No sound, player index 0 only half_size_all/
raptisoft-live-log Passive logger — taps Raptisoft's hidden in-memory status tracking system (App+0x208/0x20C/0x210) and writes live updates to live_status.txt. No gameplay changes raptisoft_live_log/
level-colors Changes per-level base colors (timer oval, timer text, race selection menu text) from a colors.txt config file. Edit at runtime level_colors/
universal-safespots Adds SAFESPOT(*) universal checkpoint — always accepted as respawn candidate regardless of active SAFESWITCH filter. Competes on distance with lettered SAFESPOTs universal-safespots/
8ball-spawn Press B to spawn an 8-ball in front of the player's hamster ball. Uses mesh index 9 (8Ball), spawns as physics debris (player_index=-1) 8ball_spawn/
8ball-goal-fix Prevents crash when 8-ball (BadBall) touches N:GOAL or E:ACTION(SCORE) triggers. Adds player_index<0 guard (same pattern Raptisoft used for E:LIMIT) to 3 patch points in DispatchCollisionEvents 8ball_goal_fix/

🔗 Related Documents

Neon Race Objects

types : docs
keywords :

📂 View source on GitHub


Neon Race Objects: Complete Reverse Engineering

Overview

Neon Race (Level7) contains 6 unique object types that can be spawned globally:

  • NEONPLATFORM — Disappearing neon floor
  • DFLOOR1-4 — Four different disappearing floor sections
  • TRODE — Neon electrode/tube objects

Factory Function

Scene_SetupLevelDark @ 0x416270 (vtable[0x48] of Neon Board vtable @ 0x4D1DF0)

This function:

  1. Loads main level mesh Levels\LevelDark → stores at Board+0x8AC
  2. Creates CollisionLevel from the main mesh → Board+0x8B0
  3. Calls Board_NeonRace_ctor (0x424440) to init the Board
  4. Calls vtable[0x80] (0x416910) = CreateNeonObjects factory

Mesh Loading (Board_NeonRace_ctor @ 0x424440)

All 6 meshes are loaded into Board offsets:

Board Offset Mesh Path String Address
+0x4374 Levels\LevelDark-NeonPlatform 0x4D1D9C
+0x4378 Levels\LevelDark-DFloor1 0x4D1D80
+0x437C Levels\LevelDark-DFloor2 0x4D1D64
+0x4380 Levels\LevelDark-DFloor3 0x4D1D48
+0x4384 Levels\LevelDark-DFloor4 0x4D1D2C
+0x4388 Levels\LevelDark-Trode 0x4D1D14

These meshes are NOT pre-loaded at App level. They are only loaded by Board_NeonRace_ctor. For global spawning, they must be loaded via MeshWorld_ctor (0x461510).

Object Creation (CreateNeonObjects factory @ 0x416910)

The factory iterates the level's MESHWORLD object list and matches object names via strnicmp:

NEONPLATFORM (string at 0x4D00F0, length 12)

alloc = operator_new(0x10EC)
Stands_CtorRotator(alloc, Board, pos_X, pos_Y, pos_Z, mesh=Board+0x4374)
AthenaList_Append(Board+0x2578, obj)
sub-object = [obj+0x10D4]  ; CollisionLevel (created internally)

DFLOOR1-4 (strings at 0x4D00E8/E0/D8/D0, length 7)

alloc = operator_new(0x1104)
DFloor_ctor(alloc, Board, pos_X, pos_Y, pos_Z, mesh=Board+0x4378/7C/80/84)
AthenaList_Append(Board+0x2578, obj)
sub-object = [obj+0x10E8]  ; CollisionLevel (created internally)

TRODE (string at 0x4D00C8, length 5)

alloc = operator_new(0x1104)
DFloor_ctor(alloc, Board, pos_X, pos_Y, pos_Z, mesh=Board+0x4388)
AthenaList_Append(Board+0x2578, obj)
sub-object = [obj+0x10E8]  ; CollisionLevel (created internally)

Constructors

Stands_CtorRotator (NEONPLATFORM) @ 0x43E110

  • Calling convention: ecx=this, ret 0x14 (5 stack params)
  • Params: Board, pos_X, pos_Y, pos_Z, mesh
  • Vtable: 0x4D5A10
  • Internal: Calls Stands_ctor(0x462850) with mesh as source → creates CollisionLevel at obj+0x10D4 via CollisionLevel_ctorWithLevel(0x465080)
  • Position stored at: obj+0x10D8/X/DC/E0

DFloor/Trode ctor @ 0x43E450

  • Calling convention: ecx=this, ret 0x14 (5 stack params)
  • Params: Board, pos_X, pos_Y, pos_Z, mesh
  • Vtable: 0x4D5A70
  • Internal: Calls Stands_ctor(0x462850) with mesh as source → creates CollisionLevel at obj+0x10E8 via CollisionLevel_ctorWithLevel(0x465080)
  • Position stored at: obj+0x10D0/D4/D8
  • Board stored at: obj+0x10E4

Collision System

Physical Collision

Both constructors create a CollisionLevel sub-object internally via CollisionLevel_ctorWithLevel (0x465080). This gives the object its own spatial collision tree built from the mesh geometry. The ball will physically collide with the geometry through the generic spatial tree collision system.

Neon-Specific Collision Events (TowerCollisionEvents @ 0x416CA0)

The Neon Board vtable has a custom collision handler at vtable[0x74] = 0x416CA0. This handles:

Event String Address Action
N:NEONPLATFORM 0x4D0120 Calls [obj+0x47C] then NeonPlatform_Activate (0x437300)
E:ZOOP 0x4D0118 Sets obj+0x7F0 = 100 (timer), creates 3D sound at ball position
E:LIGHTSOFF 0x4D010C Sets obj+0x7B4 = 100, calls vtable[4], removes object from list
E:LIGHTSON 0x4D0100 Sets obj+0x7B8, calls vtable[4], decrements Board+0x4390

On non-Neon levels: These events will NOT fire because the level's collision handler is different. Objects will have physical collision but no special behaviors (lights, zoop, disappear).

Finally calls DispatchCollisionEvents (0x40C5D0) at the end.

Global Spawn Requirements

  1. Load meshes via MeshWorld_ctor(0x461510) into Board+0x4374..+0x4388 (cached, load once)
  2. No AthenaList init needed — Board+0x2578 is initialized by Board_ctor on all levels
  3. No mesh+0x878 fix needed — Neon constructors call Stands_ctor directly, which does NOT read mesh+0x878 (unlike Glass_Level_ctor for Drawbridge)
  4. Constructor call: push mesh; sub esp,0xC; fill pos; push Board; mov ecx,alloc; call ctor; push obj; Append(Board+0x2578)

Addresses Summary

Function Address Ret
MeshWorld_ctor 0x461510 8
operator_new 0x4BA57B cdecl
Stands_CtorRotator (NEONPLATFORM) 0x43E110 0x14
DFloor/Trode ctor 0x43E450 0x14
AthenaList_Append 0x453810 4
CreateNeonObjects factory 0x416910 0x10
TowerCollisionEvents (Neon) 0x416CA0 8
Board_NeonRace_ctor 0x424440 4
Scene_SetupLevelDark 0x416270

🔗 Related Documents

No-Pause Mod v2

types : tools
keywords :

📂 View source on GitHub


No-Pause Mod v2

Prevents the pause menu from appearing via any input method — ESC key, right-click, or Win32 message pump. The game continues running normally with no pause overlay, no physics freeze, and no camera stop.

Why v1 Didn't Work

v1 only patched one of three code paths that trigger pause. Pressing ESC still paused the game through the Win32 message pump path, and right-clicking paused through the mouse event handler path.

The Three Pause Paths

Scene_CreateGameOverMenu (0x40a920) creates the pause overlay and sets scene+0x874 = 1 (pause flag). When this flag is set, GameUpdate (0x469cf0) skips calling Scene_Update on the scene — freezing all physics and game logic.

There are three independent code paths that call Scene_CreateGameOverMenu:

Path 1: DirectInput ESC Poll (Scene_Update)

Scene_Update (0x419c00)
  → Input_CheckKeyCombo(app, 2)    ; checks ESC via DirectInput
  → if pressed: Scene_CreateGameOverMenu(scene, 1)

Gating conditions: game state not in {3,4}, scene+0x220 == 0, demo timer inactive.

Path 2: Right-Click Mouse Handler (vtable[5])

App_OnMouseDown(param_3=1=right button)
  → UIWidget_HitTest → vtable[5] on Scene
  → thunk 0x4130A0 checks param_3==1, App+0x238, profile+0x95
  → if conditions met: Scene_CreateGameOverMenu

This thunk appears in 32 scene-object vtables — all scene types inherit this pause-on-right-click behavior.

Path 3: Win32 Message Pump ESC (vtable[8])

WndProc → key message dispatch → vtable[8] on Scene
  → thunk 0x40B400 checks param == 0x1B (VK_ESCAPE = 27)
  → if match: JMP Scene_CreateGameOverMenu (tail call)

This is a separate ESC detection path from Path 1. Path 1 uses DirectInput polling; Path 3 uses the Win32 message pump. Both fire on ESC press. This thunk also appears in 32 vtables.

The Patches

Three single-byte patches, one per path:

Path Address Original Patched Effect
1 — DirectInput ESC 0x419d5b 74 09 (JZ) EB 09 (JMP) Always skip pause creation in Scene_Update
2 — Right-click 0x4130b5 74 17 (JZ) EB 17 (JMP) Always skip pause in vtable[5] right-click thunk
3 — Message pump ESC 0x40b405 75 0D (JNZ) EB 0D (JMP) Always skip pause in vtable[8] message handler thunk

All three convert conditional jumps to unconditional jumps, causing the pause creation code to always be skipped.

How Pause Works (for reference)

  1. Scene_CreateGameOverMenu (0x40a920) creates a PauseMenu or PauseArenaMenu overlay and sets scene+0x874 = 1
  2. GameUpdate (0x469cf0) iterates scene objects each frame. For each object, it checks obj[0x21d] (byte at scene+0x874). When the flag is 1, it skips calling Scene_Update (vtable[1]) — freezing all game logic
  3. When the player clicks "RESUME" in the pause menu, PauseMenu_HandleButtonClick sets scene+0x874 = 0 — unfreezing the game

Installation

  1. Extract the zip into your Hamsterball game folder (next to Hamsterball.exe)
  2. Run install.bat
  3. Launch the game

Uninstallation

Run uninstall.bat to restore the original bass.dll.

Technical Details

  • Mod type: BASS.dll proxy (v3 lazy loader pattern)
  • Patch type: Three single-byte patches — conditional → unconditional jumps
  • Patch addresses: RVA 0x19d5b, 0x130b5, 0x0b405 (VA 0x419d5b, 0x4130b5, 0x40b405)
  • Side effects: None — ESC and right-click are simply ignored during gameplay. No menu, no pause, no freeze.

🔗 Related Documents

Object Catalog for Modders

types : objects
keywords :

📂 View source on GitHub


Hamsterball — Object Catalog for Modders

What This Document Is

A comprehensive catalog of game objects found in Hamsterball.exe through Ghidra decompilation. Each object includes its constructor address, approximate size, vtable, key fields (where known), and how it fits into the game's object hierarchy.

Objects already documented in depth elsewhere are referenced rather than duplicated.


Table of Contents

  1. Object Hierarchy Overview
  2. Core Singletons
  3. Base Classes (Inheritance Root)
  4. Player Objects
  5. Level / Scene System
  6. Level Objects — Mechanical
  7. Level Objects — Hazards
  8. Level Objects — Environment
  9. UI / Menu System
  10. Graphics / Render Objects
  11. Audio Objects
  12. Score / GameState Objects
  13. Utility / Container Objects
  14. Objects Not Yet Documented
  15. How to Instantiate Objects at Runtime

Object Hierarchy Overview

SceneObject (vtable 0x4D934C, size 0xD4)
  └─ Gadget (vtable 0x4D9170, size 0x870)
       ├─ Board (vtable 0x4D0260, size ~0x4368)
       │    └─ ArenaBoard (vtable 0x4D1358, size ~0x47D4)
       │    └─ GameLevel (vtable varies, size ~0x4368)
       │         ├─ Glass_Level
       │         ├─ Spinner_Level
       │         ├─ Gear_Level
       │         └─ BoardLevel3
       ├─ Platform (vtable varies, size 0x10FC)
       ├─ Stands (vtable varies, size 0x10D0)
       ├─ Secret (vtable varies, size 0x10EC)
       ├─ Looper (vtable varies, size 0x1500)
       ├─ Gear / BigGear (vtable varies, size 0x1514)
       ├─ Rotator (vtable varies, size 0x1508)
       ├─ Pendulum (vtable varies, size 0x1504)
       ├─ Tipper (vtable varies, size 0x1104)
       │    └─ TipperVisual (visual component)
       ├─ Bonk (vtable varies, size 0x1200)
       ├─ BreakBridge (vtable varies, size 0x1100)
       ├─ PopCylinder (vtable varies, size 0x10E8)
       ├─ Blockdawg (vtable varies, size 0x1154)
       ├─ Catapult (vtable varies, size 0x1108)
       ├─ Gluebie (vtable varies, size 0x110C)
       └─ Bumper / SpeedCylinder / Spinner / Sawblade / MouseTrap

Ball (vtable 0x4CF3A0, size 0xC98) — NOT in Gadget hierarchy
  └─ Ball_Split (vtable 0x4CF560)

Graphics (vtable 0x4D88A0, size ~0x540+)
MeshWorld (vtable 0x4D9CDC, size 0x488)
Level (vtable 0x4D8FB0, size 0x10D4)
CollisionLevel (vtable varies, size varies)
Scene (inherits Gadget, vtable 0x4D0260)

UI / Menu hierarchy:
  SimpleMenu
       ├─ MainMenu
       ├─ OptionsMenu
       ├─ PracticeMenu
       ├─ ConfirmMenu
       ├─ GameSelectionScreen
       ├─ HighScoreEntry
       ├─ RaceResultsMenu
       └─ SaveTourneyDialog
  UIListItem
  RaceResultPopup
  OkayDialog
  QuitDialog
  RegisterDialog
  ArenaScoreParticle
  Sprite
  ScoreObject

Core Singletons

App

  • Address: 0x004FD680 (global pointer g_App)
  • Type: App* — singleton holding all subsystem pointers
  • Size: ~0xA00+ bytes
  • Vtable: 0x004CE400
  • Constructor: App_ctor (0x46DC40)
  • Deep documentation: See APP_OBJECT.md
  • Key subsystems: graphics (+0x174), audioSystem (+0x17C), musicHandle (+0x534), registryKey (+0x54), gameMode1-5 (+0x550 to +0x55C)

Graphics

  • Constructor: Graphics_ctor (0x4542C0)
  • Vtable: 0x4D88A0
  • Size: ~0x540+ bytes
  • Deep documentation: See D3D8_RENDERING_PIPELINE.md
  • Key fields: D3D device, texture cache, viewport, frustum, render mode

Base Classes

These are abstract base classes you don't typically instantiate directly, but they define the field layout for all derived objects.

SceneObject

  • Constructor: SceneObject_ctor (0x46B4F0)
  • Vtable: 0x4D934C (destructor), 0x4D9170 (scalar dtor)
  • Size: 0xD4 bytes (212 bytes)
  • Fields:
    • +0x00 vtable
    • +0x08 app_ptr
    • +0x14 visibility flag
    • +0x18 field_18
    • +0x1C field_1c
    • +0x20 field_20
    • +0x24 field_24
    • +0x28 field_28
    • +0x2C byte_2c
    • +0x30 field_30
    • +0x34 AthenaList (child objects)
    • +0x44 AthenaList (another child list)
    • +0x54 field_54
    • +0x58 field_58
    • +0x5C field_5c
    • +0x60 field_60
    • +0x64 field_64
    • +0x68 field_68
    • +0x6C field_6c
    • +0x70 field_70
    • +0x74 field_74
    • +0x78 field_78
    • +0x7C field_7c
    • +0x80 field_80
    • +0x84 field_84
    • +0x88 field_88
    • +0x8C field_8c
    • +0x90 field_90
    • +0x94 base_scale matrix (4x4)
    • +0xA8 rotation matrix (4x4)
    • +0xBC world transform matrix (4x4)
    • +0xD0 field_d0
  • Deep documentation: See SCENE_STRUCT.md

Gadget

  • Constructor: Gadget_ctor (0x4690F0)
  • Vtable: 0x4D9170 (inherits from SceneObject)
  • Size: 0x870 bytes
  • Parent of: Board, Platform, Stands, Secret, all mechanical/hazard objects
  • Key fields:
    • +0x00 vtable → 0x4D9170
    • +0x014 app_ptr (App*)
    • +0x018 second vtable → 0x004CF584
    • +0x034 AthenaList (children)
    • +0x44C AthenaList (siblings)
    • +0x868 name string (e.g., "Generic Gadget")
  • Deep documentation: See SCENE_STRUCT.md

GameObject

  • Vtable: 0x4CF314 (used as initial vtable before Ball is fully constructed)
  • Size: ~0x810+ bytes
  • Parent of: Ball (Ball is NOT in Gadget hierarchy — it's a separate branch)
  • Used by: Ball_ctor (0x40AFE0) calls GameObject_ctor first

Player Objects

Ball

  • Constructor: Ball_ctor (0x40AFE0), Ball_ctor2 (0x4039E0)
  • Vtable: 0x4CF3A0 (9 method pointers)
  • Size: 0xC98 bytes (3224 bytes)
  • Deep documentation: See BALL_OBJECT_MODDING.md
  • Key fields:
    • +0x00 vtable
    • +0x08 collision_result
    • +0x0C string_timer
    • +0x10 app_state
    • +0x14 scene ptr
    • +0x18 player_index (-1 = AI)
    • +0x164 pos_x/y/z
    • +0x170 vel_x/y/z
    • +0x188 max_speed (5000.0f)
    • +0x18C speed_scale (1.0f)
    • +0x1A8 gravity vector
    • +0x284 radius (27.0f)
    • +0x2DC lgp_x/y/z
    • +0x768 cam_active
    • +0xC4C is_shrunk flag (odd race E:SHRINK/E:GROW)
    • +0xC88 world transform matrix (4x4)
  • Methods (vtable slots):
    • [0x10] Ball_Update (0x405E00) — main tick
    • [0x14] Ball_ApplyForceWithMultipliers (0x401590)
    • [0x18] Ball_Render (0x4027F0)
    • [0x1C] Ball_CollisionHandler (0x402DE0)
    • [0x20] Ball_InputHandler (0x402A70)
    • [0x24] Ball_PhysicsUpdate (0x405100)
    • [0x28] Ball_SpawnHandler (0x402C10)
    • [0x2C] Ball_AIUpdate (0x408390)
    • [0x30] Ball_SpecialUpdate (0x409480)

Ball_Split

  • Constructor: Ball_Split_ctor (0x408D10)
  • Vtable: 0x4CF560
  • Description: Split-ball variant (multiplayer / tournament split-screen)

Level / Scene System

Scene / Board

  • Constructor: Board_ctor (0x419020), Scene inherits from Gadget
  • Vtable: 0x4D0260
  • Size: ~0x4368 bytes (Board), Scene is the same structure
  • Deep documentation: See SCENE_STRUCT.md and SCENE_OBJECT_MODDING.md
  • Key fields:
    • +0x000 Gadget base (0x870 bytes)
    • +0x221 ArenaBoard timer data
    • +0x335 Ball list (AthenaList)
    • +0x43B Effect list (AthenaList)
    • +0x361C Ball pointer (first player ball)
    • +0x29B0 Gravity parameter
    • +0x29C0 Camera orbit distance
    • +0x3AAC Camera Vec3+Matrix pairs (5 sets)

Level

  • Constructor: Level_ctor (0x461740)
  • Vtable: 0x4D8FB0
  • Size: 0x10D4 bytes
  • Description: Container for level geometry, objects, and state. Inherits from Gadget.
  • Fields: 4 AthenaLists, Timer, LevelState

GameLevel

  • Constructor: GameLevel_ctor (0x4351F0)
  • Description: Wraps Level with game-specific logic (Stands init, clone, sound channel)

MeshWorld

  • Constructor: MeshWorld_ctor (0x4706E0, 0x46F3D0)
  • Vtable: 0x4D9CDC
  • Size: 0x488 bytes (small ctor), larger version handles strips
  • Description: Level geometry parser and container. Loads .meshworld files.
  • Deep documentation: See MESHWORLD_FORMAT.md

CollisionLevel

  • Constructor: CollisionLevel_ctor (0x4652E0)
  • Description: Collision-only level data (.meshcollision format)

Level Objects — Mechanical

Platform

  • Constructor: Platform_ctor (0x437040)
  • Size: 0x10FC bytes
  • Description: Static or animated platform. Most common level object.
  • Created by: CreatePlatformOrStands factory

Stands

  • Constructor: Stands_ctor (0x462850)
  • Size: 0x10D0 bytes
  • Description: Audience / stadium stands geometry

Looper

  • Constructor: Looper_ctor (0x435800)
  • Size: 0x1500 bytes
  • Description: Loop-de-loop ramp structure

Gear / BigGear

  • Constructor: Gear_ctor (0x437590)
  • Size: 0x1514 bytes
  • Description: Rotating gear obstacles. Uses N:BOUNCE (bounce off gear surface), N:ONGEAR (attach to gear rotation via Catapult_AddObjectConditional), and N:ONROTATOR (attach via Rotator_AddBall). Found in LevelImpossible-Gear.MESHWORLD (8 N:BOUNCE triggers). Gear rotation handled by Catapult_Update (0x43E600) which applies rotation matrix to tracked balls each frame. See Rotator System for mechanics.

Rotator

  • Constructor: Rotator_ctor (0x435940)
  • Size: 0x1508 bytes
  • Description: Rotating platform / arm. Uses N:ONROTATOR event to attach balls via Rotator_AddBall (0x43B6F0). Ball position and velocity rotated each frame by Catapult_Update (0x43E600). 10-frame grace period after ball leaves surface before release.

Pendulum

  • Constructor: Pendulum_ctor (0x437700)
  • Size: 0x1504 bytes
  • Description: Swinging pendulum obstacle

Tipper

  • Constructor: Tipper_ctor (0x437960)
  • Size: 0x1104 bytes
  • Description: Tipping platform that tilts when ball rolls on it
  • Visual component: TipperVisual_ctor (0x4661A0)

Catapult

  • Constructor: Catapult_ctor (0x437E10)
  • Size: 0x1108 bytes
  • Description: Spring-loaded launcher platform

BreakBridge

  • Constructor: BreakBridge_ctor (0x436D70)
  • Size: 0x1100 bytes
  • Description: Bridge that breaks when ball crosses

PopCylinder

  • Constructor: PopCylinder_ctor (0x436EE0)
  • Size: 0x10E8 bytes
  • Description: Cylinder that pops up from ground

Level Objects — Hazards

Bonk (Hammer)

  • Constructor: Bonk_ctor (0x438850)
  • Size: 0x1200 bytes
  • Description: Giant swinging hammer obstacle
  • Popup feedback: CreateBonkPopup (0x438B30)

Blockdawg

  • Constructor: Blockdawg_ctor (0x43C310)
  • Size: 0x1154 bytes
  • Description: Block creature / moving block hazard

Gluebie

  • Constructor: Gluebie_ctor (0x437CB0)
  • Size: 0x110C bytes
  • Description: Glue blob that slows the ball

Spinner

  • Factory: HandleArenaCollisionEvents (0x412850)
  • Description: Spinning blade / propeller hazard

Sawblade

  • Factory: CreateExpertLevelObjects (0x40E250)
  • Description: Rotating saw blade hazard

MouseTrap

  • Factory: CreateMouseTrap (0x40BF50)
  • Description: Snap-shut trap hazard

Bumper

  • Factory: CreateBumper (0x40FA20), CreateBumper2 (0x413CE0)
  • Description: Pinball-style bumper that knocks ball away

SpeedCylinder

  • Factory: CreateUpLevelObjects (0x4117B0)
  • Description: Cylinder that accelerates or decelerates ball

NoDizzy

  • Factory: DispatchCollisionEvents (0x40C5D0)
  • Description: Power-up that prevents dizzy state

Level Objects — Environment

Secret

  • Constructor: Secret_ctor (0x43DFB0)
  • Size: 0x10EC bytes
  • Description: Hidden collectible / secret area object
  • Factory: CreateSecretObjects (0x40BAA0)

Glass_Level

  • Constructor: Glass_Level_ctor (0x4384A0)
  • Description: Glass-themed level variant (transparent bridges, walls)

Spinner_Level

  • Constructor: Spinner_Level_ctor (0x4396F0)
  • Description: Level variant with spinner obstacles

Gear_Level

  • Constructor: Gear_Level_ctor (0x43A150)
  • Description: Level variant with gear obstacles

BoardLevel3

  • Constructor: LevelBoard_Dizzy_ctor (0x41D060)
  • Description: Tournament level 3 board state

UI / Menu System

SimpleMenu (base)

  • Constructor: SimpleMenu_ctor (0x448F20)
  • Description: Base class for all menu screens. Handles item list, up/down scrollers, selection.

MainMenu

  • Constructor: MainMenu_ctor (0x42DE50)
  • Description: Title screen — LET'S PLAY, HIGH SCORES, OPTIONS, CREDITS, EXIT

OptionsMenu

  • Constructor: OptionsMenu_ctor (0x442CE0)
  • Description: Settings screen — Resolution, Fullscreen, Color depth, Safe Mode, Volume, Key Remap, Mouse, Pause

PracticeMenu

  • Constructor: PracticeMenu_ctor (0x42EA30)
  • Description: "CHOOSE A PRACTICE RACE!" — 14 race items with thumbnails

ConfirmMenu

  • Constructor: ConfirmMenu_ctor (0x42B190)
  • Description: BACK / BACK2TOURNAMENT / DONE confirmation dialog

GameSelectionScreen

  • Constructor: GameSelectionScreen_ctor (0x42E060)
  • Description: Difficulty selector for tournament mode

HighScoreEntry

  • Constructor: HighScoreEntry_ctor (0x42B470)
  • Description: Name input + score display after race

RaceResultsMenu

  • Constructor: RaceResultsMenu_ctor (0x44CB10)
  • Description: Post-race screen with title, subtitle, player entries, timer

RaceResults

  • Constructor: RaceResults_ctor (0x44B8A0)
  • Description: Race result data object (timers, congratulatory text, score thresholds)

RaceResultPopup

  • Constructor: RaceResultPopup_ctor (0x44C260)
  • Description: Small popup showing rank + "TIME'S UP!" / "OUT OF TIME!" text

SaveTourneyDialog

  • Constructor: SaveTourneyDialog_ctor (0x44FD60)
  • Description: "Save tournament progress?" dialog

RegisterDialog

  • Constructor: RegisterDialog_ctor (0x4476B0)
  • Description: Purchase / registration reminder dialog

OkayDialog

  • Constructor: OkayDialog_ctor (0x440E70)
  • Description: Simple "OKAY!" button dialog with caption

QuitDialog

  • Constructor: QuitDialog_ctor (0x443E30)
  • Description: "YES" / "NO" quit confirmation dialog

UIListItem

  • Constructor: UIListItem_ctor (0x4490A0)
  • Size: 0x444 bytes
  • Description: Single selectable item in a menu list. Contains Vec3 + AthenaList.

Graphics / Render Objects

RenderContext

  • Constructor: RenderContext_Init (0x457FA0)
  • Size: 0x50 bytes
  • Vtable: 0x4D8E68
  • Description: Per-object render state (material, transform, visibility)

MeshBuffer

  • Factory: CreateMeshBuffer (0x458970)
  • Description: D3D8 vertex/index buffer wrapper

Texture

  • Loader: Graphics_LoadTexture (0x4542C0)
  • Constructor: implicit via D3D8 CreateTexture
  • Description: GPU texture object. Loaded from PNG/BMP files.

Sprite

  • Constructor: Sprite_ctor (0x45D0C0)
  • Description: 2D screen-space sprite with texture + RenderContext + material defaults

MeshNode

  • Constructor: MeshNode_ctor (0x471C20)
  • Vtable: 0x4D9C48
  • Description: Scene graph node that holds a loaded mesh file

MeshArchive

  • Constructor: MeshArchive_ctor (0x478E70)
  • Description: Mesh resource pool. Manages D3D resources, mesh groups, position tracking.

Audio Objects

BASS Music

  • Handle: App.musicHandle (+0x534) — music\music.mo3
  • Channels: musicChannel1 (+0x538), musicChannel2 (+0x53C)
  • Loader: LoadMusicFile (0x46A020)
  • Deep documentation: See AUDIO_SYSTEM.md

SFX / 3D Sound

  • Handle: Ball.sound_3d_handle (+0x700)
  • System: DirectSound8 + BASS_SampleLoad
  • Deep documentation: See AUDIO_SYSTEM_SFX.md

Score / GameState Objects

ArenaScoreParticle

  • Constructor: ArenaScoreParticle_ctor (0x44AD50)
  • Description: Floating score popup in Rumble mode. Difficulty scale: 0.02/0.03/0.04.

ScoreObject

  • Description: SceneObject subclass. Despite the name, its SetScore method (now renamed Rotator_AddBall at 0x43B6F0) does NOT set a score — it registers balls on a rotator's tracking list for physical rotation. The ScoreObject_ctor (0x44BE80) creates a SceneObject with vtable PTR_RaceGoalReached_Render (0x4D6C70), used for race goal rendering.
  • Used by: Rotator_AddBall called from ImpossibleCollisionEvents (N:ONROTATOR), ToobCollisionEvents (N:SPINNY), DizzyArenaCollisionEvents (N:SWIRL)

ArenaBoard

  • Constructor: ArenaBoard_ctor (0x4217B0)
  • Size: ~0x47D4 bytes
  • Description: Rumble mode game board. Inherits from Board. Timer-based scoring.
  • Embedded timer: ToggleTimer_Init / ToggleTimer_Cleanup

Utility / Container Objects

AthenaList

  • Function: AthenaList_Append (0x453780)
  • Description: Dynamic array/list container used throughout the engine. Resizable, append-only.
  • Used by: Almost every object for child/sibling tracking.

AthenaHashTable

  • Constructor: AthenaHashTable_ctor (0x472C20)
  • Vtable: 0x4CF584
  • Description: Hash table for string→object lookups (e.g., mesh names)

Vec3List

  • Description: Array of Vec3 structures. Used for position/path data.

Timer

  • Function: Timer_Init (various)
  • Description: Countdown / animation timer used by objects for state transitions.

Objects Not Yet Documented

These objects have constructors or factories in the binary but lack deep reverse-engineering documentation:

Object Constructor/Factory Notes
E:LIMIT boundary NeonCollisionEvents (0x410D00) Invisible race boundary
E:NODIZZY dizzy immunity zone DispatchCollisionEvents (0x40C5D0) Prevents dizzy state
E:JUMP trigger (inline in collision) Launch pad trigger object
SAFESPOT (inline in factory) Safe zone / checkpoint
CAMERALOOKAT CameraLookAt (0x413280) Camera target marker
FLAG checkpoints (inline in factory) Race checkpoint objects
BRIDGE (tipper variant) CreateLevelObjects (0x4121D0) Breakable bridge sub-type
TIPPER (via factory) CreateLevelObjects (0x4121D0) Tipping platform sub-type
BBRIDGE1/2 CreateLevelObjects (0x4121D0) Bridge variants
CATAPULT (via factory) CreateLevelObjects (0x4121D0) Catapult sub-type
GLUEBIE (via factory) CreateLevelObjects (0x4121D0) Glue blob sub-type
BLOCKDAWG1/2 CreateLevelObjects (0x4121D0) Block creature variants
POPCYLINDER (via factory) CreateLevelObjects (0x4121D0) Pop-up cylinder sub-type

How to Instantiate Objects at Runtime

All level objects are created through the object factory system in Scene/Level. The typical pattern is:

// Inside Scene::LoadLevel or Board::Init
void* obj = operator_new(0x1104);     // Allocate Tipper size
Tipper_ctor(obj, scene_ptr, param2);  // Call constructor
AthenaList_Append(scene->object_list, obj);  // Add to scene

Factory Pattern

For MESHWORLD-defined objects, the factory is CreateLevelObjects (0x4121D0):

// Pseudocode from decompilation
if (strcmp(type, "TIPPER") == 0) {
    obj = operator_new(0x1104);
    Tipper_ctor(obj, scene, params);
}
else if (strcmp(type, "BONK") == 0) {
    obj = operator_new(0x1200);
    Bonk_ctor(obj, scene, params);
}
// ... etc

Modding Approach

To add custom objects:

  1. Hook CreateLevelObjects (0x4121D0)
  2. Add your own type string check
  3. Allocate your object size
  4. Call your custom constructor
  5. Append to the scene's object list

See LEVEL_OBJECT_FACTORY.md for the full factory breakdown.


Document Revision

  • Sources: Ghidra decompilations (analysis/ghidra/decompilations/), FUNCTION_MAP.md, renames_backup.json, struct headers (analysis/ghidra/structs/)
  • Total objects cataloged: 57 constructors + 14 factories + 6 base classes + 3 singletons
  • Coverage: All major gameplay objects, UI screens, graphics/audio subsystems
  • Next additions: Deeper field analysis for undocumented mechanical objects (TipperVisual, BreakBridge, PopCylinder internals), factory parameter formats

For memory layouts of Ball, App, Scene, and Gadget, see the individual struct documents in analysis/ghidra/structs/ and the markdown docs referenced above.


🔗 Related Documents

Odd Race

types : docs
keywords :

📂 View source on GitHub


Odd Race — Ball Shrink/Grow Mechanic

Overview

The Odd Race (menu position 9) features a unique mechanic where the player's ball shrinks to half size when entering a shrink zone, then grows back when reaching a grow zone. This is used in the pipe maze section of the level.

Level File Mapping

Menu Name Board Constructor MESHWORLD File Scene Setup Function
Odd Race LevelBoard_Odd_ctor @ 0x0041ED80 Level6.MESHWORLD Scene_SetupLevel6 @ 0x0040EA90

Note: The file is named Level6.MESHWORLD but the board is LevelBoard_Odd. The numbering schemes don't match — file numbers are internal, board numbers are menu order. The Odd board's vtable (0x4D0BC0, slot +0x48) points to Scene_SetupLevel6 which loads levels\level6.

The Shrink/Grow Sequence

Three collision events drive the mechanic

All three are event-name-triggered (E: prefix) collision geometry inside Level6.MESHWORLD:

Event Name Handler Code What It Does
E:SHRINK 0x0040EF80 in OddCollisionEvents Shrinks the ball
E:GROWSOUND 0x0040F04E in OddCollisionEvents Plays grow sound (100-frame cooldown)
E:GROW 0x0040F09E in OddCollisionEvents Restores ball to full size

E:SHRINK — Ball_Shrink (0x00402200)

void __fastcall Ball_Shrink(int ball) {
    ball[0xC4C] = 1;           // is_shrunk flag = ON (ball is shrunk)
    ball[0x284] = 0x41500000;  // radius = 13.0 (was 26.0)
    ball[0x188] = 0x40200000;  // physics_scale = 2.5 (was 5.0)
    Sound_Play3D(board->sound_device->fall_sound, ball->pos);
}

Then the Odd handler:

  1. Looks up SHRINKCENTER refpoint position from the hash table
  2. Teleports the ball there: (-285.2, -2210.5, -1068.8)
  3. Sets downward trajectory: velocity = (0.0, -1.0, 0.0) (Y=-1.0, pushes ball down)

E:GROWSOUND — Sound Only

if (ball[0x1FE] == 0) {  // cooldown counter
    Sound_PlayChannel(board->sound_device->grow_sound);
}
ball[0x1FE] = 100;  // 100-frame cooldown (4 seconds at 25fps)

E:GROW — Ball_Grow (0x00402270)

void __fastcall Ball_Grow(int ball) {
    ball[0xC4C] = 0;           // is_shrunk flag = OFF (ball is normal)
    ball[0x284] = 0x41D00000;  // radius = 26.0 (restored)
    ball[0x188] = 0x40A00000;  // physics_scale = 5.0 (restored)
}

Field Changes Summary

Ball Offset Field Name Normal Value Shrunk Value Ratio
+0xC4C is_shrunk (byte) 0 1
+0x284 radius (float) 26.0 13.0 50%
+0x188 physics_scale (float) 5.0 2.5 50%

Physics Effects of Shrunk State

The is_shrunk flag (ball+0xC4C) gates several behaviors in Ball_Update (0x00405E00):

1. Speed Penalty (line ~858)

When the ball exceeds max speed and is shrunk:

if (speed > max_speed) {
    speed = max_speed;
    if (ball[0xC4C] != 0) {      // is shrunk
        speed *= 0.25;           // _DAT_004cf380 = 0.25 → 75% speed reduction
    }
}

The shrunk ball moves at 25% of max speed — significantly slower than normal.

2. Camera Skip (line ~797)

When the ball is on a fast-moving surface and shrunk:

if (speed > 1.0 && ball[0xC4C] == 0) {  // only when NOT shrunk
    ball[0x2E9] = 1;           // on_ramp flag
    Scene_SetCamera(ball->board, ball, 1);
    // ... viewport setup
}

Camera tracking is skipped when shrunk — prevents camera jerk during teleport.

3. Bumper Counter Skip (line ~839)

if (bumper_count != 0 && ball[0xC4C] == 0 && speed >= threshold) {
    bumper_count++;  // only increments when NOT shrunk
}

4. Ball_FallUpdate (vtable[65], 0x00408830)

When is_falling=1, the game calls Ball_FallUpdate instead of normal physics. This function:

  • Copies ball+0x188 (physics_scale) to physics_obj+0xC70
  • Uses ball+0x284 (radius) for collision detection via SpatialTree
  • Decays ball+0xC60 (a float, starts at 1.0, decremented by 0.02/frame) — when it hits 0, sets ball+0x2E8=1 (event flag, likely triggers respawn/end)

Rendering Effects

Ball_Render (0x00402DE0)

The ball's visual scale is derived from ball+0x284 (radius):

// Mesh scale = radius * 0.037037 (1/27)
vtable[0x38](ball->radius * _DAT_004cf39c);  // Gfx_Scale = 26.0/27 ≈ 0.963

// Shadow scale = radius * 0.028571 (1/35) * int_factor
Sprite_RenderQuad(..., ball->radius * _DAT_004cf398 * _DAT_004cf390, ...);

When shrunk: 13.0/27 ≈ 0.481 — the ball renders at half visual size.

Refpoint Locations (Level6.MESHWORLD)

Refpoint Position (X, Y, Z) Purpose
SHRINKCENTER (-285.2, -2210.5, -1068.8) Where ball teleports after shrinking
JUMPPIPE1 (-848.5, -1770.7, -645.6) First jump pipe exit (Y velocity = +16.0)
JUMPPIPE2 (-566.3, -2113.6, -645.6) Second jump pipe exit (Y velocity = +16.0)
PIPERANDOM1 (-379.8, -2620.7, -1825.9) Random pipe destination 1
PIPERANDOM2 (-268.2, -2598.9, -1805.9) Random pipe destination 2

Complete Odd Collision Event List

The Odd board's collision dispatch handler is at 0x0040ED30 (vtable[0x1D], +0x74).

Event Effect
E:GRAVITY(TYPE) Changes gravity: NORMAL/X/Z
N:JUMPFIRST Teleport to JUMPPIPE1, launch up (Y=+16.0)
N:JUMPSECOND Teleport to JUMPPIPE2, launch up (Y=+16.0)
E:SHRINK Shrink ball to 50% size, teleport to SHRINKCENTER
E:GROWSOUND Play grow sound (100-frame cooldown)
E:GROW Restore ball to full size
E:DROPLIFT Play break sound on mesh at board+0x436C
E:PIPERANDOM Random teleport to PIPERANDOM1 or PIPERANDOM2
E:LIMIT Qualify for limit gate (axis 0)
E:LIMITX Qualify for limit gate (axis 1)
E:LIMITZ Qualify for limit gate (axis 2)
E:LIMITPIPE1 Qualify for limit (pipe 1 flag)
E:LIMITPIPE2 Qualify for limit (pipe 2 flag)
E:SWALLOW Set swallow flag (ball+0xBA = 1)

Related Functions

Function Address Role
Ball_Shrink 0x00402200 Shrinks ball (radius 26→13, physics 5→2.5)
Ball_Grow 0x00402270 Restores ball (radius 13→26, physics 2.5→5)
Ball_FallUpdate 0x00408830 Physics update when shrunk (vtable[65])
Ball_Update 0x00405E00 Normal physics (checks is_falling flag)
Ball_Render 0x00402DE0 Visual scaling from radius
OddCollisionEvents 0x0040ED30 Event dispatch for Odd race
LevelBoard_Odd_ctor 0x0041ED80 Odd board constructor
Scene_SetupLevel6 0x0040EA90 Loads levels\level6 MESHWORLD for Odd

🔗 Related Documents

Options Menu

types : modding
keywords :

📂 View source on GitHub


Hamsterball Options Menu: Adding New Sliders and Decimal-Display Values

Scope: Original Hamsterball.exe (PE32, i386, Athena engine, VS2003).
Method: Direct Ghidra decompilation and PE disassembly.
Last Updated: 2026-06-14


1. Overview

The options menu is implemented as a UIList-based menu (SimpleMenu subclass).
Each entry is a normal UIListItem with:

  • A display label shown to the player (e.g. "SOUND VOLUME:").
  • A short ID code used internally to identify which entry is selected/acted on (e.g. "SV", "MV", "TQ", "MS").

The four existing slider-like items are:

Label ID Staging value offset Dirty flag Applies to
SOUND VOLUME: SV OptionsMenu + 0xD10 +0xD1C SoundDevice + 0x838
MUSIC VOLUME: MV OptionsMenu + 0xD48 +0xD54 MusicDevice + 0x8
TEXTURE QUALITY: TQ OptionsMenu + 0xD80 +0xD8C App + 0x174 graphics object, offset +0x184
MOUSE SENSITIVITY: MS OptionsMenu + 0xDF0 +0xDFC App + 0x84C

Goal of this document: show how to add a new slider entry, make it respond to left/right input, clamp and store its value, apply it to a subsystem, persist it to the registry, and display a decimal numeric readout such as FOV: 0.7 instead of a plain bar.


2. Key Functions and Addresses

Function Address Purpose
OptionsMenu_ctor 0x00442CE0 Builds the options list with UIList_AddItem calls.
OptionsMenu_AdjustVolume 0x00442680 Receives (id_code, delta) and updates the matching staging float.
GraphicsOptionsMenu_Update 0x00441E70 Applies staged values to real subsystems each frame.
UIList_AddItem 0x004497F0 Adds one menu entry. Signature: void __thiscall UIList_AddItem(void* this, char* display_text, char* id_code, ... int is_clickable).
UIList_SetTextByName 0x0044A8B0 Updates the display text of an existing item by its ID code.
AthenaString_SprintfToBuffer 0x004BAE43 printf-family formatter; supports %d, %s, and %f / %.1f.
App_SaveAllConfig 0x004284C0 Saves persistent settings to the registry on exit.
App_Ctor / registry init 0x46DC40 region Reads saved values back into App offsets at startup.

3. How an Existing Slider Works

3.1 Value change

OptionsMenu_AdjustVolume (0x00442680) does the following for each known ID:

if (__stricmp(param_1, "SV") == 0) {
    float new_val = (float)param_2 * 0.1f + *(float*)(this + 0xD10);
    if (new_val > 1.0f) new_val = 1.0f;
    if (new_val < 0.0f) new_val = 0.0f;
    *(float*)(this + 0xD10) = new_val;
    *(BYTE*)(this + 0xD1C) = 1;   // dirty flag
}

Observed from disassembly:

  • The step constant 0.1 is the double at _DAT_004CF308 (0x004CF308).
  • The upper clamp is _DAT_004CF310 = 1.0f at 0x004CF310.
  • The lower clamp is _DAT_004CF368 = 0.0f at 0x004CF368.
  • param_2 is the signed delta: -1 for left, +1 for right (actual input routing passes a scaled value; see section 8).

3.2 Value application

GraphicsOptionsMenu_Update (0x00441E70) reads each dirty flag and writes the staged float to the real subsystem. For example:

if (*(BYTE*)(this + 0xD1C)) {
    *(float*)(sound_device + 0x838) = *(float*)(this + 0xD10);
    *(BYTE*)(this + 0xD1C) = 0;
}

3.3 Value persistence

App_SaveAllConfig (0x004284C0) writes the active subsystem value to the registry. Mouse sensitivity, for instance, is saved as a DWORD named "MouseSensitivity" from App + 0x84C.


4. Adding a Brand-New Slider

This example adds a hypothetical FOV slider (field-of-view scaling). Pick whatever subsystem you actually want to control.

4.1 Reserve storage in the OptionsMenu struct

Choose unused space after the existing sliders. Example offsets:

struct OptionsMenu_extra {
    // ... existing fields up to +0xDFC ...
    float fov_staging;      // +0xE10 (example — verify no overlap)
    BYTE  fov_dirty;        // +0xE14
};

Always verify the chosen offsets are not already used by OptionsMenu_RenderControls (0x0042E840) or by GraphicsOptionsMenu_Update (0x00441E70). Use Ghidra to check cross-references before patching.

4.2 Add the menu item

In OptionsMenu_ctor (0x00442CE0), add another UIList_AddItem call near the other sliders:

UIList_AddItem(this, "FOV:", "FOV",
               /* same trailing args as a neighboring slider */,
               /* param_8 = 1 if left/right should work on it */);

The ID code "FOV" is arbitrary but must be unique in this menu and must be uppercase if you want consistent __stricmp behavior.

4.3 Handle left/right changes

Patch OptionsMenu_AdjustVolume (0x00442680) with a new branch:

if (__stricmp(param_1, "FOV") == 0) {
    float new_val = (float)param_2 * 0.1f + *(float*)(this + 0xE10);
    if (new_val > 1.0f) new_val = 1.0f;
    if (new_val < 0.0f) new_val = 0.0f;
    *(float*)(this + 0xE10) = new_val;
    *(BYTE*)(this + 0xE14) = 1;
}

4.4 Apply the value each frame

In GraphicsOptionsMenu_Update (0x00441E70), add:

if (*(BYTE*)(this + 0xE14)) {
    float fov = *(float*)(this + 0xE10);
    // example: store in App + 0x???? or pass to graphics/camera code
    *(float*)(app_ptr + 0xYYY) = fov;
    *(BYTE*)(this + 0xE14) = 0;
}

4.5 Read and write the registry

In App_SaveAllConfig (0x004284C0) add a save line:

RegKey_WriteDWORD(app + 0x54, "FieldOfView",
                  (DWORD)(*(float*)(app + 0xYYY) * 100.0f));

And add a matching read in the startup path (near where MouseSensitivity is read) to restore the value.


5. Displaying a Decimal Value

The existing sliders render only a proportional bar. They do not display numbers. To show a decimal number you must update the menu item's text each time the value changes.

5.1 Required functions

  • AthenaString_SprintfToBuffer(local_1024_buf, "%s: %.1f", label, value) — formats the string.
  • UIList_SetTextByName(this, local_1024_buf, "FOV") — pushes the formatted text to the menu entry whose ID code is "FOV".

UIList_SetTextByName is at 0x0044A8B0. It frees the old text, allocates a copy, and updates the item. It is safe to call every time the value changes.

5.2 Patch OptionsMenu_AdjustVolume to update the label

After the new "FOV" branch, immediately refresh the text:

if (__stricmp(param_1, "FOV") == 0) {
    float new_val = (float)param_2 * 0.1f + *(float*)(this + 0xE10);
    if (new_val > 1.0f) new_val = 1.0f;
    if (new_val < 0.0f) new_val = 0.0f;
    *(float*)(this + 0xE10) = new_val;
    *(BYTE*)(this + 0xE14) = 1;

    char buf[1024];
    AthenaString_SprintfToBuffer(buf, "FOV: %.1f", new_val);
    UIList_SetTextByName(this, buf, "FOV");
}

This is the minimal, safe way to make the slider show a decimal readout.

5.3 Initialize the label in OptionsMenu_ctor

When the menu is first built, the text is still "FOV:". Set it to the current value as well:

char buf[1024];
float current_fov = *(float*)(this + 0xE10);
AthenaString_SprintfToBuffer(buf, "FOV: %.1f", current_fov);
UIList_SetTextByName(this, buf, "FOV");

Place this right after the existing UIList_SetTextByName block (lines 100–112 of OptionsMenu_ctor in the decompilation).

5.4 Decimal precision

Use the normal printf precision specifiers:

Format string Output example Use case
"%.0f" FOV: 7 Whole numbers
"%.1f" FOV: 0.7 One decimal place
"%.2f" FOV: 0.75 Two decimal places

The engine uses the standard CRT vsprintf (CRT_vsprintf at 0x004BC768), so all normal float formatting works.


6. UI Layout Considerations

UIList_AddItem allocates a UIListItem of size 0x444 bytes. The item stores:

  • display text pointer (+0x00)
  • id code pointer (+0x04)
  • measured width/height
  • selection state

The list itself is anchored at:

  • OptionsMenu + 0x44C — item list
  • OptionsMenu + 0x88C — secondary list / iterator backing
  • OptionsMenu + 0x864 — current selection index

When you add a new item, UIList_Layout (0x00449D40) and UIList_Render (0x00449C20) will automatically include it. No manual layout math is required.


7. Persisting the New Setting

The registry path is:

HKEY_CURRENT_USER\Software\Raptisoft\Hamsterball

For a new float slider, the easiest compatible approach is to scale it to an integer and store as REG_DWORD:

// Save: value is 0.0..1.0, store as 0..100
DWORD scaled = (DWORD)(value * 100.0f);
RegKey_WriteDWORD(app + 0x54, "FieldOfView", scaled);

// Load: reverse the scale
DWORD raw = RegKey_ReadDWORD(app + 0x54, "FieldOfView", default_value);
float value = raw / 100.0f;

If you want to store a raw float, use Registry_SetValue (REG_BINARY) with 4 bytes, as the game does for the BestTime and Medals blobs. However, DWORD is simpler for human editing in regedit.


8. Left / Right Input Routing (what calls AdjustVolume)

The exact internal vtable dispatch is not fully decompiled here, but the practical path is:

  1. Player presses Left or Right while a slider item is selected.
  2. UIList_HandleKeyNav / UIList_ActivateCurrentItem (0x00449750) decides whether the current item is a left/right slider.
  3. For slider items, it eventually calls into the menu's override of the input-handler vtable slot, which forwards to:
    • OptionsMenu_AdjustVolume(this, id_code, delta) at 0x00442680.
  4. OptionsMenu_AdjustVolume updates the staging value and dirty flag.

For modding, the only function you need to edit is OptionsMenu_AdjustVolume. As long as your new ID code is unique and your item is added with the slider flag set, the existing routing will call your new branch automatically.


9. Build and Test

After editing the source patch or binary:

make clean && make

Or if you are patching the binary directly with Ghidra:

  1. Apply patches in Ghidra.
  2. Export the new PE.
  3. Replace Hamsterball.exe and test on Wine or Windows.
  4. Open Options menu, move selection to the new slider, press Left/Right, and confirm the decimal text updates.

10. Quick Reference: Useful Global Constants

Address Interpretation Value
_DAT_004CF308 (0x004CF308) Slider step as double 0.1
_DAT_004CF310 (0x004CF310) Slider maximum as float 1.0
_DAT_004CF368 (0x004CF368) Slider minimum as float 0.0
0x004D5EA4 String "TQ" ID code for texture quality
0x004D5EA8 String "MS" ID code for mouse sensitivity
0x004D5EAC String "MV" ID code for music volume
0x004D5EB0 String "SV" ID code for sound volume

11. Example: Complete New Slider Patch Summary

This is the high-level change list for adding a FOV decimal slider:

  1. OptionsMenu_ctor (0x00442CE0):

    • Add UIList_AddItem(this, "FOV:", "FOV", ...);
    • Add AthenaString_SprintfToBuffer(buf, "FOV: %.1f", current); UIList_SetTextByName(this, buf, "FOV");
  2. OptionsMenu_AdjustVolume (0x00442680):

    • Add __stricmp(id, "FOV") branch with clamp and dirty flag.
    • Call AthenaString_SprintfToBuffer + UIList_SetTextByName to refresh the text.
  3. GraphicsOptionsMenu_Update (0x00441E70):

    • Add dirty-flag branch that copies OptionsMenu + 0xE10 to App + 0xYYY.
  4. App_SaveAllConfig (0x004284C0):

    • Add RegKey_WriteDWORD(..., "FieldOfView", scaled).
  5. Startup read path (near MouseSensitivity read):

    • Add RegKey_ReadDWORD(..., "FieldOfView") and write to App + 0xYYY.

That is the full recipe.


🔗 Related Documents

Package Manifest - 01-BOOTS

types : agent-knowledge

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/MANIFEST.md)


Package Manifest\n\n- 01-BOOTSTRAP.md\n- 02-GHIDRA-SETUP.md\n- 03-RENAME-RESTORE.md\n- 04-DECOMP-WORKFLOW.md\n- 05-STRUCT-VERIFICATION.md\n- 06-MODDING-PATTERNS.md\n- 07-REIMPL-LESSONS.md\n- 08-TROUBLESHOOTING.md\n- A1-THISCALL.md\n- A2-OBJECT-SPAWNING.md\n- A3-CAMERA-PITFALLS.md\n- INDEX.md\n- README.md\n- scripts/check_server.py\n- scripts/verify_struct_offsets.py\n- templates/DecompileByAddress.java\n- templates/DecompileMultiAddresses.java


🔗 Related Documents

Particle & Trail System

types : physics
keywords :

📂 View source on GitHub


Particle & Trail System

Overview

Hamsterball uses a simple particle system for trail effects behind the ball.
The only particle emitter is Ball_CreateTrailParticles (0x401DD0), which spawns
small ArenaScoreParticle objects in a circular pattern around the ball.

Ball_CreateTrailParticles (0x401DD0)

Creates 9 trail particles arranged in a circle (iterates local_50 from 0 to 0x168
in steps of 0x28, giving 9 particles: 0x168/0x28 = 9).

Algorithm

for each of 9 positions (angle_step = 0x28 in local_50):
  1. Get camera matrix from Graphics (via scene->graphics)
     cam_right = (fVar3..fVar5) from graphics->0x744+0x5c (3 floats)
     cam_up    = (fVar6..fVar8) from graphics->0x744+0x68 (3 floats)
  
  2. Calculate angle: sin(Wave_Sin(local_50)), cos(Wave_Cos(local_50))
  
  3. Offset = ball_radius * (sin*cos_right + cos*cos_up)
     offset_right = radius * cam_right * sin_angle
     offset_up    = radius * cam_up * cos_angle
  
  4. Create ArenaScoreParticle particle (0x28 bytes):
     ArenaScoreParticle_ctor(ball_scene)
     
  5. Particle position = ball_pos + offset_right - offset_up
     position[0] = offset_right.x + ball.pos.x - offset_up.x
     position[1] = offset_right.y + ball.pos.y - offset_up.y
     position[2] = offset_right.z + ball.pos.z - offset_up.z
  
  6. Particle velocity = offset_right - offset_up (normalized)
     velocity = (offset_right - offset_up) * (1.0 / (RNG_Rand(0x14) + 20))
     // _DAT_004cf310 = 1.0f division factor
  
  7. Append to scene->0x3b00 (particle list)

App Struct Offsets

Offset Type Description
ball+0x14 Scene* Scene pointer
ball+0x164 Vec3 Ball position (x,y,z)
ball+0x284 float Ball radius (27.0f default)
scene+0x878 void* Graphics subsystem
scene+0x3b00 AthenaList Particle list head
graphics+0x744 void* Camera matrix base

ArenaScoreParticle Particle Struct (0x28 bytes)

Used for both ArenaBoard score popup effect and ball trail particles.

Offset Type Field
0x00 vtable* Virtual table pointer
0x08 float position.x
0x0C float position.y
0x10 float position.z
0x14 float velocity.x
0x18 float velocity.y
0x1C float velocity.z
... ... Additional fields (lifetime, color, etc.)

FlagWaver System

Water ripples are a mesh deformation system, not a particle system.

Functions

Address Name Description
0x46A820 FlagWaver_dtor Destructor
0x46A8A0 FlagWaver_AllocBuffers Allocate vertex buffers
0x46A930 FlagWaver_AdvancePhase Advance ripple phase
0x46A940 FlagWaver_DeletingDtor Deleting destructor
0x46A960 FlagWaver_UpdateVertices Deform mesh vertices based on ripple
0x46AF30 FlagWaver_Ctor Constructor
0x46B070 FlagWaver_Render Render ripple effect

How Water Ripples Work

  1. FlagWaver_Ctor initializes phase=0, allocates vertex buffer copies
  2. Each frame: FlagWaver_AdvancePhase increments phase counter
  3. FlagWaver_UpdateVertices applies concentric sine wave displacement to mesh Y coordinates
  4. FlagWaver_Render draws the deformed water mesh with alpha blending
  5. Ball_TestPlaneIntersection checks if ball is touching water surface

🔗 Related Documents

Physics System

types : physics
keywords :

📂 View source on GitHub


Hamsterball Physics System — Reverse Engineering Documentation

Decompiled from Hamsterball.exe (Athena Engine, PE32 i386, image base 0x400000)
Primary function: Ball_Update at 0x405E00 (9,442 bytes, 2,541 instructions)


1. Architecture Overview

The physics system is a single monolithic function (Ball_Update at 0x405E00) that runs every frame for every ball. It handles:

  • Timer decay and particle effects
  • Collision tree building + spatial queries
  • Collision response (3 types: floor/wall/ball-ball)
  • Force application with multipliers
  • Velocity integration and friction
  • Camera following
  • Facing angle computation
  • Spin physics
  • Teleport override

The function is called via vtable[4] (offset +0x10) from Scene_UpdateBallsAndState, wrapped by Ball_UpdateAndAI (0x408390) which adds AI targeting after the physics tick.


2. Ball State Machine

The ball has three operational modes:

Mode Flag Effect
Normal ball+0xC4C = 0 Full physics, camera follow, boost, friction
odd race Shrunk ball+0xC4C = 1 Reduced physics, no camera follow, 0.75× force, no boost
Launch ball+0x2F0 ≥ 100 Free trajectory for ~1.67s, no external forces

Ball_Shrink (0x402200)

Sets the odd race shrunk state:

  • ball+0xC4C = 1 (is_shrunk flag)
  • ball+0x284 = 13.0f (shrink radius — was 26.0f)
  • ball+0x188 = 2.5f (reduce max speed — was 5.0f)
  • Plays shrink sound from Scene+0x4D4

Ball_Grow (0x402270)

Restores normal state:

  • ball+0xC4C = 0 (clear is_shrunk)
  • ball+0x284 = 26.0f (restore radius)
  • ball+0x188 = 5.0f (restore max speed)

Ball_ApplyTrajectory (0x403750)

Called when ball hits a boost ramp/launch surface:

  • Reads trajectory from physics_body+0xCA4/CA8/CAC (launch direction)
  • Normalizes trajectory, scales by _DAT_004CF3F0 = 0.5
  • Damps Y component: body+0xCA8 *= _DAT_004CF434 = 1.25 (adds vertical boost)
  • Sets impact_counter = 100 at ball+0x2F0 (prevents force application for ~1.67s)
  • Sets ball+0x14D = 1 (rotation dirty)
  • Plays boost sound, creates trail particles

3. Force Application System

Two force application functions, both with the same multiplier chain:

Ball_ApplyForceWithMultipliers (0x402650)

Primary force applier — used for player input and direct forces.

Ball_ApplyForceV2 (0x4016F0)

Secondary force applier — used for collision-derived forces. Adds tube check (complete zeroing).

Guard conditions (all must pass for force to apply):

Condition Offset Check
Not in tarpit ball+0x2F9 == 0
Force enabled ball+0x2CC == 0
Not frozen ball+0x808 == 0
Counter within limit ball+0x2F0 < 81

Force multipliers (applied in order to the magnitude parameter):

Condition Offset Multiplier Value Notes
Recent impact ball+0x2F0 ×_DAT_004CF380 ×0.25 First few frames after hit
In tube ball+0x324 ×_DAT_004CF378 ×0.0 Complete freeze in tubes (V2 only)
On ice ball+0xC5C ×_DAT_004CF374 ×0.2 Nearly zero on ice; also sets angular velocity ×6.0
Dizzy/falling ball+0xC4C ×_DAT_004CF36C ×0.75 25% reduction when falling

Velocity accumulation:

ball+0x170 += dir_x × magnitude
ball+0x174 += dir_y × magnitude
ball+0x178 += dir_z × magnitude

Facing direction (only when direction is non-zero):

  • ball+0x748 = 0 (XZ flat): angle = atan2(dir_x, dir_z)
  • ball+0x748 = 1 (tilted): angle = atan2(-dir_y, dir_z)
  • ball+0x748 = 2 (XY vertical): angle = atan2(dir_x, dir_y)
  • Store at ball+0x198, set ball+0x19C = 1

4. Collision System

Collision Types

Type Meaning Response
1 Ball-ball Push apart, score award, clack sound
2 Wall Reflect velocity, friction, spin update
5 Floor Camera follow, boost counter, slope gravity

Type 1: Ball-Ball Collision (at 0x406BD3)

When two balls collide:

  1. If ball+0x2EC (boost counter) > 1 and not on_ramp: Ball_ApplyTrajectory (launch)
  2. Compute push direction between ball centers
  3. Apply push via vtable[6] (SetPosition) on both balls
  4. Sound_Play3D at collision point
  5. Knockoff scoring: If one ball is heavier (ball+0x284), award Difficulty_GetTimeModifier(app, 500.0) points to the heavier ball's player
  6. Display "+%d" popup via AthenaString_Format at address 0x4CF500
  7. Set string timer to 200 frames (ball+0x0C = 0xC8)

Type 2: Wall Collision (at 0x406B80)

When ball hits a wall surface:

  1. Read collision normal from result struct (piVar16[8..10])
  2. Reflect velocity across normal: v_reflected = v - 2(v·n)n
  3. If on ice (ball+0x324 == 0): compute surface friction using the collision node's direction vector
  4. The direction vector from the collision node is used to:
    • Determine if the ball is moving "with" or "against" the surface
    • Apply friction proportional to the surface's physical properties
    • Update the ball's up-direction (ball+0x6A..0x6C) for ramp/slope handling

Key insight: The collision node's direction vector (CollisionNode+0x20..0x28 in the spatial tree) stores the surface's tangent/normal information. This is what makes the ball slide along walls instead of stopping dead.

Type 5: Floor Collision (at 0x407319)

When ball touches floor geometry:

  1. Check floor depth ([edi+0x54]) against threshold (_DAT_004CF420 ≈ 0.0)
  2. If NOT shrunk (ball+0xC4C == 0):
    • Set ball+0x2E9 = 1 (on_ramp flag)
    • Scene_SetCamera(ball, 1) — camera follows ball
    • Graphics_SetViewport — update camera viewport
  3. If player (ball+0x18 != -1):
    • Boost counter logic: increment ball+0x2EC when touching floor
    • OOB detection: compare ball position against viewport bounds
    • If out of bounds: set ball+0x2E8 = 1 (fell off flag)

When shrunk (ball+0xC4C = 1):

  • SKIPS Scene_SetCamera call — camera doesn't follow falling ball
  • SKIPS on_ramp flag set — no ramp detection during fall
  • SKIPS boost counter increment — can't charge boost while falling
  • Jump target: 0x40743D which goes to player_index check

How is_shrunk Affects Collision Direction

The collision normal is NOT modified when shrunk. The collision tree is built and queried identically regardless of the is_shrunk flag. The direction vector flows through the same reflection/friction code. The is_shrunk flag only affects:

  1. Camera: No Scene_SetCamera call when shrunk (camera stays at last position)
  2. Boost: No boost counter accumulation when shrunk
  3. Friction: Different spin friction calculation (0.25× multiplier at 0x4CF380 when shrunk vs normal friction)
  4. Force: 0.75× multiplier on all applied forces when shrunk

5. Gravity and Velocity Integration

Original Game Flow (Ball_Update at 0x405E00)

The original does NOT use simple Euler integration. The flow is:

  1. Save previous position: prev_pos = pos (ball+0x158..0x160 = ball+0x164..0x16C)
  2. Clear external velocity: ball+0x170..0x178 = 0
  3. Build collision tree: SpatialTree_ctor with gravity scale from ball+0x278
  4. Query collision results: Scene vtable[1] with AABB from pos ± radius
  5. Process collision results (types 1, 2, 5)
  6. Compute velocity from position delta: velocity = pos - prev_pos
  7. Apply external forces: pos += external_velocity
  8. Clear external velocity: ball+0x170..0x178 = 0 again
  9. Facing angle: atan2 from velocity components
  10. Spin physics: 3 iterations of friction/accumulation
  11. Display position lerp: display_pos += (pos - display_pos) × lerp_factor
  12. Teleport override: If ball+0xC3C, override pos from ball+0xC40..0xC48

Critical difference from simple Euler: The original computes velocity AFTER collision resolution, not before. This means the velocity stored at ball+0x170..0x178 is actually the RESULT of collision processing, not an input to it. The collision tree determines where the ball ends up, and velocity is derived from the position change.

Reimpl Physics Flow (UpdatePhysics in win32_main.c)

The reimpl uses standard semi-implicit Euler:

  1. Apply gravity: vy -= gravity * 500 * dt
  2. Apply input force: vx/vz += input * force_scale * dt
  3. Apply damping: vx/vz *= friction
  4. Integrate position: pos += vel * dt
  5. Resolve collisions: push out + reflect velocity
  6. Slope gravity: vx/vz += gravity * normal × dt

Key discrepancy: The reimpl applies forces BEFORE collision, the original applies them AFTER. This is why the reimpl can "kill momentum" during jumps — the collision response removes the normal component of velocity that was added by input forces, while the original never adds input forces to a velocity that would be immediately removed by collision.


6. Airborne Physics

How the Ball Goes Airborne

In the original game, there is NO explicit "jump" mechanic. The ball goes airborne through:

  1. Rolling off an edge: Ball passes beyond floor geometry → no type-5 results → gravity accumulates
  2. Launch ramp: Ball_ApplyTrajectory sets impact_counter = 100 and applies trajectory vector
  3. Ball-ball collision: Knockoff sends ball upward with reflected velocity
  4. Slope/ramp transition: Ball rolls up a ramp, normal changes, velocity points up

What Happens While Airborne

When the ball is NOT touching any floor surface:

  • No type-5 collision results → floor processing doesn't fire
  • Gravity accumulates through the collision tree's gravity_scale parameter
  • Camera stops following (Scene_SetCamera not called without type-5)
  • Input forces still apply at 0.75× if dizzy, full if not
  • Friction changes: No ground friction, only air friction (0.85× per frame = _DAT_004CF4C0)

The "Momentum Kill" Issue

The problem in the reimpl: when the ball is airborne and moving horizontally, the collision response code treats any contact with geometry as a "wall hit" and reflects the velocity. This can kill horizontal momentum if the ball clips a ledge or edge while airborne.

In the original, this doesn't happen because:

  1. Forces are applied AFTER collision resolution
  2. The collision tree computes the final position directly
  3. The ball's velocity is DERIVED from the position change, not used as an input
  4. The collision node's direction vector guides the ball along surfaces, not just reflecting

7. Key Physics Constants (Verified from .rdata)

Address Value Name Usage
0x4CF310 1.0 UNIT_VALUE General purpose 1.0 constant
0x4CF368 0.0 GROUND_THRESHOLD Floor detection threshold
0x4CF36C 0.75 DIZZY_MULT Force multiplier when is_shrunk (odd race)
0x4CF374 0.2 ON_ICE_MULT Force multiplier on ice surfaces
0x4CF378 0.0 IN_TUBE_MULT Force multiplier in tube sections
0x4CF380 0.25 FIRST_FRAME_MULT Force multiplier on first frame / after impact
0x4CF3E8 6.0 ICE_ANGULAR_SCALE Angular velocity scale on ice
0x4CF3F0 0.5 LAUNCH_TRAJECTORY_SCALE Launch direction normalization scale
0x4CF418 3.0 SPEED_ACCUM_WRAP Speed gauge wrap value
0x4CF434 1.25 Y_DAMP Vertical velocity damping on launch
0x4CF4C0 0.85 SPEED_FRICTION Per-frame velocity friction
0x4CF520 0.025 LAUNCH_DIR_SCALE Launch direction scaling
0x4CF540 0.98 SPIN_DECAY_MULT Per-frame spin timer decay

8. Ball Struct Layout (Key Physics Fields)

Byte Offset Decomp Index Type Name Description
+0x0C param_1[3] int string_timer Countdown (200=show). Frees display string at 0
+0x10 param_1[4] ptr app App pointer
+0x14 param_1[5] ptr scene Scene pointer
+0x18 param_1[6] int player_index -1 = AI, 0+ = human
+0x158 param_1[0x56] Vec3 prev_pos Previous frame position
+0x164 param_1[0x59] Vec3 pos Current position
+0x170 param_1[0x5C] Vec3 vel Velocity (cleared each frame, recomputed)
+0x188 param_1[0x62] float max_speed 5000.0 (normal) / 2.5 (shrunk, odd race)
+0x198 param_1[0x66] float facing_angle Target rotation angle
+0x19C param_1[0x67] byte facing_dirty 1 = rotation needs update
+0x1A4 param_1[0x69] ptr physics_body Scene physics body (trajectory, friction)
+0x284 param_1[0xA1] float radius 26.0 (normal) / 13.0 (shrunk, odd race)
+0x2E8 byte fell_off Ball fell off level edge
+0x2E9 byte on_ramp Ball is on a ramp/slope surface
+0x2F0 param_1[0xBC] int impact_counter ≥100 = in launch, ≥81 = no force
+0x2F9 byte in_tarpit Tarpit state (blocks force)
+0x313 byte is_8ball 1 = AI 8-ball (dispatches differently)
+0x324 param_1[0xC9] byte in_tube In tube section (zeroes force)
+0x748 param_1[0x1D2] int gravity_plane 0=XZ flat, 1=tilted, 2=XY vertical
+0x808 int freeze_counter >0 = frozen, no force
+0xC4C byte is_shrunk 1 = odd race shrunk state (E:SHRINK)
+0xC50 param_1[0x314] float spin_timer Decayed by 0.98× per frame
+0xC5C param_1[0x317] int momentum_transfer Flag for ice momentum
+0xC3C param_1[0x30F] byte teleport_active 1 = teleport pending
+0xC40 param_1[0x310] Vec3 teleport_dest Teleport destination

9. Function Call Graph (Ball_Update)

Ball_Update (0x405E00)
├── Sound_Play3DAtPosition (0x458EE0) ×2
├── operator_new (0x4BA57B) ×4
│   ├── ArenaScoreParticle_ctor (0x44AD50)
│   ├── SpatialTree_ctor (0x463330)
│   └── CollisionNode_ctor (0x466CF0)
├── RNG_Rand (0x45DD60) ×4
├── AthenaList_Append (0x453810/0x453780) ×4
├── Difficulty_GetTimeModifier (0x428ED0) ×5
├── AthenaString_Format (0x466C70) ×2
├── __ftol2 (0x4BA754) ×5
├── Mesh_FindClosestCollision (0x465D90) ×2
├── Scene_SetCamera (0x419FA0) ×1
├── Graphics_SetViewport (0x454B50) ×1
├── Math_Atan2Angle (0x457DE0) ×4
├── Scene_CheckPath (0x457EC0) ×1
├── Wave_Sin (0x457DA0) ×1
├── Wave_Cos (0x457DC0) ×1
├── Collision_GradientEval_Stub (0x458190) ×1
├── Sound_CalculateDistanceAttenuation (0x466750) ×1
├── Vec3List_Free (0x453250) ×2
├── _free (0x4BA576) ×2
├── Gfx_RotateY (0x457BB0) ×1
├── Timer_Init (0x457A40) ×1
├── AthenaList_Init/Clear ×6
└── [vtable indirect calls] ×8
    ├── vtable[1] (QueryCollision)
    ├── vtable[5] (BuildSpatialTree)
    ├── vtable[6] (SetPosition)
    ├── vtable[8] (SpecialAction/Teleport)
    ├── vtable[0x34] (RenderCallback)
    └── vtable[0x74] (SceneCallback)

10. Known Issues in Reimpl Physics

  1. Momentum kill on airborne collision: Reimpl applies forces before collision, original applies after. This causes horizontal momentum to be lost when the ball clips geometry while airborne.

  2. No collision tree: Reimpl uses sphere-vs-triangle direct tests. Original uses SpatialTree + CollisionNode hierarchy with direction vectors that guide the ball along surfaces.

  3. Simple velocity reflection: Reimpl reflects velocity with fixed bounce=0.3. Original uses the collision node's direction vector for smooth sliding along walls.

  4. No is_shrunk state: Reimpl has no ball+0xC4C equivalent. The odd race shrink mechanic (Ball_Shrink/Ball_Grow) is not implemented.

  5. No launch trajectory system: Reimpl has no Ball_ApplyTrajectory equivalent. Boost ramps and launch surfaces are not implemented.

  6. Gravity before collision: Reimpl applies gravity at the start of the frame, original integrates it through the collision tree's gravity_scale parameter.


🔗 Related Documents

Player Clones Mod (v13)

types : mods
keywords :

📂 View source on GitHub


Player Clones Mod (v13)

Overview

Spawns AI-controlled clone balls for any player slot (1-4) in both races and arenas.
Clones chase and attack all entities everywhere on the track, regardless of Y level or position.

Hotkeys

  • 1 — Spawn Player 1 clone
  • 2 — Spawn Player 2 clone
  • 3 — Spawn Player 3 clone
  • 4 — Spawn Player 4 clone

Features

  • Works in both races and arenas
  • Clones chase all entities everywhere (no "going home" behavior)
  • Can spawn P2-P4 clones even when original players don't exist (uses P1 as appearance source)
  • Per-player color via copied appearance from source ball
  • Up to 8 simultaneous clones
  • Automatic cleanup of dead/stale clones

What v13 Fixes (vs V8/V11)

  1. "Going home" bug — Clear 0x2E8 (respawn needed flag) every frame. This prevents Ball_FindClosestRespawnPoint from teleporting clones back to spawn.
  2. "Not chasing" bug — Clear ALL force guards every frame: 0x2F9 (falling), 0x2CC (block), 0x808 (state), 0x2F0 (impact count). V8 only cleared 0x2F8 and 0x808.
  3. "All black" in arenas — Copy render context index (0x154), render mode flags (0x748, 0x182), and render state flags (0x6FC, 0x700, 0x708, 0x70D, 0x734) from source ball. Without these, the clone uses an uninitialized render context.
  4. P2-4 spawning — P1 fallback already worked in V8. Added 0x310=1 and 0x29C=1.0 at spawn to prevent respawn behavior.
  5. "Nothing spawned" in races — Clear all guard flags at spawn time, not just in the AI loop.

How It Works

Hook Point

0x0041B540Scene_UpdateBallsAndState. Called every frame for both races and arenas.

AI Function

Calls 0x004222D0 (Computer AI) with ball swap trick:

  1. Temporarily replaces player table entry with clone pointer
  2. Sets control mode to 0x63 (AI)
  3. Calls AI function which finds nearest target in scene+0x29D4 list
  4. Computes direction force toward target
  5. Restores original player table entry
  6. Applies force via vtable[5] (Ball_ApplyForceWithMultipliers)

Force Guard Clearing

Every frame, the AI loop clears these flags on each clone:

Offset Name Why Clear
0x2E8 respawn_needed Prevents teleport to respawn point ("going home")
0x2E9 sticky_limit Prevents stuck-on-limit behavior
0x2F8 respawn_in_progress Prevents respawn sequence
0x2F9 falling Allows force application while "falling"
0x2CC block Allows force application while "blocked"
0x808 state Must be 0 for AI to process ball
0x2F0 impact_count Must be < 0x51 (81) for force to apply

CHASE/HOME Values

Set to 99999999.0 (0x4CBEBC20) at spawn and every frame. These are used by the 8ball AI path if activated.

File

  • Script: player_clones_CE_script.CEA

🔗 Related Documents

Player Collision Detection

types : physics
keywords :

📂 View source on GitHub


Player Collision Detection — Detecting When a Player Gets Bumped

Overview

This document covers every viable approach for detecting when the player
ball collides with a badball (NPC 8-ball) or another player ball in Hamsterball.
Each approach is traced from Ghidra decompilation of the original binary,
with exact addresses, struct offsets, trade-offs, and working code patterns.

Table of Contents

  1. How the Original Game Detects Ball-Ball Collisions
  2. Ball Struct Reference
  3. Collision Entry Struct Layout
  4. Vtable Layout (Player Ball vs 8-Ball)
  5. Approach 1: Code Cave Hook (Proven — Existing Mod)
  6. Approach 2: Detour on Ball_Update Entry
  7. Approach 3: Vtable Patching (Swap vtable[4])
  8. Approach 4: Background Polling Thread
  9. Approach 5: Hook the Impulse Function (vtable[6])
  10. Approach 6: Hook Collision Event Dispatchers
  11. Approach 7: Custom Distance Check (No Hooks)
  12. Approach 8: Reimplementation
  13. Decision Matrix: Which Approach to Use
  14. Pitfalls and Lessons Learned

1. How the Original Game Detects Ball-Ball Collisions

Call Chain

Scene_UpdateBallsAndState (0x41B540)
  │  Iterates Scene+0x29D4 (Ball AthenaList)
  │  For each ball: calls vtable[4] (Ball_Update for player, AI tick for 8-ball)
  │
  └─ Ball_Update (0x405E00) — player ball per-frame physics tick
       │  23-phase physics pipeline
       │
       ├─ Phase: Build collision tree
       │    CollisionMesh vtable[6] (0x456140) → builds collision result list
       │    at PhysicsBody+0x18 (AthenaList of collision entries)
       │
       ├─ Phase: Iterate collision results (0x406B77–0x407178)
       │    Loop: walk collision entry array at PhysicsBody+0x424
       │    Each entry (EBP/piVar16):
       │      [EBP+0x00] == 2 → wall collision (type 2)
       │      [EBP+0x00] == 1 → ball-ball collision (type 1) ← TARGET
       │
       └─ For type==1 (ball-ball):
            0x406C90: MOV EDI, [EBP+0x0C]     ; EDI = OTHER BALL pointer
            0x406C93: FLD [ESI+0x16C]         ; this ball Z
            0x406C99: FLD [ESI+0x168]         ; this ball Y
            0x406CA5: FLD [ESI+0x164]         ; this ball X
            0x406CB1: FLD [EDI+0x164]         ; other ball X
            ... direction = (this - other) × 0.025, normalize ...
            0x406DA6: CALL [EDX+0x18]         ; ApplyForce to OTHER ball (push away)
            0x406DBD: CALL [EDX+0x18]         ; ApplyForce to THIS ball (push away)
            0x406FD1: FLD [EDI+0x284]         ; speed comparison begins
            0x406FF7: CMP [ESI+0x18], -1     ; is THIS ball a player?
            0x407009: CALL Difficulty_GetTimeModifier(scene, 500.0)
            ... add 500 to player score ...
            0x407094: FLD [EDI+0x284]         ; reverse speed comparison
            0x4070BA: CMP [EDI+0x18], -1      ; is OTHER ball a player?
            ... mirror scoring for reverse case ...

Key Insight: Symmetric Double-Fire

Ball_Update runs for both balls in a collision pair. When Player 1 bumps
an 8-ball, the collision is detected from both perspectives:

  • During Player 1's Ball_Update: ESI=Player1, EDI=8ball
  • During 8-ball's Ball_Update: ESI=8ball, EDI=Player1

This means any hook at the collision point fires twice per collision event.
You must deduplicate — either by pointer comparison (ESI < EDI to count once)
or by tracking collision IDs ([EBP+0x64]).


2. Ball Struct Reference

Key Offsets for Collision Detection

Offset Type Field Description
+0x10 void* scene_ptr (sound) Used for Sound_Play3D (Scene+0x43C)
+0x14 void* scene_ptr (collision) Used for Mesh_FindClosestCollision (Scene+0x8B0)
+0x18 int32 player_index -1 = NPC/badball, 0-3 = Player 1-4
+0x164 float pos_x Current world position X
+0x168 float pos_y Current world position Y
+0x16C float pos_z Current world position Z
+0x170 float vel_x Velocity accumulator X (force)
+0x174 float vel_y Velocity accumulator Y (force)
+0x178 float vel_z Velocity accumulator Z (force)
+0x1A4 void* physics_body CollisionMesh/PhysicsBody pointer
+0x284 float radius Collision radius (default ~27.0)
+0x2E8 byte respawn_flag Set during respawn handling
+0x2E9 byte impact_shatter Sticky flag (E:LIMIT collision) — NEVER use for ground check
+0x2EC int32 collision_state Counter (0 = none, 1 = active, 2+ = depth)
+0x2F0 int32 force_count Number of forces applied this frame
+0x2F4 int32 dizzy_immunity_timer Dizzy immunity timer (int32, frames remaining)
+0x2F9 byte frozen Ball is frozen/stuck
+0x2FC int32 fall_timer Countdown when falling
+0x324 byte in_tube Skip collision processing when set
+0x748 int32 gravity_axis 0=Y, 1=X, 2=Z (gravity plane)
+0x808 int32 teleport_active Non-zero = teleport in progress
+0x810 AthenaList path_list Ball path tracking
+0xC28 char* popup_text Score popup string (freed each frame)
+0xC2C char[] section_filter Current collision section name

Scene Offsets for Ball Access

Offset Type Field Description
Scene+0x178 void* (App→Scene pointer is at App+0x178)
Scene+0x29D4 AthenaList balls Player balls list
Scene+0x29D8 int32 ball_count Number of player balls
Scene+0x2DE0 void** ball_array Array of ball pointers
Scene+0x3204 AthenaList eight_balls NPC 8-ball list
Scene+0x3208 int32 eight_ball_count Number of 8-balls
Scene+0x3610 void** eight_ball_array Array of 8-ball pointers
Scene+0x8B0 void* collision_level CollisionLevel for raycasts
Scene+0x43C void* sound_manager Sound system

Global Access

void* app   = *(void**)0x005341E0;           // App global
void* scene = *(void**)((char*)app + 0x178); // App+0x178 = Scene

3. Collision Entry Struct Layout

Each collision entry is accessed via EBP (or piVar16 in decompiled C).
The struct is allocated as 32 bytes (8 DWORDs) via operator_new(0x20).

CollisionEntry {
    +0x00 (int32)  type           // 1 = ball-ball, 2 = wall, 5 = floor
    +0x04 (???)    padding
    +0x08 (???)    padding
    +0x0C (void*)  other_ball     // When type==1: pointer to the other ball
    +0x10–0x1C     ???            // Unknown fields
    +0x20 (float)  normal_x       // Surface normal X (collision response)
    +0x24 (float)  normal_y       // Surface normal Y
    +0x28 (float)  normal_z       // Surface normal Z
    +0x2C (float)  collision_pt   // Collision point or distance
    +0x30 (float)  normal2_x      // Secondary normal components
    +0x34 (float)  normal2_y
    +0x38 (float)  normal2_z
    +0x3C–0x60     ???            // Additional collision data
    +0x64 (int32)  collision_id   // Dedup token (compared against EBP register)
}

Decoded from Decompiled Code

// From Ball_Update decompilation (lines 486–624):
while (piVar16 != NULL) {
    if (*piVar16 == 2) {                    // type == 2: wall collision
        if (piVar16[0x19] == unaff_EBP) {   // collision_id matches
            // Store ball position as "last wall contact" (ball+0x2DC)
        }
    }
    if (*piVar16 == 1) {                   // type == 1: ball-ball collision
        if (piVar16[0x19] == unaff_EBP) {  // collision_id matches
            // Apply trajectory boost (catapult-like effect)
            // Check gravity plane and call vtable[8] (bounce)
        }
        if (*piVar16 == 1) {               // Still type 1 (redundant check)
            piVar2 = (int*)piVar16[3];     // other_ball = entry[3] = +0x0C

            // Calculate direction between balls
            dir_y = (ball[0x5A] - other[0x5A]) * 0.025;  // × position_scale
            dir_x = (ball[0x59] - other[0x59]) * 0.025;
            dir_z = (ball[0x5B] - other[0x5B]) * 0.025;

            // Normalize direction, clamp to minimum 3.0
            magnitude = sqrt(dir_x² + dir_y² + dir_z²);
            if (magnitude > 0 && magnitude < 3.0)
                scale = 3.0 / magnitude;
            else
                scale = 1.0;

            // Apply impulse to OTHER ball (push away)
            (*other->vtable[6])(other, -dir_x*scale, -dir_z*scale, -dir_y*scale, 1.0);

            // Apply impulse to THIS ball (push away)
            (*this->vtable[6])(this, dir_x*scale, dir_z*scale, dir_y*scale, dir_x*scale);

            // Sound effect (3D positioned)
            Sound_Play3D(scene->sound_mgr, ball_x, ball_y, ball_z);

            // Scoring: if THIS ball is faster, OTHER ball gets bumped
            if (other->radius < this->radius * 0.7) {
                (*other->vtable[8])(other);  // bounce callback
                if (this->player_index != -1) {
                    // Add 500 score × difficulty modifier
                    score = Difficulty_GetTimeModifier(scene, 500.0);
                    app_score[player_index] += score;
                    this->popup_text = format("+%d", score);
                    this->freeze_counter = 200;
                }
            }

            // Scoring: reverse — if OTHER ball is faster, THIS ball gets bumped
            if (this->radius < other->radius * 0.7) {
                (*this->vtable[8])(this);   // bounce callback
                if (other->player_index != -1) {
                    score = Difficulty_GetTimeModifier(scene, 500.0);
                    app_score[other->player_index] += score;
                    other->popup_text = format("+%d", score);
                    other->freeze_counter = 200;
                }
            }
        }
    }
    // Advance to next collision entry
    ...
}

Collision Constants

Address Value Description
0x4CF520 0.025 Position scale factor for direction calculation
0x4CF508 0.7 (double) Speed comparison multiplier (radius ratio threshold)
0x4CF418 3.0 Minimum collision direction magnitude
0x4CF518 -0.5 (double) Direction dot-product threshold for sound vs silent hit
0x4CF510 0.04 Surface angle threshold
0x4CF48C 2.0 Surface speed threshold (for sound playback)
0x4CF3E0 0.5 (double) Speed factor
0x4CF3C8 1.0 (double) Comparison threshold
0x4CF308 0.1 (double) Impact multiplier
0x4CF380 0.25 First-frame force damping
0x4CF378 0.0 In-tube force damping (zeroes all force)
0x4CF374 0.2 On-ice force damping
0x4CF36C 0.75 Dizzy force damping

4. Vtable Layout (Player Ball vs 8-Ball)

Player Ball Vtable at 0x4CF314

Index Offset Address Function
0 +0x00 0x402A50 Destructor
1 +0x04 0x4015B0 One-time init (SetupCollisionRender)
2 +0x08 0x403DC0 Unknown
3 +0x0C 0x402A70 Unknown
4 +0x10 0x405E00 Ball_Update (per-frame physics tick)
5 +0x14 0x401590 Unknown
6 +0x18 0x4016F0 Ball_ApplyForceV2 (gravity-plane-aware force)
7 +0x1C 0x402C10 Unknown
8 +0x20 0x409050 Bounce callback (called on significant hit)

8-Ball Vtable at 0x4CF3A0

Index Offset Address Function
0 +0x00 0x4027F0 Destructor
1 +0x04 0x405100 One-time init (InitPhysicsDefaults)
2 +0x08 0x402DE0 Unknown
3 +0x0C 0x402A70 Unknown (shared with player)
4 +0x10 0x408390 AI tick (per-frame, calls Ball_Update internally)
5 +0x14 0x401590 Unknown (shared with player)
6 +0x18 0x402650 Ball_ApplyForceWithMultipliers (simpler force)
7 +0x1C 0x402C10 Unknown (shared with player)
8 +0x20 0x409480 Bounce callback (8-ball version)

Important Notes

  • vtable[4] is the per-frame update. For player balls it's Ball_Update
    (0x405E00). For 8-balls it's the AI tick (0x408390), which internally calls
    Ball_Update (at 0x4083BD: CALL 0x405E00). Both run the collision loop.

  • vtable[6] is the impulse/force function. Both player and 8-ball versions
    accumulate velocity at +0x170/+0x174/+0x178 and apply multipliers based on
    ball state (frozen, dizzy, in-tube, on-ice). The player version
    (Ball_ApplyForceV2, 0x4016F0) has gravity-plane-awareness for facing angle
    computation. The 8-ball version (Ball_ApplyForceWithMultipliers, 0x402650)
    uses different scale constants but the same core logic.

  • vtable[8] is the bounce callback, called when a collision is "significant"
    (the other ball's radius × 0.7 is smaller than this ball's radius). This is
    the "you got bumped hard" indicator.


Approach 1: Code Cave Hook

Status: PROVEN — Working mod exists (mods/8ball_hit_detect/)

Concept

Patch a JMP instruction at a strategic point inside Ball_Update's collision
loop to redirect execution to a hand-assembled code cave. The cave checks ball
types, records the hit, then executes the original instruction and jumps back.

Hook Point: 0x406FD1

Original instruction: FLD DWORD [EDI+0x284]  (6 bytes: D9 87 84 02 00 00)

At this point in the code:

  • ESI = this ball (the one running Ball_Update)
  • EDI = other ball (collision partner, loaded at 0x406C90)
  • Both balls are confirmed colliding (type==1 check passed)
  • Impulse forces have already been applied (0x406DA6, 0x406DBD)
  • Speed comparison is about to begin

This is the scoring section — the game is about to check which ball was
moving faster and award score. Perfect for detection.

Code Cave Logic

; Entry: ESI = this ball, EDI = other ball
PUSHAD

MOV EAX, [ESI+0x18]      ; this ball's player_index
MOV EBX, [EDI+0x18]      ; other ball's player_index

; Check: is this a player→8-ball collision?
CMP EAX, 0xFFFFFFFF       ; is this ball an 8-ball?
JE  .check_case2          ; yes → check if other is player
CMP EBX, 0xFFFFFFFF       ; is other ball an 8-ball?
JNE .done                 ; both are players → skip (use different handler)
JMP .hit_detected         ; this=player, other=8-ball → HIT

.check_case2:
CMP EBX, 0xFFFFFFFF       ; is other ball also 8-ball?
JE  .done                 ; both 8-balls → skip
; this=8-ball, other=player → HIT (player got bumped)

.hit_detected:
INC DWORD [g_hit_count]   ; increment counter
; Store player_index for polling thread
CMP EAX, 0xFFFFFFFF
JNE .esi_is_player
MOV ECX, EBX              ; EDI is the player
JMP .set_flag
.esi_is_player:
MOV ECX, EAX             ; ESI is the player
.set_flag:
INC ECX                   ; convert to 1-based (0 = no pending)
MOV DWORD [g_hit_pending], ECX

.done:
POPAD
; Execute original instruction
FLD DWORD [EDI+0x284]
; Jump back to 0x406FD1 + 6
JMP 0x00406FD7

Implementation Pattern

// Code cave is assembled at runtime into VirtualAlloc'd memory.
// The hook site is patched with: JMP <cave> + NOP
// A background thread polls g_hit_pending and writes to hitlog.txt.
//
// CRITICAL: Never call C functions from the code cave itself.
// FPU/stack corruption crashes the game. Use the volatile flag + polling
// thread pattern (see Pitfalls section).

static volatile DWORD g_hit_count = 0;
static volatile DWORD g_hit_pending = 0;  // 0 = none, 1-4 = player index + 1

Alternative Hook Points

Address Instruction Context Best For
0x406FD1 FLD [EDI+0x284] After impulse, before scoring Hit detection + scoring
0x406C90 MOV EDI, [EBP+0x0C] Other ball just identified Modify impulse before it applies
0x406BD3 CMP [EBP], 0x1 Type check (earliest) Filter collision pairs before physics
0x406DA6 CALL [EDX+0x18] First impulse call (to other ball) Intercept/modify force direction
0x406DBD CALL [EDX+0x18] Second impulse call (to this ball) Intercept/modify force direction

Pros

  • Direct access to both ball pointers at collision time
  • Minimal overhead (a few comparisons + one memory write)
  • Can read/modify any ball field (position, velocity, radius, player_index)
  • Can modify the collision response (knockback, score, sound)
  • Proven working in production mod

Cons

  • Requires hand-assembled x86 machine code (error-prone)
  • Must not call C functions from the cave (FPU/stack corruption)
  • Symmetric double-fire: fires for both balls in a pair (need dedup)
  • Hook site is hardcoded to specific game version (byte signature check required)
  • Cannot easily add complex logic (use volatile flag + polling thread instead)

Working Reference

  • Source: mods/8ball_hit_detect/8ball_hit_detect.c
  • Build: i686-w64-mingw32-gcc -shared -o bass.dll 8ball_hit_detect.c -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc -Wl,--add-stdcall-alias
  • Install: Rename original bass.dllbass_real.dll, copy proxy bass.dll

Approach 2: Detour on Ball_Update Entry

Status: Theoretical (pattern proven in tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/)

Concept

Hook the entry point of Ball_Update (0x405E00) with a standard 5-byte JMP
detour. Your C function runs before the physics tick, giving you a chance to
inspect ball state. After your function returns, the original Ball_Update
runs normally via a trampoline.

Implementation

typedef void (__fastcall *BallUpdate_t)(void *ball);
static BallUpdate_t g_orig_Ball_Update = NULL;
static unsigned char g_trampoline[16];

// Previous frame positions for delta detection
static float g_prev_x[4] = {0};  // per player
static float g_prev_y[4] = {0};
static float g_prev_z[4] = {0};

void __fastcall hook_Ball_Update(void *ball, void *edx_dummy) {
    // Read ball state BEFORE update
    int player_idx = *(int*)((char*)ball + 0x18);
    float x = *(float*)((char*)ball + 0x164);
    float y = *(float*)((char*)ball + 0x168);
    float z = *(float*)((char*)ball + 0x16C);

    if (player_idx >= 0 && player_idx < 4) {
        // After update, the collision list at ball+0x1A4 will have results
        // We can check them in a POST-update hook
    }

    // Call original
    g_orig_Ball_Update(ball, NULL);

    // POST-update: check collision results
    if (player_idx >= 0) {
        void *physics = *(void**)((char*)ball + 0x1A4);
        if (physics) {
            // Read collision list at physics+0x18
            int count = *(int*)((char*)physics + 0x1C);
            void **entries = *(void**)((char*)physics + 0x424);
            for (int i = 0; i < count && entries; i++) {
                int *entry = (int*)entries[i];
                if (entry && entry[0] == 1) {  // type == 1 (ball-ball)
                    void *other = (void*)entry[3];  // other ball
                    if (other) {
                        int other_idx = *(int*)((char*)other + 0x18);
                        printf("Player %d hit by ball %d (idx=%d)\n",
                               player_idx, other_idx != -1 ? other_idx+1 : -1);
                    }
                }
            }
        }
    }
}

Hook Installation (5-byte JMP detour)

static int install_detour(void *target, void *hook, unsigned char *trampoline) {
    DWORD oldProtect;
    unsigned char *t = (unsigned char *)target;

    VirtualProtect(t, 16, PAGE_EXECUTE_READWRITE, &oldProtect);

    // Copy original bytes to trampoline
    memcpy(trampoline, t, 5);  // copy first 5 bytes
    // Append JMP back to target+5
    trampoline[5] = 0xE9;
    *(unsigned long*)(trampoline + 6) =
        (unsigned long)((char*)target + 5 - (char*)(trampoline + 5) - 5);

    // Make trampoline executable
    DWORD tp;
    VirtualProtect(trampoline, 16, PAGE_EXECUTE_READWRITE, &tp);

    // Overwrite target: JMP rel32
    unsigned long rel = (unsigned long)((char*)hook - (char*)target - 5);
    t[0] = 0xE9;
    *(unsigned long*)(t + 1) = rel;

    FlushInstructionCache(GetCurrentProcess(), target, 5);
    return 1;
}

Pros

  • Can inspect state both BEFORE and AFTER the physics tick
  • Written in C (no hand-assembled machine code)
  • Can call C library functions (printf, file I/O) safely
  • Can access the full collision result list after Ball_Update processes it
  • Clean uninstall (restore original bytes)

Cons

  • The collision list at physics+0x18 may be in an inconsistent state
    post-update (some entries may have been processed and cleared)
  • The AthenaList iteration uses an internal index at physics+0x408 that
    wraps at 256 — reading it externally may miss entries or get stale data
  • Trampoline requires at least 5 bytes of safe overwrite space at the
    function entry (must verify no relative jumps in first 5 bytes)
  • Post-update hook fires for ALL balls, not just collision pairs — you must
    scan the collision list yourself to find ball-ball hits
  • Thread safety: the hook runs on the game thread, so blocking operations
    (file I/O, sleeps) will stutter the game

Working Reference

  • Pattern: tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/collision_hook.c (hooks event dispatchers
    using the same detour technique, but on DispatchCollisionEvents/TowerCollisionEvents/
    ExpertCollisionEvents instead of Ball_Update)

Approach 3: Vtable Patching

Status: Theoretical (pattern proven in ball-ai-clone-system)

Concept

The Ball vtable is stored in .rdata at a fixed address. By patching the
vtable entry for vtable[4] (the per-frame update), you can redirect all
ball updates to your custom function. Your function can wrap the original
and add collision detection logic.

Vtable Addresses

Player Ball vtable: 0x4CF314  (vtable[4] at 0x4CF324 = 0x405E00)
8-Ball vtable:      0x4CF3A0  (vtable[4] at 0x4CF3B0 = 0x408390)

Implementation

// Save original vtable[4] values
static DWORD g_orig_player_v4 = 0;
static DWORD g_orig_8ball_v4 = 0;

// Custom update wrapper
void __fastcall custom_ball_update(void *ball, void *edx_dummy) {
    // Pre-update: save state
    int player_idx = *(int*)((char*)ball + 0x18);
    float prev_vel[3] = {
        *(float*)((char*)ball + 0x170),
        *(float*)((char*)ball + 0x174),
        *(float*)((char*)ball + 0x178)
    };

    // Call original update
    // NOTE: Must use the original function pointer, not vtable lookup
    // (because we patched the vtable)
    ((void (__fastcall *)(void*))g_orig_player_v4)(ball);

    // Post-update: check for velocity changes (collision signature)
    float new_vel[3] = {
        *(float*)((char*)ball + 0x170),
        *(float*)((char*)ball + 0x174),
        *(float*)((char*)ball + 0x178)
    };

    // Large velocity delta = collision impulse was applied
    float dx = new_vel[0] - prev_vel[0];
    float dy = new_vel[1] - prev_vel[1];
    float dz = new_vel[2] - prev_vel[2];
    float delta = sqrtf(dx*dx + dy*dy + dz*dz);

    if (delta > 5.0f && player_idx >= 0) {
        // Player was bumped!
        // Scan collision list for details
        void *physics = *(void**)((char*)ball + 0x1A4);
        if (physics) {
            int count = *(int*)((char*)physics + 0x1C);
            // ... iterate collision entries ...
        }
    }
}

void install_vtable_patch(void) {
    DWORD oldProtect;

    // Patch player ball vtable[4]
    DWORD *player_vt = (DWORD*)0x4CF324;  // vtable+0x10
    VirtualProtect(player_vt, 4, PAGE_READWRITE, &oldProtect);
    g_orig_player_v4 = *player_vt;
    *player_vt = (DWORD)custom_ball_update;
    VirtualProtect(player_vt, 4, oldProtect, &oldProtect);

    // Patch 8-ball vtable[4] (if needed)
    DWORD *ball8_vt = (DWORD*)0x4CF3B0;  // 8-ball vtable+0x10
    VirtualProtect(ball8_vt, 4, PAGE_READWRITE, &oldProtect);
    g_orig_8ball_v4 = *ball8_vt;
    *ball8_vt = (DWORD)custom_ball_update;  // or separate 8-ball handler
    VirtualProtect(ball8_vt, 4, oldProtect, &oldProtect);
}

Pros

  • Cleanest hook: no code modification, just data (vtable pointer swap)
  • Can intercept ALL ball updates with a single patch
  • Fully written in C
  • Easy to uninstall (restore original vtable values)
  • Can add pre/post processing around the original update
  • Does not need to know the calling convention of the original function
    (it's always __thiscall via vtable dispatch)

Cons

  • The vtable is in .rdata — VirtualProtect is needed to make it writable
  • If the game creates balls AFTER your patch, the new balls will use the
    patched vtable automatically (good) but you must ensure your wrapper
    handles all ball types correctly
  • The wrapper must not call C library functions that disturb the FPU state
    (the original Ball_Update uses heavy x87 FPU operations)
  • Velocity-delta detection is an indirect signal — it can't distinguish
    between a collision impulse and a catapult/trajectory boost
  • The vtable may be copied per-ball-instance at construction time — need
    to verify whether all balls share the same vtable pointer or have copies
  • 8-ball AI tick (0x408390) calls Ball_Update internally — if you patch
    both vtables, the 8-ball's call to Ball_Update will go through your wrapper
    TWICE (once for the AI tick wrapper, once for the internal Ball_Update call).
    Must handle this with a re-entrancy guard.

Working Reference

  • Pattern: ball-ai-clone-system (skill hamsterball-re reference
    ball-ai-clone-system.md) — creates custom vtables for AI clones by
    copying the player vtable and replacing specific slots

Approach 4: Background Polling Thread

Status: Theoretical

Concept

Run a background thread that periodically reads the ball positions from the
Scene's ball lists and performs distance-based collision detection. This
requires no code hooks at all — just memory reads.

Implementation

static DWORD WINAPI collision_poll_thread(LPVOID param) {
    (void)param;
    Sleep(5000);  // Wait for game to fully load

    void *app = *(void**)0x005341E0;
    if (!app) return 1;

    void *scene = *(void**)((char*)app + 0x178);
    if (!scene) return 1;

    // Previous positions for all balls
    float prev_pos[8][3] = {0};
    int prev_count = 0;

    while (1) {
        // Read player ball list
        int p_count = *(int*)((char*)scene + 0x29D8);
        void **p_balls = *(void***)((char*)scene + 0x2DE0);

        // Read 8-ball list
        int b_count = *(int*)((char*)scene + 0x3208);
        void **b_balls = *(void***)((char*)scene + 0x3610);

        // Check all player vs 8-ball pairs
        for (int p = 0; p < p_count && p_balls; p++) {
            void *pball = p_balls[p];
            if (!pball || IsBadReadPtr(pball, 0x200)) continue;

            float px = *(float*)((char*)pball + 0x164);
            float py = *(float*)((char*)pball + 0x168);
            float pz = *(float*)((char*)pball + 0x16C);
            float pr = *(float*)((char*)pball + 0x284);

            for (int b = 0; b < b_count && b_balls; b++) {
                void *bball = b_balls[b];
                if (!bball || IsBadReadPtr(bball, 0x200)) continue;

                float bx = *(float*)((char*)bball + 0x164);
                float by = *(float*)((char*)bball + 0x168);
                float bz = *(float*)((char*)bball + 0x16C);
                float br = *(float*)((char*)bball + 0x284);

                float dx = px - bx, dy = py - by, dz = pz - bz;
                float dist = sqrtf(dx*dx + dy*dy + dz*dz);

                if (dist < pr + br) {
                    // Collision detected!
                    printf("Player %d hit 8-ball %d (dist=%.1f)\n",
                           p, b, dist);
                }
            }

            // Also check player vs player
            for (int p2 = p+1; p2 < p_count && p_balls; p2++) {
                void *pball2 = p_balls[p2];
                if (!pball2 || IsBadReadPtr(pball2, 0x200)) continue;

                float p2x = *(float*)((char*)pball2 + 0x164);
                float p2y = *(float*)((char*)pball2 + 0x168);
                float p2z = *(float*)((char*)pball2 + 0x16C);
                float p2r = *(float*)((char*)pball2 + 0x284);

                float dx = px - p2x, dy = py - p2y, dz = pz - p2z;
                float dist = sqrtf(dx*dx + dy*dy + dz*dz);

                if (dist < pr + p2r) {
                    printf("Player %d hit Player %d (dist=%.1f)\n",
                           p, p2, dist);
                }
            }
        }

        Sleep(16);  // ~60Hz polling
    }
    return 0;
}

Pros

  • No code hooks whatsoever — pure memory reads
  • Cannot crash the game (read-only access)
  • Can detect ALL collision types (player-ball, player-player, ball-ball)
  • Works with any game version (only depends on struct offsets)
  • Can run at any frequency (60Hz, 30Hz, etc.)
  • Can read additional ball state (velocity, radius) for richer detection

Cons

  • Race condition risk: reading ball positions while the game thread is
    writing them can give inconsistent data (ball moved between X and Y reads)
  • No collision event data: you only get proximity, not the actual
    collision normal, impulse direction, or speed comparison
  • No pre/post state: you see the result after the physics tick, not the
    collision itself — you may miss fast collisions that resolve in one frame
  • Radius-based detection is approximate: the game's actual collision uses
    sphere-vs-triangle intersection with the collision mesh, not simple
    sphere-vs-sphere distance
  • Thread safety: IsBadReadPtr is technically deprecated and can cause
    issues; better to use SEH or VirtualQuery for safe reads
  • Memory overhead: polling at 60Hz with multiple balls creates CPU load
  • False positives: two balls being close doesn't mean they "collided" —
    they might be resting against each other. Need velocity or position-delta
    to distinguish a new collision from continuous contact

Best Use Case

When you need a quick, safe detection without modifying game code at all.
Good for statistics gathering, overlay displays, or simple trigger systems.


Approach 5: Hook the Impulse Function (vtable[6])

Status: Theoretical

Concept

Instead of hooking inside Ball_Update, hook the ApplyForce function
(vtable[6]) that is called during collision response. Every time a collision
impulse is applied, your hook runs and can inspect the direction and magnitude.

Addresses

Ball Type vtable[6] Address Function Name
Player 0x4016F0 Ball_ApplyForceV2
8-Ball 0x402650 Ball_ApplyForceWithMultipliers

Implementation

// Hook signature: void __thiscall ApplyForce(void *this, float dx, float dy, float dz, float multiplier)
typedef void (__fastcall *ApplyForce_t)(void *ball, void *edx, float dx, float dy, float dz, float mult);
static ApplyForce_t g_orig_ApplyForce = NULL;
static unsigned char g_trampoline[16];

void __fastcall hook_ApplyForce(void *ball, void *edx_dummy,
                                 float dx, float dy, float dz, float mult) {
    // This is called for EVERY force application, not just collisions.
    // Need to filter for collision impulses.
    //
    // Collision impulses have multiplier == 1.0 and are called in pairs
    // (one for each ball in the collision). The direction is normalized.
    //
    // Input forces (from player control) have different multipliers
    // (0.25 for first frame, 0.0 in tube, etc.)

    if (mult == 1.0f) {
        // Likely a collision impulse
        int player_idx = *(int*)((char*)ball + 0x18);
        float magnitude = sqrtf(dx*dx + dy*dy + dz*dz);

        if (magnitude > 1.0f && player_idx >= 0) {
            printf("Player %d received impulse (%.1f, %.1f, %.1f) mag=%.1f\n",
                   player_idx, dx, dy, dz, magnitude);
        }
    }

    // Call original
    g_orig_ApplyForce(ball, NULL, dx, dy, dz, mult);
}

Pros

  • Called at the exact moment of impulse application
  • Can modify the impulse direction/magnitude (custom knockback)
  • Can cancel the impulse entirely (no-clip mode)
  • Can distinguish between collision impulses (mult=1.0) and control forces
  • Written in C with standard detour technique

Cons

  • Very high call frequency: ApplyForce is called for EVERY force, not
    just collisions — player input, catapult boosts, trajectory launches,
    gravity adjustments all go through this function
  • Hard to distinguish collision impulses: the multiplier is 1.0 for
    collisions, but also for other forces. The direction is normalized for
    collisions but may be arbitrary for other forces
  • No other-ball context: the function only receives (this, dx, dy, dz, mult).
    You don't know WHICH ball caused the collision. To find the other ball,
    you'd need to scan the collision list separately
  • Asymmetric: only the ball receiving the force calls this function. You
    can't tell if it was a player-player or player-8ball collision from the
    call alone
  • May be called from non-collision paths: the 8-ball AI may call ApplyForce
    for movement, not just collisions

Best Use Case

When you need to modify collision response (e.g., custom knockback, immunity
frames, or amplified bump force) rather than just detect it.


Approach 6: Hook Collision Event Dispatchers

Status: PROVEN — Working tool exists (tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/)

Concept

Hook the three collision dispatch functions that the game calls when the ball
hits named collision objects (walls, events, triggers). These are NOT for
ball-ball collisions, but for level geometry and event trigger collisions.

Addresses

Address Function Domain
0x40C5D0 DispatchCollisionEvents Shared base handler (all events)
0x40DCD0 TowerCollisionEvents Race level events
0x40E6A0 ExpertCollisionEvents Arena events

Important Distinction

These dispatchers handle level object collisions (walls, catapults, jump
pads, trapdoors, etc.), NOT ball-ball collisions. The event name string at
collObj+0x864 identifies what was hit.

When to Use

  • Detect when a player hits a specific level object (e.g., "E:JUMP")
  • Log all collision events for debugging
  • Filter by event name (e.g., only "E:CATAPULTBOTTOM")

When NOT to Use

  • Ball-ball collision detection (these dispatchers are not called for that)
  • Detecting "player got bumped by badball" (use Approach 1 or 2 instead)

Working Reference

  • Source: tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/collision_hook.c
  • Build: i686-w64-mingw32-gcc -shared -o <a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>.dll collision_hook.c -Wl,--enable-stdcall-fixup
  • Inject: tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/injector.exe

Approach 7: Custom Distance Check (No Hooks)

Status: Theoretical (simplest possible approach)

Concept

In your existing per-frame render hook (slot 11), read all ball positions and
perform a simple sphere-vs-sphere distance check. This requires NO additional
hooks — just memory reads in your existing render callback.

Implementation (in your mod API framework)

void OnRender() {
    void* app = api->GetApp();
    if (!app) return;
    void* scene = *(void**)((char*)app + 0x178);
    if (!scene) return;

    int p_count = *(int*)((char*)scene + 0x29D8);
    void** p_balls = *(void***)((char*)scene + 0x2DE0);
    int b_count = *(int*)((char*)scene + 0x3208);
    void** b_balls = *(void***)((char*)scene + 0x3610);

    static float last_dist[4][16] = {0};  // [player][ball] previous distance
    static bool in_contact[4][16] = {false};

    for (int p = 0; p < p_count && p_balls; p++) {
        void* pball = p_balls[p];
        if (!pball) continue;

        float px = *(float*)((char*)pball + 0x164);
        float py = *(float*)((char*)pball + 0x168);
        float pz = *(float*)((char*)pball + 0x16C);
        float pr = *(float*)((char*)pball + 0x284);

        for (int b = 0; b < b_count && b_balls; b++) {
            void* bball = b_balls[b];
            if (!bball) continue;

            float bx = *(float*)((char*)bball + 0x164);
            float by = *(float*)((char*)bball + 0x168);
            float bz = *(float*)((char*)bball + 0x16C);
            float br = *(float*)((char*)bball + 0x284);

            float dx = px - bx, dy = py - by, dz = pz - bz;
            float dist = sqrtf(dx*dx + dy*dy + dz*dz);
            float threshold = pr + br;

            bool touching = (dist < threshold);

            // Only report NEW contacts (rising edge)
            if (touching && !in_contact[p][b]) {
                // Player p just bumped 8-ball b
                DrawTextOnScreen(font, "BUMP!", 300, 50, ...);
            }
            in_contact[p][b] = touching;
        }
    }
}

Pros

  • Simplest implementation — no hooks, no code caves, no vtable patching
  • Runs in your existing render hook — no new threads
  • Can draw visual feedback immediately (screen text, overlays)
  • Safe: read-only memory access, no game state modification
  • Can detect player-player and player-8ball collisions equally

Cons

  • Rising-edge detection only: you detect "just started touching", but you
    can't get the impulse direction, force magnitude, or collision normal
  • Radius is approximate: the game's actual collision uses the collision mesh
    (triangles), not simple sphere-sphere. Two balls may appear to be touching
    by distance but not actually collide in the game's physics
  • Frame-rate dependent: if two balls pass through each other in one frame
    (high speed), you may miss the collision entirely
  • No dedup with game's collision system: the game may have already resolved
    the collision (separated the balls) by the time your render hook runs

Best Use Case

Quick prototyping, visual feedback, or simple "bump counter" HUD overlays.


Approach 8: Reimplementation

Status: Partial — reimpl has basic ground collision but no ball-ball

Concept

In the Hamsterball reimplementation (reimpl/src/physics/physics.c), implement
ball-ball collision detection from scratch using sphere-sphere intersection.

Current State

The reimpl currently has:

  • A single g_ball struct (no multi-ball support)
  • Basic ground collision via get_ground_height() heightmap lookup
  • No ball-ball collision detection at all
  • No 8-ball AI or NPC balls

What Would Be Needed

typedef struct {
    Vec3 position;
    Vec3 velocity;
    float radius;
    int player_index;  // -1 = NPC, 0+ = player
    bool active;
} Ball;

#define MAX_BALLS 32
static Ball balls[MAX_BALLS];
static int ball_count = 0;

void check_ball_ball_collisions(void) {
    for (int i = 0; i < ball_count; i++) {
        for (int j = i + 1; j < ball_count; j++) {
            Ball *a = &balls[i];
            Ball *b = &balls[j];
            if (!a->active || !b->active) continue;

            float dx = b->position.x - a->position.x;
            float dy = b->position.y - a->position.y;
            float dz = b->position.z - a->position.z;
            float dist = sqrtf(dx*dx + dy*dy + dz*dz);
            float min_dist = a->radius + b->radius;

            if (dist < min_dist && dist > 0.0001f) {
                // Collision detected!
                float scale = (min_dist - dist) / dist * 0.5f;

                // Separate balls
                a->position.x -= dx * scale;
                a->position.y -= dy * scale;
                a->position.z -= dz * scale;
                b->position.x += dx * scale;
                b->position.y += dy * scale;
                b->position.z += dz * scale;

                // Calculate impulse direction (matching original game's 0.025 scale)
                float dir_x = (a->position.x - b->position.x) * 0.025f;
                float dir_y = (a->position.y - b->position.y) * 0.025f;
                float dir_z = (a->position.z - b->position.z) * 0.025f;
                float mag = sqrtf(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z);
                if (mag > 0 && mag < 3.0f) {
                    float s = 3.0f / mag;
                    dir_x *= s; dir_y *= s; dir_z *= s;
                }

                // Apply impulses
                a->velocity.x += dir_x;
                a->velocity.y += dir_y;
                a->velocity.z += dir_z;
                b->velocity.x -= dir_x;
                b->velocity.y -= dir_y;
                b->velocity.z -= dir_z;

                // Fire collision callback
                on_ball_collision(a, b);
            }
        }
    }
}

Pros

  • Full control over collision detection and response
  • Can implement custom collision rules (e.g., team-based, power-up effects)
  • No binary patching required
  • Can add detailed collision events (direction, force, contact point)

Cons

  • Requires multi-ball support in the reimpl first (currently single-ball)
  • Requires NPC 8-ball AI to exist in the reimpl (doesn't yet)
  • Must match the original game's collision feel (0.025 scale, 3.0 minimum, 0.7
    speed ratio for "significant hit")
  • Significant development effort

Decision Matrix

Approach Difficulty Reliability Can Modify Response Gets Other Ball Best For
1. Code Cave Hard High Yes (at hook point) Yes (EDI) Production mods, scoring
2. Detour Ball_Update Medium Medium Pre/post only Via collision list Analysis, logging
3. Vtable Patch Medium High Yes (wrapper) Via collision list AI mods, custom physics
4. Poll Thread Easy Low No Yes (position) Safe detection, stats
5. Hook ApplyForce Medium Low Yes (impulse) No Custom knockback
6. Event Dispatchers Easy High No No (level objects) Level event logging
7. Render Hook Dist Easy Medium No Yes (position) Quick prototyping
8. Reimpl Hard High Full Full Long-term, custom game

Recommendations

  • Just need to know when a bump happens: Approach 7 (render hook distance check)
  • Need reliable detection with logging: Approach 1 (code cave, proven in 8ball_hit_detect)
  • Need to modify collision response (knockback, immunity): Approach 5 (hook ApplyForce)
  • Need to intercept before physics runs: Approach 2 (detour Ball_Update)
  • Building a full mod with custom AI: Approach 3 (vtable patching)
  • Can't modify game code at all: Approach 4 (polling thread)

Pitfalls and Lessons Learned

1. Never Call C Functions from Hand-Assembled Code Caves

Problem: Calling log_hit(), snprintf(), or any C function from inside a
code cave in Ball_Update corrupts the FPU/stack/SEH state and crashes the game.

Root Cause: Ball_Update uses heavy x87 FPU operations. A CALL to a C
function disrupts the FPU stack alignment and may modify control words. The
PUSHAD/POPAD saves general registers but NOT the FPU state. Even
FNSAVE/FNRSTOR doesn't fully protect against SEH frame corruption.

Solution: Use the volatile flag + polling thread pattern:

  • Code cave sets a volatile DWORD g_hit_pending = player_index + 1
  • A background CreateThread loop checks g_hit_pending every 50ms
  • All file I/O and string formatting happens on the polling thread

Source: This lesson was learned the hard way during 8ball_hit_detect v4
development. See mods/8ball_hit_detect/8ball_hit_detect.c comments.

2. Symmetric Double-Fire

Problem: Ball_Update runs for both balls in a collision pair. Your hook
fires twice per collision event.

Solution: Use pointer comparison (ESI < EDI) to count each collision once,
or track the collision ID ([EBP+0x64]) and deduplicate.

3. Calling Convention: __thiscall via __fastcall

Problem: MinGW GCC doesn't support __thiscall directly on x86.

Solution: Use __fastcall with a dummy EDX parameter:

// __thiscall(this, arg1) == __fastcall(this, dummy_edx, arg1)
void __fastcall hook_func(void *this_, void *edx_dummy, void *arg1);

Both use ECX for this, and the callee cleans the stack.

4. AthenaList Iteration is Not Thread-Safe

Problem: The collision list at PhysicsBody+0x18 uses AthenaList_NextIndex
which increments an internal counter at +0x408 that wraps at 256. Reading this
list from a background thread may return stale or inconsistent data.

Solution: Either (a) hook inside Ball_Update where the list is valid, or
(b) use position-based detection (Approach 4/7) which doesn't depend on the
collision list.

5. ball+0x2E9 is NOT a Ground/On-Surface Flag

Problem: The impact_shatter at ball+0x2E9 looks like it indicates
surface contact, but it's actually a sticky flag set by E:LIMIT (arena
boundary) collisions. It's never cleared within Ball_Update.

Solution: For ground detection, use Mesh_FindClosestCollision
(0x465D90) raycasting as documented in docs/RAYCASTING_FOR_DLL_MODS.md.

6. Verify Original Bytes Before Patching

Problem: Different game versions may have different bytes at the hook site.

Solution: Always verify the original bytes match before installing a hook:

if (memcmp(hook_addr, expected_bytes, byte_count) != 0) {
    log("ERROR: Hook site bytes mismatch — wrong game version?");
    return;
}

7. Velocity Fields are Force Accumulators, Not Direct Velocity

Problem: Writing directly to ball+0x170/174/178 (velocity) REPLACES the
accumulated force, killing horizontal momentum.

Solution: Use FLD/FADD/FSTP to ADD to the accumulator:

FLD DWORD [ESI+0x174]    ; load current Y velocity
FADD <impulse>           ; add impulse
FSTP DWORD [ESI+0x174]  ; store result

8. The 8-Ball AI Tick Calls Ball_Update

The 8-ball AI tick at 0x408390 calls Ball_Update (0x405E00) at offset
0x4083BD (CALL 0x405E00). This means:

  • If you hook Ball_Update, your hook runs for both player balls and 8-balls
    (because the 8-ball AI internally calls through to Ball_Update)
  • If you patch both vtable[4] entries, the 8-ball's internal call to Ball_Update
    goes through your wrapper TWICE

Use a re-entrancy guard:

static __thread int in_update = 0;
void __fastcall hook_Ball_Update(void *ball, void *edx) {
    if (in_update) {
        g_orig_Ball_Update(ball, NULL);
        return;
    }
    in_update = 1;
    // ... your logic ...
    g_orig_Ball_Update(ball, NULL);
    // ... post-update logic ...
    in_update = 0;
}

9. Font Pointer Not Available at DLL Load

Problem: If you cache App+0x318 (font pointer) at DLL load time, it will
be NULL because the font hasn't been created yet.

Solution: Read App+0x318 fresh every frame in your render hook. The
font is valid once any scene (menu or level) has loaded.

10. UI_DrawTextCentered Does NOT Auto-Center

Despite its name, 0x409C60 does not automatically center text on screen.
The "centered" refers to vertical centering within the glyph cell. You must
calculate the text width and compute the X position yourself. See
docs/UI_TEXT_ELEMENTS.md for the GetTextWidth() implementation.


References

  • mods/8ball_hit_detect/ — Working code cave hook mod (proven)
  • tools/<a href="#63393331680655" title="collision_hook" class="record-link ">collision_hook</a>/ — Working detour hook tool for event dispatchers
  • docs/COLLISION_SYSTEM.md — Collision mesh, octree, and physics pipeline
  • docs/COLLISION_SYSTEM_DEEP.md — Deep dive on collision structs
  • docs/COLLISION_EVENT_SYSTEM.md — Event dispatcher chain (level/arena events)
  • docs/BALL_OBJECT.md — Ball struct field reference
  • docs/BALL_UPDATE_DECOMP.md — Ball_Update decompilation annotations
  • docs/RAYCASTING_FOR_DLL_MODS.md — Raycast pattern for ground/wall detection
  • docs/ARENA_SCORING.mdarena scoring system (ArenaBoard, score overflow)
  • Skill hamsterball-re reference ball-ai-clone-system.md — Vtable patching for AI clones
  • Skill hamsterball-re reference ball-ball-collision-hook.mdcollision hook addresses
  • Skill hamsterball-re reference hamsterball-dll-modding.md — DLL mod build patterns

🔗 Related Documents

Race Start Hooking Guide

types : agent-knowledge
keywords :

📂 [View source on GitHub](https://github.com/evangit2/hamsterball-re/blob/master/docs/agent-knowledge/race-start-hooking.md)


Race Start Hooking Guide

How to Execute Code When a Race Starts

This document maps the complete race-start initialization pipeline and
identifies every hookable function where modders can execute custom code
at the moment a player loads into a race — after balls and objects are
instantiated, but before the countdown finishes.

All addresses, calling conventions, and field offsets were verified via
GhidraMCP decompilation and disassembly of Hamsterball.exe (V3.6.c).


Race-Start Pipeline (Call Chain)

App_Start*Race (0x4288B0 / 0x4289F0 / 0x428B20 / 0x428C50 / 0x429230)
  ├─ App_StartRace (0x4287C0)                    ← Reset old scene, free old objects
  │    └─ Scene_UpdateChildren (0x466AC0)        ← Destroy children of old scene
  ├─ Create PlayerProfile (operator_new + ctor)
  └─ Tournament_AdvanceRace (0x427080)            ← param_1='\x01' = show menu only
       │
       └─ [Player waits at TourneyMenu, timer counts up]
       │
       └─ TourneyMenu_Tick (0x450860)              ← Timer expires
            ├─ (*menu->vtable[16])()               ← Destroy menu
            └─ Tournament_AdvanceRace (0x427080)   ← param_1='\x00' = CREATE BOARD
                 ├─ switch(race_index):
                 │    case 1: operator_new(0x436C) → LevelBoard_WarmUp_ctor
                 │    case 2: operator_new(0x644C) → LevelBoard_Beginner_ctor
                 │    case 3: operator_new(0x438C) → LevelBoard_Intermediate_ctor
                 │    ... (15 cases total)
                 │
                 │  Each ctor:
                 │    └─ Board_ctor (0x419030)
                 │         └─ Scene_CtorBase (0x457FE0)
                 │
                 ├─ (*board->vtable[18])(board)    ← Level-specific load (OVERRIDDEN per level)
                 │    │
                 │    │  Example: Scene_LoadLevel2 (0x40D280) — loads "levels\\level2":
                 │    │    ├─ MeshWorld_ctor (0x461510)         ← Load .MESHWORLD geometry
                 │    │    ├─ CollisionLevel_ctorWithLevel (0x465080) ← Load collision data
                 │    │    ├─ Level_InitScene (0x40B090)        ← Sound, camera, graphics setup
                 │    │    │    ├─ SoundChannel_Ctor
                 │    │    │    ├─ Level_SelectCameraProfile
                 │    │    │    ├─ Graphics_SetCullMode, SetProjection, SetViewportZ
                 │    │    │    └─ Audio_PlayMusicAtSpeed (start level music)
                 │    │    └─ (*board->vtable[32])(board)      ← Scene_SpawnBallsAndObjects
                 │    │         ├─ For each player entry:
                 │    │         │    ├─ Look up "START%d-%d" position from hash table
                 │    │         │    ├─ Ball_ctor2 (0x4039E0) — create ball (0xC60 bytes)
                 │    │         │    │    └─ Sets ball+0x14C = 0 (controls ENABLED)
                 │    │         │    │    └─ Sets ball+0x284 = 27.0 (initial radius)
                 │    │         │    │    └─ Sets ball+0x188 = 5000.0 (placeholder max_speed)
                 │    │         │    │    └─ Sets ball+0x2FC = 1.0 (gravity_scale)
                 │    │         │    ├─ Ball_SetTrajectory (0x403850) — set spawn position
                 │    │         │    ├─ Override fields:
                 │    │         │    │    └─ ball+0x284 = 26.0 (radius)
                 │    │         │    │    └─ ball+0x188 = 5.0 (max_speed)
                 │    │         │    │    └─ ball+0x2FC = 0.5 (gravity_scale)
                 │    │         │    │    └─ ball+0x1A0 = 1.05 (speed multiplier)
                 │    │         │    └─ AthenaList_Append(Scene+0x29D4, ball)
                 │    │         ├─ Scan SAFESPOT/SAFEPOS entries
                 │    │         ├─ CreateBadBall (if demo/tournament mode)
                 │    │         ├─ CreateMouseTrap (if demo/tournament mode)
                 │    │         ├─ CreateSecretObjects
                 │    │         ├─ Scene_CreateFlags (0x40C0F0)
                 │    │         ├─ Scene_CreateSigns (0x40C270)
                 │    │         └─ Scene_CreateDynamicObjects (0x40C430)
                 │
                 ├─ Scene_AddObject (0x469990) — add board to scene tree
                 └─ Set up player scores (App+0x5E8, +0x688, +0x728, +0x7C8)

[Next frame: game loop begins]

Scene_Update (0x419C00) / ArenaBoard_Update (0x421FE0)  ← vtable[1]
  ├─ Frame counter increment (Scene+0xD88)
  ├─ Demo timer check
  ├─ ESC / pause check
  ├─ Ball position propagation (Ball_SetTargetPos)
  ├─ Gear path follow
  ├─ ToggleTimer_Tick
  ├─ Camera shake decay
  ├─ Scene object update + render (vtable[4] / vtable[0])
  └─ Physics pipeline (gated by ball+0x14C):
       ball_count = AthenaList_GetSize(Scene+0x362C)
       if (ball_count == 1):
           ball = *(App + 0x5DC)     ← current physics ball pointer
           if (ball+0x14C != 0):      ← controls disabled
               SKIP vtable[19-22]    ← skip ALL physics + countdown
       [else: always run vtable[19-22]]
       ├─ vtable[19] (0x4C) = Scene_HandleRaceEnd (0x41B130) ← 3-2-1 countdown display
       │    ├─ If Scene+0x3A4C != 0: skip (countdown already done)
       │    ├─ If Scene+0x8B4 != 0: skip
       │    ├─ If Scene+0x3620 <= 25: skip (wait 25 frames before starting)
       │    ├─ Phase 0: display "3" texture (App+0x394), increment timer
       │    ├─ Phase 1: display "2" texture (App+0x398), increment timer
       │    ├─ Phase 2: display "1" texture (App+0x39C), play sound
       │    └─ Phase 3: set Scene+0x3A4C = 1 (countdown done)
       │         └─ Start race timers, iterate balls
       ├─ vtable[20] (0x50) = Scene_UpdateBallsAndState (0x41B540)
       │    └─ For each ball: Ball_SetCamera + Ball_Update (vtable[4])
       ├─ vtable[21] (0x54) = NoOp (0x40A040)
       ├─ vtable[22] (0x58) = Scene_ProcessRaceEnd (0x41A540) ← race-end handler
       └─ For each object: vtable[31] (0x7C) = Scene_vmethod31 (camera setup)

Two Separate Countdown Systems

1. Pre-Race 3-2-1 Countdown (Scene_HandleRaceEnd, vtable[19])

This is the visual "3, 2, 1" countdown displayed at the start of each race.

  • phase: Scene+0x3A50 (int) — 0="3", 1="2", 2="1", 3=done
  • phase_timer: Scene+0x3A54 (float) — accumulates time per phase
  • countdown_done: Scene+0x3A4C (byte) — 1 = countdown finished
  • sound_played: Scene+0x3A58 (byte) — prevents double-sound in phase 2
  • frame_counter: Scene+0x3620 (int) — must exceed 25 (0x19) before countdown starts
  • race_timer: Scene+0x3624 (float) — decrements during race
  • skip_flag: Scene+0x8B4 (int) — non-zero = skip countdown entirely

Key behavior: The 3-2-1 countdown does NOT freeze the ball. Ball+0x14C is
0 (enabled) from Ball_ctor2. The ball can move during the countdown display.
The countdown is purely cosmetic — it displays numbers and plays sounds,
then sets Scene+0x3A4C=1 and starts race timers.

The 25-frame delay (Scene+0x3620 > 25) before the countdown starts gives the
game ~0.4 seconds (at 60fps) to settle after level load before showing numbers.

2. SinkPlatform Countdown (Scene_StartCountdown, 0x437130)

Called when a ball touches a "DN:SINKPLATFORM" object. This is a
level-specific mechanism, NOT the universal pre-race countdown.

  • active: Scene+0x10F1 (byte) — 1 = countdown active
  • flag2: Scene+0x10F2 (byte) — second gate flag
  • timer: Scene+0x10F4 (int) — 400 (0x190) normal, 50 (0x32) for AI/demo
  • ball: Scene+0x10F8 (ptr) — ball being frozen

Key behavior: Sets ball+0x14C=1 (freezes ball — disables input and
physics). Restores ball+0x2FC=1.0 (gravity_scale back to 1.0 from 0.5).
The timer (Scene+0x10F4) is set to 400 but no function in the analyzed
codebase decrements it — it may be processed by a function Ghidra hasn't
identified, or it may be a dead feature that was replaced by the
Scene_HandleRaceEnd phase system.

Callers of Scene_StartCountdown: SinkPlatformArenaCollisionEvents (0x413BD0),
and various level-specific collision handlers at 0x413FA0, 0x4143AE,
0x414660, 0x414ED9, 0x4151C0, 0x4155B6, 0x41624F, 0x417620, 0x417F08,
0x41873E.


Ball+0x14C (Controls Disabled Flag)

This byte flag gates the entire physics pipeline in Scene_Update and the
player input path in Ball_Update.

  • Ball_ctor2 (0x403D1D): Sets to 0 (enabled) at ball creation
  • Scene_StartCountdown (0x43717B): Sets to 1 (disabled) on SinkPlatform touch
  • Scene_HandleRaceEnd (0x41B40D): Sets to 1 (disabled) when race timer expires
  • ArenaBoard_Update (0x421FE0): Sets to 1 (disabled) for all balls at arena end

Nobody clears ball+0x14C back to 0 after it's set to 1. The ball is
frozen permanently until respawn (which creates a new ball via Ball_ctor2).

Scene_Update Physics Gate

; At 0x419EE4-0x419F04 in Scene_Update (0x419C00)
00419ee4: MOV ECX, EBX                ; EBX = Scene+0x362C (start entries list)
00419ee6: CALL 0x004536a0             ; AthenaList_GetSize(Scene+0x362C)
00419eeb: CMP EAX, 0x1                ; ball_count == 1?
00419eee: JNZ 0x00419f06              ; If != 1: skip gate, run physics unconditionally
00419ef0: MOV EAX, [ESI + 0x878]      ; EAX = App (board+0x878)
00419ef6: MOV ECX, [EAX + 0x5dc]     ; ECX = App+0x5DC (current physics ball POINTER)
00419efc: MOV AL, [ECX + 0x14c]       ; AL = controls_disabled
00419f02: TEST AL, AL
00419f04: JNZ 0x00419f84              ; If disabled: SKIP vtable[19-22] (all physics)

The gate only applies when there is exactly one ball (single-player). In
multiplayer, physics always runs. App+0x5DC is a ball pointer (not a
count), set to the current physics ball.

Ball_Update Input Gate

; At 0x4060A1 in Ball_Update (0x405E00)
004060a1: MOV AL, byte ptr [ESI + 0x14c]   ; AL = controls_disabled
004060a7: TEST AL, AL
004060a9: JNZ 0x004082C9                   ; If disabled: jump to RETURN (skip input)

Hook Points (MinHook / DLL Detour)

Hook 1: Scene_SpawnBallsAndObjects — RECOMMENDED

Address: 0x41C5B0 (vtable[32], shared across ALL levels)

Calling convention: __fastcall (ECX = board). Plain RET (no stack
cleanup). No additional parameters.

Timing: After all balls and objects are created. This is the earliest
point where balls exist. Level geometry, collision, camera, and sound are
already initialized (done by vtable[18] which calls this internally at the
end).

Why this is the best hook:

  • Universal: vtable[32] = 0x41C5B0 in every level subclass vtable (verified)
  • Complete: everything is initialized (balls, objects, camera, sound)
  • Pre-update: no physics tick has run yet
  • Pre-countdown: the 3-2-1 countdown hasn't started (needs 25 frames first)
  • Board pointer is in ECX (thiscall this) — directly accessible
  • Clean function boundary: detour entry, call original, run your code, return
typedef void (__fastcall *SpawnBallsFn)(void* board);
SpawnBallsFn Orig_SpawnBalls = NULL;

void __fastcall Hooked_SpawnBallsAndObjects(void* board) {
    Orig_SpawnBalls(board);

    // At this point:
    // - All balls are spawned (ball_list at board+0x29D4)
    // - All objects are created (flags, signs, dynamic objects)
    // - Camera and sound are configured
    // - No physics tick has run yet
    // - Ball+0x14C = 0 (controls enabled)
    // - 3-2-1 countdown hasn't started (needs 25 frames)

    int ball_count = *(int*)((char*)board + 0x29D8);
    if (ball_count < 1) return;

    int** ball_array = *(int***)((char*)board + 0x2DE0);
    if (!ball_array || !*ball_array) return;

    for (int i = 0; i < ball_count; i++) {
        int* ball = (*ball_array)[i];
        if (!ball || IsBadReadPtr(ball, 0x20)) continue;

        float x = *(float*)((char*)ball + 0x164);
        float y = *(float*)((char*)ball + 0x168);
        float z = *(float*)((char*)ball + 0x16C);

        // Modify ball state here
        // *(float*)((char*)ball + 0x284) = 30.0f;  // radius
        // *(float*)((char*)ball + 0x188) = 7.0f;   // max_speed
    }
}

// Install:
// MH_CreateHook((LPVOID)0x41C5B0, &Hooked_SpawnBallsAndObjects, (LPVOID*)&Orig_SpawnBalls);

Hook 2: Tournament_AdvanceRace — UNIVERSAL ENTRY

Address: 0x427080

Calling convention: __thiscall (ECX = tournament profile,
[ESP+4] = char param_1). RET 0x4 (callee cleans 1 stack param).

Timing: The board is created inside this function (when param_1=0). To
access the board, call the original first, then read [this+0xC].

Parameter:

  • param_1 = '\0' (0): Create board — loads level, spawns balls
  • param_1 = '\x01' (1): Show tourney menu — stores scores, no board created

Only hook fires on the param_1=0 path. Check param_1 before running your code.

typedef void (__thiscall *AdvanceRaceFn)(void* profile, char param_1);
AdvanceRaceFn Orig_AdvanceRace = NULL;

void __thiscall Hooked_AdvanceRace(void* profile, char param_1) {
    Orig_AdvanceRace(profile, param_1);

    // Only run when a board was actually created
    if (param_1 != 0) return;

    // Board pointer is stored at profile+0x0C after creation
    int* board = *(int**)((char*)profile + 0x0C);
    if (!board || IsBadReadPtr(board, 0x100)) return;

    // Ball list
    int ball_count = *(int*)((char*)board + 0x29D8);
    if (ball_count < 1) return;

    int** ball_array = *(int***)((char*)board + 0x2DE0);
    if (!ball_array || !*ball_array) return;

    int* ball = (*ball_array)[0];
    if (!ball || IsBadReadPtr(ball, 0x20)) return;

    // Your onRaceStart code here
}

// Install:
// MH_CreateHook((LPVOID)0x427080, &Hooked_AdvanceRace, (LPVOID*)&Orig_AdvanceRace);

Important: The doc's earlier version claimed that EAX = board pointer at
address 0x4273E3 (after the vtable[18] call). This is WRONG — EAX is
clobbered by the vtable[18] call at 0x4273E0. The board pointer is stored
at [ESI+0x0C] (profile+0x0C) and loaded into EAX at 0x4273E6. When using
MinHook, you don't need to worry about register state — just detour the
function entry and read profile+0x0C after calling the original.

Hook 3: App_StartRace — EARLIEST

Address: 0x4287C0

Calling convention: __thiscall (ECX = App, [ESP+4] = int flag).
RET 0x4 (callee cleans 1 stack param). The flag is always 1 in all
observed callers (App_StartTournamentRace, App_StartPracticeRace,
Scene_ProcessRaceEnd).

Timing: Before scene reset. Old objects are still alive. The new level
hasn't been loaded yet.

Use case: When you need to run code BEFORE the old scene is destroyed
(e.g., save data from the previous race). Too early for accessing new balls.

typedef void (__thiscall *StartRaceFn)(void* app, int flag);
StartRaceFn Orig_StartRace = NULL;

void __thiscall Hooked_StartRace(void* app, int flag) {
    // Old scene is still alive at App+0x178
    int* old_scene = *(int**)((char*)app + 0x178);
    // Save data from previous race here...

    Orig_StartRace(app, flag);
    // After this: old scene destroyed, but new board not yet created
}

// Install:
// MH_CreateHook((LPVOID)0x4287C0, &Hooked_StartRace, (LPVOID*)&Orig_StartRace);

Hook 4: Level_InitScene — AFTER GEOMETRY, BEFORE BALLS

Address: 0x40B090 (base vtable[18])

Calling convention: __fastcall (ECX = board). Plain RET.

Timing: After MeshWorld_ctor and CollisionLevel_ctorWithLevel have
loaded geometry and collision data. Sound, camera, and graphics are set up
here. Balls have NOT been spawned yet (vtable[32] hasn't run).

Note: This is the BASE vtable[18]. Each level subclass overrides
vtable[18] with its own loader (e.g., Scene_LoadLevel2 at 0x40D280) which
internally calls MeshWorld_ctor → CollisionLevel_ctorWithLevel →
Level_InitScene → vtable[32]. Hooking Level_InitScene directly catches
the moment after camera/sound setup but before balls exist.

typedef void (__fastcall *InitSceneFn)(int* board);
InitSceneFn Orig_InitScene = NULL;

void __fastcall Hooked_InitScene(int* board) {
    Orig_InitScene(board);
    // Geometry loaded, collision loaded, camera/sound set up
    // Balls NOT yet spawned (vtable[32] hasn't run)
    // Good for: modifying camera, changing level music, etc.
}

Hook 5: Ball_ctor2 — AT BALL CREATION

Address: 0x4039E0

Calling convention: __thiscall (ECX = allocated memory,
[ESP+4] = scene pointer). RET 0x4.

Timing: During Scene_SpawnBallsAndObjects, for each player entry. The
ball is being constructed — fields get default values here, then
SpawnBalls overrides some of them (radius, max_speed, gravity_scale).

Use case: When you need to modify ball fields at the earliest possible
point. Note that SpawnBalls will override radius (27→26), max_speed
(5000→5), and gravity_scale (1.0→0.5) AFTER ctor returns.

typedef void* (__thiscall *BallCtorFn)(void* alloc, int scene);
BallCtorFn Orig_BallCtor = NULL;

void* __thiscall Hooked_BallCtor(void* alloc, int scene) {
    void* ball = Orig_BallCtor(alloc, scene);
    // Ball is constructed with default values
    // Fields will be overridden by SpawnBalls after this returns
    return ball;
}

Hook 6: Ball_Update — FIRST PHYSICS TICK

Address: 0x405E00 (vtable[4])

Calling convention: __thiscall (ECX = ball). Plain RET.

Timing: First physics tick after level load. The 3-2-1 countdown has
started (but ball+0x14C=0, so input is enabled). This runs EVERY FRAME for
EVERY BALL, so use a one-shot flag.

typedef void (__thiscall *BallUpdateFn)(void* ball);
BallUpdateFn Orig_BallUpdate = NULL;

static bool race_started = false;

void __thiscall Hooked_BallUpdate(void* ball) {
    if (!race_started) {
        race_started = true;
        // First ball update after level load
        // ball+0x14C = 0 (controls enabled)
        // Countdown is displaying but ball can move
    }
    Orig_BallUpdate(ball);
}

Hook 7: Scene_HandleRaceEnd — COUNTDOWN START

Address: 0x41B130 (vtable[19])

Calling convention: __fastcall (ECX = board). Plain RET.

Timing: First call is 25 frames after level load. The 3-2-1 countdown
display begins here. Also called every frame during the race (handles
race-end when timer expires).

Use case: When you need to run code exactly when the countdown starts
displaying (not before, not after). Check Scene+0x3A4C to distinguish
countdown-start from race-end.

Hook 8: Scene_AddObject — BOARD ADDED TO SCENE TREE

Address: 0x469990

Calling convention: __thiscall (ECX = scene object list,
[ESP+4] = object to add). RET 0x4.

Timing: Called by Tournament_AdvanceRace right after vtable[18] returns.
The board has been fully initialized (geometry, balls, objects) and is
being added to the scene tree for rendering.

Use case: When you need to intercept the moment the board becomes
visible to the scene tree. The added object is the board itself.

Hook 9: Scene_SetRaceActive — RACE ACTIVE FLAG

Address: 0x4366E0

Calling convention: __fastcall (ECX = board). Plain RET.

Timing: Sets App+0x10EC = 1. Called from many places (62 xrefs) —
NOT just at race start. Called whenever the game transitions to "race
active" state.

Use case: Not recommended for race-start detection (too many callers,
fires in many contexts). Included here for completeness.


Complete Race-Start Timeline

Frame 0:   App_Start*Race → App_StartRace → reset old scene
           Tournament_AdvanceRace (show menu, param_1=1)
           [OR] Tournament_AdvanceRace (create board, param_1=0)

Frame 0+:  Tournament_AdvanceRace (param_1=0) creates board:
             switch(race_index) → level-specific Board ctor → Board_ctor
             (*board->vtable[18])(board):
               [Level-specific override, e.g. Scene_LoadLevel2]:
                 MeshWorld_ctor → CollisionLevel_ctorWithLevel
                 → Level_InitScene (sound/camera/graphics)
                 → (*board->vtable[32])(board) = Scene_SpawnBallsAndObjects:
                   Ball_ctor2 (0x14C=0, radius=27, max_speed=5000, gravity=1.0)
                   Ball_SetTrajectory (spawn position)
                   Override: radius=26, max_speed=5, gravity=0.5
                   AthenaList_Append(Scene+0x29D4, ball)
                   CreateBadBall, CreateMouseTrap, CreateFlags, CreateSigns
                   CreateDynamicObjects
             Scene_AddObject (board → scene tree)
             Set up player scores

           ↑ HOOK 1 (Scene_SpawnBallsAndObjects): balls just spawned ← BEST
           ↑ HOOK 2 (Tournament_AdvanceRace): board at profile+0x0C
           ↑ HOOK 4 (Level_InitScene): geometry loaded, no balls yet
           ↑ HOOK 5 (Ball_ctor2): ball being constructed
           ↑ HOOK 8 (Scene_AddObject): board added to scene tree

Frame 1:   Scene_Update (vtable[1]):
             AthenaList_GetSize(Scene+0x362C) → ball_count
             if (ball_count==1 && ball+0x14C!=0): skip physics
             else:
               vtable[19] = Scene_HandleRaceEnd:
                 Scene+0x3620 = 1 (frame counter, < 25 → skip countdown)
               vtable[20] = Scene_UpdateBallsAndState:
                 Ball_Update (vtable[4]) — first physics tick

           ↑ HOOK 6 (Ball_Update entry): first physics tick
           ↑ HOOK 7 (Scene_HandleRaceEnd): called but countdown skipped

Frame 2-25: Scene_Update repeats. Countdown not yet displayed.
             Ball can move freely (ball+0x14C=0).

Frame 26:  Scene_HandleRaceEnd:
             Scene+0x3620 > 25 → start countdown
             Phase 0: display "3" (App+0x394 texture)
             Scene+0x3A50 = 0

           ↑ HOOK 7 (Scene_HandleRaceEnd): countdown display starts

Frame ~150: Phase 1: display "2" (App+0x398 texture)

Frame ~275: Phase 2: display "1" (App+0x39C texture), play sound

Frame ~400: Phase 3: Scene+0x3A4C = 1 (countdown done)
             Start race timers, iterate balls

Scene Vtable Map (vtable base at 0x4D0260)

All entries verified by reading raw vtable bytes from memory.

  • [0] (0x00): 0x425020 — Board_dtor (destructor)
  • [1] (0x04): 0x419C00 — Scene_Update (main game tick)
  • [2] (0x08): 0x41A2E0 — Scene_SetCamera (camera setup per ball)
  • [18] (0x48): 0x40B090 — Level_InitScene (BASE: sound, camera, graphics)
  • [19] (0x4C): 0x41B130 — Scene_HandleRaceEnd (3-2-1 countdown + race-end)
  • [20] (0x50): 0x41B540 — Scene_UpdateBallsAndState (ball physics update)
  • [21] (0x54): 0x40A040 — NoOp (unused slot)
  • [22] (0x58): 0x41A540 — Scene_ProcessRaceEnd (race-end menu/results)
  • [27] (0x6C): 0x41B710 — Scene_RenderScoreHUD (render countdown numbers + HUD)
  • [28] (0x70): 0x41BFD0 — Scene_RenderTimerHUD (render race timer)
  • [31] (0x7C): 0x41AC70 — Scene_vmethod31 (per-ball camera + render setup)
  • [32] (0x80): 0x41C5B0 — Scene_SpawnBallsAndObjects (spawn balls + all objects)
  • [33] (0x84): 0x419750 — Scene_method33

vtable[18] is OVERRIDDEN by each level subclass. The base vtable[18]
(0x40B090 = Level_InitScene) is what the BASE class uses. Each level
subclass assigns its own vtable with a different vtable[18] that loads
the level-specific .MESHWORLD file. These overrides internally call
MeshWorld_ctor → CollisionLevel_ctorWithLevel → Level_InitScene →
vtable[32].

vtable[32] is shared across ALL levels (0x41C5B0 in every subclass
vtable, verified by reading 4 different level vtables).

Example level vtable[18] overrides (verified from vtable memory reads):

  • L1 WarmUp (0x4D04A8): vtable[18] = 0x40D1C0 — loads "levels\level1"
  • L2 Cascade (0x4D1098): vtable[18] = 0x4110D0 — loads "levels\levelcascade"
  • L3 Intermediate (0x4D05A0): vtable[18] = 0x40D280 — loads "levels\level2"
  • L4 Dizzy (0x4D0890): vtable[18] = 0x40D390 — loads "levels\level3"

Key Offsets Summary

Ball (0xC60 bytes, allocated by operator_new in Scene_SpawnBallsAndObjects)

Offset Type Name Set By Value
+0x14C byte controls_disabled Ball_ctor2 (0) 0
+0x164 float pos_x Scene_SpawnBallsAndObjects START position
+0x168 float pos_y Scene_SpawnBallsAndObjects START position
+0x16C float pos_z Scene_SpawnBallsAndObjects START position
+0x18 int player_id Ball_ctor2 -1 = AI/demo, 0+ = player
+0x188 float max_speed Ball_ctor2 → SpawnBalls ctor=5000.0, SpawnBalls=5.0
+0x1A0 float speed_multiplier Scene_SpawnBallsAndObjects 1.05
+0x27C float friction_scale Scene_SpawnBallsAndObjects 0.1
+0x284 float radius Ball_ctor2 → SpawnBalls ctor=27.0, SpawnBalls=26.0
+0x2F8 byte is_falling Ball_ctor2 / SpawnBalls 0
+0x2FC float gravity_scale Ball_ctor2 → SpawnBalls ctor=1.0, SpawnBalls=0.5
+0x310 byte has_target_pos Ball_ctor2 1
+0x769 byte active_flag Scene_SpawnBallsAndObjects 1

Scene/Board (variable size, 0x436C–0x6498 depending on level)

Offset Type Name Description
+0x878 ptr app App object pointer
+0x184 ptr scene_object_list AthenaList of scene objects
+0x29D4 AthenaList ball_list Main ball list (player balls)
+0x29D8 int ball_count Number of balls in ball_list
+0x2DE0 ptr ball_array Pointer to ball pointer array
+0x3620 int frame_counter Incremented each Scene_Update
+0x3624 float race_timer Decrements during race
+0x362C AthenaList start_entries_list Start position entries (used for ball_count gate)
+0x3A4C byte countdown_done 1 = 3-2-1 countdown finished
+0x3A50 int countdown_phase 0="3", 1="2", 2="1"
+0x3A54 float countdown_phase_timer Time in current phase
+0x3A58 byte countdown_sound_played Prevents double-sound
+0x8B4 int countdown_skip_flag Non-zero = skip countdown
+0x10F1 byte sink_countdown_active SinkPlatform countdown
+0x10F4 int sink_countdown_timer 400 normal, 50 AI/demo
+0x10F8 ptr sink_countdown_ball Ball being frozen by SinkPlatform

App (global at 0x5341E0)

Offset Type Name Description
+0x174 ptr gfx_device Graphics device (D3D)
+0x178 ptr sound_device SoundDevice (SoundDevice_UpdateChannels) — NOT scene!
+0x17C ptr music_device MusicDevice (MusicDevice_FadeAll)
+0x180 ptr input_device InputDevice (InputDevice_PollAndRelease)
+0x184 ptr meshworld MeshWorld / scene manager (contains board in its list)
+0x220 ptr player_profile Current PlayerProfile (profile+0xC = board)
+0x237 byte is_arena 1 = arena mode (2P), 0 = tournament/practice
+0x23C int difficulty 0=Pipsqueak, 1=Normal, 2=Frenzied
+0x5DC ptr physics_ball Current physics ball pointer (read-only, never written in code)
+0x10EC int race_active Set to 1 by Scene_SetRaceActive

WARNING: Previous versions of this doc claimed App+0x178 = "scene". This is
WRONG. App+0x178 is the SoundDevice, verified via App_FrameUpdate (0x46C170)
which calls SoundDevice_UpdateChannels(*(App+0x178)). The scene/board is
NOT stored at any single App field — see "Getting the Scene Pointer" below.


Complete Calling Convention Reference

All verified via disassembly (RET instruction inspection).

Function Address Convention RET Params
App_StartRace 0x4287C0 __thiscall RET 0x4 ECX=App, [ESP+4]=int flag (always 1)
App_StartTournamentRace 0x4288B0 __fastcall RET ECX=App
App_StartTourneyRace 0x4289F0 __fastcall RET ECX=App
App_StartMPRace 0x428B20 __thiscall RET 0x4 ECX=App, [ESP+4]=?
App_StartPracticeRace 0x428C50 __thiscall RET 0x4 ECX=App, [ESP+4]=?
App_Start2PRace 0x429230 __thiscall RET 0x4 ECX=App, [ESP+4]=?
Tournament_AdvanceRace 0x427080 __thiscall RET 0x4 ECX=profile, [ESP+4]=char param_1
Scene_SpawnBallsAndObjects 0x41C5B0 __fastcall RET ECX=board
Scene_Update 0x419C00 __fastcall RET ECX=board
Scene_HandleRaceEnd 0x41B130 __fastcall RET ECX=board
Scene_UpdateBallsAndState 0x41B540 __fastcall RET ECX=board
Scene_ProcessRaceEnd 0x41A540 __fastcall RET ECX=board
Scene_StartCountdown 0x437130 __thiscall RET 0x4 ECX=scene, [ESP+4]=ball
Ball_Update 0x405E00 __thiscall RET ECX=ball
Ball_ctor2 0x4039E0 __thiscall RET 0x4 ECX=alloc, [ESP+4]=scene
Ball_SetTrajectory 0x403850 __thiscall RET 0x14 ECX=ball, 5 stack floats
Level_InitScene 0x40B090 __fastcall RET ECX=board
Scene_AddObject 0x469990 __thiscall RET 0x4 ECX=list, [ESP+4]=obj
MeshWorld_ctor 0x461510 __thiscall RET 0x8 ECX=alloc, [ESP+4]=d3dDevice, [ESP+8]=path
CollisionLevel_ctorWithLevel 0x465080 __thiscall RET 0x4 ECX=alloc, [ESP+4]=sourceMesh
Board_ctor 0x419030 __thiscall RET 0x4 ECX=alloc, [ESP+4]=App
Scene_SetRaceActive 0x4366E0 __fastcall RET ECX=board

Getting the Scene Pointer

There is NO global variable that directly holds the current board/scene
pointer. The board is only accessible through indirect chains or by hooking
a function that receives it as a parameter. Verified by searching the entire
binary for writes to any dedicated "current scene" field — none exist.

Method 1: Hook Scene_Update + Global (RECOMMENDED for MinHook)

Hook Scene_Update (0x419C00), save ECX to a global, and compare with the
previous frame to detect level changes.

Why Scene_Update (0x419C00) and NOT Ball_Update (0x405E00):

  • Scene_Update is called for ALL levels, ALL modes, EVERY frame
  • vtable[1] is overridden by some levels (Intermediate=0x41CC90, Dizzy=0x41D510,
    ArenaBoard=0x421FE0), but ALL overrides CALL 0x419C00 directly — so the
    MinHook at 0x419C00 fires for every level
  • Ball_Update (0x405E00) is NOT called for player balls in race mode!
    vtable[4] (Ball_AI_ChaseNearest, 0x408390) checks ball+0xC74 (AI flag)
    and App+0x237 (is_arena). If neither is set: skips Ball_Update and calls
    vtable[5] instead. Ball_Update only fires for AI balls or arena mode.
  • Scene_Update is __fastcall (ECX = board), plain RET — clean hooking

Why Scene_SpawnBallsAndObjects (0x41C5B0) may not have fired:
The function IS called for every level (verified via vtable analysis + disasm
of 4 level loaders). ASLR is disabled, address is correct. If the hook doesn't
fire, likely causes are: MinHook initialization failure (check MH_CreateHook
return code), hook not enabled (MH_EnableHook not called), or output
mechanism not working (use MessageBoxA for debugging, not fopen).

static int* g_scene = NULL;
static int* g_prev_scene = NULL;
static bool g_level_just_started = false;

typedef void (__fastcall *SceneUpdateFn)(int* board);
SceneUpdateFn Orig_SceneUpdate;

void __fastcall Hooked_SceneUpdate(int* board) {
    g_scene = board;

    if (g_scene != g_prev_scene) {
        // Level just changed (or first frame)
        g_level_just_started = true;
        g_prev_scene = g_scene;
    } else {
        g_level_just_started = false;
    }

    Orig_SceneUpdate(board);
}

// Install: MH_CreateHook((LPVOID)0x419C00, &Hooked_SceneUpdate, (LPVOID*)&Orig_SceneUpdate);
// Then anywhere: if (g_level_just_started) { ... onLevelStart code ... }

This gives you both a persistent scene pointer AND a level-change callback.

Method 2: App Global → PlayerProfile → Board

Read the App global at 0x5341E0, then follow the profile chain.

int* app = *(int**)0x5341E0;
if (!app || IsBadReadPtr(app, 0x300)) return NULL;

int* profile = *(int**)((char*)app + 0x220);
if (!profile || IsBadReadPtr(profile, 0x20)) return NULL;

int* board = *(int**)((char*)profile + 0x0C);
if (!board || IsBadReadPtr(board, 0x100)) return NULL;

// board is now the scene/board pointer

Caveat: profile+0xC is set to NULL at the START of Tournament_AdvanceRace
(before creating the new board). It's only valid during active gameplay.

Method 3: App Global → MeshWorld → Items Array → Board

Read the scene manager (MeshWorld) and iterate its object list.

int* app = *(int**)0x5341E0;
if (!app || IsBadReadPtr(app, 0x200)) return NULL;

int* meshworld = *(int**)((char*)app + 0x184);
if (!meshworld || IsBadReadPtr(meshworld, 0x500)) return NULL;

// MeshWorld+0x40C = pointer to items array (AthenaList internal)
int* items = *(int**)((char*)meshworld + 0x40C);
if (!items || IsBadReadPtr(items, 0x10)) return NULL;

// First item = board (usually the only object in the list)
int* board = *(int**)items;
if (!board || IsBadReadPtr(board, 0x100)) return NULL;

// board is now the scene/board pointer

Verified: GameUpdate (0x469CF0) uses this exact path:
MOV EAX, [EBP+0x40C]; MOV ECX, [EAX] to get the first board from the list.

Why App+0x178 Does NOT Work

Previous docs claimed App+0x178 = "scene pointer". This is wrong.
App_FrameUpdate (0x46C170) proves it:

// From App_FrameUpdate decompilation:
if (*(int*)(param_1 + 0x178) != 0)
    SoundDevice_UpdateChannels(*(int*)(param_1 + 0x178));  // App+0x178 = SOUND DEVICE
GameUpdate(*(int*)(param_1 + 0x184));                       // App+0x184 = scene manager

App+0x178 is the SoundDevice, not the scene. The confusion arose because
App_StartRace calls Scene_UpdateChildren(*(App+0x178)), and Scene_UpdateChildren
was misidentified as a scene function — it's actually a sound channel cleanup call.


Pitfalls

1. Ghidra Function Name Confusion

  • Scene_HandleRaceEnd (0x41B130, vtable[19]) = Pre-race 3-2-1 countdown
    display, NOT just race-end. It also handles post-race results.
  • Scene_ProcessRaceEnd (0x41A540, vtable[22]) = Race-end menu/results
    handler, NOT the pre-race countdown. Name is misleading.
  • Scene_StartCountdown (0x437130) = SinkPlatform freeze countdown,
    NOT the universal pre-race 3-2-1 countdown.

2. Ball+0x14C Is NOT the Countdown Gate

The 3-2-1 countdown does NOT set ball+0x14C. The ball is free to move during
the countdown. Ball+0x14C is only set by:

  • SinkPlatform collision (Scene_StartCountdown)
  • Race timer expiry (Scene_HandleRaceEnd post-countdown)
  • Arena end (ArenaBoard_Update)

3. Scene+0x10F4 Timer Is Dead Code

The SinkPlatform countdown timer (Scene+0x10F4=400) is set by
Scene_StartCountdown but no function in the analyzed codebase decrements it.
Verified by searching for 0x10F4 references in Scene_Update,
Scene_HandleRaceEnd, Scene_UpdateBallsAndState, and Scene_ProcessRaceEnd —
none reference it. It may be a legacy feature replaced by the
Scene_HandleRaceEnd phase system.

4. Multiple Scene Vtables

There are 15+ scene subclass vtables (one per level type), each at a different
address. vtable[18] is OVERRIDDEN per level (each loads a different
.MESHWORLD file), but vtable[32] (Scene_SpawnBallsAndObjects) is shared.
For universal hooks, target shared functions (0x41C5B0, 0x427080) rather
than per-level functions.

5. Arena vs Race Initialization

Arena levels use ArenaBoard_*_Init functions (e.g.
ArenaLevel_WarmUp_Init at 0x413C20) instead of Scene_LoadLevel*.
These also call vtable[32] = Scene_SpawnBallsAndObjects, so the same
hook works for both arenas and races.

6. Tournament_AdvanceRace param_1

Tournament_AdvanceRace(profile, param_1):

  • param_1 = '\0' (0): Create board — loads level, spawns balls, adds to scene
  • param_1 = '\x01' (1): Show tourney menu — stores scores, creates TourneyMenu
  • param_1 = '\x01' with App+0x237 set: Creates TourneyMenu_CreateBoard (arena)

Only the param_1 = '\0' path creates the board. Hook 1 fires on this path.

7. Ball Field Overrides

Ball_ctor2 sets initial values, but Scene_SpawnBallsAndObjects overrides
several fields AFTER Ball_ctor2 returns. If you hook Ball_ctor2 to modify
a field, SpawnBalls may overwrite your change. Hook
Scene_SpawnBallsAndObjects instead to modify fields after all overrides.

8. App_StartRace Has a Stack Parameter

App_StartRace (0x4287C0) has RET 0x4, meaning it cleans 1 stack
parameter. All callers push 1 before calling. Ghidra labels it
__fastcall but it's effectively __thiscall (ECX=App). When hooking
with MinHook, declare it as __thiscall with 2 params.


Document verified via GhidraMCP decompilation and disassembly of
Hamsterball.exe (V3.6.c, md5=7d25019366b8d7f55906325bd630d7fe). All
function addresses, vtable layouts, calling conventions (via RET
instruction inspection), and field offsets cross-referenced against
raw decompiled C code and x86 disassembly. Scene vtable at 0x4D0260
verified by reading raw memory bytes. Level subclass vtables verified
for WarmUp (0x4D04A8), Cascade (0x4D1098), Intermediate (0x4D05A0),
and Dizzy (0x4D0890).


🔗 Related Documents

Raptisoft Live Status Logger

types : mods
keywords :

📂 View source on GitHub


Raptisoft Live Status Logger

Type: bass.dll proxy mod (passive logger — no gameplay changes)
File: bass.dll → writes live_status.txt in the game folder

What It Does

Hamsterball has a hidden in-memory status tracking system that Raptisoft built for crash diagnostics. The game continuously updates three char* fields on the App struct:

Offset Field Purpose
App+0x208 Init Status Startup phase tag (e.g. "App::Initialize(5)", "Graphics::Initialize(10)", "FinishLoad(OK)")
App+0x20C Current Object Name of whatever object is being processed
App+0x210 Current Operation Runtime operation ("Background" = message pump, "Update" = game logic frame)

These fields are never written to disk or console — they exist purely as crash breadcrumbs, read only when the game crashes and feeds them to the BugTracker crash dialog (which posts XML to bugs.raptisoft.com).

This mod taps into those same fields from a background thread and logs them live to live_status.txt.

What It Logs

  1. System Info (one-time, when graphics device initializes):

    • Product name, version string
    • Fullscreen mode, resolution
    • Target FPS, refresh rate
    • Graphics/sound/music/input/scene device pointers
  2. Live Status (on every change, ~100Hz polling):

    • Timestamp (ms since mod load)
    • Init phase tag
    • Current object name
    • Current operation
  3. Device Changes (every 1s, if pointers change):

    • Sound/music/scene manager device transitions
  4. FPS (every 1s, if FPS display is enabled in game settings)

Expected Output (on real Windows)

[MOD] Raptisoft Live Status Logger loaded
=======================================================================
          HAMSTERBALL LIVE STATUS LOG - Raptisoft Debug Tap
=======================================================================

Log started: 2026-01-15 14:30:22
Process PID: 12345

--- System Info ---
  Product:     Hamsterball
  Version:     V3.6.c
  Fullscreen:  YES
  Resolution:  1920x1080
  Target FPS:  75
  Refresh:     60 Hz

--- Device Pointers ---
  Graphics:     0x0A1B2C3D (active)
  Sound:        0x0E4F5A6B (active)
  Music:        0x0C7D8E9F (active)
  Input:        0x0D1E2F3A (active)
  Scene/MeshWorld: 0x0F4A5B6C (active)

--- Live Status (updates on change) ---
[tick]     STATUS                          OBJECT              OPERATION
-----------------------------------------------------------------------------
[    50]  Startup(2)                      (null)              (null)
[   100]  Startup(3)                      (null)              (null)
[   150]  Startup(8)                      (null)              (null)
[   200]  Startup(9)                      (null)              (null)
[   250]  Startup(Constructor OK)         (null)              (null)
[   300]  App::Initialize(1)              (null)              (null)
[   350]  App::Initialize(2)              (null)              (null)
[   400]  App::Initialize(5)              (null)              (null)
[   450]  Graphics::Initialize(1)         (null)              (null)
[   500]  Graphics::Initialize(10)        (null)              (null)
[   550]  Graphics::Initialize(20)        (null)              (null)
[   600]  App::Initialize(7)              (null)              (null)
[   650]  Initialize(5)                   (null)              (null)
[   700]  Initialize(15)                  (null)              (null)
[   750]  Initialize(26)                  (null)              (null)
[   800]  FinishLoad(1)                   (null)              (null)
[   850]  FinishLoad(OK)                  (null)              (null)
[   900]  (null)                           (null)              Background
[   950]  (null)                           (null)              Update
[  1000]  (null)                           Ball                 Update
[  1050]  (null)                           Board(Beginner)      Update

Installation

  1. Copy bass.dll into your Hamsterball game folder (replacing the original)
  2. Launch the game
  3. Check live_status.txt in the same folder — it updates live while you play

Technical Details

  • App global address: 0x005341E0 (the g_App global, set in WinMain)
  • App struct size: ~0x2D00+ bytes
  • Polling rate: ~100Hz (10ms sleep between reads)
  • Log only on change: Avoids flooding the file with duplicate entries
  • Thread-safe: Uses IsBadReadPtr before every memory read
  • No gameplay changes: Pure passive observer — doesn't hook any game functions

Status Strings Found in Binary

The game contains 100+ status strings across these categories:

  • Startup: Startup(2), Startup(3), Startup(8), Startup(9), Startup(Constructor OK)
  • App Init: App::Initialize(1) through (12), App::Initialize(Ok)
  • Graphics Init: Graphics::Initialize(1) through (27)
  • Graphics Defaults: Graphics::Defaults(1) through (19), Graphics::Defaults(ok)
  • Full Init: Initialize(1) through (26) (no 14 or 24 — skipped in binary)
  • FinishLoad: FinishLoad(1) through (4), FinishLoad(OK)
  • Runtime: Background (message pump idle), Update (game logic frame)
  • Errors: "Failed: Direct3DCreate8(D3D_SDK_VERSION)", "Could not load sound (1-6)", "** No Graphics **", "** No Graphics Device **"

Crash Report System (not tapped by this mod)

The game also has a full crash reporting pipeline at 0x0047ABE0:

  1. App_BuildDiagnosticReport (0x0046D230) — collects XML tags: PRODUCT, VERSION, RUNTIME, FULLSCREEN, DXDISPLAY, RESOLUTION, SAFEMODE, OS, DDRAW, DSOUND, CURRENTOBJECT, CURRENTOPERATION, EXTENDED_INFO
  2. MWParser_DumpTags (0x004742B0) — formats as XML
  3. BugTracker_ShowDialog (0x0047A480) — shows crash dialog with "Send Report" button
  4. BugTracker_SubmitReport (0x00479FC0) — HTTP POST to bugs.raptisoft.com/cgi-bin/errorreport.cgi

Build

i686-w64-mingw32-gcc -shared -o bass.dll raptisoft_live_log.c \
    -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
    -Wl,--add-stdcall-alias

Crash Test

Tested on Wine/Xvfb (35s survival, no crash). Game doesn't fully initialize D3D on llvmpipe, so the log shows only the initial (null) state — but the mod loads cleanly and the background thread runs for the full duration. On real Windows, all status fields will populate normally.


🔗 Related Documents

Raycasting for DLL Mods

types : physics
keywords :

📂 View source on GitHub


Raycasting for DLL Mods — Reusable Ground Detection Pattern

Overview

This reference provides a reusable pattern for using the engine's raycast
function (Mesh_FindClosestCollision @ 0x00465D90) from DLL mods to detect
ground contact, wall proximity, or any spatial query against level geometry.

When to Use Raycasting in a Mod

  • Jump mods — detect if the ball is on the ground before allowing a jump
  • Wall detection — check if a wall is ahead/behind/beside the ball
  • Surface probes — find the height of terrain at a specific XZ coordinate
  • Spawn validation — verify a position is safe (not inside geometry)
  • Any spatial query that the engine's own physics system would need

The Raycast Function

typedef struct { float x, y, z; } Vec3;

/* Mesh_FindClosestCollision — __thiscall
 *   ECX = collision_level (Scene+0x8B0)
 *   Stack: out*, origin(3 floats), direction(3 floats), max_dist(float)
 *   ret 0x20 (32 bytes = 8 DWORDs on stack)
 *
 * Address: 0x00465D90
 * Returns: same pointer as `out` (always — can be ignored)
 */
typedef Vec3* (__thiscall *MeshRaycast_t)(
    void* collision_level,   // ECX = Scene+0x8B0
    Vec3* out,               // output hit point (WRITE target)
    Vec3 origin,             // ray start point (world space)
    Vec3 direction,          // ray direction (will be normalized internally)
    float max_dist           // sphere radius for AABB broad-phase
);

static MeshRaycast_t pfn_raycast = (MeshRaycast_t)0x00465D90;

Critical Typedef Rules

  1. __thiscall is mandatory. Without it, collision_level goes on the
    stack instead of ECX, all params shift left by one slot, and the function
    uses garbage for direction — returning wrong results (all directions
    return the same hit point ~26 units below origin).

  2. Vec3 by value = 3 floats on stack. Ghidra shows these as individual
    undefined4 stack slots. The C compiler handles this correctly when you
    use the Vec3 struct typedef — do NOT pass individual floats.

  3. ret 0x20 (32 bytes). The function pops 32 bytes (8 DWORDs) from the
    stack on return. The __thiscall typedef handles this automatically.

Accessing Collision Geometry

/* From a Ball pointer: */
void* scene = *(void**)((char*)ball + 0x14);    /* Ball+0x14 = Scene* */
void* cl = *(void**)((char*)scene + 0x8B0);     /* Scene+0x8B0 = CollisionLevel* */

/* From the App global: */
void* app = *(void**)0x005341E0;
void* scene = *(void**)((char*)app + 0x878);     /* App+0x878 = Scene* */
void* cl = *(void**)((char*)scene + 0x8B0);

Key Ball Offsets for Raycasting

Offset Type Field Notes
0x014 void* Scene pointer → Scene+0x8B0 for CollisionLevel
0x164 float Position X Ball+0x59 (int index)
0x168 float Position Y Ball+0x5A
0x16C float Position Z Ball+0x5B
0x284 float Collision radius Default 26.0, use for max_dist
0xC4C byte Fall mode 1 = dying/respawning, skip raycast

Ground Detection Pattern (Downward Raycast)

/* Check if ball is on the ground by casting a downward ray.
 * Returns 1 if ground is within (radius + 2.0) units below the ball. */
int Ball_IsGrounded(void* ball)
{
    char* b = (char*)ball;
    void* scene = *(void**)(b + 0x14);
    if (!scene) return 0;
    void* cl = *(void**)((char*)scene + 0x8B0);
    if (!cl) return 0;

    Vec3 pos = {
        *(float*)(b + 0x164),
        *(float*)(b + 0x168),
        *(float*)(b + 0x16C)
    };
    float radius = *(float*)(b + 0x284);

    /* Downward ray. For standard levels, (0,-1,0) is correct.
     * For tilted-gravity levels, read gravity from CollisionMesh:
     *   void* cm = *(void**)(b + 0x1A4);
     *   Vec3 down = { *(float*)(cm+0xC8C), *(float*)(cm+0xC90), *(float*)(cm+0xC94) };
     */
    Vec3 down = { 0.0f, -1.0f, 0.0f };
    Vec3 out = { 0, 0, 0 };

    /* max_dist = radius + 0.5 (matches engine's own ground probes) */
    pfn_raycast(cl, &out, pos, down, radius + 0.5f);

    /* On hit: out.y ≈ pos.y - radius (floor directly below)
     * On miss: out.y ≈ pos.y - 994 (ray endpoint far away)
     * Check absolute distance */
    float dy = out.y - pos.y;
    if (dy < 0.0f) dy = -dy;
    return (dy <= radius + 2.0f) ? 1 : 0;
}

No-Hit Behavior (IMPORTANT)

When the ray does NOT intersect any geometry, the function does NOT return
the origin. It returns the ray endpoint at ~994 units along the direction.

  • On a downward ray with no floor below: out.y ≈ pos.y - 994
  • Always check |out - origin| distance after the call
  • Never assume out == origin means "no hit"

max_dist Semantics

max_dist is the sphere radius for AABB broad-phase, not a distance limit:

  • It expands the query box around the ray
  • Spatial tree returns only triangles within this box
  • Use radius + 0.5f (what the engine uses)
  • Too large → false hits from off-axis geometry
  • Too small → misses nearby geometry

The effective ray length is always ~994 units (direction scaled to 99999,
clamped to 1000 by max_speed, damped to ~994 by friction). max_dist does
NOT control how far the ray travels.

Architecture: Code Cave + Background Thread (Pattern 4)

NEVER call C functions (including raycast) from a hand-assembled code cave.
Even with FNSAVE/FNRSTOR, calling a C function from inside a mid-function hook
corrupts the stack/FPU/SEH state and crashes the game.

The correct pattern for mods that need to call game functions:

┌─────────────────────────────────────┐
│ Code Cave (inside Ball_Update)      │  Runs per frame (60fps)
│  - Stores ball pointer in volatile  │  Hand-assembled x86, no C calls
│  - Reads volatile flags set by BG   │
│  - Applies mod effects              │
└──────────┬──────────────────────────┘
           │ g_ball_ptr (volatile DWORD)
           ▼
┌─────────────────────────────────────┐
│ Background Thread (Sleep 10ms)     │  Runs ~100x/sec
│  - Reads g_ball_ptr → ball         │  C code, can call game functions
│  - Calls Mesh_FindClosestCollision │
│  - Sets g_on_ground (volatile)     │
└─────────────────────────────────────┘

Template

/* Shared state */
static volatile DWORD g_ball_ptr = 0;    /* set by code cave, read by thread */
static volatile DWORD g_on_ground = 0;   /* set by thread, read by code cave */
static volatile DWORD g_bg_active = 1;

static DWORD WINAPI ground_check_thread(LPVOID param)
{
    (void)param;
    while (g_bg_active) {
        DWORD ball_val = g_ball_ptr;
        if (ball_val) {
            char* ball = (char*)ball_val;
            /* Skip if ball is in death/respawn state */
            if (*(BYTE*)(ball + 0xC4C) == 0) {
                g_on_ground = Ball_IsGrounded(ball);
            } else {
                g_on_ground = 0;
            }
        }
        Sleep(10);
    }
    return 0;
}

/* In DllMain DLL_PROCESS_ATTACH: */
CreateThread(NULL, 0, ground_check_thread, NULL, 0, NULL);

/* In DllMain DLL_PROCESS_DETACH: */
g_bg_active = 0;
WaitForSingleObject(g_bg_thread, 2000);

In the code cave, store the ball pointer:

/* MOV [g_ball_ptr], ESI  (ESI = ball pointer at Ball_Update hook) */
89 35 <addr_of_g_ball_ptr>

In the code cave, check the ground flag:

/* MOV EAX, [g_on_ground] */
A1 <addr_of_g_on_ground>
/* TEST EAX, EAX */
85 C0
/* JZ .done (not grounded) */
74 <offset>

Performance Notes

  • Mesh_FindClosestCollision builds a temp SpatialTree per call — not lightweight
  • Engine calls it 2-3 times per ball per tick; 5-10 extra calls per frame is safe
  • max_dist affects performance: larger = wider AABB = more triangles tested
  • Static geometry only — does NOT test against other balls or dynamic objects
  • No hit normal returned — only the hit point

Gravity Direction Per Level

For standard levels, (0,-1,0) is the correct downward direction. But some
levels have tilted gravity:

Level Gravity Direction How to detect
Most levels (0, -1, 0) Standard
Up Race (L6) (-1, 0, 0) Tilted
odd race (0, 0, 1) Flat

For robust mods, read gravity from the CollisionMesh:

void* cm = *(void**)((char*)ball + 0x1A4);  /* CollisionMesh* (physics body) */
Vec3 down = {
    *(float*)((char*)cm + 0xC8C),  /* grav_dir_x */
    *(float*)((char*)cm + 0xC90),  /* grav_dir_y */
    *(float*)((char*)cm + 0xC94)   /* grav_dir_z */
};

Working Example

See mods/jump_mod/jump_mod.c for a complete working implementation of this
pattern — a jump mod that uses raycast ground detection instead of a cooldown
timer.

Cross-References

  • references/collision-raycast-system.md — full raycast API, calling convention, max_dist semantics
  • references/ball-ground-detection.md — why built-in ball flags don't work for ground detection
  • references/jump-ground-check.md — jump-specific ground check approaches
  • references/bass-proxy-debugging.md — BASS proxy DLL pattern, code cave pitfalls
  • mods/jump_mod/README.md — working mod using this pattern

🔗 Related Documents

RE - TODO List

types : project
keywords :

📂 View source on GitHub


Hamsterball RE - TODO List

Milestone 1: Executable Environment and Binary/Resource Inventory ✅

  • [x] Download and preserve original binaries (installer + installed folder)
  • [x] Compute SHA256 hashes of all 450 files
  • [x] Extract installed game folder (1,404,928 byte EXE + 450 asset files)
  • [x] Document file formats (MESHWORLD, MESH, MO3, OGG, XML, CFG)
  • [x] Document DLL dependencies (d3d8, dinput8, dsound, bass, etc.)
  • [x] Install Wine 9.0, test game launch (partial success - no display)

Milestone 2: Subsystem Map and First Labeled Ghidra Database ✅

  • [x] Run Ghidra headless analysis (3,781 functions analyzed)
  • [x] Use r2 for initial function mapping (1,869 functions found)
  • [x] Identify entry point (0x004BB4C8)
  • [x] Identify Graphics::Initialize (0x00455380)
  • [x] Identify window class "AthenaWindow"
  • [x] Map import table (9 DLLs, 177 imports)
  • [x] Map key string addresses (App::Initialize, etc.)
  • [x] Connect Ghidra MCP for interactive decompilation (headless server on port 8089)
  • [x] Label top 50 functions in Ghidra (57 labeled, now 975+)
  • [x] Map all subsystem addresses (graphics, audio, input, physics, UI, save, DRM)
  • [x] 100% DOCUMENTED: All 3,781 functions identified and renamed (0 FUN_ remaining)*

Milestone 3: Documented Runtime Dependencies and Launch Procedure 🔄

  • [x] Document DLL dependencies (9 DLLs)
  • [x] Document registry keys (ADVAPI32 functions identified)
  • [x] Document file paths (DATA\HS.CFG, textures, levels, meshes)
  • [ ] Test full game launch with Xvfb + Wine
  • [ ] Install DirectX 8 runtime via winetricks
  • [ ] Document all registry writes during launch
  • [ ] Test multiplayer / tournament save

Milestone 4: Reconstructed Core Loop and Resource Loading ✅ (mostly)

  • [x] Decompile WinMain function
  • [x] Decompile App::Initialize steps 1-26
  • [x] Map game loop (PeekMessageA → Update → Render)
  • [x] Reverse MESHWORLD file format parser (MeshWorld_Parse at 0x470930)
  • [x] Reverse MESH file format parser (same parser, different loading)
  • [x] Reverse texture loading (Graphics_LoadTexture, D3DX embedded)
  • [x] Reverse audio initialization (BASS_Init, BASS_MusicLoad)
  • [x] Reverse input initialization (DirectInput8Create, Input_Init)

Milestone 5: Reconstructed Input/Render/Physics/Menu Pipeline ✅

  • [x] Reverse D3D8 device creation and render pipeline
  • [x] Reverse DirectInput keyboard/gamepad input (Input_Init, KeyboardDevice)
  • [x] Reverse ball physics (Ball_Update 18KB, Ball_ApplyForce, Ball_CheckCollisionPlanes)
  • [x] Reverse camera system (CAMERALOOKAT)
  • [x] Reverse level object system (N:/E: prefixes)
  • [x] Reverse AthenaList container class (0x53210)
  • [x] Document ball vtable and physics constants

Milestone 6: First Playable Open-Source Build 🔄 — Phase 2 Deep Docs Done

  • [x] Create C project scaffold with D3D8/DInput8/DSound8 (Windows APIs)
  • [x] Implement MESH file loader
  • [x] Implement MESHWORLD file loader
  • [x] Build and run on Wine (D3D8 rendering works, 30K+ FPS counter)
  • [x] WinMain (0x4278E0) mirrored with full D3D8 pipeline
  • [x] Arena-WarmUp level loads: 17 objects, ball at (184.9, 26.4, 183.4)
  • [ ] Handle DirectInput window focus in Xvfb (ball movement)
  • [ ] Implement collision plane system (Level_LoadCollision RE in progress)
  • [ ] Render full MESHWORLD geometry (not just ball/objects)

Phase 2 Deep Documentation (Complete)

  • PARTICLE_SYSTEM.md: Ball_CreateTrailParticles (9-particle ring, ArenaScoreParticle allocator)
  • CAMERA_SYSTEM.md: 5-mode camera (follow/path/shake/snap/orbit), Scene_SetCamera offsets
  • SAVE_CONFIG_REGISTRY_SYSTEM.md: 30+ registry fields, BestTime/Medals binary blobs, App struct
  • AUDIO_SYSTEM_SFX.md: BASS music + DirectSound 3D SFX, 55 sound effects with channel counts
  • LEVEL_OBJECT_FACTORY.md: 25+ Create* functions, struct sizes, scene offsets, full factory map
  • ASSET_MANIFEST.md: Complete resource loading manifest (5 fonts, 40+ textures, 14 meshes, 55 sounds)
  • GAME_STATE_RACE_LIFECYCLE.md: App state machine, start/end race, tournament progression, timer
  • D3D8_RENDERING_PIPELINE.md: 8-pass render system, z-buffer interleaving, alpha blending, lighting
  • SCENE_STRUCT.md: 50+ Scene struct offsets, creation flow, camera config, object lists
  • INPUT_SYSTEM: Ball_GetInputForce (3-mode: keyboard/mouse/joystick), Scene_HandleInput dispatch
  • Ball physics: Ball_AdvancePositionOrCollision 6-phase pipeline (damping, collision, gravity, trail)
  • Race completion: RaceGoalReached ctor, medal thresholds, best time tracking
  • UI_MENU_SYSTEM_DEEP.md: 101 menu functions, MainMenu 7 items, DifficultyMenu 4 items, UIListItem 0x444B struct
  • COLLISION_SYSTEM_DEEP.md: Octree traversal, AABB, CollisionMesh 0xCB0, .COL format, 3-tier dispatch
  • FONT_TEXT_SYSTEM.md: font.description binary format, FontList struct, SDF builder, 5 fonts
  • DIRECTINPUT_SYSTEM.md: InputDevice 0x91C, 3 input modes, DIK scan codes, Scene_HandleInput
  • GAME_LOOP_WINDOW_MANAGEMENT.md: App_Run fixed-timestep, vtable dispatch, frame timing, WinMain
  • Decompilations: camera/, particles/, save/, input/, audio/, scene/, physics/, level/

Milestone 7: Behavior-Polish and Bug Reduction

  • [ ] Match physics feel of original
  • [ ] Match camera behavior
  • [ ] Match audio triggers
  • [ ] Match menu flow
  • [ ] Match scoring system

Milestone 8: Selective Machine-Code Matching

  • [ ] Identify critical functions for codegen matching
  • [ ] Set up MSVC 6/7 build environment
  • [ ] Match specific function signatures
  • [ ] Verify with binary diff

Milestone 9: Scene System Documentation ✅

  • [x] Decompile Scene_dtor (0x419770) - master scene destructor
  • [x] Decompile Scene_Update (0x419C00) - main tick function
  • [x] Decompile Scene_Render (0x41A2E0) - 1P/2P/split render
  • [x] Decompile Scene_SetCamera (0x419FA0) - camera positioning (5 modes)
  • [x] Decompile Scene_RenderWithCamera (0x40DFA0) - two-pass render
  • [x] Decompile Scene_UpdateBallsAndState (0x41B540) - per-ball physics + respawn
  • [x] Decompile Scene_HandleRaceEnd (0x41B130) - race finish conditions
  • [x] Decompile Scene_ProcessRaceEnd (0x41A540) - race countdown
  • [x] Decompile Gear_AdvanceAlongPath (0x418930) - 8-sample gradient descent path follower
  • [x] Document Ball_Update (0x405E00) - 23-step physics pipeline
  • [x] Document Ball vtable (0x4CF3A0, 9 entries) and App_Run loop (0x46BD80)
  • [x] Document Scene vtable (0x4D0260, 36 entries, all named)
  • [x] Document SceneObject class (vtable 0x4D934C, 0xD4 bytes)
  • [x] Document 8 level setup functions (leveldark through levelup)
  • [x] Document 3 reflection render passes
  • [x] Document scene object lists (6 lists at known offsets)
  • [x] Document all 15 ArenaBoard arena init functions
  • [x] Document Board (Tournament) constructors (Toob/Rodenthood level8)
  • [x] Deep doc: SCENE_SYSTEM_DECOMP.md with full tick order, render pipeline, camera modes
  • [x] Document remaining level setup functions (if they exist as Scene_Setup)
  • [x] Document Level_UpdateAndRender (0x40B600) - 6-phase level render pipeline
  • [x] Document Level_RenderObjects (0x40B570) - transparent pass renderer
  • [x] Document Scene_CheckPath (0x57EC0) - 359-cell ring pathfinder
  • [x] Document Scene_SpawnBallsAndObjects (0x41C5B0) - level startup spawner
  • [x] Document Scene_RenderAllObjects (0x45E0E0) - 3-pass object render
  • [x] Document Scene_RenderFrame (0x60DA0) - vertex buffer construction + font render

Milestone 10: Rendering Pipeline Documentation ✅

  • [x] Document SceneObject_RenderFull (0x470150) - full render with alpha path
  • [x] Document SceneObject_RenderSingleObj (0x470440) - single object render
  • [x] Document SceneObject_BuildStrips (0x472770) - triangle strip builder
  • [x] Document SceneObject_CheckCollision (0x45dfd0) - collision thunk
  • [x] Document SceneObject_ComputeCollisionSphere (0x46fbb0) - bounding sphere
  • [x] Document MeshWorld_ctor (0x46f3d0) - mesh world construction
  • [x] Document Mesh_InitTexture (0x49338e) - D3D texture init
  • [x] Document Mesh_DrawWithTransform (0x493671) - draw with temp transform
  • [x] Document Mesh_ClearColorVertices (0x49373d) - zero matching vertices
  • [x] Document Graphics_DrawIndexedPrimitive (0x47dfb9) - D3D draw wrapper
  • [x] Document Scene_BeginFrameThenRender (0x46f3b0) - frame begin wrapper
  • [x] Document D3D vtable dispatch map (9 offsets: 0x28-0x200)
  • [x] Document Graphics_ApplyMaterialAndDraw (0x455110) - material system
  • [x] Document Graphics_RenderScene (0x454BC0) - full 3D render pipeline
  • [x] Document D3D texture system (Ctor/Dtor/Init/Release 13 functions)
  • [x] Document material structure layout (0x50 bytes, 8 fields)
  • [x] Document graphics subsystem functions (25+ functions cataloged)

Milestone 11: AthenaString System Documentation ✅

  • [x] Document AthenaString_AssignCStr (0x473500) - C string assign (75 xrefs)
  • [x] Document AthenaString_dtor (0x4736b0) - destructor (85 xrefs)
  • [x] Document AthenaString_AssignCRLF (0x473a50) - CRLF assign (21 xrefs)
  • [x] Document AthenaString_SprintfToBuffer (0x4bae43) - sprintf to buffer (71 xrefs)
  • [x] Document AthenaString_Sprintf (0x4bbdfd) - already known
  • [x] Document AthenaString_Format (0x466c70) - already known
  • [x] Document AthenaString_Assign (0x4737f0) - already known
  • [x] Document AthenaString_Init/CopyCtor/WriteTag - already known

New Documentation Created

  • BALL_PHYSICS_DECOMP.md - Full ball physics system decompilation
  • MESHWORLD_OBJECT_TYPES.md - N:/E: object type reference
  • Updated FUNCTION_MAP.md with Input, Texture, Ball systems
  • Updated KEY_DECOMPILATIONS.md

🔗 Related Documents

RE — Key Findings Summary

types : decompilation
keywords :

📂 View source on GitHub


Hamsterball RE — Key Findings Summary

Session Date: April 12, 2026

1. Game Loop (App::Run)

Address: 0x46BD80

Function Signature:

void App::Run(App* this);  // ECX = App* (0x4FD680)

Key Offsets:

Offset Field Description
+0x158 minimized 1 if minimized
+0x159 running 1 to quit
+0x15A active 1 if game active
+0x164 last_tick GetTickCount()
+0x168 frame_time 1000 / target_fps
+0x170 target_fps e.g., 60
+0x174 graphics Graphics*
+0x18C frame_counter frames since last FPS reset
+0x194 frame_count FPS display counter
+0x198 fps_buffer FPS string display
+0x1AC show_fps 1 to show FPS

Loop Logic:

  1. PeekMessage loop (message pump)
  2. If elapsed < frame_time - 5ms: skip (count skipped frames)
  3. After 10 skips: force update
  4. Otherwise: Update() → Draw() → Present()
  5. Sleep/yield between frames

2. Ball Physics

Ball Vtable (0x4CF3A0)

Index Offset Address Function Description
0 +0x00 0x4027F0 Ball_dtor Cleanup and free
1 +0x04 0x405100 Ball_Update Per-frame physics init
2 +0x08 0x402DE0 Ball_CollisionCheck Check collisions
3 +0x0C 0x402A70 OnCollision Handle collision event
4 +0x10 0x408390 Ball_Render Render the ball
5 +0x14 0x401590 vtable[5] Calls vtable[6] with extra param
6 +0x18 0x402650 Ball_ApplyForce Apply force/velocity
7 +0x1C 0x402C10 ??? Physics plane check
8 +0x20 0x409480 ??? Unknown

Ball Object Structure

Offset Type Field Description
+0x164 float x Position X
+0x168 float y Position Y
+0x16C float z Position Z
+0x170 float vx Velocity X
+0x174 float vy Velocity Y
+0x178 float vz Velocity Z
+0x17C float ang_vel_z Angular velocity Z
+0x180 float ang_vel_y Angular velocity Y
+0x184 float ang_vel_x Angular velocity X
+0x198 float facing_angle Movement direction
+0x19C char facing_valid Angle computed
+0x278 float ??? 0.5 (set by Update dispatcher)
+0x27C float friction 0.2 (set by Update dispatcher)
+0x280 char ??? Collision flag
+0x284 float radius 35.0 (ball radius)
+0x2CC char no_input No player input
+0x2E8 char flag1 Reset to 0 each frame
+0x2E9 char impact_shatter NOT "reset to 0 each frame"! Sticky flag, only cleared by Ball_ctor2. See docs/agent-knowledge/ball-ground-detection.md
+0x2EC uint32 ??? Reset to 0 each frame
+0x2F0 uint32 force_counter Forces applied this frame
+0x2F8 char update_in_progress Set to 1 during update
+0x2F9 char frozen Ball frozen (on surface)
+0x2FC uint32 freeze_timer Countdown when frozen (150)
+0x310 char ??? Set to 1 each frame
+0x314 float ang_vel_y Set to 0 on snap
+0x318 float ang_vel_x Set to 0 on snap
+0x31C char ??? Set to 0 on snap
+0x31D char ??? Render flag
+0x324 char in_tube Ball is in tube/pipe
+0x748 int gravity_plane 0=XY, 1=Y-tilted, 2=XZ
+0xC4C char is_shrunk odd race shrunk state (E:SHRINK/E:GROW)
+0xC5C int on_ice Ice surface
+0xC74 int collision_count Reset to 0 each frame
+0xC78 float ??? 0.0 (set by Update dispatcher)
+0xC7C float ??? 50.0 (set by Update dispatcher)
+0xC6C float ??? 600.0 (set by Update dispatcher)
+0xC70 float ??? 1200.0 (set by Update dispatcher)
+0xC80 char ??? Has special collision
+0xC88-+0xC94 float[4] special_col Special collision data

Ball_ApplyForce (0x402650)

void Ball_ApplyForce(ball, force_x, force_y, force_z, magnitude);

Logic:

  • If frozen or no input: ignore
  • Apply force multiplier based on:
    • force_counter (first frame = 1.0, then *0.25)
    • in_tube (*0.0 — no force in tube!)
    • on_ice (*0.2 + ice friction 6.0)
    • is_dizzy (*0.75)
  • Accumulate into velocity at +0x170/0x174/0x178
  • Compute facing angle based on gravity plane

Ball_Update (0x405190)

The main per-frame physics update:

  1. If in_tube: skip (return immediately)
  2. Reset per-frame state
  3. Find closest collision surface based on gravity_plane:
    • Plane 0: XY (standard), check surfaces with [Z] tag
    • Plane 1: Y-tilted, check surfaces with [X] tag
    • Plane 2: XZ, check surfaces with [Z] tag
  4. If surface found: snap position to surface, zero velocity, set frozen=1
  5. If no surface: ball falls freely

Global Constants

Address Float Value Usage
0x4CF368 0.0 epsilon Float comparison epsilon
0x4CF36C 0.75 dizzy_mult Force multiplier when dizzy
0x4CF374 0.2 ice_mult Force multiplier on ice
0x4CF378 0.0 tube_mult Force multiplier in tube
0x4CF380 0.25 force_mult Force multiplier after 1st frame
0x4CF39C 0.037 radius_mult Collision radius multiplier
0x4CF3E8 6.0 ice_friction Ice friction factor
0x4CF484 40.0 col_mesh_dist Collision mesh distance threshold
0x4CF48C 2.0 y_threshold Y offset threshold (radius + epsilon)

3. Binary MESHWORLD Format

Format:

[uint32: object_count]
[uint32: str_len][type_string][object_data...]
[uint32: str_len][type_string][object_data...]
...

Object Data Sizes:

  • Simple objects (START, SAFESPOT): 28 bytes after type string
    • 3 floats: position (x, y, z)
    • 4 uint32: rotation/flags
  • Complex objects (FLAG, PLATFORM): 76+ bytes
    • Transform matrix, colors, size param
    • Texture reference string
    • Face index data

Object Types Found:

  • START1-1, START2-1, START2-2 (spawn points)
  • FLAG02, FLAG04, FLAG06, FLAG07 (checkpoints)
  • SAFESPOT (safe zones)
  • PLATFORM, N:SINKPLATFORM (platforms)
  • N:BUMPER1-4 (bumpers)
  • E:NODIZZY (dizzy zones)
  • E:LIMIT (boundaries)
  • CAMERALOOKAT (camera points)
  • BADBALL (enemies)
  • BCMESH (ball mesh reference)
  • S:Walls(NOSHADOW) (walls)

4. Binary MESH Format (3D Models)

Header:

[uint32: version = 1]
[uint32: name_len]
[name_len bytes: model name]
[material data: ambient, diffuse, specular, etc.]
[uint32: texture_name_len]
[texture_name_len bytes: texture filename]
[uint32: vertex_count]
[vertex data: vertex_count * 32 bytes]
[face data: ...]

Vertex Format (32 bytes):

[float: x][float: y][float: z]     // position
[float: nx][float: ny][float: nz]  // normal
[float: u][float: v]               // texture coordinates

5. Key Functions Decompiled

Address Function Lines Description
0x46BD80 app::run 91 Game loop
0x429530 App::Initialize ~200 App init
0x405190 Ball_Update 477 Main physics
0x405100 Ball_Update_disp 27 Physics init dispatcher
0x402650 Ball_ApplyForce 47 Force/velocity
0x402DE0 Ball_CollisionCheck 99 Collision check
0x402A70 OnCollision 64 Collision event
0x402C10 ??? 68 Physics plane check
0x402860 Ball_Render 41 Ball render stub
0x408390 Ball_Render 166 Actual ball render
0x40AFE0 Ball_ctor 30 Ball constructor
0x455380 Graphics_Initialize 272 D3D init
0x470930 MeshWorld_Parse 787 ASE text parser
0x4629E0 Binary_mesh_loader 289 Binary loader

6. Virtual Tables

App vtable (0x4D8FB0)

  • +0x00: Destructor
  • +0x0C: LoadMesh
  • +0x30: LoadMeshWorld
  • +0x38: Binary mesh loader

Ball vtable (0x4CF3A0)

  • +0x00: dtor (0x4027F0)
  • +0x04: Update dispatcher (0x405100)
  • +0x08: CollisionCheck (0x402DE0)
  • +0x0C: OnCollision (0x402A70)
  • +0x10: Render (0x408390)
  • +0x14: vtable[5] (0x401590)
  • +0x18: ApplyForce (0x402650)
  • +0x1C: vtable[7] (0x402C10)
  • +0x20: vtable[8] (0x409480)

7. Files Generated

  • analysis/ghidra/decomp_*.c — Full decompilations (30+ files)
  • docs/GAME_LOOP_DECOMP.mdapp::run details
  • docs/BALL_UPDATE_DECOMP.md — Ball physics details
  • docs/MESHWORLD_FORMAT.md — Format spec
  • docs/KEY_FINDINGS.md — This summary
  • reimpl/src/level/meshworld_parser.c — Working parser
  • reimpl/build/level_viewer — SDL2+OpenGL viewer

8. Statistics

  • Total decompiled C code: ~2500 lines
  • Functions decompiled: 15+
  • Object types found: 20+
  • Level files parsed: 86

Last updated: April 13, 2026
Project: Hamsterball RE (Raptisoft, 2000s)
Permission: Original developer granted permission for RE


9. Scene System (April 13, 2026)

Scene Architecture

The game uses a virtual-dispatch scene system. Each race level creates a scene object with a vtable that provides:

  • dtor (+0x00): Master cleanup (Scene_dtor at 0x419770)
  • vmethods +0x4C-0x58: Update render passes (PreRender, RenderLevel, RenderDynamic, PostRender)
  • vmethod +0x60/0x64/0x68: Render3D, RenderObjects, RenderOverlay
  • vmethod +0x6C/0x70: RenderMenu, RenderHUD
  • vmethod +0x7C/0x80: Per-dynamic-object update, per-scene init

Scene_Update (0x419C00) is the main tick:

  1. Increment frame counter (+0xD88)
  2. Demo timer check (50-frame countdown, shows buy dialog)
  3. Unpause check (Input_CheckKeyCombo mode 2 for ESC)
  4. Ball position sync from +0xA75 list
  5. Camera tracking (ball at App+0x5DC)
  6. Screen offset animation (+0xA6E, -10/frame toward 0 or -800)
  7. Static object update + removal of dead objects (+0x22E list)
  8. Render pass vmethods (+0x4C, +0x50, +0x54, +0x58)
  9. Dynamic object iteration (+0xD8B list)

Scene_Render (0x41A2E0) dispatches by player count:

  • 1P: Single viewport, all render passes
  • 2P: Set camera from player list at +0x3A38
  • 3-4P: Split-screen per viewport, per-camera

SceneObject Class (0x4D934C)

Common renderable game object (0xD4 bytes):

  • 3 matrix transforms: base scale (+0x94), rotation (+0xA8), world (+0xBC)
  • Visible flag (+0x88), zOrder/object ID (+0x8C)
  • Bounding radius (+0xCC), type (+0xD0)
  • Scene registration via Scene_RegisterObject stores at scene+0x710+id*4

Level Setup Pattern

All level setups follow: Level_ctor → Level_Clone → Level_InitScene → vmethod +0x80, then level-specific object creation (flags, signs, bumpers, vacuum tubes, etc).

Level (internal) Address Race Special Objects
Level1 0x41CA40 Warm-up Race
levelcascade 0x4110D0 Beginner Race 8 bumpers
Level2 (base) Intermediate Race
Level3 0x416270 Dizzy Race 2-player support
Level4 0x40E190 Tower Race
Level6 0x40830 odd race PILLAR, MAGNIFYER, CLOUDSCAPE, fog
LevelDark 0x40F360 Neon Race
Level5 0x40EA90 Expert Race LAUNCH01/02/03, CHROMESHADOW
Level8 0x411F60 Toob Race 4 bumpers, TarBubble
Level7 (base) Wobbly Race
LevelGlass (base) Glass Race
Level9 (base) Sky Race
Level10 (base) Master Race
LevelUp 0x411540 Up Race VAC-IN/VAC-OUT vacuum tubes
LevelImpossible (base) Impossible Race

Scene Vtable (0x4D0260, 36 entries)

Key identified slots:

  • [0] Scene_DeletingDtor - destructor + conditional free
  • [1] Scene_Update - main 9-step tick
  • [2] Scene_Render - 1P/2P/split render dispatch
  • [3] Scene_HandleInput - menu item iteration, input check, current item at +0x864
  • [4] Scene_ActivateCurrentItem - call vmethod+0x10 on current item
  • [6] Scene_SelectCurrentItem - call vmethod+0x0C on current item
  • [11/12] Scene_ClearCurrentItem - set current item pointer to NULL
  • [14] Scene_DestroyScene - cleanup + save via SaveAndCleanup
  • [15] Scene_NotifyObjects - iterate list, call FUN_4699D0 on each
  • [16] Scene_SetDestroyed - set +0x2C=1 flag
  • [17] Scene_SaveAndCleanup - delegates to FUN_469AC0
  • [18] Level_InitScene
  • [19] Scene_HandleRaceEnd - timer decrement, lap tracking, Game Over, RaceResultPopup ctor
  • [20] Scene_UpdateBallsAndState - ball list iteration, SetCamera, Ball_Update, destroy finished
  • [22] Scene_ProcessRaceEnd - race countdown timer, scene transition on expire
  • [23] Scene_HandleBallFinish - ball finish state machine (start→150f countdown→finish→popup→done)
  • [24-26] Level render methods
  • [27] Scene_RenderScoreHUD - tournament title, countdown bar, "Score", Player 2, timer
  • [28] Scene_RenderTimerHUD - race timer, split screen divider, overlay popups
  • [9,10,13,21,30,34] NoOp_return (0x44B840) - default stubs overridden by subclasses

SceneObject Vtable (0x4D934C, 10 entries)

  • [0] SceneObject_dtor (0x46B650)
  • [1] SceneObject_SetPosition (0x46B490) - sets +0x08,x,y,z + calls vmethod+0x0C
  • [2] SceneObject_SetScale (0x46B4B0) - sets +0x14 scale + calls vmethod+0x0C
  • [3] SceneObject_Render (0x46B670) - build world matrix, D3D SetTransform+SetMaterial
  • [4] SceneObject_SetVisible (0x46B4D0) - toggle +0x88
  • [7] SceneObject_DeletingDtor (0x46B9F0)
  • SceneObject_BaseDtor (0x46B860) iterates child list, calls dtor(1) on each child

Rumble/Arena Board System

15 arena init functions (ArenaBoard_*_Init) follow this pattern:

  1. Level_ctor(arena_level_path) → store at scene+0x22B
  2. Level_Clone(source) → store at scene+0x22C
  3. CameraLookAt(scene)
  4. Virtual dispatch vmethod+0x80 (post-setup)

Special arena behaviors:

  • Warm-up (0x413C20): —
  • Beginner (0x413CE0): 4 bumpers via N:BUMPER%d (CASCADERACE = Beginner Race)
  • Toob (0x414F00): 5 bumpers via N:BUMPER%d
  • Dizzy (0x414240): Extra Level3-Swirl loaded
  • Sky (0x4158C0): PILLAR name scanning via __strnicmp
  • Neon (0x416F40): Scale matrix setup for dynamic objects, SceneObject decoration at +0x11F9

Tournament Board Constructors

Board subclasses (0x419030 base) hold sub-level arrays:

  • Toob Board (0x41F4B0): "Rodenthood" tournament with 5 sub-levels:
    Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1, Level8-Blockdawg2

Scene Object Lists (Scene offsets)

Offset Description
+0x22E Static scene objects (persistent)
+0x335 Dynamic objects (moving platforms etc)
+0x43B Sub-objects (linked to dynamic)
+0xA75 Ball list (per-ball position updates)
+0xC81 Additional object list
+0xD8B Render update list

Camera System Detail

Scene_SetCamera (0x419FA0) positions camera:

  1. Start from ball position +0x758
  2. Add scene offset (+0x434C)
  3. If boundary check enabled (+0x3F1C): compute distance from center, clamp with distance falloff
  4. If countdown timer active (+0x3F2C): snap camera to ball for initial frames
  5. Noise randomizer at ball+0x744 (jitters position)
  6. Set view/projection matrices

Difficulty System

Difficulty_GetTimeModifier (0x428ED0) returns multiplier based on +0x23C:

  • Mode 0: Easy (time * _DAT_4CF3F0)
  • Mode 1: Normal (time * 1.0)
  • Mode 2: Hard (time * 2.0)
  • Default: 0.0 (no time?)

10. Documentation Progress

  • Total functions: 3,988
  • Documented: 1,816 (45.5%)
  • Session progress: 44.1% → 45.5% (+78 functions named across sessions)
  • Key docs updated: FUNCTION_MAP.md, KEY_FINDINGS.md, STRUCTS_AND_TYPES.md

11. PRNG System (RNG_Rand, 0x45DD60)

  • 55-entry circular buffer (additive generator, like Mitchell & Moore)
  • Two pointers wrap at index 55 (0x37)
  • Returns (buf[read_ptr] + buf[write_ptr]) & 0x3FFFFFFF >> 6) % range
  • Optional signed mode: if param_2=1 and RNG_Rand(2)==0, negate result
  • 193 cross-references — used everywhere for randomization

12. Graphics Transform Pipeline

  • Direct3D 8 SetTransform wrapper chain:
    • Gfx_SetPosition (69 xrefs) → D3D world matrix translate
    • Gfx_RotateY (15 xrefs) → rotation around Y
    • Gfx_ScaleX/Y/Z (40/35/26 xrefs) → scale individual axes
    • Matrix44_Zero → clear, then set diagonals to 1.0
    • Gfx_SetAlphaBlendState → D3DRS_SRCBLEND/DESTBLEND
    • Gfx_SetCullMode → D3DRS_CULLMODE (none/CW/CCW)

13. UI List System (vtable 0x4D6A70)

  • Base class for all menu screens in the game
  • SimpleMenu_ctor (0x448F20) sets up "Simple Menu" with item list
  • Items are 0x444-byte structs with: display text, subtext, color, SceneObject icon, height
  • UIList_AddItem (86 xrefs): creates item, copies text, links SceneObject
  • UIList_AddSpacer (29 xrefs): adds empty row with height
  • Rendering: UIList_Render draws items with gradient bar selection, icons, scroll arrows
  • Input: UIList_HandleKeyNav for up/down navigation, UIList_ScrollUpdate for mouse wheel
  • UIList_ActivateCurrentItem: "Back" → sound 650, "Continue" → sound 50, else vtable dispatch
  • UIList_Layout: computes total widths, positions SceneObjects for 2D rendering

14. Rumble Board System (vtable PTR 0x4D1358)

  • ArenaBoard extends Board (which extends Scene)
  • Base score: 6000 per round
  • 25 rounds per game (offset +0x47D0 = 0x19 = 25)
  • Timer: ToggleTimer_Init / TickTimer / CleanupTimer manage round time
  • ArenaBoard_Render draws timer bar, round number ".%d", and "TIE BREAKER!" text
  • ArenaBoard_Update checks round end: finds max score, handles ties
  • Tie detection: counts how many players share max score; if ≥2, sets tie breaker flag
  • On game over: spawns ArenaScoreParticle object (FUN_4CB10), plays "Game Over" music
  • ArenaScoreParticle_ctor uses difficulty index [0,1,2] → scale [0.02, 0.03, 0.04]
  • ArenaBoard vtable at PTR_FUN_004d1358

15. Scene Rendering Pipeline (0x45E0E0)

  • Scene_RenderAllObjects is the main 3D render function
  • Three-phase draw: opaque objects → alpha-blended objects → shadow objects
  • Object flags at offsets: +0x85F (shadow), +0x860 (alpha), +0x862 (deferred), +0x863 (skip)
  • For alpha objects: temporarily shifts projection matrix for shadow rendering
  • Each object has material index (+0x83C) and bone array for skeleton rendering
  • Scene_RenderBallShadow: renders ball with depth bias for shadow pass

16. SceneObject Rendering Pipeline (Session 14)

SceneObject_RenderFull (0x470150, 40 xrefs)

  • Full SceneObject render: calls Ball_Render, then iterates strips with material application
  • Two rendering paths based on alpha flag at +0x459:
    • Alpha-blended (flag==1): per-strip material application, iterates strips via AthenaList
    • Opaque (flag==0): direct strip dispatch via Graphics_ApplyMaterialAndDraw
  • Uses Graphics_ApplyMaterialAndDraw for mesh rendering

SceneObject_RenderSingleObj (0x470440, 39 xrefs)

  • Renders a single SceneObject: applies world transform, sets material, calls DrawIndexedPrimitive or strips
  • Similar to SceneObject_RenderFull but for a specific object parameter
  • Handles both alpha and non-alpha materials

SceneObject_BuildStrips (0x472770, 39 xrefs)

  • Builds triangle strips for SceneObject rendering
  • Iterates mesh nodes, creates strip data entries into the strip list (AthenaList)
  • Interleaves vertex positions for efficient GPU upload
  • Sets alpha flag on this+0xe
  • Result stored at this+8 offset 0x454

SceneObject_CheckCollision (0x45dfd0, 38 xrefs)

  • Collision test thunk: computes bounding sphere from AABB (field 0x45c-0x470 extents * 0.5)
  • Calculates radius via distance formula, then calls Ball_CheckCollisionPlanes
  • Used per-SceneObject in collision detection

SceneObject_ComputeCollisionSphere (0x46fbb0, 38 xrefs)

  • Inner function: computes bounding sphere from AABB
  • Computes extents: (min+max) * 0.5 for each axis
  • Calculates radius: sqrt(dxdx + dydy + dz*dz)
  • Caches result at +0x480 to avoid recomputation

Scene_BeginFrameThenRender (0x46f3b0, 39 xrefs)

  • Begins graphics frame (Graphics_BeginFrame), then invokes render callback
  • Simple wrapper around vtable dispatch at +0x10

17. Mesh/Texture System

MeshWorld_ctor (0x46f3d0, 39 xrefs)

  • MeshWorld constructor: takes scene ptr, strip count, and filename
  • Allocates vertex/index/strip buffers
  • If cached file exists with valid magic (0xBEEF), loads binary strip data
  • Otherwise allocates empty buffers for scene to fill

Mesh_InitTexture (0x49338e, 32 xrefs)

  • Initializes D3D texture object from DDSURFACEDESC
  • Sets vtable at 0x4DC044, copies surface desc fields (pitch/stride/width/height)
  • If format is D3DFMT_A8R8G8B8 or D3DFMT_X8R8G8B8 (0x29/0x28), loads palette data
  • If has palette ptr, copies 256 RGBA entries; else fills with 1.0

Mesh_DrawWithTransform (0x493671, 31 xrefs)

  • Draws mesh with temporary transform override
  • Saves current transform state, applies new transform, restores

Mesh_ClearColorVertices (0x49373d, 30 xrefs)

  • Zeroes out vertices matching the clear color (transparency hack)
  • Iterates vertex buffer, clears vertices that match clear color RGBA

D3DDevice_SetFPUControl (0x49336b, 29 xrefs)

  • Sets FPU control word from device state
  • Maintains FPU precision for D3D rendering

18. AthenaString System

AthenaString_AssignCStr (0x473500, 75 xrefs)

  • AthenaString assign from C string
  • Frees old buffer at +4, allocates new buffer sized to strlen(param_1)+1
  • Copies string, sets null-flag at +0x18 if param_1 is NULL

AthenaString_dtor (0x4736b0, 85 xrefs)

  • AthenaString destructor
  • Sets vtable to base dtor vtable (0x4D290C), frees internal buffer at +4
  • Zeros length at +0x14 and capacity at +0x8

AthenaString_AssignCRLF (0x473a50, 21 xrefs)

  • Assigns CRLF ("\r\n") to AthenaString

AthenaString_SprintfToBuffer (0x4bae43, 71 xrefs)

  • sprintf into char buffer via fake FILE struct
  • Uses same FILE-trick as AthenaString_Sprintf but writes to caller-provided buffer

19. Registry Functions

RegKey_WriteBool (0x473050, 30 xrefs)

  • Write boolean to registry via RegSetValueExA (REG_BINARY)

RegKey_ReadBool (0x473130, 28 xrefs)

  • Read boolean from registry via RegQueryValueExA

RegKey_ReadString (0x473170, 23 xrefs)

  • Read string from registry with fallback attempts

20. CRT/Compiler Intrinsics (not game code, but documented)

Address Name Xrefs Description
0x4ba754 __ftol2 358 CRT float-to-int64 conversion, compiler intrinsic
0x4bc7c8 __errno 40 Returns thread-local errno pointer
0x4bc7d1 __doserrno 23 Returns thread-local DOS errno pointer
0x4bcda8 __security_init_cookie 34 CRT security cookie initialization
0x4bac20 strstr 22 String search with SIMD optimization
0x4bc0d1 strtok 50 Thread-safe string tokenizer
0x4a458c longjmp_with_cleanup 34 CRT longjmp with optional cleanup callback
0x4a45aa seh_filter_invoke 21 Invoke SEH exception filter callback
0x4c02e7 LeaveCriticalSection_indexed 23 LeaveCriticalSection by index

21. Graphics Pipeline

Graphics_DrawIndexedPrimitive (0x47dfb9, 25 xrefs)

  • D3D DrawIndexedPrimitive wrapper via vtable dispatch at offset 0x2C
  • Called from SceneObject_RenderFull for mesh rendering

🔗 Related Documents

RE Project

types : project
keywords :

📂 View source on GitHub


Hamsterball RE Project - Build Notes

Environment Setup

System

  • OS: Linux (Ubuntu)
  • Architecture: x86_64
  • Wine: 9.0 installed, game launches but no display

Tools Installed

  • Ghidra 12.0.4 at /opt/ghidra
  • radare2 (r2) - CLI disassembler/analyzer
  • pefile - Python PE parser
  • Wine 9.0 - Windows compatibility layer
  • Java 21 JDK
  • GCC toolchain

Project Layout

~/hamsterball-re/
├── originals/
│   ├── installer/
│   │   ├── setup_hamsterball.exe    (6.9 MB)
│   │   └── share_download.zip      (15.4 MB)
│   └── installed/
│       ├── Hamsterball.zip          (8.5 MB)
│       └── extracted/               (full game, 450 files)
├── analysis/
│   └── ghidra/
│       ├── HamsterballProject/     (Ghidra project, 20MB)
│       ├── functions.txt           (r2 function list)
│       └── r2_functions.txt        (r2 full function dump)
├── docs/
│   ├── RESEARCH_LOG.md
│   ├── FILE_HASHES.json
│   ├── FILE_FORMATS.md
│   ├── FUNCTION_MAP.md
│   ├── STRUCTS_AND_TYPES.md
│   ├── RUNTIME_ENVIRONMENT.md
│   └── BUILD_NOTES.md (this file)
├── reimpl/
│   ├── src/
│   ├── include/
│   ├── assets/
│   └── tools/
└── notes/

Key Findings So Far

Binary Analysis

  • PE32 i386 executable, MSVC compiled
  • Entry point: 0x004BB4C8
  • 5 sections: .text, .rdata, .data, .data1, .rsrc
  • 9 imported DLLs with ~200 function imports
  • 1,869 functions identified by r2
  • Window class: "AthenaWindow" (Raptisoft engine name)
  • Uses Direct3D 8 for rendering
  • Uses BASS library for audio (MO3 format music)

Key Function Addresses (CONFIRMED via r2)

  • fcn.00455380 - Graphics::Initialize (calls Direct3DCreate8)
    • Sets debug strings at offset +0x8 of object: "Graphics::Initialize(1)" through "Graphics::Initialize(23)"
  • entry0 (0x40BB4C8) - CRT entry point
  • fcn.00453ed0 - Graphics initialization sub-function
  • Cross-references to "AthenaWindow" at 0x46ba62, 0x46bb00, 0x46d218 (window creation)

Game Engine Architecture

  • Engine name: "Athena" (derived from window class "AthenaWindow")
  • Built by Raptisoft (John Raptis)
  • C++ with MSVC, DirectX 8 era (2010 timestamp)
  • Custom mesh format (not standard ASE, despite having ASE-style tokens)
  • Debug strings preserved in release build

Asset Format Summary

  • MESHWORLD: Binary level format with type strings
  • MESH: Binary 3D mesh with embedded texture references
  • MO3: Tracker music via BASS
  • OGG: Sound effects
  • Font: Custom bitmap font system with atlas PNGs
  • CFG: Binary save format with 64-byte player name records
  • XML: Jukebox.xml and RaceData.xml for game configuration

Next Steps

  1. Connect Ghidra MCP for interactive decompilation
  2. Map the game's main loop (WinMain → App::Initialize → game loop)
  3. Reverse Graphics::Initialize in detail
  4. Reverse the level loader (MESHWORLD parser)
  5. Begin C reimplementation scaffold

🔗 Related Documents

recon-analyzer

types : tools

📂 View source on GitHub


recon-analyzer

A tiny, program-agnostic first-pass analyzer for compiled binaries.

Usage

python3 recon-analyzer.py /path/to/binary
python3 recon-analyzer.py /path/to/binary --out report.md
python3 recon-analyzer.py /path/to/dir --recursive --out report.md
python3 recon-analyzer.py /path/to/binary --format json

What it does

  • Hashes the target (MD5, SHA256)
  • Computes entropy
  • Runs strings
  • Categorizes strings (paths, DLLs, APIs, file types, interesting keywords)
  • Summarizes PE structure (if pefile is installed)
  • Emits Markdown or JSON report

Requirements

  • Python 3.10+
  • strings command available
  • Optional: pefile (pip install pefile)

Example

python3 recon-analyzer.py ~/hamsterball-re/originals/installed/extracted/Hamsterball.exe --out /tmp/hb_recon.md

🔗 Related Documents

Reference Node (Ref) Loading S

types : docs
keywords :

📂 View source on GitHub


Hamsterball Reference Node (Ref) Loading System

Overview

Hamsterball levels load "reference nodes" (refs) from MESHWORLD files. Each ref has a name (e.g. SPEEDCYLINDER, BONK, GEAR) that determines what game object gets created. The game uses a multi-pass code-gated dispatch system — not a file-driven registry — to decide which refs to instantiate. The full pipeline runs in the master level loading function (~0x0041C8xx), which makes sequential passes over the ref point list.

Key finding: Adding a ref name to a MESHWORLD file is necessary but not sufficient. The ref must ALSO be handled by one of the dispatch passes (SAFESPOT extractor, SIGN handler, or vtable[33] factory), or the game will silently skip it. To load any object ref into any level, you must patch the Board's vtable[33] to point to a factory that handles the desired ref names, since each level's factory only recognizes its own subset of ref names.

Complete Loading Pipeline

Board constructor
  ├─ 1. Creates LevelData (board+0x8AC, size 0x10D0) with MESHWORLD file path
  │     LevelData parses MESHWORLD binary into AthenaLists:
  │     • Section 1 ref points → LevelData+0x480+0x894
  │     • Section 3 scene objects → mesh entities with name at +0x864
  │
  └─ 2. Loads sub-mesh MESHWORLD files into board+0x436C..0x4390

Master loading function (~0x0041C8xx)
  ├─ 3a. Iterates ref points, extracts SAFESPOT and START-DEBUG refs
  ├─ 3b. Conditional quality-dependent setup (scene+0x237 byte)
  ├─ 3c. Additional setup (0x0040BAA0, 0x0040C0F0)
  ├─ 3d. Scene_CreateSigns (0x0040C270) — "SIGN" prefix refs → StandsTipper objects
  ├─ 3e. Scene_CreateDynamicObjects (0x0040C430) — object refs via vtable[33] factory
  │     ├─ Factory matches ref name via __strnicmp
  │     ├─ Creates game object (Pendulum, Bonk, Gear, etc.)
  │     ├─ Appends to board+0xCD4 and scene object lists
  │     └─ Factory calls N: handler (0x0040C5D0) on created mesh entities
  └─ 3f. N: handler processes entity names from Section 3:
        • N:GOAL, N:TARPIT, N:WATER, N:SECRET, E:JUMP, E:BREAK, etc.
        • Sets behavioral flags on board/scene/mesh

Architecture

1. MESHWORLD Ref Points (Section 1)

Each MESHWORLD file contains a Section 1 with "reference points" — named position markers stored as:

  • name (string, e.g. SPEEDCYLINDER, BONK, GEAR — stored WITHOUT N: prefix)
  • position (3 floats: x, y, z at offsets +0x04, +0x08, +0x0C)
  • rotation/scale data (at offset +0x10)
  • extra float (at offset +0x14, used for scale/size calculations)
  • Additional flags and color data

The ref points are parsed from the MESHWORLD binary and stored in an AthenaList at:

  • board+0x8AC → LevelData object (0x10D0 bytes, created in Board constructor)
  • LevelData+0x480 → SceneObject
  • SceneObject+0x894 → AthenaList of ref points (count at +0x898, array at +0x40C)

Each ref entry in the array has the structure:

+0x00: char* name          // pointer to ref name string
+0x04: float posX          
+0x08: float posY
+0x0C: float posZ
+0x10: rotation/scale data
+0x14: float extraScale    // used in Timer_Init and position calculations

2. Scene_CreateDynamicObjects (0x0040C430) — The Central Dispatch

void __thiscall Scene_CreateDynamicObjects(int *board)  // ECX = board

This function iterates over all ref points in the loaded MESHWORLD level data. For each ref:

  1. Reads the ref name string from ref_entry+0x00 (*puVar4)
  2. Reads position/scale data from ref_entry+0x04 through ref_entry+0x14
  3. Calls board->vtable[33](refName, &out_obj, &out_col, ref_entry) — i.e. (**(code**)(*board + 0x84))(name, &out1, &out2, refPtr) at address 0x0040C4BA
  4. If the factory returns a non-null object (out_obj != NULL):
    • Initializes a Timer (via Timer_Init at 0x00457AD0) using a float from ref_entry+0x14
    • Sets graphics position/scale from the ref entry position data
    • Appends the object to board+0xCD4 (board-specific object list) via AthenaList_Append at 0x00453810
    • Appends to the scene's SceneObject list (LevelData+0x480+0x1C)
    • Calls the object's vtable[0x58] (Update setup) at 0x0040C531
    • Calls the object's vtable[0x54] (Render setup) with the timer at 0x0040C53F
    • If a collision object was returned, appends it to board+0x10EC and board+0x8B0+0x18

Ref entry structure (param_5 / puVar4):

  • [0x00] = name string pointer (char*)
  • [0x04],[0x08],[0x0C] = position (x, y, z as floats)
  • [0x10] = rotation data
  • [0x14] = extra float parameter (used by Timer_Init and some factories)

3. Board Vtable Slot 33 (+0x84) — The Factory Method

Each Board subclass overrides vtable[33] with its own factory function. This function receives:

  • this = Board pointer
  • param_2 (char*) = ref name string (e.g. "SPEEDCYLINDER", "BONK", "GEAR")
  • param_3 (out) = pointer to receive created visual object
  • param_4 (out) = pointer to receive created collision object
  • param_5 (int) = pointer to the full ref entry (for position/scale extraction)

The factory uses __strnicmp(refName, "KEYWORD", len) to match ref names to object constructors. Unmatched refs are silently ignored — the factory simply returns null pointers.

4. The N: Prefix — TWO Separate Systems

There are two independent dispatch systems for MESHWORLD data:

System A: Ref Points (Section 1) → vtable[33] Factory Dispatch

  • Ref names stored WITHOUT N: prefix: SPEEDCYLINDER, BONK, GEAR, LIFTER, etc.
  • Processed by Scene_CreateDynamicObjectsboard->vtable[33] factory
  • Factory uses __strnicmp(refName, "SPEEDCYLINDER", len) — bare name comparison
  • Creates game OBJECTS (visual + collision entities)

System B: Entity Names (Section 3) → N:/E: Prefix Handler (0x0040C5D0)

  • Entity names stored WITH N: or E: prefix: N:GOAL, N:TARPIT, E:JUMP, E:BREAK
  • Entity name is stored at mesh_entity+0x864 during Level_LoadMeshes / CreateMeshBuffer
  • Processed by the N:/E: handler at 0x0040C5D0, which is called from within the vtable[33] factories
  • Sets behavioral FLAGS on the board/scene (e.g. tar pit behavior, flag wave effects, jump zones, breakable surfaces)
  • Does NOT create objects — modifies existing mesh entities

The N: handler (0x0040C5D0) processes these prefixes:

  • N:SECRET, N:UNLOCKSECRET — secret level unlock flags
  • N:GOAL — goal/finish marker
  • N:TARPIT — tar pit collision behavior
  • N:WATER — water surface effect
  • N:NOCONTROL — disables ball control in this area
  • N:BRIDGE, N:SWIRL, N:WHEELEMBED, N:WATERWHEEL — mesh behavior modifiers
  • N:MACE, N:TRAPDOOR — object-specific behaviors
  • N:JUMPFIRST, N:JUMPSECOND — jump pad sequencing
  • N:WAVY, N:SQUAREWOBBLY — wavy surface behavior
  • N:BUMPER, N:BUMPER%d — bumper assignment
  • N:SAWTEETH, N:SPINNY — saw/spinner behavior
  • N:EXTRATIME, N:SPEEDCYLINDER — time/speed zone markers
  • N:SPINNER, N:NEONPLATFORM — platform behavior
  • N:BUMP, N:TENBONUS1, N:TENBONUS2 — bonus point markers
  • N:GLASS — breakable glass surface
  • N:ONGEAR, N:ONROTATOR — gear/rotator attachment
  • N:BOUNCE — bounce surface
  • N:MOUSETRAP — mouse trap behavior
  • E:JUMP, E:BREAK, E:ACTION, E:LIMIT, E:TRAJECTORY — event triggers
  • ONCE, TRUE, SCORE, X, Y, Z, PIPEBONK, POPOUT, ZIP — modifiers

The handler is called from 25 sites within the vtable[33] factory functions. Each factory calls the handler after creating the object, passing the mesh entity as this (ECX) and two additional arguments. The handler reads the entity name from mesh_entity+4 → +0x864 (the entity name set during CreateMeshBuffer when parsing MESHWORLD Section 3).

Complete list of entity names processed by the N:/E: handler (verified from binary):

Entity Name Behavior
N:SECRET Secret/unlock mechanics
N:UNLOCKSECRET Unlock secret areas
E:NODIZZY Disable dizzy effect
TIME Timer object
E:SAFESWITCH Safe switch trigger
E:LIMIT Boundary kill plane
E:BREAK Breakable surface
E:JUMP Jump pad
E:ACTION Action trigger
ONCE, TRUE, YES Boolean modifiers
SCORE Scoring object
E:TRAJECTORY Trajectory modifier
N:NOCONTROL Disable control
N:WATER Water effect
N:TARPIT Tar pit effect
DROPIN Pipe drop-in
PIPEBONK Pipe collision
POPOUT Pipe pop-out
ZIP Zip/boost effect
Goal! Goal text popup

Call site example (at 0x0040D380, Dizzy factory):

MOV EAX, [ESP+0x0C]    ; load ref entry from stack
PUSH EDI               ; push board pointer (param_3)
PUSH EAX               ; push ref entry (param_2)
MOV ECX, ESI           ; ECX = mesh entity (this for __thiscall)
CALL 0x0040C5D0        ; N: handler(mesh_entity, ref_entry, board)

5. The Scene+0x23C Quality Gate

Many (but not all) factory branches check *(int *)(*(int *)(board + 0x878) + 0x23c) != 0 before creating objects. This field is the graphics quality setting.

Quality-gated refs (only created when quality is non-zero/high):

  • TIPPER, GLUEBIE, MACE, FAN, SAWBLADE, SPINNY, BONK, POPCYLINDER, MAGNIFYER
  • BLOCKDAWG1, BLOCKDAWG2, BLOCKDAWG3
  • E:ALERTSAW2, E:BRANCH

Always-created refs (created regardless of quality setting):

  • BRIDGE, WATERWHEEL, SWIRL, SMASHER1-2, CATAPULT, DRAWBRIDGE, WINDMILL, TRAPDOOR, CHOMPER, TURRET
  • LIFTER, SPEEDCYLINDER, TIMEBUTTON, NEONPLATFORM, DFLOOR1-4, TRODE, BELL, JUDGE
  • SAW, SAW2, FALLOUT1, WOBBLY1-7, WAVY1, LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
  • BBRIDGE1-2, SECRET, SECRETUNLOCK, BADBALL, PILLAR, TARBUBBLE, MOUSETRAP, LAUNCH

40 quality gate checks found across the factory function range (0x40A000–0x419000), verified by binary pattern matching: MOV EAX, [reg+0x23C]; TEST EAX, EAX; JZ skip_creation.


Race vs Arena — Two Separate Board Systems

Hamsterball has TWO independent Board systems: one for Race mode and one for Arena mode. Each uses different Board constructors, different vtables, and different vtable[33] factories.

RACE Board System (0x41Cxxx constructors)

CORRECTED (June 2026): Previous version of this section swapped race and arena constructors. The 0x41Cxxx constructors create RACE boards (verified by decompiling all 30 constructors and reading board name strings: "Board (X)" + "X RACE"). The 0x422xxx constructors create ARENA boards ("ArenaBoard (X)" + "X ARENA").

Race Board constructors are at 0x41CA400x424C20. Each is called from a jump table at 0x42761C (15 entries, indexed by level_number - 1). The jump table is reached via JMP [EAX*4 + 0x42761C] at 0x427080.

Race Board vtables are at 0x4D14280x4D2298. The Race factories form an inheritance chain: each level-specific factory checks its own refs, then falls through to call the base factory 0x4133E0 (which handles PLATFORM, STANDS).

# Race Level Constructor Board Vtable Factory Addr Refs Handled (from decompilation)
1 Warm-up 0x4224A0 0x4D1428 0x4133E0 PLATFORM, STANDS (base only)
2 Beginner 0x422550 0x4D14F0 0x4133E0 PLATFORM, STANDS (base only)
3 Intermediate 0x4226E0 0x4D15C0 0x4133E0 PLATFORM, STANDS (base only)
4 Dizzy 0x422790 0x4D1680 0x4143D0 SPINNY, MACE, CATAPULT, TURRET, LIFTER, FAN + base
5 Tower 0x4228C0 0x4D1740 0x414680 MACE, CATAPULT, TURRET, LIFTER, FAN + base
6 Up 0x422B10 0x4D17F8 0x414A20 LIFTER, FAN, WOBBLY + base
7 Neon 0x424860 0x4D1EC8 0x4173B0 NEONPLATFORM, DFLOOR, TRODE + base
8 Expert 0x423060 0x4D18C8 0x414BD0 FAN, WOBBLY + base
9 Odd 0x423220 0x4D1980 0x4133E0 PLATFORM, STANDS (base only)
10 Toob 0x4234E0 0x4D1A40 0x4133E0 PLATFORM, STANDS (base only)
11 Wobbly 0x423690 0x4D1B18 0x415460 WOBBLY, PILLAR, POPCYLINDER + base
12 Glass 0x424B60 0x4D2048 0x4133E0 PLATFORM, STANDS (base only)
13 Sky 0x423BF0 0x4D1BD8 0x415A30 POPCYLINDER + base
14 Master 0x424380 0x4D1C80 0x4133E0 PLATFORM, STANDS (base only)
15 Impossible 0x424EC0 0x4D2298 0x418760 GEAR + base

Note: The "Refs Handled" column above lists what the factory's decompiled code can match via __strnicmp. This is NOT the same as what refs actually exist in the MESHWORLD files — see docs/VERIFIED_REFS_BY_LEVEL.md for the ground-truth list of refs that are actually placed in each level's MESHWORLD file. Several refs the factories can handle (like PLATFORM, STANDS) are utility/system refs, not game objects.

Key insight: Levels 1, 2, 3, 9, 10, 12, 14 use ONLY the base factory — they handle only PLATFORM, STANDS, N:BUMPER in race mode. Their level-specific objects (tippers, bridges, etc.) are NOT created by vtable[33] in race mode — they are either part of the static level geometry or loaded via the Arena Board system.

Race constructor chain: Like the Arena constructors, Race Board constructors also form a chained function. Race L7 (Neon) loads Impossible sub-meshes, L12 (Glass) loads Impossible sub-meshes, and L14 (Master) loads Neon sub-meshes — all via conditional level-name checks (GLASSRACE, IMPOSSIBLERACE, NEONRACE).

ARENA Board System (0x422xxx constructors)

Arena Board constructors are at 0x4224A00x424EC0. Each is called from the Arena switch function at 0x426AB0 (jump table, 15 entries).

Arena Board vtables are at 0x4D04A80x4D21C0. The Arena factories handle many more ref types than Race factories — they create ALL interactive objects for Arena mode.

# Arena Level Constructor Board Vtable Factory Addr Refs Handled (verified from binary)
1 Warm-up Arena 0x41CA40 0x4D04A8 0x419750 (none — NoOp)
2 Beginner Arena 0x4200E0 0x4D1098 0x419750 (none — NoOp)
3 Intermediate Arena 0x41CB20 0x4D05A0 0x40A550 BRIDGE, TIPPER, WATERWHEEL, SWIRL, GLUEBIE, SMASHER1, SMASHER2
4 Dizzy Arena 0x41D060 0x4D0890 0x40A5F0 TIPPER, WATERWHEEL, SWIRL, GLUEBIE, SMASHER1, SMASHER2
5 Tower Arena 0x41E340 0x4D0A08 0x40D7C0 CATAPULT, MACE, DRAWBRIDGE, WINDMILL, TRAPDOOR, CHOMPER, BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL
6 Up Arena 0x420390 0x4D11A0 0x4117B0 LIFTER, SPEEDCYLINDER, TIMEBUTTON, TarBubble, BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT
7 Neon Arena 0x424440 0x4D1DF0 0x416910 NEONPLATFORM, DFLOOR1-4, TRODE, N:NEONPLATFORM, E:ZOOP, E:LIGHTSOFF, E:LIGHTSON, FLICKNING, N:BUMP, N:GLASS, N:TENBONUS1
8 Expert Arena 0x41EA40 0x4D0B00 0x40E250 BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL, E:SCORE, E:BELL, LIFTER, E:GRAVITY
9 Odd Arena 0x41ED80 0x4D0BC0 0x40EC40 LIFTER, E:GRAVITY, WOBBLY1-7, WAVY1, N:SQUAREWOBBLY, N:WAVY, SPINNY
10 Toob Arena 0x41F4B0 0x4D0E78 0x40FB30 SPINNY, FALLOUT1, BLOCKDAWG1-3, E:ALERTSAW2, E:BRANCH, N:SPINNY, N:SAWTEETH, N:BUMPER, PILLAR, MAGNIFYER
11 Glass Arena 0x41F110 0x4D0D38 0x40F420 WOBBLY1-7, WAVY1, N:SQUAREWOBBLY, N:WAVY, SPINNY, FALLOUT1, BLOCKDAWG1-3, E:ALERTSAW2, E:BRANCH, N:SPINNY
12 Wobbly Arena 0x424A90 0x4D1F90 0x40AD80 SMASHER1, SMASHER2, SECRETUNLOCK, SECRET, BADBALL
13 Sky Arena 0x41F930 0x4D0FC8 0x410AD0 POPCYLINDER, TRAPDOOR, N:BUMPER, VAC-IN, LIFTER, SPEEDCYLINDER, TIMEBUTTON
14 Master Arena 0x4206D0 0x4D12B0 0x4121D0 BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT, GLUEBIE, N:SPINNER, N:BUMPER, E:LAUNCH
15 Impossible Arena 0x424C20 0x4D21C0 0x417FE0 LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM, N:BOUNCE, N:ONROTATOR, N:ONGEAR

Level File → Board Mapping

The game's internal level numbers map to file paths differently for race vs arena. Race file numbers do NOT match level order — e.g. level5 = Expert Race (L8), level6 = odd race (L9).

Race Level File Path Arena Level File Path
L1 (Warm-up) levels\level1 L1 (Warm-up Arena) levels\arena-WarmUp
L2 (Beginner) levels\levelcascade L2 (Beginner Arena) levels\arena-beginner
L3 (Intermediate) levels\level2 L3 (Intermediate Arena) levels\arena-intermediate
L4 (Dizzy) levels\level3 L4 (Dizzy Arena) levels\arena-dizzy
L5 (Tower) levels\level4 L5 (Tower Arena) levels\arena-tower
L6 (Up) levels\levelup L6 (Up Arena) levels\arena-up
L7 (Neon) levels\leveldark L7 (Neon Arena) levels\arena-neon
L8 (Expert) levels\level5 L8 (Expert Arena) levels\arena-expert
L9 (Odd) levels\level6 L9 (Odd Arena) levels\arena-Odd
L10 (Toob) levels\level8 L10 (Toob Arena) levels\arena-Toob
L11 (Wobbly) levels\level7 L11 (Wobbly Arena) levels\arena-Wobbly
L12 (Glass) levels\levelglass L12 (Glass Arena) levels\arena-glass
L13 (Sky) levels\level9 L13 (Sky Arena) levels\arena-Sky
L14 (Master) levels\level10 L14 (Master Arena) levels\arena-Master
L15 (Impossible) levels\levelimpossible L15 (Impossible Arena) levels\arena-impossible

Complete Ref Name → Constructor Mapping

Verified against the actual MESHWORLD binary data (46 unique object types). Numbered suffixes (e.g. TIPPER01, GEAR02) are collapsed to their base type. See docs/VERIFIED_REFS_BY_LEVEL.md for the full verified data.

Ref Name Constructor Alloc Size Quality-Gated? Race Levels Found In
BBRIDGE BreakBridge_ctor 0x1100 No Master
BELL Tipper_Level_Ctor 0x10E8 No Expert
BIGGEAR Gear_ctor 0x1514 No Impossible
BLOCKDAWG Blockdawg_ctor 0x1154 Yes Toob, Master
BONK Bonk_ctor 0x1200 Yes Expert, Master
BRIDGE (returns pre-loaded mesh) 0 No Intermediate, Expert, Master
CATAPULT Catapult_ctor 0x1108 No Tower, Master
CHOMPER (position update only) 0 No Tower
DFLOOR ArenaStands_ctor 0x1104 No Neon
DRAWBRIDGE Glass_Level_ctor 0x113C No Tower
FALLOUT1 Stands_CtorCollisionV2 0x10E8 No Toob
FAN (FAN + FANSLOW + FAN(SUPER)(UP) variants) varies varies Expert
GEAR Gear_ctor 0x1514 No Impossible
GLUEBIE Gluebie_ctor 0x110C Yes Dizzy, Master
JUDGE Gear_Level_ctor 0x1100 No Expert
LAUNCH (launch pad marker) 0 No Odd
LIFTER Stands_CtorWithCollision / Rotator_ctor_sound 0x10FC / 0x10F4 No Up, Odd
LOOPER Looper_ctor 0x1500 No Impossible
MACE CascadeStands_Ctor 0x110C Yes Tower
MAGNIFYER (magnifying glass object) 0 No Sky
MOUSETRAP GlassStands_Ctor 0x10F8 No Intermediate, Master
NEONPLATFORM Stands_CtorRotator 0x10EC No Neon
PENDULUM Pendulum_ctor 0x1504 No Impossible
PILLAR (static mesh + collision) 0 No Sky
POPCYLINDER PopCylinder_ctor / Platform_ctor 0x10E8 / 0x10F4 Conditional Sky, Master
ROTATOR Rotator_ctor 0x1508 No Impossible
SAW Stands_CtorCollision 0x1110 Yes Toob
SAW-BREAK (breakable saw marker) 0 No Expert
SAW2 Stands_CtorSpeedCylinder 0x1118 Yes Toob
SAWBLADE Sawblade_Level_Ctor 0x111C Yes Expert
SIGN-TARPIT (sign handler + tar pit data) 0 No Dizzy
SMASHER (position update only) 0 No Glass
SPEEDCYLINDER Pendulum_ctor 0x150C No Up
SPINNY Rotator_ctor 0x1508 No Toob
SWIRL (returns pre-loaded mesh) 0 No Dizzy
TARBUBBLE (tar bubble animation) 0 No Dizzy, Master
TIMEBUTTON Rotator_ctor_nosound 0x10E8 No Up
TIPPER Tipper_ctor 0x1104 Yes Dizzy, Master
TRAPDOOR GlassStands_Ctor / Rotator_ctor 0x10F8 / 0x10F4 No Tower, Sky
TRODE ArenaStands_ctor 0x1104 No Neon
TURRET Stands_ctor + CollisionLevel 0x10D0 No Tower
WATERWHEEL (returns pre-loaded mesh) 0 No Dizzy
WAVY Stands_CtorWithCollisionLevel 0x1AE7C No Wobbly
WINDMILL (returns pre-loaded mesh + collision) 0 No Tower
WOBBLY GameLevel_ctor 0x1524 No Wobbly

Special Ref Modifiers

Some refs support suffix modifiers checked via strstr():

  • (NOCOLLIDE) — on BRIDGE refs, suppresses collision object creation
  • SLOW — on Expert's [TOW] refs, sets slow flag
  • SUPER — on Expert's [TOW] refs, sets super flag
  • UP — on Expert's [TOW] refs, initializes sound channels
  • 1, 2 — on SAWBLADE and BRIDGE refs, assigns to specific board slots
  • NEG — on BRIDGE refs in Expert, sets negative rotation
  • TOUCH — on BIGGEAR refs, sets touch-activated flag

Verified Ref Names per Level

Confirmed by parsing the original MESHWORLD binary files with a spec-compliant parser. See docs/VERIFIED_REFS_BY_LEVEL.md for the full ground-truth data.

46 unique object types across 15 race levels. Refs listed below are Section 1 ref points only (object spawn markers). Utility refs (START, SAFESPOT, FLAG, BADBALL, SECRET, etc.) are omitted — see the standalone doc for the full list.

# Race Level Object Refs (Section 1)
L1 Warm-up (none)
L2 Beginner (none)
L3 Intermediate BRIDGE, MOUSETRAP
L4 Dizzy SIGN-TARPIT, TARBUBBLE, GLUEBIE, TIPPER, WATERWHEEL, SWIRL
L5 Tower CATAPULT, TRAPDOOR, DRAWBRIDGE, MACE, WINDMILL, CHOMPER, TURRET
L6 Up SPEEDCYLINDER, LIFTER, TIMEBUTTON
L7 Neon DFLOOR, TRODE, NEONPLATFORM
L8 Expert BONK, FAN, FANSLOW, SAWBLADE, BRIDGE, SAW-BREAK, JUDGE, BELL
L9 Odd LIFTER, LAUNCH
L10 Toob SPINNY, SAW, FALLOUT1, SAW2, BLOCKDAWG
L11 Wobbly WOBBLY, WAVY
L12 Glass SMASHER
L13 Sky PILLAR, MAGNIFYER, POPCYLINDER, TRAPDOOR
L14 Master BBRIDGE, BLOCKDAWG, BONK, BRIDGE, CATAPULT, GLUEBIE, MOUSETRAP, POPCYLINDER, TIPPER, TARBUBBLE
L15 Impossible LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM

Common utility refs in ALL levels: START (spawn point), SAFESPOT (respawn point), FLAG (checkpoint), BADBALL (AI ball).


How to Load Any Ref Into Any Level

Method 1: MESHWORLD-Only (Limited)

Adding a ref name to a MESHWORLD file alone will NOT cause an object to be created. The ref name must match a __strnicmp check in the Board's vtable[33] factory. If the factory doesn't handle that name, the ref is silently ignored.

Exception: Master Race's CreateLevelObjects (0x4121D0) factory handles the most ref types (BRIDGE, TIPPER, BONK, BBRIDGE1-2, POPCYLINDER, BLOCKDAWG1-2, CATAPULT, GLUEBIE). If the target level uses this factory, many refs will work from MESHWORLD alone.

Method 2: DLL Mod — Vtable Patch (Recommended)

Patch the target Board's vtable[33] entry (at vtable_addr + 0x84) to point to a combined factory function. The combined factory should:

  1. First try the original level's factory (call through the saved original pointer)
  2. If the original returns null, try factories from other levels
// Pseudo-code for combined factory
void __thiscall UniversalFactory(void* board, char* refName, void** outObj, void** outCol, int* refEntry) {
    // Try original factory first
    originalFactory(board, refName, outObj, outCol, refEntry);
    if (*outObj != NULL) return;
    
    // Try other level factories
    CreateUpLevelObjects(board, refName, outObj, outCol, refEntry);
    if (*outObj != NULL) return;
    
    CreateExpertLevelObjects(board, refName, outObj, outCol, refEntry);
    if (*outObj != NULL) return;
    
    CreateMechanicalObjects(board, refName, outObj, outCol, refEntry);
    // ... etc
}

Critical requirement: The Board must have the necessary sub-meshes pre-loaded. Each Board constructor loads specific MeshWorld files (e.g. Levels\Level3-Tipper, Levels\Level4-Catapult) into board+0x436C..0x4390 slots. Without the corresponding mesh, the factory may crash or produce invisible objects.

Method 3: DLL Mod — Hook Scene_CreateDynamicObjects

Hook the central dispatch at 0x0040C4BA. Before the original call to vtable[33], intercept the ref name and call any factory function directly. This is the most flexible approach:

// Hook at 0x0040C4BA, before the vtable[33] call
// The call site is at offset ~0x?? from function entry:
//   (**(code **)(*param_1 + 0x84))(*puVar4, &local_58, &local_54, puVar4);
// Replace this indirect call with a direct call to your dispatcher

Method 4: Binary Patch — Swap Vtable Entry

Overwrite the 4 bytes at (board_vtable_addr + 0x84) with the address of a different level's factory. For example, to give Warm-up Race the Impossible factory:

  • Patch 0x4D04A8 + 0x84 = 0x4D04EC from 0x00419750 to 0x00417FE0

This is the simplest method but only gives you ONE level's factory at a time.


Sub-Mesh Preloading

Each Board constructor preloads specific MeshWorld files into board struct slots. The table below shows which sub-mesh files each Board constructor loads (verified by tracing string references in the binary):

Board Creation — Mode Determination

The game checks App+0x237 (byte) to determine whether to use Race Board or Arena Board:

; At ~0x4270AB
CMP [EAX+0x237], BL    ; check App+0x237 (BL=0)
JZ  skip_race           ; if zero, skip race board creation
CALL 0x00426780        ; Race Board switch (jump table at 0x426AB0)
  • App+0x237 = 1 (non-zero) → Race mode → Race Board constructors (0x422xxx)
  • App+0x237 = 0 → Arena mode → Arena Board constructors (0x41Cxxx), called from 0x427080

The Race Board function (0x426780) uses a jump table at 0x426AB0 with 15 entries (indexed by level_number - 1). The Arena Board function (0x427080) uses a sequential switch at 0x427140 with sequential level IDs 1–14.

Board Constructor → Sub-Mesh File Mapping (Arena)

Board Constructor Area Sub-Meshes Loaded (verified from binary)
Arena L1 0x4200E0 LevelUp-Lifter, LevelUp-SpeedCylinder, LevelUp-Button, Level2-Bridge, Level10-2PBridge
Arena L2 0x41CB20 Level2-Bridge, Level3-Tipper, Level3-WaterWheel, Level3-Swirl
Arena L3 0x41D060 Level3-Tipper, Level3-WaterWheel, Level3-Swirl, Level3-Gluebie
Arena L4 0x41E340 Level4-Catapult, Level4-Drawbridge, Level4-Mace, Level4-Windmill, Level4-Turret
Arena L5 0x420390 LevelUp-Lifter, LevelUp-SpeedCylinder, LevelUp-Button, Level2-Bridge, Level10-2PBridge, Level3-Tipper, Level10-Bridge1-2, Level9-PopCylinder1-2, Level8-Blockdawg1-2, Level4-Catapult, Level3-Gluebie
Arena L6 0x424440 LevelDark-NeonPlatform, LevelDark-DFloor1-4, LevelDark-Trode, LevelDark-Flickring
Arena L7 0x41EA40 Level5-Bridge, Level7-Wobbly1
Arena L8 0x41ED80 Level7-Wobbly1-7
Arena L9 0x41F4B0 Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1-2, Level9-PopCylinder1-2, Level9-Trapdoor
Arena L10 0x41F110 Level7-Wobbly1-7, Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1-2
Arena L11 0x424A90 LevelImpossible-Looper, Gear, BigGear, Rotator, Pendulum
Arena L12 0x41F930 Level9-PopCylinder1-2, Level9-Trapdoor
Arena L13 0x4206D0 Level2-Bridge, Level10-2PBridge, Level3-Tipper, Level10-Bridge1-2, Level9-PopCylinder1-2, Level8-Blockdawg1-2, Level4-Catapult, Level3-Gluebie
Arena L14 0x424C20 LevelImpossible-Looper, Gear, BigGear, Rotator, Pendulum

Factory → Board Sub-Mesh Slot Dependencies

Each Arena factory accesses specific board struct offsets (0x4344–0x4398) to retrieve pre-loaded sub-mesh data. If a slot is null (because the Board constructor didn't load that mesh), the factory will skip that ref type or crash.

Factory Board Slots Accessed Refs Dependent on Slots
Beginner (0x40A550) +0x436C, +0x4370, +0x4374 BRIDGE, TIPPER, WATERWHEEL/SWIRL
Intermediate (0x40A5F0) +0x436C, +0x4370, +0x4374 TIPPER, WATERWHEEL, SWIRL, GLUEBIE
Dizzy (0x40D7C0) +0x436C, +0x4370, +0x4378, +0x437C, +0x4390, +0x43A4, +0x43B0, +0x43B4 CATAPULT, MACE, DRAWBRIDGE, WINDMILL, TRAPDOOR, TURRET (8 slots)
Tower (0x4117B0) +0x436C, +0x4370, +0x4374, +0x4378, +0x4394, +0x4398 LIFTER, SPEEDCYLINDER, TIMEBUTTON, BRIDGE, TIPPER, BONK
Neon (0x416910) +0x4374, +0x4378, +0x437C, +0x4380, +0x4384, +0x4388, +0x438C, +0x4390 NEONPLATFORM, DFLOOR1-4, TRODE, FLICKNING (8 slots)
Expert (0x40E250) +0x436C, +0x4370, +0x4374 BONK, SAWBLADE, BRIDGE, JUDGE, BELL
Odd (0x40EC40) +0x436C, +0x4370, +0x4374, +0x4378, +0x437C LIFTER, WOBBLY1-5, WAVY1
Toob (0x40FB30) +0x436C, +0x4370, +0x4374, +0x4378, +0x437C, +0x4380, +0x4384 SPINNY, SAW, FALLOUT, BLOCKDAWG (7 slots)
Glass (0x40F420) +0x436C, +0x4370, +0x4374, +0x4378, +0x437C, +0x4380, +0x4384 WOBBLY1-7, WAVY1, SPINNY, FALLOUT, BLOCKDAWG (7 slots)
Wobbly (0x40AD80) +0x4344 SMASHER1-2 (1 slot)
Sky (0x410AD0) +0x436C, +0x4374, +0x4378, +0x437C, +0x4380, +0x438C, +0x4390 POPCYLINDER, TRAPDOOR (7 slots)
Master (0x4121D0) +0x436C, +0x4370, +0x4394, +0x4398 BRIDGE, TIPPER, BONK, BBRIDGE, POPCYLINDER, BLOCKDAWG, CATAPULT, GLUEBIE
Impossible (0x417FE0) +0x436C, +0x4370, +0x4374, +0x4378, +0x437C LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM

Critical implication for the universal ref loader: When the DLL mod tries a factory from another level, that factory will access board slots that weren't loaded by the current level's constructor. Factories do NOT null-check their sub-mesh slots — they dereference the slot pointer directly after checking only that operator_new succeeded. If the slot is NULL, the factory will crash with an access violation.

The DLL mod must check sub-mesh slots BEFORE calling each factory, or pre-load the required sub-meshes. The slot dependency table above provides the exact offsets each factory accesses.

Arena Board Constructor Chain Architecture

The Arena Board constructors are NOT separate functions — they are chained sections of one large function starting at 0x41CA40. Each section:

  1. Compares the level name string (e.g. "BEGINNERRACE", "INTERMEDIATERACE", "DIZZYRACE", "UPRACE", etc.)
  2. If the name matches, loads the appropriate sub-meshes for that level
  3. Falls through to the next section for the next level

The jump table at 0x42761C calls operator_new with different allocation sizes for each level (ranging from 17,260 to 25,752 bytes), then calls the constructor at 0x41CA40 which chains through all level sections.

Key implication: Since all Arena constructors share one function, ALL sub-meshes can be loaded by bypassing the level name comparison. The level name check (not separate function calls) is what prevents loading unrelated sub-meshes.

Complete Verified Sub-Mesh Loading Table

Arena Level Level Name String Sub-Meshes Loaded
L1 Warm-up (none — first section) (no sub-meshes)
L2 Beginner CASCADERACE (no sub-meshes)
L3 Intermediate INTERMEDIATERACE Level2-Bridge, MOUSETRAP
L4 Dizzy DIZZYRACE Level3-Tipper, Level3-WaterWheel, Level3-Swirl, Level3-Gluebie
L5 Tower (checks TOWERRACE) Level4-Catapult, Level4-Drawbridge, Level4-Mace, Level4-Windmill, Level4-Turret
L6 Up UPRACE LevelUp-Lifter, LevelUp-SpeedCylinder, LevelUp-Button, Level2-Bridge, Level10-2PBridge, Level3-Tipper, Level10-Bridge1, Level10-Bridge2, Level9-PopCylinder1-2, Level8-Blockdawg1-2, Level4-Catapult, Level3-Gluebie
L7 Neon (checks NEONRACE) LevelDark-NeonPlatform, LevelDark-DFloor1-4, LevelDark-Trode, LevelDark-Flickning
L8 Expert EXPERTRACE Level5-Bridge
L9 Odd ODDRACE Level7-Wobbly1-7
L10 Toob TOOBRACE Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1-2
L11 Glass GLASSRACE Level7-Wobbly1-7
L12 Wobbly (no section found) (no sub-meshes)
L13 Sky SKYRACE meshes/skypillar, meshes/magnifyingglass, Level9-PopCylinder1-2, Level9-Trapdoor, textures/clouds.png
L14 Master MASTERRACE Level2-Bridge, Level10-2PBridge, Level3-Tipper, Level10-Bridge1-2, Level8-Blockdawg1-2, Level4-Catapult, Level3-Gluebie, Level4-Mace, Level4-Catapult, Level4-Turret, Level7-Wobbly8
L15 Impossible IMPOSSIBLERACE LevelImpossible-Looper, LevelImpossible-Gear, LevelImpossible-BigGear, LevelImpossible-Rotator, LevelImpossible-Pendulum, LevelImpossible-Gear

All 46 sub-mesh files referenced by Arena Board constructors:

Source Level Sub-Mesh Files
Level2 Level2-Bridge
Level3 Level3-Tipper, Level3-WaterWheel, Level3-Gluebie, Level3-Swirl
Level4 Level4-Catapult, Level4-Drawbridge, Level4-Mace, Level4-Windmill, Level4-Turret, Level4-Trapdoor1, Level4-Trapdoor2
Level5 Level5-Bridge, Level5-Bonk
Level6 Level6-Lifter
Level7 (Neon) Level7-Wobbly1-8, Level7-Wavy1
Level8 Level8-Spinny, Level8-Saw, Level8-Fallout, Level8-Blockdawg1-2
Level9 (Sky) Level9-Trapdoor, Level9-PopCylinder1-2
Level10 Level10-Bridge1-2, Level10-2PBridge
LevelDark (Neon) LevelDark-NeonPlatform, LevelDark-DFloor1-4, LevelDark-Trode, LevelDark-Flickning
LevelUp LevelUp-Lifter, LevelUp-SpeedCylinder, LevelUp-Button
LevelImpossible LevelImpossible-Looper, LevelImpossible-Gear, LevelImpossible-BigGear, LevelImpossible-Rotator, LevelImpossible-Pendulum

Note: "Level7" in the sub-mesh names = Neon Race (internal level 7), while "LevelDark" = the dark/neon-themed mesh set. Both are used by the Neon Arena constructor.

To load additional sub-meshes at runtime from a DLL mod:

// Load a mesh world into a free board slot at runtime
// MeshWorld_ctor address: find from Board constructor code
// operator_new: 0x00449E70
typedef void* (__cdecl *operator_new_t)(size_t);
typedef void* (__thiscall *MeshWorld_Load_t)(void* mesh, void* graphics, const char* path);

void load_submesh(void* board, int slot_offset, const char* path) {
    operator_new_t op_new = (operator_new_t)0x00449E70;
    // Allocate and load the mesh
    void* mesh = op_new(0x10D0);  // MeshWorld struct size
    if (mesh) {
        // Call MeshWorld constructor with graphics device and path
        // The graphics device is at board+0x8AC (Scene pointer) → scene+0x480
        void* scene = *(void**)((char*)board + 0x878);
        void* graphics = *(void**)((char*)scene + 0x480);
        // MeshWorld_Load(mesh, graphics, path);
        // Store in board slot
        *(void**)((char*)board + slot_offset) = mesh;
    }
}

Master Race as the Universal Level

Master Race's Board constructor is the most inclusive — it preloads sub-meshes from 5 other levels (Tower, Toob, Dizzy, Intermediate, Wobbly) plus its own meshes. This is why Master's CreateLevelObjects factory (0x4121D0) can create 9 different object types. The universal ref loader DLL mod will work most reliably on Master Race levels, or on any level where you manually preload the required sub-meshes.


Scene_CreateSigns (0x0040C270)

A separate dispatch function handles SIGN refs. This function is not vtable-gated — it runs for all levels and creates StandsTipper objects for any ref starting with "SIGN". Special case: SIGN-TARPIT gets additional tar-pit data from scene+0x27C.


Complete Ref Name → Factory Reverse Index

This table shows every verified Section 1 ref name and which factory(ies) can create it. Only 46 verified object types are listed — refs that were previously listed here but do not exist in any original MESHWORLD file have been removed. See docs/VERIFIED_REFS_BY_LEVEL.md for the ground-truth source data.

Note: The "Arena Factories" column lists which Arena Board vtable[33] factory can create the object. The "Race Factories" column lists which Race Board factory handles it. Many refs are Arena-only (no Race factory handles them).

Ref Name Arena Factories Race Factories Required Slots
BBRIDGE Master +0x436C, +0x4370, +0x4394, +0x4398
BELL Expert +0x436C, +0x4370, +0x4374
BIGGEAR Impossible +0x436C, +0x4370, +0x4374, +0x4378, +0x437C
BLOCKDAWG Toob, Master +0x436C to +0x4384
BONK Dizzy, Expert, Master varies
BRIDGE Beginner, Dizzy, Tower, Expert, Master +0x436C, +0x4370, +0x4374
CATAPULT Tower, Master Race Tower +0x436C, +0x4378
CHOMPER Tower +0x436C, +0x4378, +0x43B0
DFLOOR Neon +0x4374 to +0x4390
DRAWBRIDGE Tower +0x436C, +0x4378, +0x4390
FALLOUT1 Toob, Glass +0x436C to +0x4384
FAN Expert Race Expert varies
FANSLOW Expert +0x436C, +0x4370, +0x4374
GEAR Impossible Race Impossible +0x436C, +0x4370, +0x4374, +0x4378, +0x437C
GLUEBIE Beginner, Intermediate, Master +0x436C, +0x4370, +0x4374
JUDGE Expert +0x436C, +0x4370, +0x4374
LAUNCH Race Odd (race)
LIFTER Tower, Expert, Odd Race Up, Odd varies
LOOPER Impossible +0x436C, +0x4370, +0x4374, +0x4378, +0x437C
MACE Tower Race Tower +0x436C, +0x4378
MAGNIFYER Sky +0x436C to +0x4384
MOUSETRAP Master +0x436C
NEONPLATFORM Neon +0x4374 to +0x4390
PENDULUM Impossible +0x436C to +0x437C
PILLAR Sky +0x436C
POPCYLINDER Sky, Master Race Sky varies
ROTATOR Impossible +0x436C to +0x437C
SAW Toob +0x436C to +0x4384
SAW-BREAK Expert +0x436C, +0x4370, +0x4374
SAW2 Toob +0x436C to +0x4384
SAWBLADE Expert +0x436C, +0x4370, +0x4374
SIGN-TARPIT (separate Scene_CreateSigns dispatch) N/A
SMASHER Glass +0x4344
SPEEDCYLINDER Tower, Sky Race Up +0x436C, +0x4394, +0x4398
SPINNY Toob, Glass +0x436C to +0x4384
SWIRL Beginner, Intermediate +0x436C, +0x4370, +0x4374
TARBUBBLE Tower +0x436C, +0x4394
TIMEBUTTON Tower, Sky Race Up +0x436C, +0x4394, +0x4398
TIPPER Beginner, Intermediate, Master +0x436C, +0x4370, +0x4374
TRAPDOOR Tower, Sky +0x436C, +0x4378, +0x4390
TRODE Neon +0x4374 to +0x4390
TURRET Race Tower (race)
WATERWHEEL Beginner, Intermediate +0x436C, +0x4370, +0x4374
WAVY Odd, Glass Race Wobbly +0x436C to +0x4384
WINDMILL Tower +0x436C, +0x4378, +0x43A4
WOBBLY Odd, Glass Race Wobbly varies
  1. Two independent Board systems: RACE Boards (constructors at 0x422xxx, vtables 0x4D1428–0x4D2298) and ARENA Boards (constructors at 0x41Cxxx, vtables 0x4D05A0–0x4D21C0). Race mode uses App+0x237=0; Arena mode uses App+0x237=1. Each has its own vtable[33] factory.
  2. vtable[33] dispatch: Scene_CreateDynamicObjects (0x0040C430) iterates MESHWORLD Section 1 ref points, and for each ref, calls board->vtable[33](board, refName, &outObj, &outCol, refEntry) at instruction 0x0040C4BA. If the factory returns NULL, the ref is silently ignored.
  3. Factory chain (Race): Race factories form an inheritance chain — each level-specific factory checks its own refs, then falls through to the base factory 0x4133E0 which handles PLATFORM, STANDS, N:BUMPER.
  4. Factory chain (Arena): Arena factories are standalone — each handles its complete ref set without falling through. The most inclusive Arena factory is Master (13 ref types), followed by Dizzy (12 ref types) and Tower (13 ref types).
  5. Sub-mesh preloading: Each Board constructor preloads specific MESHWORLD files into board struct slots (+0x4344 through +0x43B4). Factories access these slots to get mesh data for object creation. Factories do NOT null-check these slots — calling a factory with unloaded slots causes an access violation crash.
  6. N:/E: handler (0x0040C5D0): A separate system that processes entity names from Section 3 objects to set behavioral flags (gravity modifiers, launch pads, bumpers, etc.). Called from 25 sites within the Arena factories.
  7. universal ref loader mod: A bass.dll proxy that hooks the vtable[33] dispatch at 0x0040C4BA and tries all 13 Arena factories in sequence, with per-factory sub-mesh slot safety checks. Allows loading any ref type into any level (limited by sub-mesh availability).

How to Load Any Ref Into Any Level

Option A — MESHWORLD mod only (limited):
For refs handled by the Race base factory (PLATFORM, STANDS), simply add the ref name to the MESHWORLD Section 1. No code changes needed. Note: the base factory handles very few object types — most objects require level-specific factory support.

Option B — MESHWORLD mod + DLL hook (any ref):

  1. Add the desired ref name to the level's MESHWORLD Section 1.
  2. Install the universal-ref-loader bass.dll proxy.
  3. The DLL mod will try all Arena factories when the level's own factory doesn't recognize the ref.
  4. Limitation: The ref's sub-mesh data must already be loaded in a board slot. If the ref requires a sub-mesh that wasn't loaded by the current Board constructor, the factory will be skipped (safety check prevents crash, but ref won't be created).

Option C — MESHWORLD mod + pre-loaded sub-meshes (full support):

  1. Add the ref name to the level's MESHWORLD Section 1.
  2. Pre-load the required sub-mesh by modifying the Board constructor to load additional MESHWORLD files (binary patch the constructor to add MeshWorld loading calls).
  3. Install the universal-ref-loader bass.dll proxy.
  4. All 46 verified ref types can now be loaded into any level.

🔗 Related Documents

Registry System

types : project
keywords :

📂 View source on GitHub


Hamsterball Registry System

Overview

Hamsterball stores all persistent settings (display, audio, unlocks, controls, best times) in the Windows Registry under HKEY_CURRENT_USER\Software\Raptisoft\Hamsterball.
The game wraps raw Win32 ADVAPI32 calls in a thin RegKey_* helper layer. This document covers the exact key path, value names, data types, App offsets, and how to read/write registry data the same way the engine does.

Last verified against: Hamsterball.exe (Athena engine, VS2003) via Ghidra decompilation.


Registry Key Path

HKEY_CURRENT_USER\Software\Raptisoft\Hamsterball

The path is built at runtime using the format string at 0x4D3978:

  • "Raptisoft\\%s" where %s = "Hamsterball"

The game opens this key once at startup and caches the HKEY handle at App+0x54.


Win32 Registry API Imports

From ADVAPI32.dll (standard Windows registry API):

Import # API Name Purpose
480 RegOpenKeyA Open existing key
481 RegOpenKeyExA Open with options
459 RegCreateKeyA Create if missing
491 RegQueryValueExA Read value
504 RegSetValueExA Write value
456 RegCloseKey Close handle

Engine Wrapper Functions

The game does not call Win32 APIs directly from every save site. Instead it uses these wrapper helpers (addresses inferred from App_SaveAllConfig usage):

RegKey_Open — Open/Creates the Key

  • Address: Inlined / called from App_SaveAllConfig
  • Input: int* reg_handle_ptr (points to App+0x54)
  • Behavior: Calls RegCreateKeyA to create Software\Raptisoft\Hamsterball if missing.

RegKey_WriteDWORD

  • Input: (void* handle, const char* name, DWORD value)
  • Behavior: Calls RegSetValueExA(handle, name, 0, REG_DWORD, &value, 4)

RegKey_WriteBool

  • Input: (void* handle, const char* name, BYTE value)
  • Behavior: Same as RegKey_WriteDWORD but writes a 1-byte REG_DWORD (0 or 1).

Registry_SetValue

  • Input: (void* handle, const char* name, BYTE* data, DWORD size)
  • Behavior: Calls RegSetValueExA(handle, name, 0, REG_BINARY, data, size)

RegKey_Close

  • Input: (int handle)
  • Behavior: Calls RegCloseKey(handle) and invalidates the cached handle.

App Object Registry Offsets

The App struct (base at App or App_ptr) stores all values that are mirrored to the registry. Here are the confirmed offsets used by App_SaveAllConfig:

Offset Type Registry Name Description
+0x054 HKEY (handle) Cached registry key handle
+0x238 BYTE RightButtonPause Right-click pauses the game
+0x84C DWORD `MouseSensitivity`` Mouse sensitivity (0–10 typical)
+0x850 BYTE MirrorTournament Mirror-mode tournament flag
+0x851 BYTE DizzyRace Unlock: Dizzy race
+0x852 BYTE TowerRace Unlock: Tower race
+0x853 BYTE UpRace Unlock: Up race
+0x854 BYTE ExpertRace Unlock: Expert race
+0x855 BYTE OddRace Unlock: odd race
+0x856 BYTE ToobRace Unlock: Toob race
+0x857 BYTE WobblyRace Unlock: Wobbly race
+0x858 BYTE SkyRace Unlock: Sky race
+0x859 BYTE MasterRace Unlock: Master race
+0x85A BYTE DizzyArena Unlock: Dizzy arena
+0x85B BYTE TowerArena Unlock: Tower arena
+0x85C BYTE UpArena Unlock: Up arena
+0x85D BYTE ExpertArena Unlock: Expert arena
+0x85E BYTE OddArena Unlock: Odd arena
+0x85F BYTE ToobArena Unlock: Toob arena
+0x860 BYTE WobblyArena Unlock: Wobbly arena
+0x861 BYTE SkyArena Unlock: Sky arena
+0x862 BYTE MasterArena Unlock: Master arena
+0x863 BYTE NeonRace Unlock: Neon race
+0x864 BYTE GlassRace Unlock: Glass race
+0x865 BYTE ImpossibleRace Unlock: Impossible race
+0x866 BYTE NeonArena Unlock: Neon arena
+0x867 BYTE GlassArena Unlock: Glass arena
+0x868 BYTE ImpossibleArena Unlock: Impossible arena
+0x86C BYTE[0x50] BestTime 80-byte blob of best race times
+0x8BC BYTE[0x50] Medals 80-byte blob of medal bitmasks
+0xB28 DWORD 2PController1 2P mapping slot 1
+0xB2C DWORD 2PController2 2P mapping slot 2
+0xB30 DWORD 2PController3 2P mapping slot 3
+0xB34 DWORD 2PController4 2P mapping slot 4

Save Function Reference

App_SaveAllConfig0x4284C0

The master save routine. Called on game exit and whenever settings change.

void __fastcall App_SaveAllConfig(void* app)
{
    App_WriteDisplaySettings(app);          // 0x?????? — saves resolution/quality

    RegKey_Open(*(int*)(app + 0x54));      // Ensure key is open

    // ---- Scalar settings ----
    RegKey_WriteDWORD(app + 0x54, "MouseSensitivity",      *(DWORD*)(app + 0x84C));
    RegKey_WriteBool (app + 0x54, "MirrorTournament",      *(BYTE* )(app + 0x850));
    RegKey_WriteBool (app + 0x54, "RightButtonPause",      *(BYTE* )(app + 0x238));

    // ---- Race unlocks (12) ----
    RegKey_WriteBool(app + 0x54, "DizzyRace",      *(BYTE*)(app + 0x851));
    RegKey_WriteBool(app + 0x54, "TowerRace",      *(BYTE*)(app + 0x852));
    RegKey_WriteBool(app + 0x54, "UpRace",         *(BYTE*)(app + 0x853));
    RegKey_WriteBool(app + 0x54, "ExpertRace",     *(BYTE*)(app + 0x854));
    RegKey_WriteBool(app + 0x54, "OddRace",        *(BYTE*)(app + 0x855));
    RegKey_WriteBool(app + 0x54, "ToobRace",       *(BYTE*)(app + 0x856));
    RegKey_WriteBool(app + 0x54, "WobblyRace",     *(BYTE*)(app + 0x857));
    RegKey_WriteBool(app + 0x54, "SkyRace",        *(BYTE*)(app + 0x858));
    RegKey_WriteBool(app + 0x54, "MasterRace",     *(BYTE*)(app + 0x859));
    RegKey_WriteBool(app + 0x54, "NeonRace",       *(BYTE*)(app + 0x863));
    RegKey_WriteBool(app + 0x54, "GlassRace",      *(BYTE*)(app + 0x864));
    RegKey_WriteBool(app + 0x54, "ImpossibleRace", *(BYTE*)(app + 0x865));

    // ---- Arena unlocks (12) ----
    RegKey_WriteBool(app + 0x54, "DizzyArena",      *(BYTE*)(app + 0x85A));
    RegKey_WriteBool(app + 0x54, "TowerArena",      *(BYTE*)(app + 0x85B));
    RegKey_WriteBool(app + 0x54, "UpArena",         *(BYTE*)(app + 0x85C));
    RegKey_WriteBool(app + 0x54, "ExpertArena",     *(BYTE*)(app + 0x85D));
    RegKey_WriteBool(app + 0x54, "OddArena",        *(BYTE*)(app + 0x85E));
    RegKey_WriteBool(app + 0x54, "ToobArena",       *(BYTE*)(app + 0x85F));
    RegKey_WriteBool(app + 0x54, "WobblyArena",     *(BYTE*)(app + 0x860));
    RegKey_WriteBool(app + 0x54, "SkyArena",        *(BYTE*)(app + 0x861));
    RegKey_WriteBool(app + 0x54, "MasterArena",     *(BYTE*)(app + 0x862));
    RegKey_WriteBool(app + 0x54, "NeonArena",       *(BYTE*)(app + 0x866));
    RegKey_WriteBool(app + 0x54, "GlassArena",      *(BYTE*)(app + 0x867));
    RegKey_WriteBool(app + 0x54, "ImpossibleArena", *(BYTE*)(app + 0x868));

    // ---- Binary blobs (best times & medals) ----
    Registry_SetValue(app + 0x54, "BestTime", (BYTE*)(app + 0x86C), 0x50);
    Registry_SetValue(app + 0x54, "Medals",   (BYTE*)(app + 0x8BC), 0x50);

    // ---- 2P controller mappings ----
    RegKey_WriteDword(app + 0x54, "2PController1", *(DWORD*)(app + 0xB28));
    RegKey_WriteDword(app + 0x54, "2PController2", *(DWORD*)(app + 0xB2C));
    RegKey_WriteDword(app + 0x54, "2PController3", *(DWORD*)(app + 0xB30));
    RegKey_WriteDword(app + 0x54, "2PController4", *(DWORD*)(app + 0xB34));

    RegKey_Close(*(int*)(app + 0x54));
}

Load Function Reference

LoadOrSaveConfig0x4279F0

Called during startup to read (or create) the configuration. It mirrors the save structure but uses RegQueryValueExA instead of RegSetValueExA.

(Ghidra decompilation is large; the relevant registry-read logic is structurally identical to App_SaveAllConfig but reads into the same App offsets instead of writing.)


Display Settings

Resolution, color depth, texture quality, and full-screen mode are saved by App_WriteDisplaySettings (called from App_SaveAllConfig). The exact value names for display settings live in the string table near 0x4D5EB8 ("Resolution: %d x %d") and are written as a separate sub-key or value blob.


How to Add Custom Registry Values (Modding Guide)

If you are injecting code or writing a trainer/mod, follow the same pattern the engine uses:

1. Open the key

// app = pointer to App object (passed to most __fastcall functions)
int hKey = *(int*)((BYTE*)app + 0x54);
RegKey_Open(hKey);   // wrapper at inferred address; or call RegCreateKeyA yourself

2. Write a scalar

// DWORD example
DWORD myValue = 42;
RegSetValueExA(hKey, "MyModValue", 0, REG_DWORD, (BYTE*)&myValue, 4);

// BOOL example (same as DWORD but 0/1)
BYTE myFlag = 1;
RegSetValueExA(hKey, "MyModFlag", 0, REG_DWORD, &myFlag, 4);

3. Write binary data

BYTE myBlob[64] = { ... };
RegSetValueExA(hKey, "MyModData", 0, REG_BINARY, myBlob, sizeof(myBlob));

4. Close

RegKey_Close(hKey);   // or RegCloseKey(hKey)

Important Notes

  • The game uses REG_DWORD for both true DWORDs and booleans (not REG_SZ).
  • Binary blobs (BestTime, Medals) use REG_BINARY with exact 80-byte (0x50) sizes.
  • The registry handle at App+0x54 is cached; do not leak it—always close after use.
  • Display settings are saved separately; if you are adding display-related values, hook App_WriteDisplaySettings instead.

String Table References (Binary Offsets)

String Binary Offset Usage
"Raptisoft\\%s" 0x4D3978 Registry key format
"MouseSensitivity" 0x4D2898 Save/load
"MirrorTournament" 0x4D2884 Save/load
"BestTime" 0x4D274C Save/load
"Medals" 0x4D2744 Save/load
"2PController1" 0x4D2734 Save/load
"Resolution: %d x %d" 0x4D5EB8 Display UI
"Texture Quality" 0x4D8780 Display UI

Quick Reference: Reading from Outside the Game

If you want to read Hamsterball settings from an external tool (Python / C# / etc.):

import winreg

key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Raptisoft\Hamsterball")

# Read a DWORD (e.g., MouseSensitivity)
val, typ = winreg.QueryValueEx(key, "MouseSensitivity")
print(val)  # int

# Read a binary blob (e.g., BestTime)
val, typ = winreg.QueryValueEx(key, "BestTime")
print(len(val))  # 80 bytes

winreg.CloseKey(key)

See Also

  • App_SaveAllConfig — master save routine @ 0x4284C0
  • LoadOrSaveConfig — startup load/create @ 0x4279F0
  • OptionsMenu_RenderControls — control binding UI @ 0x42E840
  • SoundDevice_ReadVolume — audio registry read @ 0x466570
  • SoundDevice_dtor — audio registry save @ 0x4668A0

🔗 Related Documents

Reimplementation API Strategy

types : project
keywords :

📂 View source on GitHub


Reimplementation API Strategy

Core Principle: Same APIs as Original

The goal is to open-source Hamsterball, not rewrite it with different APIs.
Every call must match what the original binary does:

  • Direct3D 8 (not OpenGL/SDL) for rendering
  • DirectInput8 (not SDL input) for keyboard/mouse/joystick
  • BASS.dll (shipped alongside, like original) for audio
  • Win32 API (not SDL window) for window management

Original binary is PE32 i386 (32-bit), so our build targets 32-bit Windows.
Wine handles D3D8→OpenGL translation for Linux users automatically.

WinMain Flow (0x4278E0)

WinMain → RegisterClassExA("AthenaWindow") → CreateWindowExA(800,600)
  → App_Initialize_Full(0x429530)
    → App_Initialize(0x46BB40): DirectInput8Create, BASS_Init, D3D8 creation
    → LoadCursorA("BLANKCURSOR")
    → SetDisplayMode(800,600) via graphics vtable
    → SetRenderState(D3DRS_LIGHTING=TRUE, SHADEMODE=3)
    → Graphics_FindOrCreateTexture("shadow.png")
    → MusicChannel_LoadAndAppend("music\\music.mo3")
    → LoadJukebox("jukebox.xml")
    → RegKey_ReadDword("PlayCount", default=20)
    → 4x InputDevice_SetType (kb=1, mouse=2, joy1=4, joy2=5)
  → App_Run(0x46BD80): game loop (PeekMessage/TranslateMessage/DispatchMessage)
  → App_Shutdown(0x46DB10)

Build Target

  • Compiler: i686-w64-mingw32-gcc (32-bit Windows)
  • Link: -ld3d8 -ldinput8 -ldsound -ldxguid -lwinmm -lole32 -loleaut32
  • BASS.dll: Shipped alongside exe (same as original — it's a third-party lib, not game code)
  • D3D8: System DLL on Windows, Wine provides it on Linux

D3D8 Calls to Replicate (from Ghidra)

App_Initialize (0x46BB40):
Direct3DCreate8()
IDirect3D8_CreateDevice(D3DDEVTYPE_HAL, ...)
SetRenderState(D3DRS_LIGHTING, TRUE)
SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD=3)

DirectInput8 Calls

DirectInput8Create(hInstance, DIRECTINPUT_VERSION, &IID_IDirectInput8A, ...)
CreateDevice(GUID_SysKeyboard)
CreateDevice(GUID_SysMouse)
EnumDevices(DI8DEVCLASS_GAMECTRL, ...) // for joysticks

BASS Audio Calls

BASS_Init(-1, 44100, 0, 0, NULL) // default device, 44.1kHz
BASS_StreamCreateFile(FALSE, "music\music.mo3", ...)
BASS_ChannelPlay(stream, TRUE)


🔗 Related Documents

Reimplementation Plan

types : plans
keywords :

📂 View source on GitHub


Hamsterball Reimplementation Plan: Working Game with Comprehensive Tests

For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.

Goal: Build a working Hamsterball reimplementation that loads all original assets the same way and calls the same APIs, with a comprehensive test suite to verify correctness and diagnose problems.

Architecture: Replace the current stub-heavy codebase with a layered, testable engine. Each module matches the original binary's subsystem (filesystem, mesh, meshworld, texture, audio, input, physics, scene, camera, UI/menu, game state). All asset loading uses the original game's file formats and directory structure. SDL2+OpenGL replaces D3D8, SDL_mixer replaces BASS. Tests verify each layer against known binary behavior.

Tech Stack: C11, SDL2, OpenGL 2.1, SDL_mixer, SDL_image, GLU, CMake, bash test harness


Phase 1: Test Infrastructure & Asset Loading (Foundation)

Task 1: Create test harness infrastructure

Objective: Set up a test runner that can run unit tests and integration tests, report pass/fail, and capture output.

Files:

  • Create: reimpl/tests/test_runner.sh
  • Create: reimpl/tests/test_helpers.h
  • Create: reimpl/tests/test_helpers.c
  • Modify: reimpl/CMakeLists.txt

Step 1: Create test runner shell script

#!/bin/bash
# test_runner.sh - Run all tests, report results
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="${1:-$SCRIPT_DIR/../build}"
PASS=0; FAIL=0; TOTAL=0
run_test() {
    local name="$1" cmd="$2"
    TOTAL=$((TOTAL+1))
    printf "  TEST %-50s " "$name"
    if output=$($cmd 2>&1); then
        PASS=$((PASS+1))
        echo "PASS"
    else
        FAIL=$((FAIL+1))
        echo "FAIL"
        echo "$output" | sed 's/^/    /'
    fi
}
# Unit tests (no display needed)
for t in "$BUILD_DIR"/test_*; do
    [ -x "$t" ] && run_test "$(basename $t)" "$t"
done
# Integration tests need display
export DISPLAY=${DISPLAY:-:99}
for t in "$BUILD_DIR"/itest_*; do
    [ -x "$t" ] && run_test "$(basename $t)" "timeout 10 $t"
done
echo ""
echo "Results: $PASS/$TOTAL passed, $FAIL failed"
[ $FAIL -eq 0 ]

Step 2: Create C test helper macros

// test_helpers.h - Simple C test macros
#ifndef TEST_HELPERS_H
#define TEST_HELPERS_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

static int g_tests_run = 0, g_tests_pass = 0, g_tests_fail = 0;

#define TEST(name) void name(void)
#define RUN_TEST(name) do { \
    printf("  %-50s ", #name); \
    g_tests_run++; \
    name(); \
    g_tests_pass++; \
    printf("PASS\n"); \
} while(0)

#define ASSERT_EQ(a, b) do { \
    if ((a) != (b)) { \
        g_tests_fail++; g_tests_pass--; \
        printf("FAIL\n    %s != %s (line %d): %ld != %ld\n", \
               #a, #b, __LINE__, (long)(a), (long)(b)); \
        return; \
    } \
} while(0)

#define ASSERT_FEQ(a, b, eps) do { \
    if (fabs((a)-(b)) > (eps)) { \
        g_tests_fail++; g_tests_pass--; \
        printf("FAIL\n    %s != %s (line %d): %f != %f\n", \
               #a, #b, __LINE__, (double)(a), (double)(b)); \
        return; \
    } \
} while(0)

#define ASSERT_NEQ(a, b) do { if ((a) == (b)) { \
    g_tests_fail++; g_tests_pass--; \
    printf("FAIL\n    %s == %s (line %d)\n", #a, #b, __LINE__); return; \
} } while(0)

#define ASSERT_NULL(p) do { if ((p) != NULL) { \
    g_tests_fail++; g_tests_pass--; \
    printf("FAIL\n    %s not NULL (line %d)\n", #p, __LINE__); return; \
} } while(0)

#define ASSERT_NOT_NULL(p) do { if ((p) == NULL) { \
    g_tests_fail++; g_tests_pass--; \
    printf("FAIL\n    %s is NULL (line %d)\n", #p, __LINE__); return; \
} } while(0)

#define ASSERT_STR_EQ(a, b) do { if (strcmp((a),(b)) != 0) { \
    g_tests_fail++; g_tests_pass--; \
    printf("FAIL\n    %s != %s (line %d): \"%s\" != \"%s\"\n", \
           #a, #b, __LINE__, (a), (b)); return; \
} } while(0)

#define TEST_MAIN() int main(void) { \
    printf("Running tests...\n"); \
    test_all(); \
    printf("\n%d/%d passed\n", g_tests_pass, g_tests_run); \
    return g_tests_fail > 0 ? 1 : 0; \
}

#endif

Step 3: Add CMake test targets

# At end of CMakeLists.txt, add:
enable_testing()

# Find all test source files
file(GLOB TEST_SOURCES "tests/test_*.c")
file(GLOB ITEST_SOURCES "tests/itest_*.c")

foreach(src ${TEST_SOURCES})
    get_filename_component(name ${src} NAME_WE)
    add_executable(${name} ${src} src/level/meshworld_parser.c src/level/mesh_parser.c)
    target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/include)
    target_link_libraries(${name} m)
    add_test(NAME ${name} COMMAND ${name})
endforeach()

# Integration tests need SDL+GL
foreach(src ${ITEST_SOURCES})
    get_filename_component(name ${src} NAME_WE)
    add_executable(${name} ${src} src/level/meshworld_parser.c src/level/mesh_parser.c)
    target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/include ${SDL2_INCLUDE_DIRS})
    target_link_libraries(${name} ${SDL2_LIBRARIES} ${OPENGL_LIBRARIES} ${GLEW_LIBRARIES} m)
endforeach()

Step 4: Run to verify

cd ~/hamsterball-re/reimpl/build && cmake .. && make test_runner 2>/dev/null; bash ../tests/test_runner.sh

Expected: 0 tests (empty harness), exit 0.

Step 5: Commit

cd ~/hamsterball-re && git add reimpl/tests/ reimpl/CMakeLists.txt && git commit -m "reimpl: test harness infrastructure - shell runner, C macros, CMake targets"

Task 2: Asset path configuration and filesystem module

Objective: Create a centralized asset path resolver that mirrors the original game's directory structure.

Files:

  • Create: reimpl/include/core/filesys.h
  • Create: reimpl/src/core/filesys.c
  • Create: reimpl/tests/test_filesys.c

Original game reads from these locations (relative to exe directory):

  • Levels/ — .MESHWORLD level files
  • Meshes/ — .MESH model files
  • Textures/ — .PNG texture files (also .BMP, .JPG)
  • Sounds/ — .WAV sound files
  • Music/ — .MO3 music files
  • Data/ — game data (racedata.xml, jukebox.xml, HS.CFG)
  • Fonts/ — font.description + font PNGs

Step 1: Write filesystem module

// filesys.h
#ifndef FILESYS_H
#define FILESYS_H
#include <stddef.h>
typedef struct { char base_path[512]; } filesys_t;
void filesys_init(filesys_t *fs, const char *exe_path);
void filesys_set_base(filesys_t *fs, const char *path);
size_t filesys_resolve(const filesys_t *fs, const char *subdir, const char *name, const char *ext, char *out, size_t out_size);
int filesys_file_exists(const char *path);
int filesys_read_file(const char *path, uint8_t **data, size_t *size);
#endif

Step 2: Write test

// test_filesys.c
#include "test_helpers.h"
#include "core/filesys.h"

TEST(test_filesys_init) {
    filesys_t fs;
    filesys_init(&fs, "/path/to/Hamsterball.exe");
    ASSERT_STR_EQ(fs.base_path, "/path/to");
}

TEST(test_filesys_resolve) {
    filesys_t fs;
    filesys_init(&fs, "/games/hamsterball");
    char out[512];
    size_t len = filesys_resolve(&fs, "Levels", "Level1", "MESHWORLD", out, sizeof(out));
    ASSERT_NEQ(len, 0);
    ASSERT_STR_EQ(out, "/games/hamsterball/Levels/Level1.MESHWORLD");
}

TEST(test_filesys_exists) {
    // Use a file we know exists
    ASSERT_EQ(filesys_file_exists("/dev/null"), 1);
    ASSERT_EQ(filesys_file_exists("/nonexistent/file.xyz"), 0);
}

void test_all(void) {
    RUN_TEST(test_filesys_init);
    RUN_TEST(test_filesys_resolve);
    RUN_TEST(test_filesys_exists);
}

TEST_MAIN()

Step 3: Implement, build, test, commit.

Task 3: MESHWORLD parser tests against all 87 original levels

Objective: Verify the existing parser works on ALL original level files and report statistics.

Files:

  • Create: reimpl/tests/test_meshworld.c

Step 1: Write test that iterates all levels

// test_meshworld.c
#include "test_helpers.h"
#include "level/meshworld_parser.h"
#include "core/filesys.h"
#include <dirent.h>

#define LEVELS_DIR "../originals/installed/extracted/Levels"

static int count_levels(void) {
    DIR *d = opendir(LEVELS_DIR);
    if (!d) return 0;
    int count = 0;
    struct dirent *ent;
    while ((ent = readdir(d))) {
        if (strstr(ent->d_name, ".MESHWORLD")) count++;
    }
    closedir(d);
    return count;
}

TEST(test_all_levels_parse) {
    DIR *d = opendir(LEVELS_DIR);
    ASSERT_NOT_NULL(d);
    int parsed = 0, total = 0;
    struct dirent *ent;
    while ((ent = readdir(d))) {
        if (!strstr(ent->d_name, ".MESHWORLD")) continue;
        total++;
        char path[512];
        snprintf(path, sizeof(path), "%s/%s", LEVELS_DIR, ent->d_name);
        mw_level_t *level = meshworld_parse_file(path);
        if (level && level->object_count > 0) parsed++;
        if (level) meshworld_free(level);
    }
    closedir(d);
    printf("(%d/%d parsed) ", parsed, total);
    ASSERT_EQ(parsed, total);  // All must parse
    ASSERT_EQ(total, 87);      // Known count
}

TEST(test_level1_objects) {
    mw_level_t *level = meshworld_parse_file(LEVELS_DIR "/Level1.MESHWORLD");
    ASSERT_NOT_NULL(level);
    ASSERT_NEQ(level->object_count, 0);
    // Level1 has known START objects
    int starts = 0;
    for (int i = 0; i < level->object_count; i++) {
        if (level->objects[i].type == MW_OBJ_START) starts++;
    }
    ASSERT_NEQ(starts, 0);
    meshworld_free(level);
}

TEST(test_arena_levels_exist) {
    const char *arenas[] = {"Arena-WarmUp", "Arena-Beginner", "Arena-Dizzy",
        "Arena-Expert", "Arena-Glass", "Arena-Impossible"};
    for (int i = 0; i < 6; i++) {
        char path[512];
        snprintf(path, sizeof(path), "%s/%s.MESHWORLD", LEVELS_DIR, arenas[i]);
        mw_level_t *level = meshworld_parse_file(path);
        ASSERT_NOT_NULL_MSG(level, arenas[i]);
        meshworld_free(level);
    }
}

void test_all(void) {
    RUN_TEST(test_all_levels_parse);
    RUN_TEST(test_level1_objects);
    RUN_TEST(test_arena_levels_exist);
}

TEST_MAIN()

Step 2: Build and run: cd build && make test_meshworld && ./test_meshworld

Step 3: Fix any parse failures, commit.

Task 4: MESH parser tests against all 33 original meshes

Objective: Same as Task 3 but for .MESH model files.

Files:

  • Create: reimpl/tests/test_mesh.c

Step 1: Test all meshes parse, verify vertex counts match known values.

Step 2: Build, run, fix, commit.

Task 5: Texture loading module (PNG/BMP/JPG via SDL_image)

Objective: Load texture files the same paths the original game uses (Textures/ dir, with -mip1 variants).

Files:

  • Create: reimpl/include/graphics/texture.h
  • Create: reimpl/src/graphics/texture.c
  • Create: reimpl/tests/itest_texture.c

Original game texture loading from 0x476770 (Texture_Create):

  1. Strip extension from filename
  2. Try: {path}{name}-mip1.{fmt} then {path}{name}.{fmt}
  3. Try formats: PNG, JPG, BMP in order
  4. Store in texture cache (AthenaList at Graphics+0x2E4)

Step 1: Implement texture module with SDL_image backend.

Step 2: Write integration test that loads known textures from original game dir.

Step 3: Build, test, commit.

Task 6: Material and mesh rendering integration

Objective: Load a .MESH file, apply its material, load its texture, and render it with OpenGL.

Files:

  • Create: reimpl/include/graphics/mesh_render.h
  • Create: reimpl/src/graphics/mesh_render.c
  • Create: reimpl/tests/itest_mesh_render.c

Step 1: Implement mesh GL upload (VBO from vertex array + texture).

Step 2: Integration test: load Sphere.MESH, load HamsterBall.png, render to offscreen, verify no GL errors.

Step 3: Build, test, commit.

Task 7: Audio loading module (WAV via SDL_mixer)

Objective: Load .WAV sound files from original game's Sounds/ directory.

Files:

  • Create: reimpl/include/audio/sound.h
  • Create: reimpl/src/audio/sound.c
  • Create: reimpl/tests/itest_audio.c

Original game at 0x459660 (Sound_LoadOggOrWav): tries .ogg then .wav fallback.
At 0x459810 (Sound_GetNextChannel): circular buffer channel allocator.

Step 1: Implement WAV loading with SDL_mixer.

Step 2: Test: load 5 known sounds, verify they play without error.

Step 3: Build, test, commit.

Task 8: Font loading module

Objective: Load font.description + font PNG files from original Fonts/ directory.

Files:

  • Create: reimpl/include/graphics/font.h
  • Create: reimpl/src/graphics/font.c
  • Create: reimpl/tests/itest_font.c

Font_Load at 0x457130. Font format: text .description file mapping char codes to UV rects in font PNG atlas.

Step 1: Implement font loader.

Step 2: Test: load showcardgothic16 (used by splash screen), verify glyph metrics.

Step 3: Build, test, commit.

Phase 2: Game Subsystems

Task 9: Input module with key rebinding

Objective: Full DirectInput8 replacement with SDL keyboard/mouse/joystick, 6 rebindable actions.

Files:

  • Modify: reimpl/src/input/input.c
  • Modify: reimpl/include/input/input.h
  • Create: reimpl/tests/itest_input.c

Original input: KeyboardDevice at 0x46E250, 6 key bindings at +0x143-0x148.
Ball_GetInputForce at 0x46EC30: mode 1=keyboard, 2=mouse, 4-7=joystick.

Step 1: Implement configurable key mapping with SDL scancode support.

Step 2: Test: simulate key events, verify input state changes.

Step 3: Commit.

Task 10: Physics engine with correct constants

Objective: Implement ball physics with the exact constants from the original binary, replacing placeholder values.

Files:

  • Modify: reimpl/src/physics/physics.c
  • Modify: reimpl/include/physics/physics.h
  • Create: reimpl/tests/test_physics.c

Original constants (from data section):

  • Ball radius: 35.0f (0x420C0000) — NOT 3.0f
  • max_speed: 5000.0f (at Ball+0x188)
  • speed_scale: (at Ball+0x18C)
  • Gravity: -0.15f default
  • Friction: 0.95f (_DAT_004CF4C0)
  • Y damp: 0.8f (_DAT_004CF434)
  • Speed friction: 0.99f (_DAT_004CF4B8)

Step 1: Replace all placeholder constants with correct binary values.

Step 2: Write unit tests: apply force → verify position after N steps matches expected trajectory.

Step 3: Commit.

Task 11: Collision system - mesh collision

Objective: Implement the core collision detection using the spatial tree system from the original.

Files:

  • Create: reimpl/include/physics/collision.h
  • Create: reimpl/src/physics/collision.c
  • Create: reimpl/tests/test_collision.c

Key original functions:

  • Collision_TraverseSpatialTree (0x465EF0): octree traversal
  • Ball_AdvancePositionOrCollision (0x4564C0): core physics+collision
  • AABB_ContainsPoint (0x4580D0)
  • Mesh_FindClosestCollision (0x465D90)

Step 1: Implement AABB, spatial tree, ray-mesh collision.

Step 2: Test: sphere vs known triangle, verify hit point.

Step 3: Commit.

Task 12: Camera system

Objective: Camera follow system matching original behavior.

Files:

  • Create: reimpl/include/graphics/camera.h
  • Create: reimpl/src/graphics/camera.c

Original: CameraLookAt (0x413280), Level_SelectCameraProfile (0x40ACA0).
Camera follows ball with lerp, configurable height/distance per level.

Step 1: Move camera code from physics.c into camera.c. Add CameraLookAt targets from level data.

Step 2: Commit.

Task 13: Scene graph / object system

Objective: Scene graph matching the original's object lifecycle: add, update, render, remove.

Files:

  • Create: reimpl/include/scene/scene.h
  • Create: reimpl/src/scene/scene.c

Original: Scene_AddObject (0x469990, 77 xrefs), GameUpdate (0x469CF0), Scene_RenderAllObjects (0x45E0E0).

Step 1: Implement SceneObject base, Scene container, update/render dispatch.

Step 2: Commit.

Task 14: Game state machine

Objective: Title screen, menu, race countdown, race, results — matching original app state flow.

Files:

  • Create: reimpl/include/core/gamestate.h
  • Create: reimpl/src/core/gamestate.c

Original flow: WinMain→App_Initialize→App_ShowMainMenu→[menu dispatch]→App_StartRace→Scene_SpawnBallsAndObjects→3-2-1-GO→racing→Scene_HandleBallFinish→results

Step 1: Implement state machine with transitions.

Step 2: Commit.

Phase 3: Integration & Rendering

Task 15: Level renderer - mesh geometry from MESHWORLD

Objective: Actually render the mesh geometry stored in MESHWORLD files (not just object markers).

Files:

  • Create: reimpl/include/graphics/level_render.h
  • Create: reimpl/src/graphics/level_render.c

The existing meshworld_parser extracts object positions but not the triangle geometry section. Need to extend the parser to read vertex/face data and upload to GL.

Step 1: Extend meshworld_parser to read Section 5 (vertex array) and face data.

Step 2: Implement GL VBO upload from parsed vertex data.

Step 3: Test: render Level1 with actual geometry visible.

Step 4: Commit.

Task 16: 3-pass render pipeline (opaque, alpha, shadow)

Objective: Match original's 3-pass render: opaque → alpha → shadow (Scene_RenderAllObjects 0x45E0E0).

Files:

  • Modify: reimpl/src/graphics/renderer.c

Step 1: Sort scene objects by render pass type. Render in correct order with depth/stencil states.

Step 2: Commit.

Task 17: Ball rendering with MESH model

Objective: Render the ball using the actual Sphere.MESH + HamsterBall.png texture, matching Ball_Render (0x402860).

Files:

  • Modify: reimpl/src/physics/physics.c

Step 1: Load Sphere.MESH at startup. Render with texture, rolling animation.

Step 2: Commit.

Task 18: UI rendering with fonts

Objective: Render text overlays (timer, speed, menu) using loaded fonts.

Files:

  • Create: reimpl/include/graphics/ui_render.h
  • Create: reimpl/src/graphics/ui_render.c

Original: Font_DrawGlyph (0x457440), UI_DrawTextCentered (0x409C60), UI_DrawTextShadow (0x4012C0).

Step 1: Implement glyph quad rendering from font atlas.

Step 2: Commit.

Task 19: End-to-end integration test

Objective: Automated test that runs the game through a complete scenario.

Files:

  • Create: reimpl/tests/itest_e2e.c

Step 1: Test: init → load level → spawn ball → apply input → verify ball moves → verify camera follows → verify render completes → shutdown. All headless with GL offscreen.

Step 2: Commit.

Phase 4: Asset Compatibility Verification

Task 20: Asset compatibility test suite

Objective: Comprehensive test that verifies ALL original assets load correctly.

Files:

  • Create: reimpl/tests/test_asset_compat.c

Test matrix:

  • All 87 .MESHWORLD levels parse with >0 objects
  • All 33 .MESH models parse with >0 vertices
  • All 229 textures load without error
  • All 63 sounds load and play
  • Font files load
  • RaceData.xml parses
  • Music files load

Step 1: Implement, run, fix any failures.

Step 2: Commit.

Task 21: Save/load config compatibility

Objective: Read/write HS.CFG in the same format as the original.

Original: LoadConfig (0x42AE80), SaveConfig (0x42B6E0).

Files:

  • Create: reimpl/include/core/config_rw.h
  • Create: reimpl/src/core/config_rw.c
  • Create: reimpl/tests/test_config.c

Step 1: Implement, test with original HS.CFG if present.

Step 2: Commit.

Key Verification Commands

# Build everything
cd ~/hamsterball-re/reimpl/build && cmake .. && make -j$(nproc)

# Run all unit tests (no display needed)
make test

# Run integration tests (needs X)
DISPLAY=:99 make test

# Run game
DISPLAY=:99 ./hamsterball --level ../originals/installed/extracted/Levels/Level1.MESHWORLD

# Asset compatibility test
./test_asset_compat

# Full test suite
bash ../tests/test_runner.sh

🔗 Related Documents

Rendering Iteration Log (Sessi

types : rendering
keywords :

📂 View source on GitHub


Rendering Iteration Log (Sessions 50+)

Goal

Pixel-perfect visual parity between D3D8 reimplementation and original Hamsterball (2004).

Reference Screenshots

  • reimpl/reference-screenshots/original/05_level2_intermediate.png — Level2 (Intermediate)
  • reimpl/reference-screenshots/original/06_warmup2.png — Level1 (Warm Up)

Iteration History

v5 (pre-session)

  • Wall shadow too dark (deep blue), walls too dim overall
  • Floor too dark, sky close but slightly off

v6 — Wall shadow lightened, lit_factor increased

  • Wall lit=(0.80,0.92,1.0), shadow=(0.35,0.50,0.73)
  • lit_factor = 0.85NdotL1 + 0.15NdotL2
  • Vision result: walls (125,155,170) vs original (185,230,255) — STILL TOO DARK

v7 — Target-matched colors approach

  • Rewrote to define output lit/shadow colors directly matching original
  • Wall lit=(0.73,0.90,1.0), shadow=(0.35,0.50,0.73) → (89,128,186)
  • Floor lit=(0.96,0.96,1.0), shadow=(0.47,0.55,0.78)
  • Vision: walls (125,155,170) still too dark, floor too blue

v8 — Gamma curve added

  • Added pow(lit_factor, 0.55) for steeper falloff
  • Vision: contrast 2/10, wall shadow (110,140,175) vs orig (65,95,145) — too bright
  • Root cause: fill light (L2) always adds brightness even to shadow faces

v9 — Fill light removed, deeper shadows

  • Set fill light weight to 0.0 (shadow color encodes ambient)
  • Wall shadow=(0.18,0.25,0.38), lit_factor=pow(x,0.45)
  • Floor shadow=(0.70,0.78,1.0) — pale lavender
  • Sky: (85,120,215) deep royal blue
  • Vision: shadow (82,116,153) — closer but room for improvement

v10 — Shadow values tuned for zero-fill-light model

  • Wall shadow=(0.24,0.35,0.49) → at lit_factor=0: (61,89,125) ≈ original (62,88,124) ✓
  • lit_factor=pow(x,0.35) — aggressive gamma
  • Floor bright nearly white, floor shadow lavender
  • Pink checker: warm pink alternate squares
  • Blue checker: light blue alternate squares
  • Vision: contrast 4/10, shadows still reported as (110,140,175)

v11-v14 — Checker pattern investigation

  • Checker code WAS running but pattern invisible in screenshots
  • Root cause: per-vertex Gouraud shading cannot show square checker patterns
    because D3D interpolates smoothly between vertex colors
  • If all 3 vertices of a triangle are on the same checker cell → uniform triangle
  • Tried 2x UV scale (u*2.0) — still invisible
  • Also found: D3DTSS_COLOROP was set to MODULATE for textured geoms,
    which meant the (broken) BMP texture was overriding vertex colors
    → Changed to SELECTARG1+DIFFUSE for all geoms
  • Still no visible checker pattern because per-vertex approach is fundamentally limited

v15 — Programmatic checker textures (BREAKTHROUGH)

  • Created 128x128 D3D textures via CreateTexture+LockRect at runtime
  • PinkChecker: white + pale pink (0xFFE0D8EA / 0xFFFFF8FF)
  • BlueChecker: white + light blue (0xFFD0E0FF / 0xFFFFF8FF)
  • Set D3DTSS_COLOROP=MODULATE, COLORARG1=TEXTURE, COLORARG2=DIFFUSE
    → Per-pixel texture × per-vertex lighting gives visible checker!
  • Removed per-vertex checker code (no longer needed)
  • Vision: checkerboard IS now visible on floor surfaces!

Current Color Targets (v15)

Walls (non-textured geoms)

State RGB Float values
Lit (172,222,254) target → actual ~(185,230,255) (0.72,0.90,1.0)
Shadow (62,88,124) target → actual ~(61,89,125) (0.24,0.35,0.49)

Floor (textured geoms)

State RGB Float values
Lit (245,245,255) (0.96,0.96,1.0)
Shadow (180,200,255) (0.70,0.78,1.0)

Sky

  • Clear color: D3DCOLOR_RGBA(85,120,215) — deep royal blue

Lighting formula

NdotL1 = 0.35*nx + 0.85*ny + 0.35*nz  (primary: above-left)
NdotL2 = -0.3*nx + -0.6*ny + -0.3*nz  (fill: unused, weight=0)
raw_factor = 1.0 * NdotL1 + 0.0 * NdotL2
lit_factor = pow(raw_factor, 0.35)
final_color = shadow + (lit - shadow) * lit_factor

Checker textures

  • Programmatic 128×128, 8×8 grid (16px cells)
  • MODULATE mode: texture_RGB × vertex_RGB per pixel
  • Pink: alternate cells are pale pink (0xE0D8EA) vs near-white (0xFFF8FF)
  • Blue: alternate cells are light blue (0xD0E0FF) vs near-white (0xFFF8FF)

Remaining Issues

  1. Wall shadow mid-tones still slightly bright — vision reports (100,135,175) for
    partially-shadowed faces vs original (62,88,124). The gamma curve pow(x,0.35)
    may need further tuning, or the shadow base values need slight reduction.

  2. Floor checker contrast — pattern IS visible at v15 but very subtle.
    Need to increase color difference between checker squares.
    Pink and blue alternate squares should be more distinctly colored.

  3. Missing visual elements:

    • GO! arrow (red arrow decal)
    • HUD timer (showcardgothic72 font + timerblot.png)
    • Start pad (green circle: Decal-Start.png)
    • Railing/pipes (purple pipe geometry)
    • Ball transparency (hamster inside glass sphere)
  4. Level2-specific: bluebrick.png geom isn't getting checker texture
    (texture name detection only matches "Checker" not "brick").
    Need to decide if bluebrick wall geoms should get any special treatment.

Key Architectural Decision

Target-matching approach: Instead of simulating the full D3D material×lighting
pipeline (which produces wrong colors on llvmpipe), we define lit/shadow output
colors that directly match the original game's appearance. The lit_factor interpolates
between them using a steep gamma curve. This is the inverse of standard rendering
but produces correct visual results.

llvmpipe Texture Bug

Wine/llvmpipe cannot render BMP/PNG-loaded D3D8 textures. Workaround: create
programmatic D3D textures (CreateTexture+LockRect) at runtime which DO render.
This is used for checker patterns via MODULATE blending.


🔗 Related Documents

Rendering Pipeline

types : rendering
keywords :

📂 View source on GitHub


Hamsterball Rendering Pipeline

Overview

The rendering pipeline is a 3-pass system: opaque objects first, then alpha-
blended objects, then shadow/depth objects. It uses D3D8 Direct3D with
custom sorting and z-buffer interleaving.

Pipeline Entry Points

App_Run (0x46BD80)
  └─ App::Render() [vtable call]
       └─ Graphics_BeginFrame (0x453B50)
            └─ Graphics_RenderScene (0x454BC0)
                 ├─ Graphics_SetupLights
                 ├─ Gfx_SetViewMatrix
                 ├─ Matrix_ComputeFrustum (0x4762B0)
                 ├─ 8 render callbacks at Graphics+0x710
                 ├─ D3DRS_ZENABLE, ZFUNC, ZWRITEENABLE restore
                 └─ Graphics_SetViewportZ
            └─ Scene_RenderAllObjects (0x45E0E0)
                 ├─ Pass 1: Opaque objects (flag 0x85F, 0x860, 0x862, 0x863 all 0)
                 ├─ Pass 2: Alpha objects (flag 0x860=1)
                 ├─ Pass 3: Shadow objects (flag 0x85F=1)
                 └─ Cleanup callbacks
       └─ Graphics_PresentOrEnd (0x455A90)
            └─ IDirect3DDevice8::Present

Graphics_RenderScene (0x454BC0)

Core D3D8 scene setup:

  1. SetupLights — configure D3D lights
  2. Copy matrices — World (Graphics+0x224), View (Graphics+0x264), Projection (Graphics+0x2A4)
  3. SetViewMatrix — apply view transform, compute frustum
  4. D3D SetTransformdevice->vtable[0x94](0x100, &projection) — set projection matrix
  5. Copy back world matrix, recompute frustum
  6. 8 render callbacks — function pointers at Graphics+0x710..0x72C
  7. Restore render states — ZENABLE (0x8B), ZFUNC, ZWRITEENABLE (0x1C)
  8. SetViewportZ — restore Z range from Graphics+0x73C/0x740
  9. Final callbacks — Graphics+0x5C vtable[0x88] and vtable[0x78]

Graphics Object Matrix Offsets

Offset Size Description
+0x224 64B (4x4) World matrix
+0x264 64B (4x4) View matrix
+0x2A4 64B (4x4) Projection matrix

Scene_RenderAllObjects (0x45E0E0)

Signature: Scene_RenderAllObjects(this, do_sorting, render_mode)

Sorting Phase (param_1 != 0)

  1. Clear 3 lists: shadow list (this+0x488), alpha list (this+0x8A0), deferred list (this+0xCB8)
  2. If ball rendering enabled (Scene+0x480→+0x434): call Ball_InitRenderState

Object Classification

Each scene object has flags at these offsets:

Flag Offset Meaning
0x85F +0x85F Shadow caster
0x860 +0x860 Alpha blended
0x861 +0x861 Depth bias (z-fight fix)
0x862 +0x862 Deferred render
0x863 +0x863 Skip rendering

Pass 1: Opaque Objects (all flags = 0)

For objects where no flags are set:

  1. Set render context: obj+0x83C = Graphics+0x7C4 (current frame counter)
  2. Compute material address: obj_index * 0x50 + mesh_base_ptr (at mesh+0x28)
  3. Call Graphics_ApplyMaterialAndDraw(graphics, material_address)
  4. If ball mesh NOT in cutscene mode (Scene+0x480→+0x10C4 == 0):
    • Iterate material list (obj+0x424), set D3D stream source per sub-material
    • device->vtable[0x118](5, stride, offset) — SetStreamSource
  5. If ball IS in cutscene: set vertex declaration and draw

Pass 2: Alpha Objects (flag 0x860 = 1)

Objects with alpha flag are collected into this+0x8A0 list:

  1. Disable Z-write: D3DRS_ZENABLE = 0 (device vtable[200](0xE, 0))
  2. For each alpha object:
    • Apply material and draw
    • Handle sub-materials same as opaque pass
  3. Re-enable Z-write: D3DRS_ZENABLE = 1 (device vtable[200](0xE, 1))

Pass 3: Shadow Objects (flag 0x85F = 1)

Objects with shadow flag are collected into this+0x488 list:

  1. Adjust projection for depth bias: proj_x += bias, proj_y += bias (DAT_004CF308)
  2. Enable stencil: D3DRS_STENCILENABLE = 1 (device vtable[200](0x37, 1))
  3. For each shadow object:
    • If depth-bias flag (0x861) set: toggle Z-write mid-render
    • Apply material and draw
  4. Disable stencil: D3DRS_STENCILENABLE = 3 (device vtable[200](0x37, 3))
  5. Restore projection: proj_x -= bias, proj_y -= bias
  6. Re-enable Z if needed

Post-Render Cleanup

Iterate scene children at this+0x480→+0x428, call vtable[0x48](1, 1) for each
(child cleanup callback).

Render Pass D3D State Changes

Pass Z-write Z-test Stencil Alpha blend
Opaque ON ON OFF OFF
Alpha OFF ON OFF ON
Shadow ON ON ON OFF (depth bias)

SceneObject Render Flags (at object+offset)

Offset Flag Description
+0x83C uint Frame counter (set to Graphics+0x7C4 each render)
+0x858 uint Vertex declaration index (for cutscene mode)
+0x850 uint Vertex buffer offset (for SetStreamSource)
+0x861 byte Depth bias flag — toggles Z-write during shadow pass

Key Render Functions

Address Name Xrefs Description
0x453B50 Graphics_BeginFrame 39 Begin D3D frame, clear backbuffer
0x454BC0 Graphics_RenderScene Full scene setup (lights, matrices, frustum)
0x45E0E0 Scene_RenderAllObjects 33 3-pass object render (opaque→alpha→shadow)
0x455D60 Graphics_DrawScreenRect 63 2D rectangle via TLVERTEX tristrip
0x455110 Graphics_ApplyMaterialAndDraw 17 Set material + draw primitive
0x455A90 Graphics_PresentOrEnd D3D Present or EndScene
0x454A30 Gfx_SetViewMatrix Set D3DTS_VIEW matrix
0x453900 Graphics_Clear D3D Clear (target+Z or Z+stencil)
0x4539A0 Graphics_SetupFog Linear fog: FOGSTART, FOGEND, FOGCOLOR
0x460450 Scene_RenderBallShadow 38 Ball shadow with depth bias pass
0x460DA0 Scene_RenderFrame 38 Full scene render with z-buffer interleaving
0x454190 Graphics_SetRenderMode Set shading mode, vertex shader, render states

Graphics Object Key Offsets

Offset Type Description
+0x07C IDirect3D8* D3D8 interface
+0x154 IDirect3DDevice8* D3D device
+0x224 float[16] World matrix (4x4)
+0x264 float[16] View matrix (4x4)
+0x2A4 float[16] Projection matrix (4x4)
+0x2E4 AthenaList* Texture cache
+0x5C void** Secondary render vtable (callbacks 0x78/0x88)
+0x70C byte Z-write state cache (0=off, 1=on)
+0x710 void*[8] 8 render callback function pointers
+0x730 uint Cached ZENABLE state
+0x734 byte Cached ZWRITEENABLE state
+0x738 uint Cached ZFUNC state
+0x748 void* Frustum data (for Matrix_ComputeFrustum)
+0x790 float Projection X offset (for depth bias)
+0x794 float Projection Y offset (for depth bias)
+0x7C4 uint Frame counter (incremented each render)
+0x7C8 uint Render call counter
+0x7CC uint Sub-material draw counter

Depth Bias Value

DAT_004CF308 is a small float offset added to the projection matrix X/Y during
the shadow render pass. This pushes shadow geometry slightly forward in clip
space to prevent z-fighting with the floor geometry.

Level Rendering Pipeline

Level_UpdateAndRender (0x40B600)

6-phase render for level geometry and objects:

  1. Build visible_list from primary (+0x29D4) and secondary (+0x3204) ball object lists
  2. Opaque pass: AlphaTest OFF, iterate both lists calling vtable0x1C per object
  3. Alpha pass: AlphaTest ON, iterate both lists calling vtable0x1C per object
  4. Waypoint arrow: If race active and not game-over, show next waypoint arrow (timer=0.45s)
  5. Visible list render: iterate combined visible_list, call vtable0x08 per object
  6. Ball shadows: If ripple_list has entries, render shadows for all balls

Level_RenderObjects (0x40B570)

Transparent pass renderer:

  1. Graphics_BeginFrame(gfx, 0)
  2. level->vtable0x4C - render level mesh (terrain geometry)
  3. Graphics_BeginFrame(gfx, 0)
  4. For each object in visible_list: vtable0x0C (RenderTransparent)

Scene_RenderFrame (0x60DA0)

Per-frame scene update with vertex buffer construction:

  1. Set scene+0x10C4 = 1 (render active)
  2. If root scene: allocate MeshWorld + transform buffer
  3. Sprite update: call vtable[0x3C] per sprite
  4. Triangle strip construction: For each mesh object, build zigzag strips:
    • Alternating vertex triples (degenerate-triangle strip pattern)
    • 0x20 bytes per vertex (pos + normal + texcoord)
    • SpriteAnim_SetRange for each object
  5. Font_RenderToTextureComplex for text→texture
  6. Mesh_SaveAndFree + free temp MeshWorld

Scene_CheckPath (0x57EC0)

Ring topology pathfinder on 360-cell (0x167=359) circular grid.
Used by Ball_Update for track-snapping collision.

  • Returns 1 = forward reachable, -1 = backward reachable, 0 = unreachable
  • Two walkers: forward (+1) and backward (-1), wrap at 0↔359

Scene_SpawnBallsAndObjects (0x41C5B0)

Level startup: spawn player balls + all interactive objects:

  1. For each start entry: lookup "START%d-%d" in hash table for position
  2. Ball_ctor2(0xC60 bytes) at position, radius=26.0, max_speed=5.0, gravity=0.5
  3. 1-player random start for race types 5/11/12/14
  4. Scan SAFESPOT/SAFEPOS entries
  5. Tournament/demo: CreateBadBall + CreateMouseTrap
  6. CreateSecretObjects + Scene_CreateFlags + Scene_CreateSigns + Scene_CreateDynamicObjects

D3D8 Device Vtable Dispatch Map

The game uses IDirect3DDevice8 COM vtable calls throughout. Key vtable offsets:

Vtable Offset D3D Method Usage in Game
+0x28 DrawIndexedPrimitiveUP D3DDevice_DrawIndexedPrimitiveUP (0x47DD56)
+0x2C DrawIndexedPrimitive Graphics_DrawIndexedPrimitive (0x47DFB9)
+0x78 EndScene Post-render cleanup
+0x88 BeginScene Pre-render setup
+0x94 SetTransform Matrix setup (View/Projection/World/Texture)
+0xA8 SetMaterial Apply D3DMATERIAL8
+0xF4 SetTexture Texture stage 0 setup
+0xFC SetTextureStageState Multi-texture config
+0x118 SetStreamSource Vertex buffer binding
+0x14C DrawPrimitiveUP Immediate-mode vertex draw
+0xC8 SetVertexShader FVF / declarator setup
+0x200 (0x200 = SetRenderState) SetRenderState D3D render state changes

Graphics_DrawIndexedPrimitive (0x47DFB9)

Thin wrapper: device->vtable[0x2C](device, 0, 0, prim_type, index_count | 0x800)

  • The 0x800 flag is D3DPT_TRIANGLELIST with additional index buffer flags
  • Called from Graphics_ApplyMaterialAndDraw

D3DDevice_DrawIndexedPrimitiveUP (0x47DD56)

User-pointer draw: parses vertex declaration via D3DX_ParseDeclarationType,
then calls device->vtable[0x28]. Falls back to cached declaration if NULL.

Graphics_ApplyMaterialAndDraw (0x455110)

Complex material system dispatch:

  1. Material selection: gfx+0x7C0 custom material override, or param_1
  2. Scale transform: If gfx+0x7A8 flag, apply Matrix_ScaleTransform with gfx+0x7B0..0x7BC
  3. Render state setup based on material type:
    • Simple (material[0x12]==0): Set alpha test mode via D3DTS_ALPHA
    • Complex: Set texture stage, blend mode, cull mode from material struct
  4. D3DDevice->SetMaterial (vtable[0xA8]) with material+4
  5. Lighting state: D3DRS_LIGHTING (0x39) per material flag
  6. Transform: Level_SetObjectTransform sets world matrix
  7. Draw: If scaled, call vtable0x4 (render callback)

Material System

Material Structure (0x50 bytes per material)

Materials are stored as arrays of 0x50-byte entries in the mesh vertex data.
Referenced via: object_index * 0x50 + mesh_base_ptr

Offset Size Description
+0x00 4 Material vtable / type flag
+0x04 64 D3DMATERIAL8 structure (SetMaterial payload)
+0x12 4* Sub-material pointer (NULL=simple)
+0x13 1 Alpha blend mode (0=solid, 1=alpha blend)
+0x1C 1 Source blend factor
+0x1D 1 Destination blend factor
+0x1E 1 Cull mode (0=CCW, 1=CW)
+0x4D 1 Lighting mode (0=off, 1=on)

Material Application Flow

Graphics_ApplyMaterialAndDraw(gfx, material_ptr)
  ├─ Select material (override or default)
  ├─ Apply scale transform if needed
  ├─ Set alpha test state (D3DRS_ALPHATESTENABLE via vtable[200](0x1B))
  ├─ Set texture blend operation (vtable[0xFC])
  ├─ Set texture (vtable[0xF4])
  ├─ Set material (vtable[0xA8])
  ├─ Set lighting (vtable[200](0x39))
  ├─ Level_SetObjectTransform (world matrix)
  └─ Draw (vtable[4])

Graphics Subsystem Functions

Address Name Description
0x453B50 Graphics_BeginFrame Set view transform + copy to frustum
0x453900 Graphics_ClearViewport D3D Clear (target+Z+stencil)
0x453C90 Graphics_CreateDevice D3D8 device creation + FPU control
0x454BC0 Graphics_RenderScene Full 3D render: lights+matrices+callbacks
0x454AB0 Graphics_SetProjection Set projection matrix
0x455110 Graphics_ApplyMaterialAndDraw Material + draw dispatch
0x455A90 Graphics_PresentOrEnd D3D Present / EndScene
0x455C50 Graphics_FindOrCreateTexture Texture cache lookup/create
0x455D60 Graphics_DrawScreenRect 2D rectangle (TLVERTEX)
0x457A50 Graphics_DisableRenderState Disable a render state
0x57A90 Gfx_SetRenderStateThunk Thin SetRenderState wrapper
0x457AB0 Gfx_LoadMatrixFromStack Load matrix from stack to D3D
0x457B50 Gfx_SetPosition Set world position
0x457B80 Gfx_SetPositionAndRender Position + render
0x457BB0 Gfx_RotateY Y-axis rotation
0x457C60 Gfx_ScaleX X-axis scale
0x457C90 Gfx_ScaleY Y-axis scale
0x457CC0 Gfx_ScaleZ Z-axis scale
0x457D40 Gfx_SetAlphaBlendState Alpha blend state change
0x459400 Gfx_ResetRenderState Reset all render states
0x46F100 Gfx_ApplyLightingState Apply lighting from scene
0x46F1E0 Gfx_ResetLighting Reset D3D lights
0x53960 Gfx_CallVTable8C Call device vtable[0x8C]
0x53970 Graphics_SetCullMode2 D3DRS_CULLMODE wrapper
0x53940 Gfx_ResetRenderState Full state reset

D3D Texture System Functions

Address Name Description
0x472B80 D3DTexture_Ctor Texture constructor
0x472C00 D3DTexture_DeletingDtor Texture destructor (calls Release)
0x48300C D3DTexture_Init Initialize from D3D surface
0x483A44 D3DTexture_InitLocked Init with locked rect
0x483BA4 D3DTexture_CreateFromDesc Create from texture desc
0x483F2A D3DTexture_CopyIndexData Copy index (16-bit)
0x483D6D D3DTexture_CopyLockedData Copy locked surface data
0x85525 D3DTexture_CreateSimple Create simple texture
0x85610 D3DTexture_CloneFromDesc16 Clone from 16-bit desc
0x4C3A D3DTexture_CopySurfaceData Copy surface data
0x82F9C D3DResource_ReleaseA Release D3D resource
0x82FD4 D3DResource_ReleaseB Release variant
0x8A900 D3DResourcePool_Release Release from pool

Mirror Mode

When App+0x7D2 (mirror_cull_flag) is set:

  • Gfx_SetCullMode(0x427940) changes D3DRS_CULLMODE
  • Gfx_SetViewMatrix flips the view matrix horizontally
  • This effectively mirrors the entire 3D scene

🔗 Related Documents

Reverse Engineering Experiment

types : root
keywords :

📂 View source on GitHub


Hamsterball — Reverse Engineering Experiments

A collection of reverse-engineering experiments targeting Hamsterball (2004, Raptisoft) —
a 3D marble racing game built on the Athena engine (PE32 i386, DirectX 8 / BASS audio / DirectInput8).

This project explores the game's internal architecture, binary file formats, physics systems,
and modding capabilities through Ghidra decompilation, DLL proxy mods, and custom level creation.


What's in this repo

🗺️ Interactive Knowledge Map — browse all 183 documents as an interactive graph with full-text search (Ctrl+K).

📖 Documentation (81 files across 11 categories)

Comprehensive reverse-engineering documentation generated from Ghidra decompilation of the
original Hamsterball.exe, covering every major game subsystem:

Category Docs Highlights
objects/ 15 App, Ball, Scene struct layouts, object factory, global variables
physics/ 13 collision system, ball physics, event planes, raycasting, particle system
meshworld/ 7 Binary file format spec, object types, parser decompilation
rendering/ 7 D3D8 pipeline, camera system, font/text rendering, iteration logs
gameplay/ 7 Arena scoring, rumble board, 8-ball AI, tournament state machine
decompilation/ 7 key function decompilations, full function map, game loop analysis
modding/ 6 DLL modding guides, function reference, custom controls, audio modding
project/ 12 Build notes, file formats, registry system, asset manifest
ui/ 3 Menu system, HUD/timer, text elements
audio/ 2 audio system, SFX reference
input/ 2 DirectInput, full input system with control remapping

Plus <a href="#22639509422765" title="docs/agent-knowledge" class="record-link ">docs/agent-knowledge</a>/ — a structured onboarding guide for AI agents working on the codebase.

🛠️ DLL Mods (15+ mods)

Runtime modifications via bass.dll proxy injection — no game patches required:

  • player_clones — Spawn AI-controlled player balls with custom targeting
  • jump_mod — Jump physics with ground detection via raycasting
  • 8ball_hit_detect — Detect and log 8-ball collisions
  • entity-limit-fixer — Fix game crashes from too many entities
  • fps_unlock — Uncap the 30 FPS framerate limit
  • half_size_balls — Shrink ball collision radius
  • water_mod — Water surface rendering experiment
  • collision_hook — Intercept ball-ball collision events
  • unlimited_tris — Remove triangle render limit

...and more. See mods/README.md for the full catalog.

🎮 Custom Levels

Custom .MESHWORLD level files created from scratch, verified working in the original game:

  • DualPlatformArena.MESHWORLD — Two-platform arena layout
  • DualPlatformArenaV2.MESHWORLD — Refined version with railings

🔧 Tools

  • tools/mw_create.py — Python MESHWORLD level generator
  • tools/d3d8_proxy_logger/ — D3D8 COM proxy logger for API call tracing
  • tools/<a href="#40045643926753" title="recon-analyzer" class="record-link ">recon-analyzer</a>/ — Automated binary reconnaissance
  • tools/decompile_batch.py — Batch Ghidra decompilation
  • tools/hbtestd/ — Automated game testing harness

📊 Binary Analysis

  • analysis/ — Function catalogs, struct layouts, and JSON exports from Ghidra
  • reference/raptisoft-exporter/ — Raptisoft's official MESHWORLD exporter reference

Technical Details

Target binary: Hamsterball.exe (PE32 i386, ~580KB)

Engine: Athena engine — DirectX 8 (D3D8), BASS audio library, DirectInput8

Analysis tools: Ghidra (with custom MCP server for programmatic decompilation),
Cheat Engine for runtime hooks, MinGW for cross-compiling DLL proxies.

Key addresses:

Symbol Address
App global 0x005341E0
WinMain 0x004278E0
Game loop 0x0046BD80
Ball vtable 0x004CF3A0
Scene vtable 0x004D0260

Repo Structure

docs/           # 81 RE docs in 11 category subfolders
mods/           # 15+ compiled DLL mods with source
tools/          # Python/C tools for analysis and level creation
analysis/       # JSON catalogs and struct exports from Ghidra
reference/      # Raptisoft official exporter reference
*.MESHWORLD     # Custom level files

Credits

RodentRacer — Contributions to the project

Artizard — Contributions to the project

BookwormKevin — Contributions to the project

Makyuni — Contributions to the project

XRow — Contributions to the project

License

Analysis and mod code is original work. Original Hamsterball is copyright Raptisoft.
No original game binaries are distributed in this repo.


🔗 Related Documents

Reverse Engineering Playbook

types : playbook

📂 View source on GitHub


Reverse Engineering Playbook

Program-agnostic reverse-engineering methodology, with Hamsterball.exe as a running example.

  • INDEX.md — start here
  • case-studies/hamsterball.md — concrete application of the playbook
  • scripts/ — reusable Python helpers for any target
  • templates/ — Ghidra Java script templates

🔗 Related Documents

Reverse Engineering Project

types : project

📂 View source on GitHub


Hamsterball Reverse Engineering Project

Project Overview

Goal: Create a documented, legally clean, open-source recreation of Hamsterball (2000s Windows game) that is playable, buildable, and behaviorally faithful to the original.

Permission: Original developer has granted permission for reverse engineering and recreation.

Binary Inventory

File Size MD5 SHA256
Hamsterball.exe 1,404,928 7d25019366b8d7f55906325bd630d7fe 3379e9041c7ab83abd07da1bcf974529...
bass.dll 97,336 fd5ec122f4dd201b3c3ef19e3058af81 11075ca0a1a3064bd971a3faa3856595...
unins000.exe 871,194 aa765b9cb2afb116f955d063bb7a2b36 53da811094504adcfaf6c220bfbc2d65...

Dependencies Identified

Runtime DLLs (bundled)

  • bass.dll - BASS audio library

System DLLs (Windows)

  • d3d8.dll, d3d9.dll - DirectX 8/9
  • ddraw.dll - DirectDraw
  • dinput8.dll - DirectInput 8
  • dsound.dll - DirectSound
  • kernel32.dll, user32.dll, gdi32.dll - Windows core
  • advapi32.dll - Windows API
  • shell32.dll - Windows Shell
  • ws2_32.dll, wsock32.dll - Networking (for eSellerate DRM)
  • comctl32.dll - Common Controls
  • ole32.dll, oleaut32.dll - OLE Automation
  • riched32.dll - Rich Edit
  • version.dll - Version Info

External Requirements

  • DirectX 8 or higher
  • Windows 2000/XP/Vista/7

File Formats

Custom Formats

  • .MESHWORLD - Level format (custom binary format)
  • .MESH - 3D mesh format
  • .MESHCOLLISION - Collision mesh data

Standard Formats

  • .ogg - Ogg Vorbis audio (sounds)
  • .mo3 - MO3 audio (music, MOD/tracker format with BASS)
  • .png - Textures (with mipmaps)
  • .bmp - Textures
  • .xml - Configuration (Jukebox.xml, RaceData.xml)
  • .CFG - Save/config (HS.CFG)
  • .SAV - Tournament save (TOURNAMENT.SAV)

Asset Inventory

  • Levels: 80+ MESHWORLD files (Arena-, Level, Secret-*, etc.)
  • Meshes: 40+ custom 3D mesh files
  • Textures: 250+ texture files (PNG, BMP with mipmaps)
  • Sounds: 80+ OGG sound effects
  • Music: 1 MO3 music file
  • Fonts: Custom bitmap fonts (ArialNarrow, ShowcardGothic)

Identified Subsystems

  1. Graphics Engine - DirectX 8/9 based renderer
  2. Audio Engine - BASS library for music and sound effects
  3. input system - DirectInput8 for keyboard/gamepad
  4. Physics - Custom physics for ball movement
  5. Level System - Custom meshworld format loader
  6. ui/menu system - In-game menus and UI
  7. Game Logic - Tournament, race, time trial modes
  8. Save System - HS.CFG and TOURNAMENT.SAV
  9. DRM System - eSellerate activation

Development Notes

  • Game is PE32 executable for Windows
  • Uses C++ with likely custom game engine
  • Class structure visible in symbols: App, Graphics, BoardLevel3
  • Custom collision detection system
  • Supports both single player and 2-player modes

Status

  • [x] Original files acquired and hashed
  • [x] Asset inventory complete
  • [ ] Wine execution environment tested
  • [ ] Ghidra analysis started
  • [ ] Core subsystems documented
  • [ ] Reimplementation started

🔗 Related Documents

Reverse Engineering Skill

types : skill
keywords :

📂 View source on GitHub


Hamsterball Reverse Engineering Skill

Overview

Reverse engineer and recreate the Hamsterball game (2000s Windows game by Raptisoft).

Engine Name Note: The binary only contains one reference to "Athena" — the Win32 window class name "AthenaWindow" at 0x4D9374. There is no "Athena Engine" string, no about box, no internal branding. "Athena" as the engine name is derived from the window class. "Raptisoft" is the company name (Nick Raptis), not the engine name. Use "Athena" as the working name for namespace prefixes (AthenaString, AthenaList, etc.).

How to prove engine names from window class strings: In Win32 games, WNDCLASSEX.lpszClassName is set during window registration and traditionally carries the engine name. Proof from open-source Quake 3 Arena (code/win32/win_glimp.c): #define WINDOW_CLASS_NAME "Quake 3: Arena" followed by wc.lpszClassName = WINDOW_CLASS_NAME; RegisterClass(&wc);. Similarly, Half-Life/Source uses "Valve001", Unity uses "UnityWndClass", GameMaker uses "YYGODragonWindow". These class names are developer-chosen identifiers visible via Spy++ or GetClassName(). In Hamsterball, at 0x46D218 the code does mov [esp+0x30], 0x4D9374 ("AthenaWindow") as lpszClassName before calling RegisterClassExA. This is standard practice — the window class name IS the engine identifier.

Prerequisites

  • Ghidra 12.0+ at /opt/ghidra_12.0.4_PUBLIC
  • GhidraMCP headless server (v5.2.0) on port 8089 — PRIMARY analysis tool
  • radare2 (r2) for quick checks (supplementary, but can be primary if GhidraMCP is down)
  • User preference: use ALL available tools in parallel — both Ghidra and r2 simultaneously for this difficult project
  • Wine 9.0 for execution testing
  • Python 3 with pefile
  • CRITICAL: Reimpl must use same APIs as original (D3D8, DirectInput8, BASS) — NOT SDL2+OpenGL. Target i686-w64-mingw32-gcc, link -ld3d8 -ldinput8 -ldsound -ldxguid. Wine handles D3D8→OpenGL for Linux users. See docs/API_STRATEGY.md for details.

Key Locations

  • Binary: ~/hamsterball-re/originals/installed/extracted/Hamsterball.exe
  • Ghidra project: ~/hamsterball-re/analysis/ghidra/HamsterballProject/
  • Docs: ~/hamsterball-re/docs/
  • C reimplementation: ~/hamsterball-re/reimpl/
  • Level viewer: ~/hamsterball-re/reimpl/build/level_viewer

Critical Function Addresses (CONFIRMED via Ghidra + r2)

Address Function Notes
0x4BB4C8 entry CRT entry point (GetVersionEx, heap init)
0x4278E0 WinMain Calls App_InitFull→App_Run→App_Shutdown
0x429530 App_Initialize_Full 26 init steps, debug strings at this+0x208
0x46BB40 App_Initialize 12-step base init (vtable calls + D3D8)
0x46BD80 App_Run Game loop: PeekMessage→Update→Render→Present
0x46BA10 App_Shutdown Sets this->running=1, vtable jump
0x455380 Graphics_Initialize Calls Direct3DCreate8, device creation
0x455A60 Graphics_Defaults Set default render states
0x453B50 Graphics_BeginFrame Begin frame/render setup
0x455A90 Graphics_PresentOrEnd Present frame or end scene
0x45DE30 LoadMeshWorld Load .meshworld level file
0x4015b0 Ball_SetupCollisionRender Init collision mesh render objects from level data
0x4016f0 Ball_ApplyForceV2 Alternate force application (gravity plane, ice/dizzy/tube)
0x402c10 Ball_RenderWithCollision Ball render with collision plane check + shadow
0x4280e0 App_ShowMainMenu Create MainMenu (0xCDC bytes), store at App+0x224
0x4288b0 App_StartTournamentRace Start tournament: config mirror, create scene, advance race
0x446b80 RegisterDialog_ValidateSerial Validate serial via XOR cipher key "54138", registry write
0x459660 Sound_LoadOggOrWav Load sound: try .ogg first, then .wav fallback
0x459310 Sound_LoadOgg Load OGG Vorbis file into D3D sound buffer
0x459810 Sound_GetNextChannel Circular buffer sound channel allocator
0x4706E0 MeshWorld_ctor Constructor (0x488 bytes), vtable at 0x4D9CDC
0x470930 MeshWorld_Parse Parse text format (*MATERIAL, *MESH tokens)
0x46A020 LoadMusicFile BASS_MusicLoad wrapper
0x46A4D0 LoadJukebox Parse jukebox.xml
0x42DE50 MainMenu_ctor Main menu (LET'S PLAY, HIGH SCORES, OPTIONS, CREDITS, EXIT)
0x442CE0 OptionsMenu_ctor Options screen (Resolution, Fullscreen, Color, Volume, Key Remap, Mouse)
0x42B470 HighScoreEntry_ctor High score entry screen (name input + score display)
0x42BD40 HighScoreEntry_Render Render high score entry UI
0x42E060 GameSelectionScreen_ctor Tournament difficulty selector screen
0x44FD60 SaveTourneyDialog_ctor Save tournament dialog
0x4476B0 RegisterDialog_ctor Register/purchase dialog
0x4652E0 CollisionLevel_ctor Collision-only level (.meshcollision format)
0x465260 Level_LoadCollision Load binary collision mesh (planes, objects, AABB)
0x4624C0 Level_Cleanup Level destructor (free objects, VBs, textures)
0x413C20 ArenaLevel_WarmUp_Init Initialize Warm-Up arena (levels\arena-WarmUp)
0x416F40 ArenaLevel_Neon_Init Initialize Neon arena (levels\arena-neon)
0x456E20 Font_MeasureText Measure text string width for centering
0x457440 Font_DrawGlyph Core glyph rendering (1 call = 1 glyph quad)
0x4013A0 UI_DrawTextCenteredAbsolute Draw centered text (x - width/2)
0x409C60 UI_DrawTextCentered Draw centered text with shadow
0x4012C0 UI_DrawTextShadow Draw text with shadow (offset + main)
0x40A120 LoadRaceData Parse racedata.xml
0x413280 CameraLookAt Camera look-at targets (CAMERALOOKAT)
0x429200 ESellerate_Init eSellerate DRM init
0x4254E0 CreditsScreen_ctor Credits scrolling screen (formerly mislabeled Physics_Init)
0x469CF0 GameUpdate Main game tick - iterates objects, calls vtable+4 (Update) and vtable+0x3C (Render)
0x428160 PauseGame Pause game (RightButtonPause)
0x42FAD0 QuitRace Quit current race
0x4298C0 TimerDisplay Race timer display (timerblot.png)
0x40FA20 CreateBumper BUMPER1/2/3/4 objects
0x40E250 CreateExpertLevelObjects SAWBLADE objects
0x4117B0 CreateUpLevelObjects SPEEDCYLINDER
0x412850 HandleArenaCollisionEvents N:SPINNER
0x410D00 NeonCollisionEvents Neon board collision handler (E:PEGS, E:HEATON, E:LIMIT, etc.)
0x40BF50 CreateMouseTrap MOUSETRAP
0x40C5D0 DispatchCollisionEvents Main collision event dispatcher — handles ALL object types: E:NODIZZY, E:SAFESWITCH, E:LIMIT, E:BREAK, E:JUMP, E:ACTION (ONCE/SCORE), E:TRAJECTORY, N:NOCONTROL, N:WATER, N:TARPIT, N:GOAL, N:MOUSETRAP, N:SECRET, N:UNLOCKSECRET, DROPIN, PIPEBONK, POPOUT
0x40E6A0 ExpertCollisionEvents Expert board events: E:CALLHAMMER, E:HAMMERCHASE, E:ALERTSAW1/2, E:ACTIVATESAW1/2, E:ALERTJUDGES, E:SCORE, E:JUMP, E:BELL (+delegates to DispatchCollisionEvents)
0x40DCD0 TowerCollisionEvents Tower board events: E:CATAPULTBOTTOM, E:OPENSESAME, N:TRAPDOOR, E:BITE, E:MACETRIGGER, N:MACE (+delegates to DispatchCollisionEvents)
0x434770 Saw_AlertActivate Saw blade alert mode (clear flag + 3D sound)
0x434A50 Saw_Activate Saw blade full activate (set flag + 3D sound)
0x434C40 Judge_Reset Reset judge objects
0x434C80 ScoreDisplay_SetTime Set score display time value
0x434E20 Bell_Activate Activate bell (+5 seconds extra time)
0x438BB0 Hammer_ChaseStart Start hammer chase sequence
0x434290 Catapult_Launch Catapult launch (set active + timer)
0x4344D0 Trapdoor_Open Open trapdoor (scale 0→1.0)
0x438410 Trapdoor_Activate Activate trapdoor (3D sound + timer)
0x459860 Sound_Play3D Play 3D positioned sound (set BASS pos + play)
0x4597B0 Sound_PlayChannel Play sound channel (pool dispatch)
0x4595B0 Sound_StartSample Start BASS sample (vtable: reset, volume, 3D pos)
0x46EC30 Ball_GetInputForce 3
0x466750 Sound_CalculateDistanceAttenuation 3
0x44C260 RaceResultPopup_ctor Race end popup (rank image + TIME'S UP!/OUT OF TIME!)
0x438B30 CreateBonkPopup BONKPOPUP feedback
0x40BAA0 CreateSecretObjects Create SECRET and SECRETUNLOCK objects
0x43DFB0 Secret_ctor Secret object constructor (0x10EC bytes)
0x4121D0 CreateLevelObjects Factory: BRIDGE, TIPPER, BONK, BBRIDGE1/2, POPCYLINDER, BLOCKDAWG1/2, CATAPULT, GLUEBIE
0x4133E0 CreatePlatformOrStands Factory: PLATFORM, STANDS
0x417FE0 CreateMechanicalObjects Factory: LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM
0x37040 Platform_ctor Platform constructor (0x10FC bytes)
0x462850 Stands_ctor Stands constructor (0x10D0 bytes)
0x435800 Looper_ctor Looper constructor (0x1500 bytes)
0x437590 Gear_ctor Gear/BigGear constructor (0x1514 bytes)
0x435940 Rotator_ctor Rotator constructor (0x1508 bytes)
0x437700 Pendulum_ctor Pendulum constructor (0x1504 bytes)
0x437960 Tipper_ctor Tipper constructor (0x1104 bytes)
0x4661A0 TipperVisual_ctor Tipper visual component
0x465200 TipperVisual_Attach Attach visual to tipper
0x438850 Bonk_ctor Bonk (hammer) constructor (0x1200 bytes)
0x436D70 BreakBridge_ctor Breakable bridge constructor (0x1100 bytes)
0x436EE0 PopCylinder_ctor Pop cylinder constructor (0x10E8 bytes)
0x43C310 Blockdawg_ctor Blockdawg constructor (0x1154 bytes)
0x437E10 Catapult_ctor Catapult constructor (0x1108 bytes)
0x437CB0 Gluebie_ctor Gluebie (glue blob) constructor (0x110C bytes)
0x41D060 LevelBoard_Dizzy_ctor BoardLevel3 constructor, vtable at 0x4D0890
0x40ABA0 CheckArenaUnlock Check arena unlock conditions
0x4279F0 LoadOrSaveConfig Config load/save dispatcher
0x42AE80 LoadConfig Load HS.CFG
0x42B6E0 SaveConfig Save HS.CFG
0x433AC0 GameSelectionManager Tournament save/load
0x457130 LoadFont Load font.description + PNG glyphs
0x429450 FinishLoad Final setup after loading
0x46EE10 Input_Init DirectInput8 init (0x438 byte object), stored at App+0x180
0x46C110 Input_Create Creates Input object, stores at param+0x180
0x46F010 Input_Cleanup Input object cleanup/release
0x46F0E0 Input_dtor Input destructor (delegates to Input_Cleanup)
0x46E250 KeyboardDevice_ctor DI keyboard device (0x524 bytes), vtable at 0x4D9840
0x46DE60 KeyboardDevice_dtor Keyboard cleanup, releases DI device, unbinds keys
0x461510 Level_ctor Level object constructor (loads MeshWorld, creates LevelState)
0x461460 LevelState_ctor Ball/physics state object (0x10D4 bytes)
0x4629C0 Level_dtor Level destructor
0x65080 Level_Clone Clone level object for secondary display
0x46F310 Level_base_ctor Base level init (before vtable set)
0x19030 Board_ctor Base Board/Screen class (vtable at 0x4D0260)
0x453210 AthenaList_Init Init 256-entry hash list (used everywhere)
0x453280 AthenaList_Clear Clear list contents
0x4532B0 AthenaList_GetIndex Get list iteration index
0x4534D0 AthenaList_Remove Remove item from list
0x453780 AthenaList_Append Append item to dynamic list (malloc/realloc)
0x4532E0 AthenaList_SortedInsert Insert with insertion-sort (ascending/descending)
0x402BC0 AthenaList_SetIndex Set list iteration index
0x40A020 AthenaList_GetAt Get element by index with bounds check; returns 0 if OOB

Sound System — Extended (Session 2026-04-13)

Address Function Notes
0x458f40 SoundDevice_LoadWAV Load WAV file→DirectSound buffer. Validates RIFF/WAVE header, "data" chunk, 6 error paths
0x4668a0 SoundDevice_dtor Release all DS buffers, save volume to registry, free memory
0x466570 SoundDevice_ReadVolume Read Sound Volume float from registry (default 1.0f)

Race Timer System (NEW — Session 2026-04-13)

Address Function Notes
0x451df0 RaceTimer_Tick Advance frame, adjust speed, accumulate score, determine rank (0-15), load rank sprite
0x44c880 RaceTimer_ctor Init timer, score thresholds (100-900), load weasel.png, update high score

Registry/GameInfo Utilities (Session 2026-04-13)

Address Function Notes
0x473000 RegKey_WriteDword Write REG_DWORD (type 3) to registry key
0x475430 GameInfo_AddSoundEntry Add "SOUND" resource entry (0x48 bytes)
0x4752f0 GameInfo_AddSpriteEntry Add "SPRITE" resource entry (0x48 bytes)
0x475270 GameInfo_AddCMeshEntry Add "BCMESH" collision mesh entry (0x48 bytes)
0x46d91d GameInfo_WriteDSoundTags Write DSOUND/CURRENTOBJECT/CURRENTOPERATION tags
0x46d230 GameInfo_BuildFullReport Build full diagnostic report (PRODUCT, VERSION, D3D8/9, DSOUND, etc.)
0x4764f0 D3DXERR_ToString Convert D3DX error code to human-readable string

Graphics/UI — Extended (Session 2026-04-13)

Address Function Notes
0x453ed0 Graphics_ReadQualitySettings Read Texture Quality, ColorMode, SafeMode from registry
0x453f90 Graphics_WriteQualitySettings Write Texture Quality, ColorMode, SafeMode to registry
0x4794d0 SplashScreen_ctor Brand logo + raptisoftlogo.png + showcardgothic16 font
0x487070 FontFormatString_WriteFloat printf-style float formatting (%f, %g, %e, %a)
0x4819ec D3DXMesh_AttributeSort Sort mesh faces by attribute ID, rebuild attribute table
0x444880 AbortConfirm_Render "REALLY ABORT?" dialog with YES/NO, score reset warning
0x445410 RollbackConfirm_Render "REALLY ROLL BACK?" dialog, score zeroing warning
0x4431e0 KeyRemapMenu_ctor Keyboard remap menu (Up/Down/Left/Right/Action1/Action2)
0x474900 LoaderGadget_OK LoaderGadget OK handler, set operation name

Math Utilities (NEW — Session 2026-04-13)

Address Function Notes
0x401AA0 Vec3_NormalizeAndScale 59 xrefs — most common math utility. Normalizes and scales to length param_1.
0x401D60 Matrix_TransformVec3 15 xrefs. Transform 3D vector by 4x3 matrix.
0x402BF0 Vec3_Copy 18 xrefs. Copy 3 floats with self-check.
0x40A050 Color_RandomRGBA Generate 32-bit color from 4 random bytes.
0x4580D0 AABB_ContainsPoint Test if (x,y,z) inside AABB. Used by collision spatial tree.
0x453150 Matrix_Scale4x4 Set 4x4 matrix row scale values.
0x453200 Matrix_Identity Set matrix to identity.
0x458B50 Matrix_ScaleTransform Create 4x4 by scaling source matrix rows.

Rendering Pipeline — Extended (NEW)

Address Function Notes
0x4542C0 Graphics_ctor Constructor (vtable 0x4D88A0, init render context, texture cache, frustum)
0x455360 Graphics_dtor Destructor (cleanup + optional free)
0x454550 Graphics_Cleanup Release D3D objects, free texture path, clear cache
0x454000 Graphics_SetTexturePath Set custom texture prefix (strdup at +0x7D8)
0x454060 D3DFMT_ToString Convert D3DFORMAT enum to debug string
0x454B50 Graphics_SetViewport Set viewport dimensions
0x454D30 Graphics_Reset Reset device with new params (CreateDevice twice)
0x455D60 Graphics_DrawScreenRect Draw 2D rectangle via TLVERTEX tristrip. 63 xrefs.
0x455110 Graphics_ApplyMaterialAndDraw Apply material states + draw. 17 xrefs.
0x454190 Graphics_SetRenderMode Set shading mode, vertex shader, render states
0x455B80 Graphics_SetStreamBuffers Set vertex buffer streams
0x457FA0 RenderContext_Init Init 0x50 byte render context (vtable 0x4D8E68)
0x401160 Graphics_SetViewportClip Set viewport clip from 4x4 matrix

Ball Physics — Extended (NEW)

Address Function Notes
0x403100 Ball_SetTiltedGravity Gravity plane=1, normal (-1,0,0)
0x403150 Ball_SetFlatGravity Gravity plane=2, normal (0,0,1)
0x403850 Ball_SetTrajectory Set trajectory direction + force scale
0x403750 Ball_ApplyTrajectory Apply trajectory force (normalize, scale, sound, counter=100)
0x403980 Ball_FindMeshCollision Mesh_FindClosestCollision wrapper
0x401DD0 Ball_CreateTrailParticles Create trail particles (10 iterations, 0x28 byte objects)
0x401920 Ball_RenderShadow Render ball shadow at XYZ
0x401DD0 Ball_CreateTrailParticles Create trail particles: 9 iterations, spherical camera-relative distribution using ArenaScoreParticle objects, velocity=RNG/(RNG+20), appended to scene+0x3b00
0x413280 CameraLookAt Arena camera init: Load arena-spawnplatform + arena-stands meshes, find CAMERALOOKAT target, set distance=45f, height=800f, max_height=800f
0x419FA0 Scene_SetCamera 5 camera modes: Default follow, Path rail (spring+sin dampening, dist<700→0, >700→offset-sin), Shake (±50 random offset), Snap (countdown frames→hard position), Orbit (sin/cos rotation at +0x29BC angle, dist +0x29C0). Ball target at +0x758, actual at +0x76C
0x4284C0 App_SaveAllConfig Save all config to registry: MouseSensitivity, MirrorTournament, 11 race unlock flags, 9 arena unlock flags, RightButtonPause, BestTime[0x50 bytes], Medals[0x50 bytes], 2PController1-4. Registry key at App+0x54
0x4279F0 LoadOrSaveConfig Config destructor: delete all resources (graphics, meshes, sounds, UI elements), save config, call ShellExecuteA("http://www.raptisoft.com") if !registered, call App_Shutdown
0x446730 Tourney_SaveTournament Save tournament: remove DATA\tournament.sav, call vtable save function
0x446730 Tourney_SaveTournament Save tournament: remove DATA\tournament.sav, call vtable save function

Collision System — Extended (NEW)

Address Function Notes
0x465EF0 Collision_TraverseSpatialTree Recursive octree traversal calling AABB test for each face
0x458000 Collision_InitDefaultAABB Set AABB bounds to ±39M (0x4b18967f)
0x4583F0 AABB_TriangleIntersect2 Double AABB-triangle test (calls AABB_TriangleTest6Edges twice)
0x458190 Collision_GradientEval_Stub Empty stub — called from Ball_Update and Gear_AdvanceAlongPath
0x456D80 CollisionMesh_ctor CollisionMesh constructor: 0xCB4 bytes, vtable 0x4D8E10. Initializes triangle list (AthenaList at +0x18), material list (AthenaList at +0x430), vertex list (AthenaList at +0x848), position at +0xCA4, scale factors at +0xC64/0xC68
0x456120 CollisionMesh_AddTriangle Add triangle ref to collision mesh: appends to list at +0x430, sets back-pointer at +0x08
0x4564C0 Ball_AdvancePositionOrCollision CORE PHYSICS FUNCTION: Advances ball position with collision detection. Uses SpatialTree for mesh traversal, accumulates velocity (gravity + scale), handles sub-step interpolation with position += direction * (1 - step), collision response via vtable+0x1C callback, material tracking. Offsets: +0x14=hasTrailData, +0x18=trail data, +0x430=triangles, +0x848=vertex normals, +0xC64=scale, +0xC68=friction, +0xC70=max_distance, +0xC74=accumulated_distance, +0xC90-C98=gravity, +0xCA4=position(XYZ), +0xC7C=useGravityCallback flag
0x463330 SpatialTree_ctor SpatialTree (octree) constructor for collision spatial partitioning. vtable 0x4D9038. Init values: +0x0C=0.1f (scale), +0x10=6 (max_depth), +0x14=0.9f (0x3F666666, min_extent). Flags +0x18-0x1E=1 (enable all axes)
0x4632E0 SpatialTree_Free Free spatial tree leaf nodes + cleanup

Board Level Constructors (Session 2026-04-13 Batch 5)

Pattern: Each Board constructor calls Board_ctor, sets vtable, name strings ("Board (Name)"), level unlock flag, Vec3_Init for background color, Matrix_Identity, LoadRaceData, then creates Level_ctor + Level_Clone for split-screen, and sometimes MeshNode_ctor for static geometry.

Map Case Level Board Address Levels Loaded
1 Warm-Up 0x41CA40 none (uses existing data)
2 Intermediate 0x41CB20 Level2-Bridge (Intermediate)
3 Dizzy 0x41D060 (known) Level3-Swirl
5 Tower 0x41E340 Level4-Catapult, Level4-Drawbridge, Level4-Mace, Level4-Windmill, Level4-Turret + YellowLink, Chomper meshes
8 Expert 0x41EA40 Level5-Bridge + 3x hammyjudge meshes
9 Odd 0x41ED80 none (single level)
12 Wobbly 0x41F110 Level7-Wobbly1 through Level7-Wobbly7 (7 levels!)

Input System (NEW)

Address Function Notes
0x46E0B0 Input_IsKeyDown Multi-device key check (keyboard=0, mouse=1, joystick=3-7)
0x46EC30 Ball_GetInputForce Get ball input direction from keyboard/mouse/joystick
0x46EE10 Input_Init DirectInput8 init
0x46C110 Input_Create Creates Input object, stores at App+0x180
0x46F010 Input_Cleanup Input object cleanup/release

Mesh System (Extended)

Address Function Notes
0x456390 Mesh_Clear Free vertex/index buffers, clear AthenaLists
0x46F340 MeshBuffer_Cleanup Cleanup mesh buffer resources, vtable 0x4D9C48
0x46F670 Mesh_SaveAndFree Save mesh data to file (magic 0xBEEF), free buffers
0x46FD60 Mesh_AddVertex Add vertex (8 floats: pos+normal+uv), dedup, error "Too many vertices"
0x46C970 Texture_SetDimensions Set texture size, compute UV scale factors

Game Object Factories (NEW)

Address Function Notes
0x418760 Scene_CreateObject_Gear Create GEAR/BigGear object (0x1514 bytes)
0x418930 Gear_AdvanceAlongPath 8-direction gradient descent path following

Game Object Factories — Extended (Session 17)

Address Function Notes
0x4143d0 CreateSpinny Factory: matches "SPINNY" → Rotator_ctor (0x1508 bytes)
0x414a20 CreateLifter Factory: matches "LIFTER" → Lifter_ctor (0x436920, 0x10f4 bytes), falls through to CreatePlatformOrStands
0x415460 CreateWobbly1 Factory: matches "WOBBLY1" → GameLevel_ctor (0x1524 bytes), falls through to CreatePlatformOrStands
0x436920 Lifter_ctor Lifter object constructor (0x10f4 bytes)
0x413fc0 ArenaBoard_RenderMultiView Render 4 dynamic objects with zoom step adjustment
0x4151e0 ArenaBoard_RenderMultiView5 Render 5 dynamic objects with zoom step adjustment
0x4155d0 ArenaBoard_InitScene4 Init scene objects for 4-player split, assign materials + Level_SetObjectTransform
0x415d90 ArenaBoard_InitScene5 Init scene objects for 5-player split, assign materials + Level_SetObjectTransform

String/Utility Functions (NEW)

Address Function Notes
0x463790 Vec3_CrossProduct Cross product: this × param2 → param1
0x467750 Array_CopyDWords Copy N dwords ptr-to-ptr
0x470650 Array_FillDWords Fill N dwords with same value
0x472DA0 Transform_ctor Transform object constructor
0x4730C0 Registry_ReadFloat Read float value from Windows registry
0x473740 StdString_AppendCharN Append N copies of a char
0x4738B0 StdString_AppendN Append N chars with strncat
0x473B10 AthenaString_AssignFormatted Format and assign AthenaString

Rendering Pipeline — Extended (Session 16)

Address Function Notes
0x456150 Ray_SetDirection Set ray direction, normalize, compute length
0x456E80 Font_WordWrap Word-wrap text to fit pixel width
0x459610 Scene_RenderIfVisible Render scene object only if visible flag set
0x466AC0 Scene_UpdateChildren Traverse scene tree, update children
0x4602F0 Scene_CollectByNameFilter Collect scene objects by name string filter
0x440DD0 Graphics_DrawRectAndReset Draw rect then reset matrix to identity

Scene/Board System (NEW)

Address Function Notes
0x429520 Game_SetInProgress Set game in-progress flag (+0x200=1)
0x44BE80 ScoreObject_ctor Score display constructor (vtable 0x4D6C70, score=200000)
0x446B80 RegisterDialog_ValidateSerial Validate serial via XOR cipher key "54138"

Level Rendering — Extended (NEW)

Address Function Notes
0x40B090 Level_InitScene Init scene (projection, fog, find CAMERALOCUS)
0x40B420 Level_RenderDynamicObjects Render moving objects via Timer positions
0x40B570 Level_RenderObjects Iterate objects calling vtable+0x0C
0x40B600 Level_UpdateAndRender Full update: merge lists, pre-render, shadow, cleanup
0x40B9C0 Level_SetObjectTransform Set world transform from position
0x40ACA0 Level_SelectCameraProfile Select camera by difficulty (4-15)

App Object Structure (partial, at 0x4FD680)

+0x00:  vtable pointer
+0x04:  hInstance (Windows HINSTANCE)
+0x08:  cmdShow (nCmdShow)
+0x54:  registryKey (Config* pointer)
+0x5A:  frameTimeMs (frame delta)
+0x5B:  fpsDenominator
+0x5C:  targetFPS
+0x5D:  renderTarget (render target ptr)
+0x65:  frameCounter (per-second count)
+0x84:  profilingSection (char* current section name)
+0x15A:  someFlag
+0x15C:  width (window width)
+0x158:  windowed (windowed mode flag)
+0x159:  quitFlag (1 = game should quit)
+0x160:  height (window height)
+0x174:  Graphics* pointer (D3D device wrapper)
+0x17C:  audioSystem pointer (BASS audio)
+0x180:  Input* pointer (DirectInput8 subsystem, 0x438 bytes)
+0x1B4:  versionString
+0x1CC:  loadedCount (objects counter)
+0x200:  initialized flag
+0x208:  char[256] debug_string (current init step name)
+0x240:  HCURSOR blank_cursor
+0x278:  void* shadow_texture
+0x534:  MusicPlayer* music handle
+0x538:  int music_channel2
+0x53C:  int music_channel1
+0x550:  gameMode1 (mode value 1)
+0x554:  gameMode2 (mode value 2)
+0x558:  gameMode3 (mode value 4)
+0x55C:  gameMode4 (mode value 5)
+0x914:  int play_count (shareware trial)

App::Initialize Sequence (0x429530) — 26 Steps

Steps 1-12 are in App_Initialize (0x46BB40):

  1. vtable+0x94: Parse command line
  2. Registry/config init (0x472F50)
  3. vtable+0x0C: Window creation (AthenaWindow class)
  4. vtable+0x1C: Subsystem init
  5. vtable+0x18: Subsystem init
  6. vtable+0x30: Subsystem init
  7. vtable+0x3C: Subsystem init
  8. vtable+0x34: Subsystem init
  9. vtable+0x38: Subsystem init
  10. vtable+0x40: Subsystem init
  11. Graphics_Initialize (Direct3DCreate8 + device creation)
  12. Version string extraction

Steps 13-26 are in App_Initialize_Full (0x429530):
13. Set graphics->initialized = true
14. Load "BLANKCURSOR"
15. Set display mode 800x600
16. Configure D3D render states
17. N/A (skipped)
18. Load "shadow.png" texture
19. Load "music\music.mo3"
20. Load "jukebox.xml"
21. Set up music channel 1
22. Set up music channel 2
23. Config::Load + read "PlayCount"
24. Set initialized flag
25-28. Create 4 game mode objects (values 1,2,4,5)
29. Config save
30. vtable+0xA0: final init callback

Session Progress (From Context Compaction)

| 0x478680 | Texture_dtor | 4 | Destroy texture: free sub-textures, pixel buffers, palette, alpha, vtable release |
| 0x46DB10 | App_Shutdown | 4 | Application cleanup: destroy window, release 5 COM objects, CoUninitialize, free string buffer |
| 0x473640 | AthenaString_Find | 4 | strstr wrapper: returns offset of substring or -1 |
| 0x473600 | AthenaString_Length | 4 | Recalculate and cache string length, clear dirty flag at +0x10 |
| 0x469B20 | UIWidget_HitTest | 3 | Hit-test UI widget list against (x,y) point |
| 0x477670 | Vec3_ClosestPointOnLine | 3 | Project point onto line segment, clamp t to [0,lineLength] |
| 0x475DC0 | CRC32_Compute | 3 | CRC32 using 256-entry lookup table at DAT_004F7534 |
| 0x472990 | Gadget_LabelCtor | 8 | Gadget_Label constructor: vtable 0x4D9E68, name at +0x878, value at +0x87C |
| 0xA00000 | Key Vtable Addresses | | |
| 0x4DA65C | D3DX_RegistryGetter_vtable | | DirectX registry path struct vtable |
| 0x4E998C | AthenaList_ctor_vtable | | AthenaList constructor vtable |
| 0x4D9750 | App_Shutdown_vtable | | App shutdown vtable |

Ball Physics Constants (CONFIRMED from Ghidra decompilation)

  • Ball radius = 35.0 at offset +0x284
  • Position: +0x164/168/16C (XYZ), Velocity: +0x170/174/178 (XYZ)
  • First-frame force modifier at +0x2F0 (1.0→0.25), Ice at +0xC5C, is_shrunk at +0xC4C (odd race E:SHRINK/E:GROW only, NOT falling/dizzy)
  • Gravity plane at +0x748 (0=XY, 1=tilted, 2=XZ)
  • Ball vtable entry +0x04 is dispatcher at 0x405100 that sets defaults: +0x278=0.5, +0x27C=0.2, +0x188=6.0, +0xC6C=600.0, +0xC70=1200.0

Level::BinaryLoader (0x4629E0) — Confirmed Binary MESHWORLD Format

Opens file with _open(path, 0x8000), reads: material_count → materials (with extended/texture data) → mesh_buffers → game_objects → bounding_box → vertex_array. See SKILL.md for full format spec.

MESH Parser Status

  • v1 (Sphere, 8Ball, Hamster, FunBall): FULLY WORKING
  • v2-v5: Partial (finds texture, may miss sub-object vertices)

OpenGL Level Viewer v2

  • Renders object markers from MESHWORLD levels in 3D fly-through
  • 86 levels load OK, Level1 has 140+ objects
  • Build: gcc -o build/level_viewer_v2 src/level/mesh_parser.c src/level/level_viewer_v2.c -I include -lm (pkgconfigcflagssdl2SDL2imageglglew)(pkg-config --cflags sdl2 SDL2_image gl glew) (pkg-config --libs sdl2 SDL2_image gl glew) -lGLU

TODO Next Steps

  • Parse MESHWORLD vertex/face geometry for real mesh rendering
  • MESH v2-v5 sub-object support
  • Collision plane system RE (Ball_CheckCollisionPlanes 0x402810) — partially done: spatial tree + AABB test documented
  • Connect GhidraMCP for interactive decompilation — DONE, working
  • rendering pipeline RE — substantially advanced (ctor, dtor, DrawScreenRect, ApplyMaterialAndDraw, SetRenderMode documented)
  • Ball physics gravity + trajectory system documented (SetTiltedGravity, SetFlatGravity, SetTrajectory, ApplyTrajectory)

Scene Vtable (0x4D0260) — 36 Entries (144 bytes)

Index Address Function Description
0 0x419770 Scene_DeletingDtor Destructor (scalar deleting)
1 0x419C00 Scene_Update Main update tick
2 0x41A2E0 Scene_Render Main render tick
3 0x41C620 Scene_HandleInput Input dispatch (menu navigation, ball control)
4 0x41C7D0 Scene_ActivateCurrentItem Activate selected menu item
6 0x41CA00 Scene_SelectCurrentItem Select/highlight menu item
7 0x409D90 (3-byte nop stub) Unused vtable slot
11 0x41CB60 Scene_ClearCurrentItem[1] Clear item selection variant 1
12 0x41CBF0 Scene_ClearCurrentItem[2] Clear item selection variant 2
14 0x419900 Scene_DestroyScene Destroy scene + cleanup
15 0x419A40 Scene_NotifyObjects Notify all scene objects of event
16 0x419970 Scene_SetDestroyed Set destroyed flag (+0x2C)
17 0x4198E0 Scene_SaveAndCleanup Save state + cleanup
19 0x419E20 Scene_HandleRaceEnd Handle race completion
20 0x41A180 Scene_UpdateBallsAndState Update ball physics + race state
22 0x41A050 Scene_ProcessRaceEnd 3-2-1-GO countdown logic
23 0x419E80 Scene_HandleBallFinish Ball finish state machine (5 states)
27 0x41A560 Scene_RenderScoreHUD Render score HUD overlay
28 0x41A680 Scene_RenderTimerHUD Render timer HUD overlay
32 0x41C5B0 Scene_SpawnBallsAndObjects Create game objects (balls, traps, secrets, flags)
35 0x41A9A0 Scene_ComputeInputForceDirection Computes 3D force vector from strongest player input across balls in slot (Ghidra label "Scene_ComputeLighting" is a MISNOMER)

Scene_Update (0x419C00) — Main Game Tick (10 phases)

Phase Description Key Offsets
1 Frame counter++ this+0xD88 (= 0x3620)
2 Demo timer countdown this+0x10D6 (active), +0x10D7 (frames left), +0x10D8 (elapsed), +0x10D9 (counter)
3 ESC check → Scene_CreateGameOverMenu this+0x10DA (ignore ESC), App+0x5FC (input mode)
4 Ball position propagation this+0xA6C (flag), iterate +0xA75 ball list
5 Gear path follow (single player) this+0xFC7 (gear enabled), +0xFC8 (path data)
6 ArenaBoard timer ticks this+0x221, this+0x226 (two ArenaBoard timers)
7 Camera shake decay this+0xE93 (shake active), +0xA6E (magnitude, ±800, decay -10/frame)
8 scene object update+render iterate this+0x22E, vtable[4]=Update, vtable[0]=Render
9 Per-frame update pipeline vtable[0x4C]=Scene_HandleRaceEnd, [0x50]=Scene_UpdateBallsAndState (ball update+respawn), [0x54]=NoOp, [0x58]=Scene_ProcessRaceEnd + physics objects at this+0xD8B
10 Post-physics callback this+0xEBF+0x04()

Scene_HandleBallFinish State Machine (5 states)

state 0: start → set counter=150.0f, advance to state 1
state 1: countdown → decrement counter by dt, when ≤0 advance to state 2
state 2: finish → save race time, play "Goal!" music, advance to state 3
state 3: popup → show finish popup, advance to state 4
state 4: done → wait for popup dismissal

Scene Key Offsets

  • +0x2C: destroyed flag (set by Scene_SetDestroyed)
  • +0x864: current item pointer (menu selection)
  • App+0x10EC: race active flag (1=active, set by Scene_SetRaceActive at 0x4366E0, 62 xrefs)

SceneObject Vtable (0x4D934C) — 10 Entries

Index Address Function Description
1 0x46B560 SceneObject_SetPosition Set world position (XYZ)
2 0x46B5A0 SceneObject_SetScale Set scale vector
3 0x46B4B0 SceneObject_Render Render scene object
4 0x46B4D0 SceneObject_SetVisible Toggle visibility flag
7 0x46B860 SceneObject_BaseDtor Base dtor: iterates child list, calls each child's dtor(1), clears list
8 0x46B650 SceneObject_DeletingDtor Scalar deleting destructor

Tournament & Level System (NEW — Session 2026-04-13)

Tournament_AdvanceRace (0x427080) — CORRECTED from decompilation

15-case switch creates specific Board constructors. Key discovery: CreateExpertLevelObjects (0x40E250) is the MASTER ARENA OBJECT FACTORY — creates ALL arena hazard types (Sawblade, TowerLevel, Spinner, Gear/Judge, Tipper/Bell, Bonk/Hammer) by name prefix matching.

Case Level Board Constructor Size
1 Warm-Up LevelBoard_WarmUp_ctor 0x436C
2 Beginner LevelBoard_Beginner_ctor 0x644C
3 Intermediate LevelBoard_Intermediate_ctor 0x438C
4 Dizzy LevelBoard_Dizzy_ctor 0x4BE0
5 Tower LevelBoard_Tower_ctor 0x5418
6 Up LevelBoard_Up_ctor 0x4790
7 Neon Race Board_NeonRace_ctor 0x4394
8 Expert LevelBoard_Expert_ctor 0x4FD8
9 Odd LevelBoard_Odd_ctor 0x43B0
10 Toob Race LevelBoard_Toob_ctor 0x646C
11 Wobbly LevelBoard_Wobbly_ctor 0x4388
12 Glass Board_Glass_ctor 0x4390
13 Sky LevelBoard_Sky_ctor 0x47F8
14 Master BoardLevel_Master_Ctor 0x6498
15 Impossible Board_Impossible_ctor 0x4380

Also saves score, computes difficulty time bonus (+1000ms normal, +500ms frenzied), stores race timestamps. When param_1=true, creates TourneyMenu instead (retry current race).

PracticeMenu_ctor (0x42EA30)

"Practice Menu" scene with 14 race items and 14 thumbnail textures (practice-level1..practice-impossible.png). Lock check via App+0x851-0x865 boolean flags.

Level Unlock Flags (App offsets)

Offset Level
+0x851 Level 1 (Warm-Up)
+0x852 Level 2 (Beginner)
+0x853 Level 3 (Intermediate)
+0x854 Level 4 (Dizzy)
+0x855 Level 5 (Tower)
+0x856 Level 6 (King of the Hill)
+0x857 Level 7 (Up)
+0x858 Level 8 (Expert)
+0x859 Level 9 (Odd)
+0x85A Level 10 (Toob Race)
+0x85B Level 11 (Sky)
+0x85C Level 12 (Wobbly)
+0x85D Level 13 (Master)
+0x85E Level 14 (Race of Ages)
+0x85F Level 15 (Impossible)
+0x860-0x865 Arena unlock flags

Scene_SpawnBallsAndObjects (vtable[32], 0x41C5B0)

Creates GameObject instances (0xC60 bytes) for each ball, sets start positions via START%d-%d naming, scans SAFESPOT/SAFEPOS objects, creates BadBall/MouseTrap/SecretObjects/Flags/Signs/DynamicObjects.

App_ShowResults (0x428060)

Creates results screen scene (0x87C bytes). Called after tournament race completion.

Difficulty_GetTimeModifier (0x428ED0)

Returns time bonus multiplier based on difficulty level.

ArenaBoard Arena Init Functions (14 total — CORRECTED)

Arena Init Function Level Path Special Logic
WarmUp ArenaLevel_WarmUp_Init (0x413C20) levels\arena-WarmUp
Beginner ArenaBoard_Beginner_Init (0x414180) levels\arena-intermediate
Intermediate ArenaLevel_Intermediate_Init (0x414180) levels\arena-Intermediate
Dizzy ArenaLevel_Dizzy_Init (0x414240) levels\arena-dizzy Loads bonus level Levels\Level3-Swirl
Tower ArenaLevel_Tower_Init levels\arena-Tower
Up ArenaLevel_Up_Init levels\arena-Up
Expert ArenaLevel_Expert_Init (0x414B10) levels\arena-expert
Odd ArenaLevel_Odd_Init (0x414CE0) levels\arena-Odd
Sky ArenaLevel_Sky_Init (0x4158C0) levels\arena-Sky "PILLAR" object collection
Neon ArenaLevel_Neon_Init (0x416F40) levels\arena-neon 2x AthenaList transforms + boundary sphere
Glass ArenaLevel_Glass_Init (0x417DF0) levels\arena-Glass
Master ArenaLevel_Master_Init (0x416080) levels\arena-Master
Race of Ages (Cascade) levels\arena-Cascade
Impossible ArenaLevel_Impossible_Init (0x418540) levels\arena-impossible

Scene Vtable Discovery Technique

Scene vtable at 0x4D0260 has 36 entries (144 bytes). Read memory at the vtable address, decode as 36 little-endian 32-bit pointers. Some entries may be 3-byte nop stubs (0x409D90) — these are unused slots. Plate comments can ONLY be set at function addresses, not data addresses (failed at 0x4D0260 and 0x4D934C).

Workflow: High-Xref FUN_* Function Identification

  1. Use search_functions_enhanced with has_custom_name=false, min_xrefs=15, sort_by=xrefs_desc to find undocumented functions with many callers
  2. Batch decompile the top candidates (4-6 at a time via individual decompile_function calls — batch_decompile often fails with "Function not found")
  3. Identify patterns: AthenaString_, Scene_, Graphics_, MWParser_, Vec3*, etc.
  4. Cross-reference callers via get_function_callers or get_xrefs_to on data items to understand usage context
  5. Rename with descriptive Module_Action format, set plate comments
  6. Key data items can also be labeled: list_data_items_by_xrefs reveals high-value globals like g_renderIndex, string constants like s_BACK

New Functions (Session 2026-04-13 Batch 4 — Menu System, Scene/Texture, Game Flow)

Address Name Xrefs Description
0x42f810 TimeTrialMenu_ctor 3 "Time Trial Menu" — extends PracticeMenu with race items + lock checks
0x42fc10 PartyMenu_ctor 3 "CHOOSE A PARTY RACE!" — extends PracticeMenu, vtable 0x4D4738
0x42fc40 ArenaMenu_ctor 3 Arena menu with 14 arena items (Warm-Up to Impossible), lock icons, vtable 0x4D47B8
0x4326d0 MPMenu_ctor 3 Multiplayer menu: Party Race, Rodent Rumble, controller config (1-4P)
0x44aa40 Scene_FindTextureByName 4 Find texture by case-insensitive name, return ptr+dimensions+width
0x44ab00 Scene_FindTextureDimensions 4 Find texture and measure text width via Font_MeasureText
0x44abf0 Scene_AddTextureToList 4 Add texture reference to scene list by name, mark dirty at +0xCBC
0x443ac0 SceneObject_RenderScaled 3 Render object scaled (ScaleX, SetPosition, vtable callbacks + Timer)
0x4410c0 SceneObject_FreeStrings 3 Free 2 string ptrs, re-init BaseObject, call SceneObject_dtor
0x453c50 Texture_RemoveRef 3 Decrement texture refcount, remove from cache and free when 0
0x457a50 Graphics_DisableRenderState 3 Thunk → Graphics_SetRenderState (disable mode)
0x428c50 App_StartPracticeRace 3 Start practice/tournament race: calls App_StartRace, PlayerProfile, Tournament_AdvanceRace
0x434580 Sound_InitChannels 3 Allocate sound channels, get next sample, play 3D positioned, set timer 0x140
0x43b6f0 Rotator_AddBall 3 Find score by ID and set to 10, or create new entry with value 10
0x44bef0 Timer_Decrement 4 Timer tick: value = end - 100, set flag at +0x2A
0x448620 ScoreDisplay_DeletingDtor 3 ScoreDisplay scalar deleting destructor
0x4470d0 ScoreDisplay_dtor 3 Clean up: free strings, timers, 5 BaseObjects, SceneObject_dtor
0x44acb0 UIList_Clear 4 Empty stub (returns 0)

Menu System — Arena Unlocks (from ArenaMenu_ctor at 0x42fc40)

App offsets for arena unlock flags (0-based, boolean):
+0x852=Level 2 (Beginner), +0x853=Intermediate, +0x85A=Dizzy, +0x85B=Tower, +0x85C=Up, +0x85D=Expert, +0x85E=Odd, +0x85F=Toob, +0x860=Wobbly, +0x867=Glass, +0x861=Sky, +0x862=Master, +0x868=Impossible

App_StartPracticeRace (0x428c50) — Key Findings

  • Calls App_StartRace first, then resolves scene objects by checking priority values (field+0x525 offsets)
  • Sets mirror mode culling: App+0x7D2 (0=normal, 1=mirror), calls Gfx_SetCullMode
  • Adjusts viewport dimensions from App+0x27C/280/284 and +0x288/28C/0x290
  • Creates 0x528-byte scene object at App+0x90C (Tournament scene manager?)
  • Creates PlayerProfile (0x98 bytes) at App+0x220, copies level name from App+0x29B4
  • Flags set: App+0x717=1, App+0x7B7=1, App+0x5D7=0, App+0x677 (0 unless mirror)
  • When mirror: App+0x677=0, +0x23C=1
  • Calls Tournament_AdvanceRace(param_1, 0)

Batch Workflow Efficiency Notes

  • The batch_decompile tool often fails with "Function not found" — prefer individual decompile_function calls
  • For menu constructors: string references ("Time Trial Menu", "CHOOSE A PARTY RACE!", etc.) are the most reliable identifiers
  • Arena lock flags at App+0x851-0x868 were confirmed from ArenaMenu_ctor decompilation
  • Scene texture functions (FindTextureByName, FindTextureDimensions, AddTextureToList) share a common pattern: iterate AthenaList at +0x890 with index tracking at +0x894
Address Name Xrefs Description
0x466C70 AthenaString_Format 98 sprintf wrapper returning internal buffer
0x4BBDFD AthenaString_Sprintf - Internal sprintf using FILE struct trick
0x469990 Scene_AddObject 77 Add SceneObject to scene (uniqueness check, vtable notify)
0x46F390 Scene_BeginFrame 39 Begin rendering frame (Graphics_BeginFrame + vtable callback)
0x460DA0 Scene_RenderFrame 38 Full scene render pipeline with z-buffer interleaving
0x461370 Scene_RenderOpaque 38 Render opaque pass (object vtable[0x28] + mesh buffers)
0x461890 Scene_LoadMeshWorld 38 Load .meshworld from stream (materials, textures, objects)
0x461F00 Scene_Subdivide 38 Subdivide scene space into regular 3D grid
0x462100 Scene_SubdivideRandom 38 Subdivide scene with random grid positions
0x4629E0 Scene_LoadCached 37 Load .cached scene file (binary format)
0x46A750 Window_Notify 37 Send WM_COPYDATA message to window
0x498200 BitStream_ReadBits 95 Read N bits from byte-aligned stream
0x4792FB D3DX_DetectShaderProfile 48 Detect pixel shader version (1.x/2.x/3.x)
0x492BDA Vertex_Transform 32 Transform vertices between coordinate spaces
0x4737F0 AthenaString_Assign 52 String copy/assign operator
0x4605E0 AthenaHashTable_Lookup 36 Case-insensitive hash table lookup
0x4691C0 SceneObject_dtor 31 SceneObject destructor
0x4693C0 Scene_AddAllObjects 25 Batch add all SceneObjects
0x469600 MWParser_ReadTag 24 XML/SGML tag parser for MW files
0x4695D0 StreamReader_dtor 24 Close file handle, free buffer
0x45D450 Sprite_DrawColoredRect 23 Draw colored quad with random vertex colors
0x472AF0 AthenaString_Init 18 Default string constructor
0x472F30 RegKey_Close 18 RegCloseKey wrapper
0x473670 AthenaString_CopyCtor 16 String copy constructor
0x4740D0 AthenaString_WriteTag 16 Build XML tag content
0x489610 Pool_FreeList 16 Walk free list, decrement refcounts
0x459B24 Graphics_InitShaderDispatch 19 D3DX shader init dispatch thunk
0x45A439 Graphics_SetRenderState 29 Render state dispatch thunk
0x4AB9B8 DivCeil 15 Ceiling division (a-1+b)/b
0x4D2334 s_BACK 15 "BACK" string constant
0x5341CC g_renderIndex - Global render index counter

Documentation Status (2026-04-13 Session 20)

  • Total functions: 3,811
  • Documented: 2,291 (60.1%)
  • User-labeled: 400+ functions
  • Progress from baseline: +121 functions documented (was 42.8% at start)
  • Session 20 batch: BitStream_ReadBitsSSE2, Matrix_Inverse4x4_SSE2, SSE2_SetFPControlWord, Mem_Zero, Malloc_OrLongjmp, StrCat_Fast, BitStream_CopyToOutput, Noop2, ReturnZero, D3DX_ShaderDispatch0/1/2, AthenaList_Ctor, Audio_StopChannel, App_Shutdown, NetworkConnection_Ctor, Font_RenderToTextureComplex, Gadget_LabelCtor, UIWidget_HitTest, SceneObject_EmptyListCtor, AthenaString_Reserve, AthenaString_Length, AthenaString_Find, D3DX_RegistryGetter, CRC32_Compute, Vec3_ClosestPointOnLine, Texture_StreamRead, Texture_BinarySearch, Texture_LoadFromStream, Texture_dtor, Texture_SetPool, Pool_dtor, Texture_ValidateOrBuild, Mesh_dtor, Pool_Free, Texture_ComputeChecksum, Texture_DestroyBuffers, Mesh_Init, Pool_Alloc, VertexDeclaration_dtor, VertexShader_dtor, Gfx_ResizeBuffers
  • Batch workflow: see references/batch-doc-workflow.md for efficient decompile→identify→rename→comment→commit cycle

Session 2026-04-13 Key Findings

  • Ball_ApplyForceV2 (0x4016f0): Second force application variant with gravity plane awareness
  • Ball_RenderWithCollision (0x402c10): Full collision check + shadow + render pipeline
  • Ball_SetupCollisionRender (0x4015b0): Initialize collision mesh render objects from level data
  • App_ShowMainMenu (0x4280e0): Creates MainMenu (0xCDC bytes), stores at App+0x224
  • App_StartTournamentRace (0x4288b0): Configures mirror mode, creates tournament scene
  • RegisterDialog: Complete registration system with serial validation, XOR cipher (key "54138"), writes DXCaps to registry
  • Sound_LoadOggOrWav (0x459660): Sound loader with .ogg/.wav fallback pattern
  • Sound_GetNextChannel (0x459810): Circular buffer channel allocator
  • Graphics_Initialize: Full 27-step D3D8 init sequence documented (adapter enumeration, mode selection, dual device creation, VB setup)
  • Graphics object offsets confirmed: +0x7c=D3D8, +0x154=VertexBuffer, +0x164=PrimaryPP, +0x174=SelectedMode, +0x7d3-7d6=ResolutionAvailable flags
  • App_StartTournamentRace reveals App offsets: +0x237=flag, +0x717=flag, +0x7b7=flag, +0x5d7=flag, +0x677=flag, +0x236=mirror_mode, +0x27c/280/284=viewport, +0x7d2=mirror_cull_flag

Session 20 — SSE2/Math/CRT/Texture Functions (NEW)

Address Name Xrefs Description
0x4B22AB BitStream_ReadBitsSSE2 19 SSE2 bitstream reader: reads N bits in 8-bit chunks, 0xFF marker handling, maintains stream state
0x4A0F3A Matrix_Inverse4x4_SSE2 9 SSE2 4x4 matrix inverse via cofactor expansion + Newton-Raphson reciprocal. Returns NULL if singular
0x4A6B80 SSE2_SetFPControlWord 9 Store FPU control word to global DAT_00535280
0x4ABA49 Mem_Zero 8 Optimized memset-to-zero: dwords first (count>>2), then bytes (count&3)
0x4AD576 Malloc_OrLongjmp 9 Safe malloc: calls malloc, longjmps with "Out of Memory" on failure. Returns NULL if ptr/size==0
0x4BC360 StrCat_Fast 9 Optimized strcat: dword-aligned null detection (0x7efefeff trick), fast copy
0x4B8564 BitStream_CopyToOutput 8 Copy data from bitstream to output buffer with checksum callback. Double-buffered streaming
0x4C183E Noop2 8 Empty no-op function
0x4C7152 ReturnZero 8 Returns 0 (stub/trap)
0x459D96 D3DX_ShaderDispatch0 3 D3DX shader dispatch via PTR_FUN_004F7194 vtable
0x459E34 D3DX_ShaderDispatch1 4 D3DX shader dispatch via PTR_FUN_004F71B8 vtable
0x459ED1 D3DX_ShaderDispatch2 4 D3DX shader dispatch via PTR_FUN_004F71CC vtable
0x467E40 AthenaList_Ctor 3 Init AthenaList with vtable 0x4E998C, StdString_Substr copy
0x46A0D0 Audio_StopChannel 9 Stop BASS audio channel (BASS_ChannelStop on handle at +0x08)
0x46DB10 App_Shutdown 4 Application cleanup: destroy window, release 5 COM objects, CoUninitialize, free string buffer
0x46DFA0 NetworkConnection_Ctor 4 Init network connection: "Not Connected", id at +0x04, +0x0C=1.0f
0x472340 Font_RenderToTextureComplex 4 Complex text→texture rendering using D3D vertex buffers and shaders
0x472990 Gadget_LabelCtor 8 Gadget_Label constructor: vtable 0x4D9E68, name string at +0x878, value at +0x87C
0x469B20 UIWidget_HitTest 3 Hit-test UI widget list against (x,y) point. Checks +0x420 rect first, else iterates children by bounds
0x46B840 SceneObject_EmptyListCtor 3 Init SceneObject with DeletingDtor vtable 0x4D9368 + empty AthenaList
0x473480 AthenaString_Reserve 4 Reserve string capacity: alloc new buffer, copy old, free old
0x473600 AthenaString_Length 4 Recalculate and cache string length, clear dirty flag at +0x10
0x473640 AthenaString_Find 4 strstr wrapper: returns offset of substring or -1
0x477010 D3DX_RegistryGetter 4 Set vtable 0x4DA65C (DirectX registry path struct)
0x475DC0 CRC32_Compute 3 CRC32 using 256-entry lookup table at DAT_004F7534
0x477670 Vec3_ClosestPointOnLine 3 Project point onto line segment. Clamp t to [0,lineLength]
0x477970 Texture_StreamRead 3 Stream read from bitstream with 0x400-byte blocks and progress callback
0x477AC0 Texture_BinarySearch 4 Binary search in texture archive with checksum and callback
0x477D60 Texture_LoadFromStream 4 Texture loading from stream: init pool, loop with D3DX_Uninit/retry, up to 3 attempts
0x478680 Texture_dtor 4 Destroy texture: free sub-textures, pixel buffers, palette, alpha, vtable release
0x48A160 Texture_SetPool 6 Set texture pool reference (param_2), clear struct
0x48A180 Pool_dtor 6 Free pool list, zero 8 dwords
0x48A560 Texture_ValidateOrBuild 5 Validate/assemble texture from stream chunks, checksum mismatch → -0xF3
0x48A900 Mesh_dtor 6 Destroy mesh: free D3D vertex buffers, release texture refs, free allocations
0x489DF0 Pool_Free 4 Free pool entries, zero 0x14 dwords
0x489E20 Texture_ComputeChecksum 4 Compute texture checksum from stream bytes (0xFF skip marker, accumulate sum)
0x48A860 Texture_DestroyBuffers 5 Free D3D index/vertex buffers, free allocation ptrs
0x48A8D0 Mesh_Init 6 Zero mesh struct, calloc 0xCA8 bytes for mesh data
0x48B1C0 Pool_Alloc 6 Pool allocator: allocate aligned block, chain old block, return offset
0x48B2A0 VertexDeclaration_dtor 3 Free vertex declaration, release D3D decl, zero struct
0x48B530 VertexShader_dtor 3 Free vertex shader: release D3D shader, free texture/palette/alpha buffers
0x480C4D Gfx_ResizeBuffers 5 D3D texture/surface buffer resize with format detection (D3DFMT checking)

High-Xref Utility Functions

Address Name Xrefs Description
0x4BA754 __ftol2 358 CRT float-to-int64 conversion (compiler intrinsic) — MOST CALLED FUNCTION
0x45DD60 RNG_Rand 193 PRNG with 55-entry circular buf (Mitchell & Moore). Returns (buf[read]+buf[write])>>6 % range. Signed mode: param_2=1 negates 50% of results
0x453250 Vec3List_Free 283 Free Vec3List + member data at +0x103
0x44B840 Noop 280 Empty stub (vtable placeholder)
0x4736B0 AthenaString_dtor 85 AthenaString destructor — frees buffer, sets vtable to base dtor (0x4D290C)
0x473500 AthenaString_AssignCStr 75 AthenaString assign from C string
0x536A0 AthenaList_GetSize 60 Return count at +4
0x453640 AthenaList_FindByValue - Linear search, returns index or -1
0x443990 AthenaString_Clear 14 Free buf, reset to 15-char inline capacity
0x4531E0 Vec3_Init 13 Set Vec3 vtable + zero + w=255.0
0x4BC0D1 strtok 50 CRT strtok — thread-safe string tokenizer
0x4BAC20 strstr 22 CRT strstr — string search with SIMD optimization
0x4BAE43 AthenaString_SprintfToBuffer 71 sprintf into char buffer via fake FILE struct

UI List System (vtable 0x4D6A70, base for ALL menus)

Address Name Xrefs Description
0x448F20 SimpleMenu_ctor 15 "Simple Menu" base with item list + scrollers
0x4490A0 UIListItem_ctor - Init 0x444-byte item (Vec3 + AthenaList)
0x4497F0 UIList_AddItem 86 Add named item (text, subtext, colors, icon, height)
0x449430 UIList_AddSpacer 29 Add empty row with height
0x4494D0 UIList_ScrollUpdate 17 Scroll, mouse wheel, vtable dispatch
0x449B00 UIList_Cleanup 27 Free all items (strings, SceneObjects, Vec3Lists)
0x449C20 UIList_HandleKeyNav 18 Up/down/pgup/pgdn key navigation
0x449D40 UIList_Render 18 Draw items: gradient bars, text, icons, scroll arrows
0x44A570 UIList_Layout 18 Compute widths, position SceneObjects, set scrollers
0x44A8B0 UIList_SetTextByName 27 Find item by subtext, replace display text
0x449750 UIList_ActivateCurrentItem 18 Activate: Back→sound 650, Continue→50, else callback

UIListItem (0x444 bytes): +0x00=display_text, +0x04=subtext, +0x0C-0x0F=RGBA, +0x1C4=icon, +0x244=height, +0x110=is_icon_row flag, +0x441=highlighted flag

ArenaBoard System (vtable PTR 0x4D1358, extends Board extends Scene)

Address Name Xrefs Description
0x4217B0 ArenaBoard_ctor 15 Init with "ArenaBoard", timer, base score=6000
0x421880 ArenaBoard_dtor 24 Cleanup timer, release SceneObjects, Scene_dtor
0x421910 ArenaBoard_Render 16 Timer bar, round ".%d", "TIE BREAKER!"
0x421FE0 ArenaBoard_Update 16 Check round end, resolve ties, "Game Over" music
0x458E60 ToggleTimer_Init 12 Initialize round timer
0x458E80 ToggleTimer_Cleanup 32 Cleanup round timer
0x458E90 ToggleTimer_Tick 12 Tick countdown
0x44AD50 ArenaScoreParticle_ctor 12 Init vtable + difficulty scale [0.02, 0.03, 0.04]

Key ArenaBoard offsets: +0x47AC=base_score(6000), +0x47C5=is_tie_breaker, +0x47CC=tie_active, +0x47D0=max_rounds(25), +0x11EB=round_end_timer, +0x11F1=game_over_flag, +0x11ED-0x11F0=4 player scores

Graphics Transform Pipeline (Session 2026-04-13)

Address Name Xrefs Description
0x457B10 Matrix44_Zero 12 Clear 4x4, set diagonals to 1.0
0x457B50 Gfx_SetPosition 69 D3D SetTransform world translate
0x457BB0 Gfx_RotateY 15 Rotation around Y axis
0x457C60 Gfx_ScaleX 40 Scale X axis
0x457C90 Gfx_ScaleY 35 Scale Y axis
0x457CC0 Gfx_ScaleZ 26 Scale Z axis
0x457FD0 Matrix4_Identity 40 Set identity vtable
0x425FE0 Gfx_SetAlphaBlendState 9 D3DRS_SRCBLEND/DESTBLEND mode 3
0x427940 Gfx_SetCullMode 13 D3DRS_CULLMODE (none/CW/CCW)

Wave Math System

Address Name Xrefs Description
0x457DA0 Wave_Sin 38 sin(time * freq * 2π/360)
0x457DC0 Wave_Cos 23 cos(time * freq * 2π/360)

Sprite System (vtable 0x4D8F84)

Address Name Xrefs Description
0x45D0C0 Sprite_ctor 30 Init with texture, RenderContext, material defaults
0x45D660 Sprite_RenderQuad 15 Render textured quad via material + DrawPrimitive

Sprite (0xD4 bytes): +0x00=vtable, +0x04=gfx, +0x08-0x60=RenderContext, +0x50=texture, +0xC8=width, +0xCC=height, +0xD0=visible(1), +0xD1=flag

Scene Rendering Extensions (Session 2026-04-13)

Address Name Xrefs Description
0x45E0E0 Scene_RenderAllObjects 33 Main 3D render: BeginFrame → sort (opaque/alpha/shadow) → draw
0x460450 Scene_RenderBallShadow 38 Ball shadow with depth bias pass
0x45DF80 SceneObject_CallUpdate 47 Dispatch +0x434 vtable[1] (Update)
0x45DF90 SceneObject_CallRender 47 Dispatch +0x434 vtable[2] (Render)
0x437130 Scene_StartCountdown 11 Start countdown (3..2..1, 400 or 50 frames)

Object flags in Scene_RenderAllObjects: +0x85F=shadow, +0x860=alpha, +0x862=deferred, +0x863=skip

PRNG (RNGState struct)

struct RNGState {
    VTable* vtable;         // +0x00
    int read_ptr;           // +0x04 (wraps at 55)
    int write_ptr;          // +0x08 (wraps at 55)
    uint32_t buffer[55];    // +0x0C..0xE4 (circular buf)
};
// Algorithm: buf[read] = (buf[read] + buf[write]) & 0x3FFFFFFF
// Return: (result >> 6) % range
// Signed: if param_2==1 && Rand(2)==0, negate

Dialog System

Address Name Xrefs Description
0x440E70 OkayDialog_ctor 12 "Okay Dialog" with caption + "OKAY!" button
0x401480 GameObject_dtor 9 Release timers, free Vec3Lists, cleanup matrices

Phase 3: System Documentation

After achieving 100% function naming (3781/3781), the project moved to comprehensive
system documentation. Key docs created:

Doc Content
docs/RUMBLEBOARD_SYSTEM.md 15-race tournament order, all 9 arena asset paths, timer system, HUD layout, menu commands, mirror unlock, time bonus
docs/UI_MENU_SYSTEM.md Menu hierarchy (SimpleMenu/UIList), all dispatch tables (Main/Pause/Difficulty), color system, dialog classes
docs/ARENA_HAZARD_SYSTEM.md 6 hazard types (Sawblade/Tower/Spinner/Gear/Bell/Bonk), CreateExpertLevelObjects factory, name suffix modifiers, difficulty values
docs/LEVEL_OBJECTS.md Catapult/Trapdoor/ScoreDisplay/HighScore/Damage/JumpPad, RNG (55-element LFSR), Ball trail particles
docs/COLLISION_SYSTEM.md CollisionFace/MeshBuffer structs, .COL binary format, Arena/Level/GameObject event dispatch

Quick Analysis Commands

Using GhidraMCP Headless Server — PRIMARY analysis tool

# Start the headless server (run once per session):
export GHIDRA_HOME=/opt/ghidra_12.0.4_PUBLIC
MCP_JAR=/home/evan/.config/ghidra/ghidra_12.0.4_PUBLIC/Extensions/GhidraMCP/lib/GhidraMCP-5.12.0.jar
CLASSPATH="$MCP_JAR"
for jar in $GHIDRA_HOME/Ghidra/Framework/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done
for jar in $GHIDRA_HOME/Ghidra/Features/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done
for jar in $GHIDRA_HOME/Ghidra/Processors/*/lib/*.jar; do CLASSPATH="${CLASSPATH}:${jar}"; done

java -Xmx4g -XX:+UseG1GC \
    -Dghidra.home=$GHIDRA_HOME -Dapplication.name=GhidraMCP \
    -classpath "$CLASSPATH" \
    com.xebyte.headless.GhidraMCPHeadlessServer \
    --port 8089 --bind 127.0.0.1 &

# Load binary and run analysis:
curl -s -X POST -d "file=/home/evan/hamsterball-re/originals/installed/extracted/Hamsterball.exe" http://127.0.0.1:8089/load_program
curl -s -X POST http://127.0.0.1:8089/run_analysis

# Read operations (GET):
curl -s "http://127.0.0.1:8089/decompile_function?address=0x004278E0"
curl -s "http://127.0.0.1:8089/list_functions?limit=50"
curl -s "http://127.0.0.1:8089/search_strings?search_term=App&limit=10"
curl -s "http://127.0.0.1:8089/get_xrefs_to?address=0x004D9384"
curl -s "http://127.0.0.1:8089/list_imports?limit=200"

# Write operations (POST with JSON body — form-encoded does NOT work):
curl -s -X POST -H "Content-Type: application/json" \
    -d '{"function_address":"0x00455380","new_name":"Graphics_Initialize"}' \
    http://127.0.0.1:8089/rename_function_by_address

# IMPORTANT: API param naming uses underscores (function_address, new_name, search_term)
# IMPORTANT: POST writes require JSON body, form-encoded params silently fail

Using r2 (supplementary — good for quick checks)

cd ~/hamsterball-re/originals/installed/extracted
r2 -q -c "aaa; afl" Hamsterball.exe | wc -l  # 1869 functions
r2 -q -c "aaa; s 0x429530; pdf" Hamsterball.exe  # App::Initialize
r2 -q -c "aaa; axt @ sym.imp.d3d8.dll_Direct3DCreate8" Hamsterball.exe

MESHWORLD File Format (CONFIRMED)

Binary Structure

[Geometry Section]  — vertex/face data, variable length
[Object Section]    — starts with uint32 count, then objects
[Trailer]           — closing data

Object Format

Each object: [uint32 str_len][type_string][data...][optional_texture_string]

Simple objects (START, SAFESPOT): 28 bytes after type string

  • 12 bytes: position (3 floats: x, y, z)
  • 16 bytes: rotation/flags (4 values)

Complex objects (FLAG, PLATFORM, BUMPER): variable size

  • 12 bytes: position
  • 16 bytes: rotation/flags
  • 32 bytes: transform matrix (8 floats, identity=1.0)
  • 16 bytes: diffuse color (4 floats: RGB + alpha)
  • 16 bytes: ambient color (4 floats)
  • 4 bytes: size parameter (e.g., 35.0 for platforms)
  • 8 bytes: flags (2 uint32)
  • [4+N bytes: texture string (length-prefixed)]
  • [face index data: pairs of uint32]

Object Types

START1-1, START2-1, START2-2, START2-3, START2-4 — Player start positions
FLAG02, FLAG04, FLAG06, FLAG07 — Checkpoint flags
SAFESPOT — Safe landing zones
CAMERALOOKAT (CameraLocus) — Camera targets
PLATFORM, N:SINKPLATFORM — Level geometry
N:BUMPER1/2/3/4 — Bumpers
E:NODIZZY — Anti-dizzy zones
E:LIMIT — Arena limits
E:GROWSOUND — Sound triggers

Parsing Approach (IMPORTANT)

The format is NOT straightforward sequential parsing — object data sizes vary per type and can't be determined without knowing the type. The correct approach is:

  1. Scan entire file for all length-prefixed strings
  2. Classify each as "primary type" or "texture"
  3. Primary strings define new objects, texture strings attach to the preceding object
  4. Read position data from the bytes following each primary string
  5. Transform/material data lives between the main type string and the texture string

Parser Test

cd ~/hamsterball-re/reimpl
# Build and test (86 levels, 1533 objects confirmed)
./build/level_viewer ~/hamsterball-re/originals/installed/extracted/Levels/Level1.MESHWORLD
# Headless test with Xvfb:
DISPLAY=:99 timeout 3 ./build/level_viewer <level_file>

Running the Game

cd ~/hamsterball-re/originals/installed/extracted
wine Hamsterball.exe  # Needs X display for rendering

App::Run Game Loop (0x46BD80) — CONFIRMED

1. while (!quitFlag):
2.   if PeekMessage(&msg, NULL, 0, 0, PM_REMOVE):
3.     if msg == WM_QUIT: quitFlag = 1; break
4.     TranslateMessage(&msg); DispatchMessage(&msg)
5.   else:
6.     frameStart = GetTickCount()
7.     this->Update()   // vtable call (0x469CF0 = GameUpdate)
8.     this->Render()   // vtable call
9.     this->Present()  // vtable call (0x455A90 = Graphics_PresentOrEnd)
10.    frameEnd = GetTickCount()
11.    elapsed = frameEnd - frameStart
12.    if elapsed < targetFrameTime: Sleep(targetFrameTime - elapsed)
13.    if elapsed > 0: this->fpsDenominator = 33 / elapsed  // frame scaling

Pitfalls / Lessons Learned

CRITICAL: Always Backup Renames to Git

  • The Ghidra project DB is NOT version controlled
  • If the project is re-imported or gets corrupted, ALL renames are LOST
  • This happened April 2026: dropped from 75.5% to 40% documented
  • After every RE session: run ghidra-rename-export skill to backup to analysis/ghidra/renames_backup.json
  • If DB lost: run ghidra-restore-renames skill to restore from docs/FUNCTION_MAP.md
  • Server restart: use ghidra-mcp-headless skill with --program /Hamsterball.exe (leading slash required)
  • Related skills: ghidra-rename-backup, ghidra-restore-renames, ghidra-mcp-headless, ghidra-rename-export

Binary Format RE — String Scanning Works Best

  • Sequential parsing of MESHWORLD failed because object sizes are variable per type
  • Scanning for all length-prefixed strings first, then building objects from the string list, is the only reliable approach
  • The string [uint32 length][ASCII data] pattern is very robust for finding object boundaries

GhidraMCP Headless Server — Key Gotchas

  • POST writes require JSON body: Form-encoded params (-d "key=value") silently fail with "parameter required" errors. Must use -H "Content-Type: application/json" -d '{"key":"value"}'
  • Param names use underscores: function_address not functionAddress, new_name not newName, search_term not searchTerm
  • Import addresses are EXTERNAL:ordinal: Not regular program addresses, so get_xrefs_to won't work on them. Use the string name (e.g., "BASS_Init") to find the IAT thunk, then find xrefs to the thunk.
  • Ghidra GUI + MCP plugin is fragile: The plugin requires the GUI CodeBrowser to be open with the project loaded. The headless server (GhidraMCPHeadlessServer) is much more reliable for automation.
  • MCP bridge (bridge_mcp_ghidra.py) discovers instances via Unix domain sockets in /tmp/ghidra-mcp-{user}/*.sock. The headless server uses TCP on port 8089 instead. Set GHIDRA_SERVER_URL env var or use connect_instance with the TCP URL.

GhidraMCP Struct Creation — JSON Fields Format

  • create_struct fields MUST be JSON array format: [{"name":"field_name","offset":0,"type":"float"},...] — colon/comma string formats all fail with "No valid fields provided"
  • add_struct_field works with explicit offset: Use for incremental building of large structs
  • import_data_types not implemented: Returns "Import functionality not yet implemented"
  • run_script_inline broken: "BundleHost null" OSGi error since session 47 — use create_struct/add_struct_field instead
  • Valid type strings: float, int, uint, byte, char, uint32, int32, byte[20], uint (pointer), or any Ghidra built-in type name
  • Workflow for large structs: create_struct with initial minimal fields (0-32 bytes), then add_struct_field per field with explicit offset — this avoids size conflicts when adding overlapping fields

Ghidra Issues

  • -readOnly discards all changes: The -readOnly flag in analyzeHeadless discards labels/analysis on exit. Always omit it.
  • Python scripts fail in headless: "Ghidra was not started with PyGhidra"
  • GUI won't start in headless env: Xvfb doesn't work reliably for Ghidra's Swing UI
  • Use GhidraMCP headless server instead: Full 191 REST API endpoints, no GUI needed

Function Discovery Workflow

  1. Search for game-specific strings: search_strings?search_term=X
  2. Find xrefs to those strings: get_xrefs_to?address=0x{addr}
  3. Cross-reference to identify functions: each xref tells you which function references it
  4. Decompile to confirm: decompile_function?address=0x{addr}
  5. Rename: rename_function_by_address with JSON body

Vtable Discovery Technique (CRITICAL)

GhidraMCP's get_xrefs_from only returns ONE xref per data address (the first entry). To discover ALL vtable method pointers, scan each 4-byte offset:

for i in range(0, 0x40, 4):
    result = get_xrefs_from(address=vtable_base + i)
    # Each result links to a function pointer
  • Dtor is always at +0x00 (first entry)
  • The second entry (+0x04) is typically the Update method in this engine
  • Some entries may point to addresses Ghidra hasn't defined as functions (thunks/jumps) — these still exist and can be found by searching nearby function addresses
  • The Ghidra headless API returns "No function found" for undefined thunks — try decompiling nearby addresses to find the real target

Indirect Object Access Pattern

The App global (0x4FD680) has few direct xrefs because the game typically passes this pointers through function calls. To find code accessing specific App fields:

  1. Find functions that receive App* as param_1 or this
  2. Look for offset accesses matching the App layout (e.g., +0x174 = Graphics, +0x180 = Input)
  3. Use string references as anchoring points (debug strings like "App::Initialize(N)")

MESHWORLD Position Values

  • Y is typically negative (below Y=0) in world space
  • X and Z span from -200 to +200 for arena levels, -1500 to +1500 for race levels
  • Position values for N:SINKPLATFORM/N:BUMPER may be transform-scale (0,0,0 or 1,1,1) rather than world positions

Input Subsystem (DirectInput8)

keyboard Device Layout (0x524 bytes, vtable 0x4D9840)

+0x00:  vtable pointer
+0x04:  Input* back-pointer (parent Input object)
+0x08:  IDirectInputDevice8* device
+0x0C:  key_state_old[256] (DWORD[0x40] — previous frame key states)
+0x10C:  key_bindings[256] (pointer[0xFF] — DIK code → action object mapping)
+0x143:  KeyDown DIK code (uint8, DIK_* code for Down action)
+0x144:  KeyUp DIK code
+0x145:  KeyLeft DIK code
+0x146:  KeyRight DIK code
+0x147:  KeyAction1 DIK code
+0x148:  KeyAction2 DIK code
+0x184:  ptr to key action object
+0x1CC:  ptr to key action object
+0x1C4:  ptr to key action object
+0x18C:  ptr to key action object
+0x154:  ptr to key action object
+0x194:  ptr to key action object
+0x198:  ptr to key action object
+0x1A0:  ptr to key action object
+0x1A4:  ptr to key action object
+0x1D0:  ptr to key action object
+0x1D4:  ptr to key action object
+0x168:  ptr to key action object
+0x16C:  ptr to key action object
+0x170:  ptr to key action object
+0x14C:  ptr to key action object
+0x158:  ptr to key action object
+0x188:  ptr to key action object
+0x15C:  ptr to key action object
+0x138:  vtable/data pointer

Key Mapping (6 rebindable actions)

  • KeyUp (DIK code at KeyboardDevice+0x143)
  • KeyDown (DIK code at KeyboardDevice+0x144)
  • KeyLeft (DIK code at KeyboardDevice+0x145)
  • KeyRight (DIK code at KeyboardDevice+0x146)
  • KeyAction1 (DIK code at KeyboardDevice+0x147)
  • KeyAction2 (DIK code at KeyboardDevice+0x148)

Input Subsystem Functions

  • Input_Init (0x46EE10): DirectInput8Create with device fallback, EnumDevices for keyboard
  • Input_Create (0x46C110): Allocates Input object (0x438 bytes), stores at App+0x180
  • KeyboardDevice_ctor (0x46E250): Creates DInput keyboard device (0x524 bytes), sets cooperative level, acquires
  • KeyboardDevice_dtor (0x46DE60): Releases DI device, unbinds key mappings
  • Mouse input uses DirectInput8 mouse device (not yet fully mapped)

Level/Object System

Level Object Layout (0x10D0 bytes, vtable at 0x4D8FB0)

+0x00:  vtable pointer
+0x04:  AthenaList* object_list (game objects)
+0x08:  int object_count
+0x18:  AthenaList* (secondary list)
+0x410:  void** object_array (pointer to list data)
+0x41C:  GameObject* tracked_object_1 (e.g., player ball)
+0x420:  GameObject* tracked_object_2
+0x424:  AthenaList* managed_list
+0x428:  AthenaList* (object pool)
+0x430:  uint8 flag_1
+0x431:  uint8 flag_2
+0x4368:  LevelData* (secondary level — clone for split screen)
+0x436C:  Level* level_mesh_1 (MeshWorld)
+0x4370:  Level* level_clone_1
+0x4374:  Level* level_mesh_2 (secondary)
+0x4378:  AthenaList* objects_list_1
+0x4790:  AthenaList* objects_list_2
+0x480:  LevelState* state (ball/physics state, 0x10D4 bytes)
+0x484:  uint8 flag
+0x844:  LevelContext* context (shared rendering state)
+0x868:  char* level_name (e.g., "Board (Dizzy)")
+0x878:  App* back-pointer

LevelState (Ball/Physics) Object (0x10D4 bytes)

+0x04:  uint8 enable_flag
+0x14:  int counter
+0x18:  uint32 value (0xf = 15, possibly ball size param)
+0x1C:  AthenaList* (objects)
+0x440:  void* collision_data (freed on cleanup)
+0x444:  IDirect3DVertexBuffer8* vb_1 (freed on cleanup)
+0x448:  IDirect3DVertexBuffer8* vb_2 (freed on cleanup)
+0x44C:  IDirect3DVertexBuffer8* vb_3 (freed on cleanup)
+0x450:  unknown struct (0x14 bytes)
+0x464:  unknown struct (0x14 bytes)
+0x478:  AthenaList* (game entities)
+0x894:  AthenaList* (render entities)
+0xCAC:  AthenaList* (physic entities)
+0x10C4: uint8 flag
+0x10C8: uint32 extra_data (freed on cleanup)
+0x10D0: uint8 flag

Game Object Vtable Layout (inferred from GameUpdate)

+0x00:  destructor (called with param=1 for free)
+0x04:  Update() — main update tick (called for active objects)
+0x08:  Release() / IUnknown Release
+0x30:  Render/Cleanup method (called when object removed)
+0x3C:  Render(context) — render with level context param

Game Object Layout (common fields)

+0x00:  vtable pointer
+0x0B:  uint8 removed_flag (non-zero = pending removal)
+0x21D: uint8 inactive_flag (non-zero = skip update)
+0x21A: int object_id (used as context during updates)
+0x21C: padding/flags

GameUpdate (0x469CF0) — Object Lifecycle

  1. Active pass: Iterate object list, call vtable+4 (Update) for each active object (removed_flag=0, inactive_flag=0)
  2. Removal pass: For objects with removed_flag set:
    • Set LevelContext+0x210 = "Remove Object"
    • If tracked_object_1, call LevelContext+0x68 with (-10000,-10000) and LevelState+0x20C=object_id
    • Call vtable+0x30 (cleanup render)
    • Remove from managed list
    • Call vtable+0x3C (render with level context)
    • Call vtable+0x00 (destructor) with free=1
    • Set LevelContext+0x210 = "Update"

Board/Screen Hierarchy

  • Board_ctor (0x419030): Base class, vtable at 0x4D0260
  • LevelBoard_Dizzy_ctor (0x41D060): "Dizzy" board, vtable at 0x4D0890
  • Board types: Beginner, Intermediate, Dizzy, Tower, Expert, Odd, Wobbly, Toob, Sky, Up, Master
  • ArenaBoard: Arena variants (Warmup, Beginner, Intermediate, Dizzy, Tower, Up, Expert)

MESHWORLD Object Types (Complete List)

N: (Named/Physical) Objects

  • N:MOUSETRAP, N:TARPIT, N:WATER, N:NOCONTROL, N:UNLOCKSECRET, N:SECRET
  • N:BRIDGE, N:SWIRL, N:WHEELEMBED, N:WATERWHEEL, N:MACE
  • N:TRAPDOOR, N:JUMPSECOND, N:JUMPFIRST, N:WAVY, N:SQUAREWOBBLY
  • N:BUMPER, N:BUMPER%d (numbered), N:SAWTEETH, N:SPINNER
  • N:EXTRATIME, N:SPEEDCYLINDER, N:NEONPLATFORM, N:BUMP
  • N:TENBONUS2, N:TENBONUS1, N:GLASS, N:ONGEAR, N:ONROTATOR
  • N:BOUNCE, N:SINKPLATFORM (also DN:SINKPLATFORM)

E: (Event/Trigger) Objects

  • E:TRAJECTORY, E:ACTION, E:JUMP, E:BREAK, E:LIMIT
  • E:SAFESWITCH, E:NODIZZY, E:MACETRIGGER, E:BITE, E:OPENSESAME
  • E:CATAPULTBOTTOM, E:BELL, E:SCORE, E:ALERTJUDGES
  • E:ACTIVATESAW1, E:ACTIVATESAW2, E:ALERTSAW1, E:ALERTSAW2
  • E:HAMMERCHASE, E:CALLHAMMER, E:LIMITPIPE2, E:LIMITPIPE1
  • E:SWALLOW, E:LIMITX, E:LIMITZ, E:PIPERANDOM, E:DROPLIFT
  • E:GROW, E:GROWSOUND, E:SHRINK, E:GRAVITY
  • E:BRANCH, E:HEATON, E:HEATOFF, E:NOPEGS, E:PEGS
  • E:TRAPPOP, E:VACPOPOUT, E:HELPINERTIA, E:UNHELPINERTIA
  • E:LAUNCH, E:LIGHTSON, E:LIGHTSOFF, E:ZOOP

Ball Physics System

Ball Vtable (0x4CF3A0)

The ball is a GameObject subclass with dedicated physics. Base class is GameObject at 0x4CF314.

Offset Address Function Description
+0x00 0x4027F0 Ball_dtor Destructor (calls Ball_Cleanup)
+0x04 0x405100 (thunk→0x405190) Update method
+0x08 0x402DE0 Ball_CollisionCheck Per-frame collision check
+0x0C 0x402A70 (not defined) Render setup
+0x10 0x408390 (not defined) Unknown
+0x14 0x401590 (not defined) Unknown
+0x18 0x402650 Ball_ApplyForce Apply directional force
+0x1C 0x402C10 (not defined) Unknown
+0x20 0x409480 (not defined) Unknown

Ball Object Layout (0xC98 bytes)

Offset Type Description
0x00 void** Vtable pointer (0x4CF3A0)
0x04 int level reference (parent)
0x08 int Parent object reference
0x0C float[6] Collision planes (4 floats each: ax+by+cz+d)
0x42 Timer Per-ball timer
0x59 float Ball X position (legacy)
0x5A float Ball Y position (legacy)
0x5B float Ball Z position (legacy)
0x62 float Ball size (default 3.0f = 0x40400000)
0x69 void* Sound object
0x96 int Sound handle
0x154 IDirect3DDevice8* D3D device pointer
0x164 float X position (authoritative)
0x168 float Y position (authoritative)
0x16C float Z position (authoritative)
0x170 float X velocity (zeroed on collision)
0x174 float Y velocity (zeroed on collision)
0x178 float Z velocity (zeroed on collision)
0x17C float Acceleration X (zeroed on collision)
0x180 float Acceleration Y (zeroed on collision)
0x184 float Acceleration Z (zeroed on collision)
0x1A4 int Level state reference
0x284 float Ball radius / height offset
0x2CC byte Force disable flag
0x2DC float X position (secondary)
0x2E0 float Y position (secondary)
0x2E4 float Z position (secondary)
0x2F0 int Frame counter (affects force scaling)
0x2F8 byte Active flag
0x2F9 byte Collision occurred flag
0x300 int Value 0x96 (150) - mass/timer constant
0x310 byte State flag (1 = active)
0x314 float Home X position
0x318 float Home Y position
0x324 byte Shrunk/special state flag
0x440 void* Collision data
0x6FC int Render initialized flag
0x700 byte Render mode (affects lighting)
0x708 int Render parameter
0x70D byte Render state initialized
0x734 byte Sub-render mode
0x748 int Collision mesh pointer
0x76A byte Flag (zeroed on collision)
0x7C8 int Render call counter
0x808 int Freeze counter (skip force if > 0)
0x810 AthenaList Force application list
0xA1 float Ball radius (from SIZE param)
0xC74 int Collision counter
0xC84 Quaternion Object rotation

Physics Constants (Data Section)

Address Value Description
0x4CF368 float Collision radius threshold
0x4CF374 float Force multiplier (C5C flag set)
0x4CF378 float Force multiplier (shrunk state)
0x4CF380 float Force scaling (first frame)
0x4CF3E8 float Force direction multiplier
0x4CF48C float Gravity/height constant

Key Ball Functions

  • Ball_Update (0x405190): 18KB main physics loop. Iterates objects, checks collisions, applies forces, handles reset on collision.
  • Ball_ApplyForce (0x402650): Applies force vector (x,y,z,magnitude) with state-dependent multipliers.
  • Ball_CheckCollisionPlanes (0x402810): Tests ball against 6 collision planes using formula: distance = Az + By + C*x + D. Returns true if all planes pass (ball inside convex volume).
  • Mesh_FindClosestCollision (0x465D90): DDA ray traversal through spatial hash to find closest mesh triangle hit point. Returns 3D position.
  • Ball_CollisionCheck (0x402DE0): Per-frame collision check against mesh, increments counter on hit.
  • Ball_Render (0x402860): D3D8 render: SetRenderState calls, texture setup, DrawPrimitiveUP.
  • Ball_ResetCollisionMesh (0x4030B0): Resets collision mesh and orientation.
    | 0x40C5D0 | DispatchCollisionEvents | Main collision event dispatcher — handles ALL object types: E:NODIZZY (grants TIME frames of dizzy immunity), E:SAFESWITCH (with parenthesized data), E:LIMIT (arena unlock tracking per player 0-3), E:BREAK (ball vtable[0x20] bounce), E:JUMP (plays 3D sound, freeze 10 frames, counter=200), E:ACTION (ONCE flag + SCORE award via Difficulty_GetTimeModifier), E:TRAJECTORY (X/Y/Z params set ball trajectory), N:NOCONTROL (disables input 10 frames), N:WATER (sets water flag + 10 frame counter), N:TARPIT (plays 3D sound, marks tar state, clears velocity), N:GOAL (reached goal! plays "Goal!" music, sets flags), N:MOUSETRAP (redirects ball with trap animation using trajectory dir + DAT_004CF370 speed), N:SECRET (marks Rotator triggered), N:UNLOCKSECRET (calls CheckArenaUnlock). DROPIN (checks trajectory magnitude, plays sound, counter=50), PIPEBONK (random sound from 3, counter=10), POPOUT (sound, counter=50) also handled |
  • Ball_ctor (0x40AFE0): Calls GameObject_ctor, sets vtable to 0x4CF3A0, initializes quaternion to identity

Rendering Pipeline (CONFIRMED from Ghidra)

  • Graphics_BeginFrame (0x453B50) → Graphics_RenderScene (0x454BC0) → Graphics_PresentOrEnd (0x455A90)
  • Graphics_RenderScene: SetupLights, copy world/view/projection matrices, SetTransform on D3D device, compute frustum for culling, call 8 render callbacks, set Z/stencil/alpha render states
  • Graphics_SetViewTransform (0x454A30): Set D3DTS_VIEW matrix, flip for mirror mode (+0x7D2), recompute frustum
  • Graphics_SetupFog (0x4539A0): D3DRS_FOGENABLE, FOGVERTEXMODE, FOGSTART, FOGEND, FOGCOLOR — uses linear fog with distance-based scaling
  • Graphics_Clear (0x453900): IDirect3DDevice8::Clear — target+Z (3) or Z+stencil (7)
  • Graphics_DrawScreenQuad (0x455F40): DrawPrimitiveUP for 2D overlay quads (UI, menus)
  • Matrix_ComputeFrustum (0x4762B0): Build view frustum from projection params
  • Graphics_InitRenderStates (0x42C810): Set background color
  • Level_Update (0x4606D0): Build collision vertex buffer from mesh faces, assemble into D3D vertex format (32 bytes: pos+norm+uv), create spatial hash
  • Level_ExtractByMaterial (0x471830): Extract triangles matching material filter, create new MeshWorld, compute AABB bounds
  1. CreateBadBall (0x40BCA0): Creates 0xC98 byte ball object, parses N:BADBALL params (CHASE, HOME, SIZE)
  2. Ball_ctor (0x40AFE0): Calls GameObject_ctor, sets vtable to 0x4CF3A0, initializes quaternion to identity
  3. GameObject_ctor (0x4039E0): Base class ctor, sets vtable to 0x4CF314

Athena List Utility Functions

AthenaList (vtable 0x4D875C) is a generic container class used throughout the engine:

  • AthenaList_Init (0x453210): Initialize 256-entry hash table list
  • AthenaList_Clear (0x453280): Clear list contents
  • AthenaList_GetIndex (0x4532B0): Get current iteration index
  • AthenaList_Remove (0x4534D0): Remove item from list

List structure (0x418 bytes):

+0x000: vtable (PTR_FUN_004D875C)
+0x004: next_id / param
+0x008: count (number of items)
+0x00C: iteration counters (int[256])
+0x100: data area (256 entries, zero-initialized)
+0x410: void** data (pointer to dynamic array)
+0x414: uint32 capacity

Texture System

Texture Loading Pipeline

The game embeds libpng, libjpeg, and D3DX8 statically — no external DLL dependencies for image loading.

  1. Graphics_LoadTexture (0x455C50): Cache-aware texture loader

    • Checks texture cache (Graphics+0x2E4 AthenaList)
    • If cached: increments refcount at texture[4]
    • If not cached: calls Texture_Create
  2. Texture_Create (0x476770): Creates 0x74-byte texture object

    • Strips file extension from filename
    • Tries multiple file formats: %s%s-mip1.%s, then without "-mip1"
    • Attempts up to 3 format variations (PNG, JPG, BMP)
    • Uses "textures\" prefix (or custom path if Graphics+0x7D8 set)
    • Calls Image_LoadFromFile to actually load
  3. Image_LoadFromFile (0x489217): D3DX image file loader wrapper

    • FUN_00487E5E: Initialize file handle struct (sets to -1,-1,0,0)
    • FUN_00487E70: Read file into memory buffer
    • FUN_004891D7 → FUN_00488B64: Create D3D texture from decoded image
  4. D3DX_CreateTextureFromMemory (0x488B64): 15KB D3DX texture creation

    • Parses image header (IJG JPEG or libpng)
    • Creates D3D texture with mipmap chain
    • Returns D3DERR_INVALIDCALL (0x8876086C) or E_FAIL (0x80004005) on error

Texture Object Layout (0x74 bytes, vtable 0x4DA648)

+0x00:  void** vtable (PTR_FUN_004DA648)
+0x04:  IDirect3DTexture8* texture (primary)
+0x08:  IDirect3DTexture8* texture_mip1 (first mip level)
+0x0C:  Graphics* graphics (back-pointer)
+0x18:  char* filename (loaded from)
+0x1C:  uint8 flag_1
+0x1D:  uint8 flag_2
+0x1E:  uint8 flag_3
+0x20:  uint32 width
+0x24:  uint32 height
+0x28:  uint32 mip_levels
+0x2C:  uint32 format (D3DFMT_*)
+0x30:  uint32 refcount (incremented on cache hit)
+0x34:  uint32 pool (D3DPOOL_*)
+0x38-0x70: internal D3D data

Graphics Object Texture Cache

  • Graphics+0x2E4: AthenaList* texture_cache (list of loaded textures)
  • Graphics+0x6F0: void** texture_array (pointer to cache data)
  • Graphics+0x2E8: int texture_count

Mipmap Support

The game uses explicit mipmap files:

  • texture.png — base level (mip0)
  • texture-mip1.png — first mip level
  • texture-mip2.png — second mip level
  • Debug strings confirm: "Create Texture with %d Mip Levels...", "Plotting mip level..."

Supported Formats

  • PNG: Primary format for most textures (119 references)
  • BMP: Used for checker patterns (purplechecker.bmp, brightgreenchecker.bmp, etc.)
  • JPG: Supported (JPEG error strings in binary)

Built-in Libraries (Statically Linked)

  • libpng: "Incompatible libpng version...", "Width too large..."
  • libjpeg: "Invalid SOS parameters...", "Corrupt JPEG data..."
  • D3DX8: "D3DXERR_INVALIDMESH", "D3DXERR_CANNOTATTRSORT", etc.

.MESH Binary Model Format (v1-v5, CONFIRMED)

Version 1 (Sphere, 8Ball, Hamster, etc.)

[4] version (=1)
[4] name_length
[name_length] name string
[88] material data: ambient(4f), diffuse(4f), specular(4f), shine, shadow_bias, shadow_scale
[4] texture_name_length
[texture_name_length] texture filename
[4] vertex_count
[4] unknown (material group index?)
[vertex_count * 32] vertex array:
  [float x, y, z] [float nx, ny, nz] [float u, v] = 32 bytes per vertex
[remaining] face/triangle data

Versions 2-5 (Chomper, FanBody, HammyJudge, etc.)

Same header structure but with additional sub-object sections. Texture string can be found by scanning for ".png"/".jpg" length-prefixed strings. Vertex data may be split into multiple mesh buffers with separate headers.

Parser

  • Code: reimpl/src/level/mesh_parser.c, header: reimpl/include/level/mesh_parser.h
  • Test: reimpl/src/level/mesh_test.c
  • Successfully parses: Sphere (59 verts), Hamster (119 verts), 8Ball (42 verts), FunBall, Eye, Sawblade, TarBubble, etc.
  • v1 fully working, v2-v5 partially working (finds texture, may miss vertices in sub-objects)

Binary MESHWORLD Format (from Ghidra Decompilation of 0x4629E0)

Complete Section Layout

Section 1: Materials
  [4] material_count
  For each material:
    [4] name_length + [name_length] name
    [4] face_start, [4] face_count, [4x4] unknown (6 uint32 total)
    [4] extended_flag (checked as char, 4 bytes read)
    If extended:
      [4x4] ambient RGBA, [4x4] diffuse RGBA, [4x4] specular RGBA
      [4] shine, [4] has_reflective, [4] has_texture
      If has_texture: [4] tex_name_length + [tex_name_length] texture filename

Section 2: Mesh Buffers
  [4] mesh_buffer_count
  For each buffer:
    [4] name_length + [name_length] name
    [4] face_count
    For each face: [3 reads of 4 bytes] + FUN_004685e0 call

Section 3: Game Objects
  [4] object_count
  For each object:
    [4] object_type (0=straight, others=special)
    If type==0: [3x4] position, [3x4] size, [3x4] scale, material init

Section 4: Bounding Box
  [6x4] min_x, min_y, min_z, max_x, max_y, max_z

Section 5: Vertex Array
  [4] vertex_count
  [vertex_count * 32] vertex data (pos+norm+uv, 32 bytes each)

Section 6: Post-load
  level+0x43C = 0 (vertex_data_ready)
  level+0x47C = this (self_ptr)
  Timer_Init, vtable callback, __close

Level Object Offsets (LevelState/MeshWorld)

Offset Field Description
+0x438 vertex_count Number of vertices
+0x440 vertex_array Vertex data (32 bytes each)
+0x454 min_x Bounding box min X
+0x458 min_y Bounding box min Y
+0x45C min_z Bounding box min Z
+0x468 max_x Bounding box max X
+0x46C max_y Bounding box max Y
+0x470 max_z Bounding box max Z
+0x894 materials Material list (AthenaList)
+0xCAC mesh_buffers Mesh buffer list (AthenaList)

Ball Physics Constants (from Ball_Update dispatcher 0x405100)

Value Usage
radius = 35.0 (0x420C0000) Ball collision radius
max_speed = 600.0 (0x44160000) Maximum velocity
max_speed2 = 1200.0 (0x44960000) Maximum velocity (secondary)
friction1 = 0.5 (0x3F000000) Surface friction
friction2 = 0.2 (0x3E4CCCCD) Low friction
damping = 0.2 (0x3E4CCCCD) Velocity damping

Ball ApplyForce Multipliers

Condition Multiplier Address
First frame 1.0 → 0.25 0x4CF380
In tube 0.0 (no force!) 0x4CF378
On ice 0.2 0x4CF374
Is dizzy 0.75 0x4CF36C

Level Vtable (0x4D8FB0) — 16 Entries

Index Offset Address Function
0 +0x00 0x4629C0 Level_dtor
1 +0x04 0x4606D0 Level_Update (233 lines)
2 +0x08 0x472770 Level_vtbl2
3 +0x0C 0x471750 Level_LoadMesh
4 +0x10 0x470440 Level_vtbl4
5 +0x14 0x46F3B0 Level_vtbl5
6 +0x18 0x470150 Level_vtbl6
7 +0x1C 0x46F390 Level_vtbl7
8 +0x20 0x471830 Level_DrawBuffer
9 +0x24 0x45DFD0 Level_vtbl9
10 +0x28 0x461370 Level_vtbl10
11 +0x2C 0x44ACB0 Level_vtbl11
12 +0x30 0x45DE30 Level_LoadMeshWorld
13 +0x34 0x461890 Level_vtbl13
14 +0x38 0x4629E0 Level_BinaryLoader
15 +0x3C 0x460DA0 Level_vtbl15

Level Viewer v2

Build & Run

cd ~/hamsterball-re/reimpl
gcc -o build/level_viewer_v2 \
    src/level/mesh_parser.c src/level/level_viewer_v2.c \
    -I include -lm \
    $(pkg-config --cflags sdl2 SDL2_image gl glew) \
    $(pkg-config --libs sdl2 SDL2_image gl glew) -lGLU

# Run with a level:
DISPLAY=:99 ./build/level_viewer_v2 ~/hamsterball-re/originals/installed/extracted/Levels/Level1.MESHWORLD

# Controls: WASD=move, mouse=look, Space=up, Shift=down, L=wireframe, T=textures, ESC=quit

Features

  • Loads .MESHWORLD files, scans for all object types (START, FLAG, SAFESPOT, PLATFORM, BUMPER, etc.)
  • Renders with OpenGL: grid, axes, start positions (green crosshairs), flag markers (yellow), other objects (blue)
  • Loads .MESH models for ball (Sphere.MESH) rendering at start position
  • Camera centered on level bounding box

.MESH Parser Test

cd ~/hamsterball-re/reimpl
gcc -o build/mesh_test src/level/mesh_parser.c src/level/mesh_test.c -I include -lm
./build/mesh_test ~/hamsterball-re/originals/installed/extracted/Meshes/Sphere.MESH
# Output: Name: Sphere01, Vertices: 59, Texture: HamsterBall.png, BBox: X[-4.88,25.00]...

.MESH Python Renderer (Visual Identification)

Located at ~/hamsterball-re/tools/render_meshes.py. Uses numpy + matplotlib to render .MESH files as PNG images for visual identification.

Workflow:

  1. Parse .MESH binary format (v1 working, v2-v5 partial)
  2. Render multi-view (3 angles: 30°, 120°, 210°) with face data + scatter overlay
  3. Save to ~/hamsterball-re/analysis/screenshots/meshes/{name}.png
  4. Vision-analyze the rendered images to identify game objects
  5. Use identified names for Ghidra function/structure renaming

Usage:

source ~/hb_venv/bin/activate
python ~/hamsterball-re/tools/render_meshes.py

Current Issues:

  • v2-v5 meshes (HammyJudge, DawgShoe, MagnifyingGlass, SkyPillar) parse with 0 vertices — sub-object format not yet handled
  • Face/index data not extracted — only vertex scatter rendering for v1 meshes
  • NaN errors on some meshes (Bell, FanBody, GlassBonus-Smashed) — need bounds checking
  • The v1 format after texture name: vertex_count (uint32), unknown (uint32), then vertices (32 bytes each)

v1 Format Details (confirmed from Sphere.MESH hex analysis):

offset 0: version (1)
offset 4: name_length (9)
offset 8: name ("Sphere01\0")
offset 17: material data (88 bytes):
  - ambient RGBA (4 floats)
  - diffuse RGBA (4 floats) 
  - specular RGBA (4 floats)
  - shine (1 float, e.g. 25.0)
  - shadow_bias (1 float, 0.0)
  - shadow_scale (1 float, stored as int 1)
offset 105: texture_name_length (16)
offset 109: texture name ("HamsterBall.png\0")
offset 125: after texture, two uint32s:
  - 0x3B = 59 (vertex_count!)
  - then vertex data starts (32 bytes each: xyz + normals + uv)

v2+ Format (needs more RE — from Chomper.MESH hex analysis):

  • Version 2+: sub-objects with separate mesh buffers
  • After name: position(3 floats), then material(88 bytes)
  • Sub-objects: count, name+texture per sub-object, transform data per sub-object
  • Each sub-object has its own vertex/face data section
  • Need to match decompiled FormatReader (0x4629E0) logic for full parsing

C Reimplementation (Milestone 6 — In Progress)

Module Structure (~/hamsterball-re/reimpl/src/)

  • main.c — Game loop: SDL init, load level, physics tick, render, input polling
  • physics.c / physics.h — Ball physics: gravity, acceleration, collision planes, position/velocity
  • input.c / input.h — Keyboard polling: up/down/left/right/action1/action2
  • renderer.c / renderer.h — OpenGL render stub (D3D8→GL translation layer)
  • audio.c / audio.h — Audio stub (BASS→SDL_mixer)
  • level.c / level.h — Level management stub
  • config.c / config.h — Config management, types (Vec3, App, GameObject)
  • ui.c / ui.h — UI stub

Build System

CMake-based. Key fix: SDL2 must be found via pkg-config, not FindSDL2.cmake:

find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED sdl2)
target_include_directories(hamsterball PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_directories(hamsterball PRIVATE ${SDL2_LIBRARY_DIRS})
target_link_libraries(hamsterball ${SDL2_LIBRARIES} GL m pthread)

Type Conflict Fix

config.h and game.h both defined bool, Vec3, App, GameObject — resolved by using unique
type names (GameApp, GameVec3, etc.) or #include guards with #ifndef checks.

Current Status

  • Builds and runs successfully on Linux (SDL2 + OpenGL)
  • Loads MESHWORLD levels (57+ levels parse OK)
  • Game loop runs: init → load → update → render → shutdown (clean exit)
  • Input: keyboard polling works, 6 game keys mapped
  • Physics: gravity + acceleration + velocity applied per frame, collision planes checked
  • Rendering: stub — needs actual mesh rendering pipeline (GL draw calls from MeshWorld data)
  • Audio: stub — needs BASS→SDL_mixer bridge

D3D8/DInput8/DSound8 Cross-Compilation (32-bit Windows)

The reimplementation uses the original Windows APIs, not SDL2+OpenGL. This allows
faithful recreation and Wine compatibility on Linux.

Toolchain Requirements:

  • i686-w64-mingw32-gcc (32-bit MinGW cross-compiler)
  • D3D8 headers: /usr/share/mingw-w64/include/d3d8.h (standard MinGW install)
  • DInput8 headers: /usr/share/mingw-w64/include/dinput.h
  • DSound8 headers: /usr/share/mingw-w64/include/dsound.h

Critical Fixes for Compilation (FIXED 2026-04-15):

  1. WAVEFORMATEX requires mmsystem.h — Include before dsound.h:

    #include <windows.h>
    #include <mmsystem.h>  /* Needed before dsound.h for WAVEFORMATEX */
    #include <d3d8.h>
    #include <dinput.h>
    #include <dsound.h>
    
  2. D3DPRESENT_PARAMETERS field names — Use correct field names:

    • PresentationInterval does NOT exist in D3D8 headers
    • Use FullScreen_PresentationInterval instead:
    g_d3dpp.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
    
  3. D3D8 has no D3DX math functions — D3DXVECTOR3, D3DXMATRIX, D3DX_PI are in D3DX8,
    not D3D8. Use plain math:

    • Replace D3DXVECTOR3 with struct { float x,y,z; } or floats
    • Replace D3DX_PI with 3.14159265f
    • Build view matrices manually (see win32_main.c SetMatrices function)
  4. No D3DXCreateTextureFromFile — D3D8 doesn't have this. Use D3D8 API directly:

    • IDirect3DTexture8 creation via device methods
    • Or link d3dx8.lib if using D3DX (separate DLL)

Working Compilation Command:

i686-w64-mingw32-gcc -std=c11 -m32 -O2 -g -Wall \
  -Iinclude -DWIN32_LEAN_AND_MEAN \
  src/core/win32_main.c src/level/meshworld_parser.c src/level/mesh_parser.c \
  -o hamsterball.exe \
  -ld3d8 -ldinput8 -ldsound -ldxguid -lole32 -lwinmm -mwindows

Link Libraries:

  • -ld3d8 — Direct3D8 API
  • -ldinput8 — DirectInput8 API
  • -ldsound — DirectSound8 API
  • -ldxguid — DirectInput/DirectSound GUIDs
  • -lole32 — COM initialization
  • -lwinmm — Windows Multimedia (for timer functions)
  • -mwindows — Windows GUI mode (no console)

Testing with Wine:

# On Linux, Wine translates D3D8 → OpenGL automatically
wine hamsterball.exe

Important: The original game ships bass.dll alongside the exe. For the open-source
reimplementation, BASS.dll can be:

  1. Purchased separately and placed alongside the exe (eventually)
  2. Replaced with SDL_mixer or similar (interim, not faithful to original API)

The current approach uses D3D8/DInput8/DSound8 for authenticity, BASS can be stubbed
for now until licensing is resolved.
SDL2 headers use #include <SDL2/SDL.h> but SDL_image/mixer headers include <SDL.h>
internally. You need BOTH -I (for SDL2/SDL.h style) AND -isystem (for bare SDL.h
style from within SDL2_image/mixer headers):

cd ~/hamsterball-re/reimpl
bash build/build_win64.sh   # Uses the saved build script

The build script is at reimpl/build/build_win64.sh. Key flags:

  • -I paths for SDL2/SDL.h, SDL2/SDL_image.h, SDL2/SDL_mixer.h
  • -isystem paths for bare SDL.h (included from within SDL2_image/mixer)
  • Links: -lopengl32 -lglu32 -lsetupapi -lole32 -loleaut32 -limm32 -lversion -lwinmm
  • Output: build-win64/hamsterball.exe (~15MB static)

IMPORTANT: Use opendir() instead of fopen() for directory existence checks.
On Windows/Wine, fopen("path/to/dir", "rb") returns NULL even if the directory exists.
opendir("path/to/dir") works correctly on both Linux and Windows.

Run (Linux)

cd ~/hamsterball-re/reimpl/build
./hamsterball  # Runs with default level, 3-sec timeout in headless mode

Run (Wine — test Windows build)

cd ~/hamsterball-re/reimpl/build-win64
DISPLAY=:99 WINEDEBUG=-all timeout 10 wine hamsterball.exe 2>/dev/null
# Copies of game assets (Levels/, Textures/, etc.) must be alongside hamsterball.exe

Legacy Level Viewer (still works)

cd ~/hamsterball-re/reimpl
gcc -o build/level_viewer \
    src/level/meshworld_parser.c src/level/level_viewer.c \
    -I include \
    $(pkg-config --cflags sdl2 SDL2_image SDL2_mixer gl glew) \
    $(pkg-config --libs sdl2 SDL2_image SDL2_mixer gl glew) -lm

🔗 Related Documents

rsksJIT

types : mods
keywords :

📂 View source on GitHub


rsksJIT — Universal Ref Loader (Logging Build)

Based on RodentRacer's v3 JIT mesh injection mod (mods/universal-ref-loader/).
This is a separate project that adds comprehensive logging without modifying the original.

What It Does

A bass.dll proxy that hooks Scene_CreateDynamicObjects at 0x0040C4BA and logs
everything the universal ref loader does:

  • Every ref name seen by the factory dispatch
  • Every board slot check (offset, value before/after JIT injection)
  • Every JIT mesh load (path, cache hit/miss, success/fail)
  • Every clone operation (Level_CloneTree)
  • Every difficulty bypass (save/restore App+0x23C)
  • Every safety check pass/fail
  • Board pointer, vtable pointer, App pointer values

Log Output

Log file: Z:\tmp\ref_loader_log.txt (maps to C:\tmp\ref_loader_log.txt on Windows,
or /tmp/ref_loader_log.txt on Wine/Linux).

Usage

  1. Copy bass.dll into the Hamsterball game directory (backup original first!)
  2. Ensure bass_real.dll is the true original BASS library (89710 bytes), NOT
    another proxy DLL
  3. Launch the game
  4. Navigate to a race level
  5. Read C:\tmp\ref_loader_log.txt

Build

i686-w64-mingw32-gcc -shared -o bass.dll jit_log_mod.c \
  -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

BASS Proxy Notes

The original game imports 20 BASS functions. The v3 original proxy only forwarded 15.
This build adds 5 missing exports that the game's import table requires:

  • BASS_Start — Start audio output
  • BASS_Stop — Stop audio output
  • BASS_MusicPlayEx — Play music module (extended)
  • BASS_ErrorGetCode — Get last error code
  • BASS_ChannelSetAttributes — Note: plural form (game imports this, not singular BASS_ChannelSetAttribute)

Without all 20 exports, Wine aborts with "unimplemented function BASS.dll.BASS_Start".

Testing on Wine/llvmpipe

The game renders black on Wine with llvmpipe software rendering. Key navigation
via xdotool does not reliably work when the screen is black. This mod should be tested
on real Windows or with a GPU-accelerated Wine setup.

The hbtestd MCP tool can crash-test the DLL (verifies it loads without crashing),
but cannot navigate to a race level on llvmpipe due to the black screen issue.

Original Project

This is based on mods/universal-ref-loader/ by RodentRacer. The original project
should NOT be modified — this is a separate fork for logging/analysis purposes.


🔗 Related Documents

ArenaBoard Arena System

types : gameplay
keywords :

📂 View source on GitHub


ArenaBoard Arena System

The multiplayer arena battle mode in Hamsterball. Players compete on floating
platforms ("boards") with hazards, trying to knock each other off or survive
the longest.

Architecture Overview

ArenaBoard (base class)
├── ArenaBoard_WarmUp_ctor        → Warm-up Arena (ID 1)
├── ArenaBoard_Beginner_ctor      → Beginner Arena (ID 2)
├── ArenaBoard_Intermediate_ctor  → Intermediate Arena (ID 3)
├── ArenaBoard_Dizzy_ctor         → Dizzy Arena (ID 4)
├── ArenaBoard_Tower_ctor         → Tower Arena (ID 5)
├── ArenaBoard_Up_ctor       → Up Arena (ID 6)
├── ArenaBoard_Neon_ctor     → Neon Arena (ID 7)
├── ArenaBoard_Expert_ctor   → Expert Arena (ID 8)
├── ArenaBoard_Odd_ctor      → Odd Arena (ID 9)
├── ArenaBoard_Toob_ctor     → Toob Arena (ID 10)
├── ArenaBoard_Wobbly_ctor   → Wobbly Arena (ID 11)
├── BoardLevel_Glass_ctor          → Glass Arena (ID 12)
├── ArenaBoard_Sky_ctor      → Sky Arena (ID 13)
├── ArenaBoard_Master_ctor   → Master Arena (ID 14)
├── ArenaBoard_Impossible_ctor    → Impossible Arena (ID 15)
└── CollSlices variants (collision mesh slicing)

Tournament Race Order (from Tournament_AdvanceRace 0x427080)

The 15-race tournament sequence, revealed by the switch statement:

Race Ctor Level Name Size (bytes) Asset Path
1 LevelBoard_WarmUp_ctor Warm Up 0x436C
2 LevelBoard_Beginner_ctor Beginner 0x644C
3 LevelBoard_Intermediate_ctor Intermediate 0x438C
4 LevelBoard_Dizzy_ctor Dizzy 0x4BE0
5 LevelBoard_Tower_ctor Tower 0x5418
6 LevelBoard_Up_ctor Up 0x4790
7 Board_NeonRace_ctor Neon Race 0x4394
8 LevelBoard_Expert_ctor Expert 0x4FD8
9 LevelBoard_Odd_ctor Odd 0x43B0
10 LevelBoard_Toob_ctor Toob 0x646C
11 LevelBoard_Wobbly_ctor Wobbly 0x4388
12 Board_Glass_ctor Glass 0x4390
13 LevelBoard_Sky_ctor Sky 0x47F8
14 BoardLevel_Master_Ctor Master 0x6498
15 Board_Impossible_ctor Impossible 0x4380

ArenaBoard Arena Asset Paths

Each arena loads a MeshWorld and CollisionLevel pair from these paths:

Arena Init Function Arena Mesh Path Extra Level
ArenaBoard_Beginner (0x22550) "ArenaBoard (Beginner Arena)" Race: "Beginner Race" (internal: Cascade Race)
ArenaLevel_Intermediate_Init (0x414180) levels\arena-intermediate
ArenaLevel_Dizzy_Init (0x414240) levels\arena-dizzy Levels\Level3-Swirl
ArenaLevel_Expert_Init (0x414B10) levels\arena-expert
ArenaLevel_Neon_Init (0x416F40) levels\arena-neon 2x AthenaList transforms + boundary sphere
ArenaLevel_Glass_Init (0x417DF0) levels\arena-Glass
ArenaLevel_Impossible_Init (0x418540) levels\arena-impossible
ArenaLevel_Sky_Init (0x4158C0) levels\arena-Sky "PILLAR" object collection
ArenaLevel_Master_Init (0x416080) levels\arena-Master
ArenaLevel_Toob_Init (0x414F00) (in ctor)
ArenaLevel_Odd_Init (0x414CE0) levels\arena-Odd
ArenaLevel_Toob_Init (0x414F00) levels\arena-Toob 5x "N:BUMPER%d" objects

Special Arena Init Logic

ArenaLevel_Dizzy_Init: Loads TWO mesh worlds — the arena
(levels\arena-dizzy) and a bonus level (Levels\Level3-Swirl) at this+0x11F8.

ArenaLevel_Sky_Init: Iterates all objects in the arena MeshWorld looking
for "PILLAR" name prefix (case-insensitive via __strnicmp). Adds matching
objects to a pillar list at this+0x11FB.

ArenaLevel_Toob_Init: Collects 5 bumper objects named "N:BUMPER1" through
"N:BUMPER5" via Scene_CollectByNameFilter. Each bumper stored at this+0x11F8
(stride 0x106). Bumper count array at this+0x1716.

ArenaLevel_Neon_Init: Most complex init — loads arena, then applies
Matrix transformations to all objects in two AthenaLists (first at
this+0xA75, second at this+0xB7B). Each object gets 3 matrix transforms
applied to its position/orientation, with a "visible" check based on whether
the object's float value differs from DAT_004CF3C8. Also creates a SceneObject
with 10x10 scale and 400.0 radius for the arena boundary.

ArenaBoard Timer System

Timer Structure (0x14 bytes)

Offset Type Field Description
+0x00 vtable* vtable Timer vtable (0x4D8E74)
+0x04 byte active Timer is running
+0x08 int32 period Total ticks before expiry (default: 100)
+0x0C int32 count Current tick count
+0x10 byte expired Set to 1 when count >= period

ToggleTimer_Init (0x458E60)

ToggleTimer_Init(Timer *t) {
    t->vtable = &Timer_Vtable;
    t->active = 0;
    t->period = 100;
    t->count = 0;
}

ToggleTimer_Tick (0x458E90)

ToggleTimer_Tick(Timer *t) {
    t->expired = 0;
    t->count++;
    if (t->count >= t->period) {
        t->count = 0;
        t->active = !t->active;  // Toggle on/off
        t->expired = 1;           // Signal expiry
    }
}

ToggleTimer_Cleanup (0x458E80)

ToggleTimer_Cleanup(Timer *t) {
    t->vtable = &Timer_Vtable;  // Reset to default (deactivate)
}

ArenaBoard_TickDown (0x472A50)

Decrements the tick counter at this+0x21F*4 (object lifetime counter).
When it reaches < 1, calls vtable[0x4C] (cleanup) and vtable[0x40] (destructor).

ArenaBoard Object System

ArenaSceneObj_Tick (0x42B660)

Per-frame update for arena objects (hammers, saws, etc.):

ArenaSceneObj_Tick(Object *obj) {
    // Animate vertical position (float up)
    obj->float_offset += DAT_004cf524;  // small upward velocity
    if (obj->float_offset > DAT_004cf438)  // max float height
        obj->float_offset = 0.75f;         // clamp

    // Animate cooldown timer (count down)
    obj->cooldown -= DAT_004cf370;  // decrement
    if (obj->cooldown < DAT_004cf368)  // zero threshold
        obj->cooldown = 0;

    // Tick two embedded timers (offset 0x888 and 0x89C)
    ToggleTimer_Tick(obj + 0x888);
    ToggleTimer_Tick(obj + 0x89C);
}

Object offsets:

  • +0x880: float position (vertical bob)
  • +0x884: cooldown timer (float)
  • +0x888: Timer struct (0x14 bytes) — primary animation timer
  • +0x89C: Timer struct (0x14 bytes) — secondary animation timer

ArenaBoard Render System (0x421910)

HUD Layout (4-player split screen)

+--------+--------+   Screen: 800x600
| P1     | P2     |   Quadrants: 400x300 each
| 75,75  | 725,75 |   Timer font at app->timer_font (offset 0x318/0x328)
+--------+--------+   Shadow font at app->shadow_font (offset 0x318)
| P3     | P4     |
| 75,525 | 725,525|
+--------+--------+

Timer Display

  • Main timer: centered at screen_center - 88, drawn with large font
  • Sub-seconds: ".N" format (tenths of a second)
  • Flashing red when time < 1100 ticks (with expired flag)
  • Flashing blue when time < 600 ticks
  • "TIE BREAKER!" text at (400, 40) when both timer_started and tie_breaker flags set

Player Visibility Flags (in App struct)

Offset Flag Meaning
+0x677 p2_hidden Hide P2 quadrant
+0x717 p3_hidden Hide P3 quadrant
+0x7B7 p4_hidden Hide P4 quadrant

Menu Command System (from GameSelectionManager 0x433AC0)

The title screen dispatches menu commands as string comparisons:

Command Description Menu Created
"BACK" Return to title MainMenu_ctor
"LOCKED" Mirror locked dialog OkayDialog ("NOT UNLOCKED!")
"1PT" 1-Player Tournament DifficultyMenu or TourneyContinueDialog
"1PMT" 1-Player Mirror Tournament DifficultyMenu or TourneyContinueDialog
"1PP" 1-Player Practice PracticeMenu_ctor
"1PTT" 1-Player Time Trial TimeTrialMenu_ctor
"PARTY" Multiplayer Party MPMenu_ctor

Mirror Tournament Unlock

The mirror tournament requires winning a regular tournament at Normal or
Frenzied difficulty. The unlock check message:

"THE MIRROR TOURNAMENT ISN'T UNLOCKED YET! TO UNLOCK THE MIRROR TOURNAMENT,
YOU NEED TO WIN A TOURNAMENT AT NORMAL OR FRENZIED DIFFICULTY!"

Tournament Save System

  • Save file: DATA\TOURNAMENT.SAV
  • Uses _check_file_access() to detect existing save
  • If save exists and no game in progress: shows TourneyContinueDialog ("Resume?")
  • If no save or new game: shows DifficultyMenu

App Tournament Flags (App struct offsets)

Offset Flag Meaning
+0x234 (set 0) Clear on menu entry
+0x235 is_tournament 1=tournament mode, 0=practice/time trial
+0x236 is_mirror 1=mirror tracks (reverse direction)
+0x237 tournament_complete 1=all 15 races done
+0x5D4 player_count_flag Set to 1 for single player
+0x5D5 (cleared) Cleared on tournament start
+0x5D7 (cleared) Cleared on tournament start

Tournament Time Bonus System

Time Award Per Difficulty (from Tournament_AdvanceRace)

Difficulty Base Bonus Increment
Normal (0) +1000 ms DAT_004CF6F4 (per-race increment)
Frenzied (1) +500 ms DAT_004CF3D8 (smaller per-race increment)

Player Time Tracking

Each player slot (stride 0xA0, up to 4 players at App+0x5E8):

Player Current Time Extra Time Race Index Level Name Active
P1 App+0x5E8 App+0x5EC App+0x60C App+0x610 App+0x5D8
P2 App+0x688 App+0x68C App+0x6CC App+0x6D0 App+0x678
P3 App+0x728 App+0x72C App+0x76C App+0x770 App+0x718
P4 App+0x7C8 App+0x7CC App+0x80C App+0x810 App+0x7B8

For races 1-2: current_time = base_time, extra_time = 0
For races 3+: extra_time = base_time (adds to existing accumulated time)

Tournament Object Structure (Tournament_AdvanceRace this)

Offset Type Field
+0x04 int* App pointer (scene state)
+0x08 int current_race_index (1-15)
+0x0C void* current_board_object
+0x10 byte skip_bonus_flag
+0x11 byte no_time_flag
+0x14 float[N] race_times_array (time at start of each race)
+0x50 int[N] race_times_ms_array (millisecond timestamps)

Related Functions

Address Name Purpose
0x427080 Tournament_AdvanceRace Create next race in tournament
0x433AC0 GameSelectionManager Title screen command dispatcher
0x422550 ArenaBoard_Beginner_ctor Beginner arena constructor
0x4226E0 ArenaBoard_Intermediate_ctor Intermediate arena ctor
0x422790 ArenaBoard_Dizzy_ctor Dizzy arena ctor
0x423060 ArenaBoard_Expert_ctor Expert arena ctor
0x424860 ArenaBoard_Neon_ctor Neon arena ctor
0x423BF0 ArenaBoard_Sky_ctor Sky arena ctor
0x4234E0 ArenaBoard_Toob_ctor Toob arena ctor
0x423220 ArenaBoard_Odd_ctor Odd arena ctor
0x424EC0 ArenaBoard_Impossible_ctor Impossible arena ctor
0x421910 ArenaBoard_Render HUD/timer rendering
0x42B660 ArenaSceneObj_Tick Arena hazard animation
0x458E60 ToggleTimer_Init Timer struct initializer
0x458E90 ToggleTimer_Tick Timer tick + toggle logic
0x458E80 ToggleTimer_Cleanup Timer deactivation
0x472A50 ArenaBoard_TickDown Lifetime countdown
0x4288B0 App_StartTournamentRace Tournament launch entry
0x445230 Scene_StartTournament Scene tournament setup
0x446730 Tourney_SaveTournament Save tournament state
0x42E060 GameSelectionScreen_ctor Tournament results screen

🔗 Related Documents

Runtime Environment

types : project

📂 View source on GitHub


Hamsterball - Runtime Environment

System Requirements (from original game)

  • OS: Windows 2000/XP/Vista/7
  • DirectX: Version 8 or higher required
  • CPU: Pentium II or equivalent
  • RAM: 64MB minimum
  • Video: DirectX 8 compatible 3D accelerator
  • Sound: DirectX compatible sound card

Registry Keys

The game uses ADVAPI32.dll functions:

  • RegOpenKeyExA, RegOpenKeyA - Open registry keys
  • RegCreateKeyA - Create registry keys
  • RegQueryValueExA - Read values
  • RegSetValueExA - Write values
  • RegCloseKey - Close keys

Likely Registry Locations (CONFIRMED from eSellerate API):

  • HKLM\SOFTWARE\Raptisoft\Hamsterball - Game installation path
  • HKLM\SOFTWARE\Microsoft\DirectX - DirectX version check
  • eSellerate DRM keys for activation

File Paths Used

From binary string analysis:

  • DATA\HS.CFG - High scores and configuration save file (CONFIRMED)
  • Levels\*.MESHWORLD - Level data files (CONFIRMED)
  • Meshes\*.MESH - 3D model files (CONFIRMED)
  • Textures\*.png, Textures\*.bmp - Texture files (CONFIRMED)
  • Sounds\*.ogg - Sound effects (CONFIRMED)
  • Music\Music.mo3 - Background music (CONFIRMED)
  • Fonts\*\font.description - Font metric data (CONFIRMED)
  • Fonts\*\Data0.png - Font texture atlases (CONFIRMED)
  • %s.mesh - Format string for mesh loading
  • %s.meshcollision - Format string for collision mesh loading
  • %s.meshworld - Format string for level loading
  • %s.ogg, %s.wav - Format strings for sound loading
  • %s\data%d.png - Format string for mipmapped texture loading

Configuration Files:

  • Jukebox.xml - Music mapping (CONFIRMED)
  • RaceData.xml - Race timing/scoring parameters (CONFIRMED)
  • DATA\HS.CFG - Save data with player names and high scores (CONFIRMED)
  • DATA\TOURNAMENT.SAV - Tournament progress (CONFIRMED)

DLL Dependencies

Required at Runtime:

DLL Purpose Notes
d3d8.dll DirectX 8 3D rendering Primary graphics API
DINPUT8.dll DirectInput 8 Input handling (keyboard/gamepad)
DSOUND.dll DirectSound Audio output via DirectSound
BASS.dll BASS audio library Music playback (MO3 format)
KERNEL32.dll Windows core API File I/O, memory, threads
USER32.dll Windows user interface Window creation, message loop
GDI32.dll Graphics Device Interface Text rendering
ADVAPI32.dll Windows registry Settings storage
SHELL32.dll Windows Shell ShellExecute for URLs
ole32.dll COM COM initialization
WS2_32.dll Winsock Network for eSellerate
VERSION.dll Version checking DLL version queries

Optional/Conditional:

DLL Purpose Notes
d3d9.dll DirectX 9 fallback Referenced in strings, may be D3D8->D3D9 compat
d3d8d.dll DirectX 8 debug Debug version checking
eSellerateEngine.dll DRM/activation Third-party licensing
RICHED32.DLL Rich text Possibly for license dialogs
COMCTL32.dll Common controls UI elements

DirectX 8 Interfaces Used

From import analysis:

  • Direct3DCreate8 - Main D3D8 entry point (CONFIRMED)

    • Creates IDirect3D8 interface
    • Used to create IDirect3DDevice8
    • Game uses D3D8 for all 3D rendering
  • DirectSound (via DSOUND.dll ordinal import)

    • Sound playback for effects
  • DirectInput8 (via DirectInput8Create)

    • Keyboard and mouse input at minimum
    • Possibly gamepad/joystick support

BASS Audio Library Usage

BASS_Init()          - Initialize audio system
BASS_Start()         - Start audio playback
BASS_Stop()          - Stop all audio
BASS_Free()          - Free audio resources
BASS_SetConfig()     - Configure audio settings
BASS_ErrorGetCode()  - Get last error
BASS_MusicLoad()     - Load MO3 music module
BASS_MusicPlayEx()   - Play music with position control
BASS_ChannelSetAttributes() - Set volume/pan/rate
BASS_ChannelStop()   - Stop specific channel

Wine Execution Testing

Status: PARTIALLY WORKING

  • Wine 9.0 initializes and launches the process
  • No display server available for actual rendering
  • Audio ALSA errors (no sound card in sandbox)
  • Process stays running, indicating game loop starts

Next Steps for Wine Testing:

  1. Set up X virtual framebuffer (Xvfb)
  2. Install DirectX 8 runtime via winetricks
  3. Test with: xvfb-run wine Hamsterball.exe
  4. Consider using Wine with D3D shader converter (dxvk)
  5. Document any crashes or error dialogs

eSellerate DRM

The game contains eSellerate activation code:

  • Purchase dialog strings present
  • Network activation via eSellerate Engine
  • Free play counter (shareware/trial version)
  • "CLICK HERE TO BUY!" / "CLICK HERE TO REGISTER" strings
  • For reimplementation: skip DRM entirely (we have permission)

Window Configuration

From WinMain analysis:

  • Window class registered via RegisterClassExA / RegisterClassA
  • Window created via CreateWindowExA
  • Uses PeekMessageA for game loop (non-blocking message pump)
  • SetCursor/ShowCursor for cursor management
  • SetCapture/ReleaseCapture for mouse capture
  • ClientToScreen/ScreenToClient for coordinate conversion
  • Game window likely uses WS_EX_APPWINDOW style

🔗 Related Documents

SAFESWITCH Checkpoint System

types : gameplay
keywords :

📂 View source on GitHub


SAFESWITCH Checkpoint System

The checkpoint/respawn-point selection system in Hamsterball. Allows level designers
to place named respawn markers along the track so the ball respawns at the last
checkpoint it passed, instead of going all the way back to START.

Overview

The system has two components placed in the level's MESHWORLD file:

  1. SAFESPOT objects — invisible reference points placed at specific track positions.
    They can carry a letter tag in parentheses (e.g. SAFESPOT(B)) or square brackets
    (e.g. SAFESPOT[X]) to enable filtered respawn selection.

  2. E:SAFESWITCH(X) events — invisible collision trigger volumes placed along the
    track. When the ball touches one, the letter parameter is copied into a filter
    field on the ball, changing which SAFESPOTs are valid respawn candidates.

When the ball falls off the track and hits an E:LIMIT boundary, the respawn system
uses the current filter to find the nearest matching SAFESPOT — creating a
"last checkpoint" progression system.

Binary Implementation

E:SAFESWITCH Handler (DispatchCollisionEvents @ 0x40C5D0)

Address: 0x40C6FC in DispatchCollisionEvents

The handler performs a stricmp against "E:SAFESWITCH" (string at 0x4CF8A8).
If the event string matches, it executes the following logic:

// Pseudocode of the SAFESWITCH handler at 0x40C6FC

if (stricmp(event_string, "E:SAFESWITCH") == 0) {
    char* paren = strchr(event_string, '(');   // 0x4BACC0 = strchr, 0x28 = '('
    
    if (paren == NULL) {
        // No letter parameter — clear the filter
        ball->c2c_filter[0] = '\0';              // ball+0xC2C = empty string
    } else {
        // Copy "(X)" from event string into ball+0xC2C
        // (byte-by-byte copy until null terminator)
        char* dst = ball + 0xC2C;
        char* src = paren;
        do {
            *dst = *src;
            dst++; src++;
        } while (*src != '\0');
    }
    
    // FALL THROUGH to E:LIMIT handler (no return/jump past it)
    // This means SAFESWITCH ALSO deactivates the ball:
    ball->is_active = 0;         // +0x768 = 0
    ball->flag_2E9 = 1;          // +0x2E9 = 1
    // ... arena scoring logic if applicable ...
}

Key behavior: E:SAFESWITCH always falls through to the E:LIMIT handler. This means
touching a SAFESWITCH volume immediately deactivates the ball AND sets the checkpoint
filter — the ball doesn't just pass through and continue racing. The deactivation
triggers the respawn cycle, which uses the newly-set filter to select the respawn point.

Ball Filter Field

Offset Type Size Description
ball+0xC2C char[] ~8 bytes SAFESWITCH filter string (e.g. "(B)", "(C)", or empty "")
  • Empty string — no SAFESWITCH has been hit yet (or a plain E:SAFESWITCH with no
    letter was hit). The respawn search accepts any plain SAFESPOT (rejects [X]/[Z] tagged ones).
  • "(B)" — the ball hit an E:SAFESWITCH(B) volume. The respawn search only accepts
    SAFESPOTs whose name contains (B).
  • Set at construction to empty. Persists across respawns until changed by another SAFESWITCH.

Respawn Point Filter Logic (Ball_FindClosestRespawnPoint @ 0x405190)

The filter logic is in the respawn-point search loop (decompiled lines 121-144):

// For each SAFESPOT candidate in the board's respawn list:

// 1. Calculate filter string length
int filter_len = strlen(ball->c2c_filter);   // ball+0xC2C

if (filter_len == 0) {
    // NO FILTER — accept this SAFESPOT unless it has [Z] or [X] tags
    bool accept = true;
    if (strstr(safespot_name, "[Z]") != NULL) accept = false;
    if (strstr(safespot_name, "[X]") != NULL) accept = false;
    
} else {
    // FILTER ACTIVE — check if SAFESPOT name contains '('
    char* paren = strchr(safespot_name, '(');
    if (paren != NULL) {
        // Compare first 2 chars: "(B" from SAFESPOT name vs "(B" from filter
        if (strnicmp(paren, ball->c2c_filter, 2) == 0) {
            // Match! Accept this SAFESPOT (subject to [Z]/[X] check below)
            goto accept_safespot;
        }
        // No match — skip this SAFESPOT
    }
    // If no '(' in SAFESPOT name, skip it
}

// At accept_safespot label:
// Check [Z]/[X] tags (same as no-filter path)
// Then compute 3D distance from ball to SAFESPOT
// Track the closest matching SAFESPOT

The 2-character strnicmp comparison compares the first two characters after (:

  • "(B" from SAFESPOT(B) vs "(B" from ball+0xC2C → match (case-insensitive)
  • "(B" from SAFESPOT(B) vs "(C" from ball+0xC2C → no match

This means the filter only checks the letter, not the closing paren. "(B)" in the
filter matches "(B)" in the SAFESPOT name, but the comparison is only on "(B".

Gravity-Mode Interaction (ball+0x748)

The respawn search has three modes based on ball+0x748 (gravity plane index):

Value Gravity Search Behavior Levels
0 Y-up (standard) Iterates board's SAFESPOT list normally Most races
1 X-axis (tilted) Uses random selection from SAFESPOT list Up Race (LevelUp)
2 Z-axis (flat) Uses random selection from SAFESPOT list odd race (Level6)

In mode 0, the search iterates ALL SAFESPOTs and finds the closest by 3D distance.
In modes 1 and 2, the search picks a random SAFESPOT from the list (using RNG_Rand).

Multiplayer proximity check: In 2-player mode (App+0x234 flag), the search also
checks if the other player's ball is within ball+0x284 (ball radius) of the SAFESPOT.
If so, that SAFESPOT is rejected to prevent respawning on top of the other player.

Per-Level SAFESPOT/SAFESWITCH Inventory

Verified from binary strings extraction of each level's .MESHWORLD file:

Race Level File SAFESPOTs E:SAFESWITCH Triggers
1 Warm-Up Level1 SAFESPOT (plain)
2 Beginner LevelCascade SAFESPOT (plain)
3 Intermediate Level2 SAFESPOT (plain)
4 Dizzy Level3 SAFESPOT (plain)
5 Tower Level4 SAFESPOT, (A), (B) E:SAFESWITCH, (A), (B)
6 Up LevelUp SAFESPOT (plain)
7 Neon LevelDark SAFESPOT, (A) E:SAFESWITCH, (A)
8 Expert Level5 SAFESPOT, (A), (B), (C), (D), (E) E:SAFESWITCH, (A), (B), (C), (D), (E)
9 Odd Level6 SAFESPOT, (A), (B), (C), [X], [Z] E:SAFESWITCH, (A), (B), (C)
10 Toob Level8 SAFESPOT (plain)
11 Wobbly Level7 SAFESPOT, (B), (C) E:SAFESWITCH(B), E:SAFESWITCH(C)
12 Glass LevelGlass SAFESPOT (plain)
13 Sky Level9 SAFESPOT, (B), (C), (D), (E), (F), (G), (H) E:SAFESWITCH(B) through (H)
14 Master Level10 SAFESPOT, (A), (C), (D), (Q), (X), (Z) E:SAFESWITCH, (A), (C), (D), (Q), (X), (Z)
15 Impossible LevelImpossible SAFESPOT, (A), (B) E:SAFESWITCH, (A), (B)

Observations

  • Early races (1-4, 6, 10, 12) have no checkpoint system — only plain SAFESPOT.
    The ball respawns at the nearest one regardless of progress.
  • Later races (5, 7-9, 11, 13-15) use lettered checkpoints to create progression.
    The ball respawns at the last SAFESPOT whose letter matches the current filter.
  • Sky Race (Level9) has the most checkpoints: 7 lettered SAFESPOTs (B through H).
  • Master Race (Level10) uses unusual letters: Q, X, Z — alongside A, C, D.
  • odd race (Level6) is unique in having both () and [] tagged SAFESPOTs:
    • (A), (B), (C) — filtered by the SAFESWITCH letter system
    • [X], [Z] — filtered by the gravity-mode system (ball+0x748), NOT by SAFESWITCH

Square Bracket Tags ([X], [Z])

Some SAFESPOTs use square brackets instead of parentheses. These are not related
to the SAFESWITCH system — they are filtered by the gravity-mode system:

  • SAFESPOT[X] — only used when ball+0x748 == 1 (tilted X-axis gravity, Up Race)
  • SAFESPOT[Z] — only used when ball+0x748 == 2 (flat Z-axis gravity, odd race)

When ball+0x748 == 0 (standard Y-up gravity), SAFESPOTs with [X] or [Z] tags
are rejected during the search. This prevents a Y-up race from using a tilted-gravity
respawn point that might be positioned in an unexpected location.

Currently, only odd race (Level6) has both () and [] tagged SAFESPOTs in the
same level file, because it uses gravity mode 2 (Z-axis) which has a different search
behavior than the standard mode 0.

Complete Respawn Flow

When a ball falls off the track:

  1. E:LIMIT or E:SAFESWITCH collisionball+0x768 = 0 (deactivate), ball+0x2E9 = 1
  2. Ball_FallUpdate (0x408830) countdown: ball+0xC60 -= 0.02 per frame
  3. Timer expires (ball+0xC60 < 0.0) → ball+0x2E8 = 1 (needs_respawn)
  4. Ball_FindClosestRespawnPoint (0x405190) runs:
    • Resets ball state (render_scale=0, is_falling=1, invisible)
    • Reads ball+0x748 to determine search mode (0=nearest, 1/2=random)
    • Reads ball+0xC2C to determine filter string
    • Iterates SAFESPOT list on board+0x1518 (AthenaList)
    • For each SAFESPOT: check filter match, check [X]/[Z] tags, compute distance
    • Selects closest matching SAFESPOT (or random in modes 1/2)
    • Teleports ball to selected SAFESPOT position
  5. Ball_Update (0x405E00) recovery: render_scale grows from 0.0 back to 1.0
  6. When render_scale >= 1.0: is_falling = 0, ball is interactive again

Modding Implications

  • To add checkpoints to a custom level: Place SAFESPOT(X) reference points
    and E:SAFESWITCH(X) collision volumes in the MESHWORLD file. The engine handles
    the rest automatically.
  • To change which SAFESPOT a ball respawns at: Write a new filter string to
    ball+0xC2C (e.g. "(C)"). The next respawn will search for SAFESPOT(C).
  • To force respawn at START: Clear ball+0xC2C to empty string. The search
    will find the nearest plain SAFESPOT or START point.
  • To disable checkpoint progression: Hook the SAFESWITCH handler at 0x40C6FC
    and NOP the filter-copy loop, or always clear ball+0xC2C after each respawn.

See Also


🔗 Related Documents

Save & [[40558402976403|registry system]]

types : project
keywords :

📂 View source on GitHub


Save & Registry System

Tournament Save File

Path: DATA\TOURNAMENT.SAV
Format: Binary, raw struct dump (no header/magic)

Write Function: TourneyMenu_WriteSave (0x4264B0)

Read Function: TourneyMenu_LoadSaveAndShow (0x4265A0)

The save file is a simple sequential binary write of PlayerProfile fields + App state:

Offset (file) Size Source (PlayerProfile) Description
0x00 4 Profile+0x08 current_race (1-14)
0x04 4 Profile+0x14 (unknown int)
0x08 0x3C Profile+0x18 race_time_array[15] (float × 15)
0x44 0x3C Profile+0x54 race_time_array_2[15] (float × 15)
0x80 4 Profile+0x90 accumulated_time_int
0x84 1 Profile+0x94 difficulty_flag
0x85 1 Profile+0x95 has_rollback_available
0x86 1 App+0x236 is_mirror_mode
0x87 4 App+0x23C race_active_flag
0x8B 4 App+0x5E8 total_time_float
0x8F 4 App+0x5E4 ranking_time_float
0x93 4 App+0x5F4 (unknown App float)
Total ~151 bytes

Write Process

TourneyMenu_WriteSave(profile, "DATA\\TOURNAMENT.SAV"):
  1. CRT_OpenFileExclusive(path, 0x180)  // Create/truncate exclusive
  2. Close handle (file created empty)
  3. FID_open(path, 0x8002)  // Open for write (O_WRONLY|O_CREAT)
  4. Sequential __write() calls — raw struct fields
  5. __close()

Read Process

TourneyMenu_LoadSaveAndShow(profile, "DATA\\TOURNAMENT.SAV"):
  1. FID_open(path, 0x8000)  // Open for read
  2. Sequential __read() calls — same fields in same order
  3. __close()
  4. Create TourneyMenu (0x111C bytes) with param_2=1 (play music)
  5. Scene_AddObject() — add tourney menu to scene

Reimplementation Notes

The save format is fragile (no version, no magic, no checksum). For reimplementation:

  • Add a magic header + version number
  • Use JSON for debuggability
  • Race time arrays are 15 floats each (one per race slot, some may be 0.0 for unplayed)
  • Float sizes assumed to be IEEE 754 32-bit (4 bytes each)

Registry System (Windows)

The game uses Windows Registry for persistent settings:

  • Key path: Under HKEY_CURRENT_USER\Software\Hamsterball (or similar)
  • App+0x54 = Registry key handle

Stored Values

Registry Key Type Description Default
Sound Volume DWORD/float Master sound volume 1.0 (0x3F800000)
Music Volume DWORD/float Music volume 1.0
(difficulty) DWORD Selected difficulty 0

Registry Functions

  • RegKey_Open(handle) — Opens registry key
  • RegKey_Close(handle) — Closes registry key
  • RegKey_ReadString(handle, name) → bool — Check if value exists
  • Registry_ReadFloat(handle, name) → float — Read float value
  • RegKey_WriteDWORD(handle, name, value) — Write DWORD value
  • RegKeyList_AppendStr(name, hex_id) — Append to registry string list

Read on Startup

Level_ReadSoundVolume (0x466570):

Level_ReadSoundVolume(SoundDevice):
  RegKey_Open(app->registry)
  if RegKey_ReadString("Sound Volume"):
    device->volume = Registry_ReadFloat("Sound Volume")
  else:
    device->volume = 1.0
  RegKey_Close()

Write on Shutdown

SoundDevice_dtor (0x4668A0):

SoundDevice_dtor(device):
  RegKey_Open(app->registry)
  RegKey_WriteDWORD("Sound Volume", device->volume)
  RegKey_Close()
  ... cleanup ...

High Score System

High scores are gated behind registration:

  • Demo version: "BUY HAMSTERBALL AND YOU CAN SAVE YOUR HIGH SCORES!"
  • Full version: Scores saved to registry or local file
  • No separate high score file found — likely stored in registry

Demo vs Full Version Gates

The binary contains extensive demo limitation code:

  • "You have reached the end of the demo version of Hamsterball!"
  • Tournament save/continue only in full version
  • High scores only in full version
  • Registration check via name+serial (at http://www.raptisoft.com/buyhamsterball)
  • App+0x200 = is_registered flag
  • App+0x918 = has_mini_games flag (for "MG" menu button)

Key Address Map

Address Function Description
0x4264B0 TourneyMenu_WriteSave Write TOURNAMENT.SAV
0x4265A0 TourneyMenu_LoadSaveAndShow Read TOURNAMENT.SAV + show menu
0x466570 Level_ReadSoundVolume Read volume from registry
0x4668A0 SoundDevice_dtor Save volume to registry on exit

Reimplementation Notes (SDL2)

Save System

// Use JSON for save files
struct TournamentSave {
    int current_race;
    int unknown_field;
    float race_times[15];
    float race_times_alt[15];
    int accumulated_time;
    bool difficulty;
    bool has_rollback;
    bool is_mirror;
    bool race_active;
    float total_time;
    float ranking_time;
    float unknown_app_float;
};

// Save as JSON
void WriteSave(const TournamentSave& save, const char* path) {
    std::ofstream f(path);
    // Use nlohmann/json or simple serialization
    json j = { ... };
    f << j.dump(2);
}

Registry Replacement

Replace Windows Registry with a simple INI/JSON config file:

[Settings]
SoundVolume=1.0
MusicVolume=1.0
Difficulty=0
Registered=true
MiniGamesUnlocked=true
MirrorTournamentUnlocked=false

🔗 Related Documents

Save, Config & [[40558402976403|registry system]]

types : project
keywords :

📂 View source on GitHub


Save, Config & Registry System

Overview

Hamsterball uses a dual persistence system:

  1. Registry (ADVAPI32) for game settings and unlock flags
  2. Files (DATA/*.sav) for tournament progress

Both are loaded at startup (App_Initialize step 12-13) and saved at shutdown.

App_SaveAllConfig (0x4284C0)

Called when game exits or settings change. Writes to Windows registry:

Registry Path: HKEY_CURRENT_USER\/software\reapti\banana (likely Hamsterball)
  → Based on RegKey_SetSoftwarePath (0x472F50) called from startup

Fields written:
  MouseSensitivity     DWORD  (App+0x84C, float as DWORD)
  MirrorTournament     BOOL   (App+0x850)
  
  // Race unlock flags (App+0x851..0x859)
  DizzyRace            BOOL
  TowerRace            BOOL
  UpRace               BOOL
  ExpertRace           BOOL
  OddRace              BOOL
  ToobRace             BOOL
  WobblyRace           BOOL
  SkyRace              BOOL
  MasterRace           BOOL
  
  // Arena unlock flags (App+0x85A..0x868)
  DizzyArena           BOOL
  TowerArena           BOOL
  UpArena              BOOL
  ExpertArena          BOOL
  NeonRace             BOOL   // Note: Neon is race, not arena
  GlassRace            BOOL
  ImpossibleRace       BOOL
  NeonArena            BOOL
  GlassArena           BOOL
  ImpossibleArena      BOOL
  OddArena             BOOL
  ToobArena            BOOL
  WobblyArena          BOOL
  SkyArena             BOOL
  MasterArena          BOOL
  
  // Binary blobs (0x50 = 80 bytes each)
  BestTime             BINARY (App+0x86C) - array of best times per level
  Medals               BINARY (App+0x8BC) - array of medal status per level
  
  // 2P controller mappings
  2PController1        DWORD  (App+0xB28)
  2PController2        DWORD  (App+0xB2C)
  2PController3        DWORD  (App+0xB30)
  2PController4        DWORD  (App+0xB34)

App Struct Field Layout (Config)

Offset Type Description
App+0x54 RegKey* Registry handle
App+0x84C float MouseSensitivity
App+0x850 bool MirrorTournament
App+0x851 bool DizzyRace
App+0x852 bool TowerRace
App+0x853 bool UpRace
App+0x854 bool ExpertRace
App+0x855 bool OddRace
App+0x856 bool ToobRace
App+0x857 bool WobblyRace
App+0x858 bool SkyRace
App+0x859 bool MasterRace
App+0x85A bool DizzyArena
App+0x85B bool TowerArena
App+0x85C bool UpArena
App+0x85D bool ExpertArena
App+0x85E bool OddArena
App+0x85F bool ToobArena
App+0x860 bool WobblyArena
App+0x861 bool SkyArena
App+0x862 bool MasterArena
App+0x863 bool NeonRace
App+0x864 bool GlassRace
App+0x865 bool ImpossibleRace
App+0x866 bool NeonArena
App+0x867 bool GlassArena
App+0x868 bool ImpossibleArena
App+0x86C uint8[0x50] BestTime (per-level best times, in milliseconds)
App+0x8BC uint8[0x50] Medals (per-level medal status: 0=none, 1=bronze, 2=silver, 3=gold)
App+0xB28 DWORD 2PController1 (DirectInput device index)
App+0xB2C DWORD 2PController2
App+0xB30 DWORD 2PController3
App+0xB34 DWORD 2PController4

LoadOrSaveConfig (0x4279F0)

Called at shutdown. Frees all config-related resources:

  • Closes registry (RegKey_Close)
  • Frees music channels (vtable[0x243], vtable[0x244])
  • Saves to DATA/*.sav files via vtable[0x8C]+8
  • Clears all level data at App+0x88..0x9D
  • Calls ShellExecuteA to open raptisoft.com if no reg key flag set (0x80)
  • Calls App_Shutdown to close window

Tourney_SaveTournament (0x446730)

Saves tournament progress to DATA\tournament.sav:

1. vtable[0x40]() - flush pending writes
2. CRT_remove() - delete existing file
3. vtable[0x54](&DAT_004d48a0) - write tournament save data

DAT_004D48A0 is a global string constant containing the save format signature.

Registry Functions

Address Name Description
0x472EA0 RegKey_Ctor Constructor
0x472EC0 RegKey_Open Open registry key
0x472F30 RegKey_Close Close registry key
0x472F50 RegKey_SetSoftwarePath Set software name for key path
0x472FD0 RegKey_WriteDword Write DWORD value
0x473000 RegKey_WriteDWORD Write DWORD (alternate)
0x473050 RegKey_WriteBool Write boolean
0x473080 RegKey_ReadDword Read DWORD
0x4730A0 RegKey_ReadDword Read DWORD (alternate)
0x473130 RegKey_ReadBool Read boolean
0x473170 RegKey_ReadString Read string
0x473100 RegKey_QueryValue Query registry value
0x473220 RegKey_DeletingDtor Deleting destructor
0x46A0E0 RegKeyList_AppendStr Append string to list
0x46A3C0 RegKeyList_CopyFromSibling Copy from sibling key

Tournament Save Format

The TOURNAMENT.SAV file format (DATA\tournament.sav):

  • Written via Tourney vtable[0x54]
  • Signature: DAT_004D48A0 (likely a magic header string)
  • Contains: race completion status, best times, medal counts
  • Deleted and rewritten each save cycle (atomic write pattern)

Config File: DATA/

File Description
HS.CFG Main config (loaded via vtable[0x8C]+8)
tournament.sav Tournament progress save

Startup Sequence (App_Initialize_Full 0x429530)

Step 12: RegKey_Open(App+0x54)
Step 13: RegKey_ReadString/ReadDword(App+0x54, PlayCount)
Step 15-22: 4x InputDevice_SetType (types 1=kb, 2=mouse, 4=joy1, 5=joy2)
Step 23: RegKey_Close(App+0x54)

On exit: App_SaveAllConfig → LoadOrSaveConfig → Tourney_SaveTournament


🔗 Related Documents

Sawblade & Drawbridge System

types : docs
keywords :

📂 View source on GitHub


Sawblade & Drawbridge System (Expert + Tower Races)

Complete reverse-engineering analysis of the Sawblade (Expert Race) and Drawbridge (Tower Race) systems.


SAWBLADE (Expert Race)

Binary Addresses

Function Address Purpose
CreateSawblade (Arena Factory) 0x40E590 Factory — creates SAWBLADE from level refs
Sawblade_Level_Ctor 0x434660 Constructor
Sawblade_Update (vtable[11]) 0x439BB0 Per-frame update (spin, debris, collision, movement)
Sawblade_Render (vtable[18]) 0x4347E0 Renders saw mesh + effects
Saw_Activate 0x434A50 Activates saw (E:ACTIVATESAW1/2)
Saw_AlertActivate 0x434770 Alert sound (E:ALERTSAW1/2)
Sawblade_SetActive 0x434640 Set active state
Sawblade_SetBreakSound 0x434AB0 Assign break sound
Sawblade_Level_Dtor 0x434760 Destructor
Sawblade vtable 0x4D5240 Vtable pointer

Strings

String VA Purpose
SAWBLADE 0x4CFA28 Factory lookup (strnicmp 8 chars)
Meshes\sawblade 0x4D3390 Mesh file
SAW1-BREAK / SAW2-BREAK Break sound names
SAWPATH / SMALLSAWPATH Path names for saw movement
sounds\saw / sawcut / sawspeedy / sawstartup Sound files

Object Structure (0x111C bytes = 4380)

Offset Size Type Description
+0x0000 4 ptr Vtable (0x4D5240)
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Position X (current, moves during activation)
+0x10D8 4 float Position Y
+0x10DC 4 float Position Z (current, moves during activation)
+0x10E0 4 float Home position X
+0x10E4 4 float Home position Y
+0x10E8 4 float Home position Z (boundary check)
+0x10EC 4 int Unused?
+0x10F0 4 float Rotation angle (RNG 0–360, decreases = clockwise)
+0x10F4 4 float Spin speed accumulator (0 → 25.0)
+0x10F8 4 int Direction (1=Z-axis movement, 2=X-axis movement)
+0x10FC 4 int Debris spawn counter
+0x1100 4 float X-axis boundary limit
+0x1108 4 float Z-axis boundary limit
+0x110C 1 byte Break triggered flag
+0x110D 1 byte Alert flag (1=can alert, 0=already alerted)
+0x1110 4 float Spin speed (500.0 initial, decays ×0.95 when not alerted)
+0x1114 1 byte Activated flag (0=idle, 1=active/moving)
+0x1118 4 float Movement velocity (accumulates)

Creation Flow

Factory (CreateSawblade @ 0x40E590, Arena mode only):

if (strnicmp(name, "SAWBLADE", 8) == 0 && App+0x23C != 0) {  // difficulty gate!
    obj = operator_new(0x111C);                    // 4380 bytes
    Sawblade_Level_Ctor(obj, board, x, y, z);
    AthenaList_Append(board+0x2578, obj);           // general list
    if (strstr(name, "1")) {
        board+0x4370 = obj;                         // saw 1
        Sawblade_SetBreakSound(obj, 1);
    }
    if (strstr(name, "2")) {
        board+0x4374 = obj;                         // saw 2
        Sawblade_SetBreakSound(obj, 2);
    }
}

Constructor (Sawblade_Level_Ctor @ 0x434660):

Level_ctor(this, d3d_device);                     // loads "Meshes\sawblade" mesh internally
vtable = 0x4D5240;
board = param_1;                                   // +0x10D0
pos = {x, y, z};                                   // +0x10D4 (current) AND +0x10E0 (home)
rotation = RNG(0, 360);                            // +0x10F0
spin_speed = 0;                                    // +0x10F4
alert_flag = 1;                                    // +0x110D (can alert)
spin_velocity = 500.0;                             // +0x1110
activated = 0;                                     // +0x1114
velocity = 0;                                     // +0x1118

Activation

Two-stage activation via collision events in ExpertCollisionEvents (0x40E6A0):

  1. Alert (E:ALERTSAW1 / E:ALERTSAW2):

    Saw_AlertActivate(board+0x4370 or board+0x4374);
    // If alert_flag (+0x110D) != 0: set to 0, play alert sound
    
  2. Activate (E:ACTIVATESAW1 / E:ACTIVATESAW2):

    Saw_Activate(board+0x4370 or board+0x4374);
    // Set +0x1114 = 1 (activated), play startup sound
    

Update Function (vtable[11] @ 0x439BB0)

void Sawblade_Update(this) {
    if (alert_flag == 0) {                         // +0x110D — no longer alerting
        // Decay spin speed
        spin_velocity *= 0.95;                     // +0x1110
        if (spin_velocity < 1.0) spin_velocity = 0;
        
        // Build up movement speed
        spin_speed += 0.1;                         // +0x10F4
        if (spin_speed > 25.0) spin_speed = 25.0;
        
        // Debris spawning
        debris_counter--;                          // +0x10FC
        if (debris_counter <= 0) {
            if (activated) debris_counter = 2;     // fast debris when active
            else debris_counter = 10;               // slow debris when idle
            spawn_debris_particles(this);
        }
        
        // Movement (only when activated)
        if (activated) {                           // +0x1114
            if (direction == 1) {                 // +0x10F8 — Z-axis
                pos.z -= velocity;                 // +0x10DC
                velocity += 4.17e-8;               // +0x1118 (gravity-like accel)
                if (abs(pos.z - home.z) >= boundary) {
                    // Hit boundary — trigger break
                    if (!break_triggered) {
                        break_triggered = 1;       // +0x110C
                        SceneObject_SpawnWithSound(board+0x4380);
                    }
                }
            }
            if (direction == 2) {                 // X-axis
                pos.x += velocity;                 // +0x10D4
                velocity += 4.17e-8;
                // Similar boundary check with board+0x4798
            }
        }
    }
    
    // Ball collision detection (always runs)
    for each ball in board+0x29D4:
        distance = sqrt((ball.x-pos.x)² + (ball.y-pos.y)² + (ball.z-pos.z)²)
        if (distance <= ball.radius + 100.0) {    // 100.0 = saw kill radius
            // Alignment check (axis-specific)
            if (alignment < ball.radius) {
                ball->vtable[8]();                // call damage/split function
            }
        }
    
    // Update rotation
    rotation -= spin_speed;                        // +0x10F0 (clockwise spin)
}

Key Constants

Address Value Purpose
0x4D092C 0.95 Spin velocity decay
0x4D0924 0.1 Spin speed increment
0x4CFECC 25.0 Max spin speed
0x4CF4DC 8.0 Speed range check
0x4CF48C 2.0 Speed range check
0x4CF480 75.0 Position offset for debris
0x4D5C78 4.17e-8 Velocity increment (gravity)
0x4D5C68 9999.0 Large boundary value
0x4CF454 100.0 Ball collision radius
0x4CF310 1.0 Minimum spin threshold

Vtable (0x4D5240)

Index Offset Address Function
0 0x00 0x439B90 Sawblade_Level_scalar_dtor
11 0x2C 0x439BB0 Sawblade_Update
18 0x48 0x4347E0 Sawblade_Render

DRAWBRIDGE (Tower Race)

The Tower race has TWO types of bridge objects:

Type 1: BRIDGE (Spinning Drawbridge Platform)

Created by the Arena factory (CreateSawblade @ 0x40E590) for Arena mode.
In Tower RACE mode, bridges are pre-placed (configured via CreateLevelObjects).

Function Address Purpose
Spinner_Level_ctor 0x4396F0 Constructor
Spinner_Update (vtable[11]) 0x439870 Per-frame update (spin animation)
Spinner_Render (vtable[18]) 0x45E0E0 Render
Spinner vtable 0x4D51E0 Vtable pointer

Object Structure (0x10FC bytes = 4348):

Offset Size Type Description
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Position X
+0x10D8 4 float Position Y
+0x10DC 4 float Position Z
+0x10E0 4 float Rotation angle
+0x10E4 4 int State (0=normal)
+0x10E8 4 float Angular velocity
+0x10F0 4 int Timer (init 100)
+0x10F4 4 ptr CollisionLevel pointer
+0x10F8 4 float Direction multiplier (1.0 or -1.0)

Constructor (Spinner_Level_ctor @ 0x4396F0):

Stands_ctor(this, board+0x4378);                  // uses pre-loaded mesh at board+0x4378
vtable = 0x4D51E0;                                // Spinner vtable
board = param_1;                                   // +0x10D0
pos = {x, y, z};                                   // +0x10D4
rotation = param_5;                                // +0x10E0
angular_velocity = 0;                              // +0x10E8
timer = 100;                                       // +0x10F0
direction = 1.0;                                   // +0x10F8
// Creates CollisionLevel internally
collision_level = CollisionLevel_ctorWithLevel(operator_new(0x10D0), this);

Mesh Dependency: Stands_ctor reads from board+0x4378. The Tower constructor loads Levels\Level4-Drawbridge at board+0x4370 and Levels\Level4-Mace at board+0x4378. For global spawn, use JIT mesh injection: save board+0x4378, load drawbridge mesh there, call ctor, restore.

Type 2: BBRIDGE (Breakable Bridge Sections)

Created by CreateLevelObjects (0x4121D0) for "BBRIDGE1" / "BBRIDGE2" refs.

Function Address Purpose
BreakBridge_ctor 0x436D70 Constructor
BreakBridge_Update (vtable[11]) 0x43DD80 Per-frame update
BreakBridge_Render (vtable[18]) 0x45E0E0 Render
BreakBridge vtable 0x4D5890 Vtable pointer

Object Structure (0x1100 bytes = 4352):

Offset Size Type Description
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Position X
+0x10D8 4 float Position Y
+0x10DC 4 float Position Z
+0x10E0 4 ptr CollisionLevel pointer
+0x10E4 1 byte Active flag (1=active)
+0x10E8 4 float Z position (boundary)
+0x10EC 4 float Position offset
+0x10F0 4 float Position offset
+0x10F4 4 int Trigger counter (0=not triggered)
+0x10F8 4 float Direction (0.0)
+0x10FC 1 byte Active flag 2

Constructor (BreakBridge_ctor @ 0x436D70):

Stands_ctor(this, mesh_ptr);                      // uses pre-loaded mesh (board+0x5410 or +0x5414)
vtable = 0x4D5890;                                // Pendulum/BreakBridge vtable
board = param_1;
pos = {x, y, z};
collision_level = CollisionLevel_ctorWithLevel(operator_new(0x10D0), this);
active = 0;                                       // +0x10FC
trigger_counter = 0;                              // +0x10F4
active_flag = 1;                                  // +0x10E4

Tower Level Pre-loaded Meshes

LevelBoard_Tower_ctor (0x41E340) loads:

Board Offset Mesh File Purpose
+0x436C Levels\Level4-Catapult Catapult mesh
+0x4370 Levels\Level4-Drawbridge Drawbridge mesh
+0x4374 Meshes\YellowLink Yellow link mesh
+0x4378 Levels\Level4-Mace Mace mesh
+0x437C Levels\Level4-Windmill Windmill mesh
+0x4390 Meshes\Chomper Chomper mesh
+0x43B4 Levels\Level4-Turret Turret mesh

Arena Collision Events (Tower/Expert Arena)

Event Action
E:ALERTSAW1 Saw_AlertActivate(board+0x4370) — alert saw 1
E:ALERTSAW2 Saw_AlertActivate(board+0x4374) — alert saw 2
E:ACTIVATESAW1 Saw_Activate(board+0x4370) — activate saw 1
E:ACTIVATESAW2 Saw_Activate(board+0x4374) — activate saw 2

All saw events require App+0x23C != 0 (difficulty gate — Normal/Frenzied only).


Global Spawn Approach

Sawblade Global Spawn

Uses the same pattern as Global Bonk:

  1. Hook at Ball_Update (0x405E22)
  2. Save player position
  3. On spawn trigger:
    • operator_new(0x111C)
    • Sawblade_Level_Ctor(obj, board, x, y, z) — loads saw mesh internally
    • Set +0x10F8 = 1 (direction 1, Z-axis) or 2 (X-axis)
    • AthenaList_Append(board+0x2578, obj) — register to general list
    • Append collision level to board+0x10EC
  4. Per-frame: call Saw_Activate(obj) to auto-activate (set +0x1114=1)

No pre-loaded mesh needed — Level_ctor loads Meshes\sawblade internally.

Drawbridge Global Spawn

Uses JIT mesh injection (same pattern as Global Lifters):

  1. Pre-load Levels\Level4-Drawbridge mesh → board+0x4378 (temporarily)
  2. Save old board+0x4378 value
  3. operator_new(0x10FC)
  4. Spinner_Level_ctor(obj, board, x, y, z, 0.0) — reads mesh from board+0x4378
  5. Restore board+0x4378
  6. AthenaList_Append(board+0x2578, obj)
  7. Append collision level to scene lists

The drawbridge mesh is loaded once and cached for subsequent spawns.


🔗 Related Documents

Scene Object

types : objects
keywords :

📂 View source on GitHub


Scene Object — Complete Modder's Reference

Verified via direct Ghidra decompilation of Hamsterball.exe (Athena engine, PE32 i386).
All offsets below were extracted from the live decompiled code via the GhidraMCP headless server.
Confidence markers: ✅ = Verified in raw decompiled C (2+ functions), ⚠️ = Verified in 1 function only, ❓ = Inferred from struct layout / not found in raw C.


Quick Stats

Property Value
Primary update function Scene_Update @ 0x00419C00
Ball physics function Scene_UpdateBallsAndState @ 0x0041B540
Render function Scene_Render @ 0x0041A2E0
VTable 0x004D0260
Destructor Scene_dtor @ 0x00419770
Total struct size ~0x5000 bytes (inherits from Gadget, extends to 0x4FD4+)

How to Get the Scene Pointer

Method 1: From the Global App Singleton (Easiest)

the app object lives at g_App = 0x004FD680. It stores the current Scene pointer at App+0x5DC.

// Global App singleton
void** g_App = (void**)0x004FD680;

// Get current Scene pointer
void* scene = *(void**)((char*)g_App + 0x5DC);

Alternative offsets into App for Scene access:

App Offset Points to Verified By
+0x5DC Current Scene* Scene_Update reads position from App+0x5DC→+0x758
+0x878 Scene back-pointer Collision handler: int* app = *(int**)((int)this + 0x878)

Method 2: From Any Ball Object

Every Ball stores a back-pointer to its parent Scene at Ball+0x14:

// ball is any Ball* pointer
void* scene = *(void**)((char*)ball + 0x14);

Verified by: Ball_ctor2 @ 0x004039E0 stores param_1 (Scene*) at this+0x14.

Method 3: From Any SceneObject

SceneObjects embed a back-pointer to Scene/App:

// SceneObject has app_ptr at +0x10 (from Gadget inheritance)
void* scene = *(void**)((char*)sceneObject + 0x10);

Method 4: Hook Scene_Update (0x419C00)

// Hook the main game tick — param_1 IS the Scene*
void __fastcall MySceneUpdate(void* scene) {
    printf("Scene tick: frame=%d\n", *(int*)((char*)scene + 0x3620));
    OriginalSceneUpdate(scene);
}

Complete Scene Struct Layout (Address-Ordered)

Source: SCENE_SYSTEM_DECOMP.md (explicit byte offsets from int-indexed Ghidra fields), decomp_scene_update.c, decomp_scene_spawnballs.c, decomp_level_render.c, decomp_collision_events.c

Inheritance Chain

GameObject (base)
  └─ Gadget (+0x870 bytes)
       └─ Scene (extends Gadget, adds game state)

Scene inherits from Gadget. The Gadget base contains:

  • vtable_ptr at +0x000
  • app_ptr at +0x014 (App* back-pointer)
  • Lists, transform data, name string

How to Read This Table

  • All offsets are byte addresses from the start of the Scene struct.
  • int-indexed fields from decomp outputs have been multiplied by ×4.
  • ⚠️ = From comment-only decomp files (no raw C code available)
  • ❓ = Inferred from single decomp source — may need confirmation.

Offset Type Field Description Confidence
+0x000 void** vtable Scene vtable 0x4D0260
+0x004 (Gadget base) Inherited fields — vtable, refs, transform data
+0x014 App* app_ptr Gadget-level back-pointer to App singleton 0x4FD680
+0x868 char* name "Generic Gadget" / "Board" (Gadget inherited)
+0x870 (Scene extension start) First field after Gadget base (0x870 bytes)
+0x874 byte is_skydome 0 = skybox, 1 = skydome
+0x878 App* scene_manager D3D device / render container (also resolves as App* in some contexts)
+0x87C void* viewport_obj D3D viewport interface
+0x884 ArenaBoard rumble_timer_1 First rumble/haptics timer (0x14 bytes)
+0x898 ArenaBoard rumble_timer_2 Second rumble/haptics timer (0x14 bytes)
+0x8AC Level* level_ptr Level geometry / collision data
+0x8B0 Level* skydome_ptr Skydome level (alternative sky rendering)
+0x8B8 AthenaList scene_object_list All scene objects (update + render pass)
+0x8BC int scene_object_count Number of objects in above list
+0x8C0 int scene_object_iterator Current iteration index ❓
+0x910 WaypointList* waypoint_list Race checkpoint tracking ⚠️️
+0xCC4 SceneObject** scene_object_array Direct pointer array to scene objects
+0x1518 AthenaList collision_list Collision surface list (planes, walls, floors)
+0x151C int collision_count Number of collision entries
+0x1520 int collision_iterator Collision list iteration index ❓
+0x1924 void** collision_array Collision object pointer array
+0x2160 AthenaList ripple_list Water ripple effects
+0x29B0 byte ball_positions_dirty 1 = need to propagate ball positions this frame
+0x29B8 int shake_magnitude Camera shake intensity. Starts at -800, decays by +10/frame
+0x29BC float camera_orbit_angle Y-axis orbit rotation around ball
+0x29C0 float camera_distance Orbit distance from ball
+0x29D0 Ball* current_ball_ptr Ball currently tracked by camera
+0x29D4 AthenaList ball_list_1 Player 1 ball list head
+0x29D8 int ball_list_1_count Number of P1 balls
+0x29DC int ball_list_1_iterator P1 list iteration state ❓
+0x2DE0 Ball** ball_list_1_array P1 Ball pointer array — direct read
+0x3204 AthenaList ball_list_2 Player 2 ball list head (split-screen)
+0x3208 int ball_list_2_count Number of P2 balls
+0x320C int ball_list_2_iterator P2 list iteration state ❓
+0x3610 Ball** ball_list_2_array P2 Ball pointer array — direct read
+0x361C SceneObject* waypoint_arrow Next-waypoint arrow
+0x3620 int frame_counter Total frames since scene start. Also called tick_count
+0x362C AthenaList player_list Player viewport list. Decomp calls this physics_objects at int +0x0D8B
+0x3630 int player_count 0=none, 1=single player, 2=split-screen
+0x3634 int player_iterator Player list iteration state ❓
+0x3A38 Ball** player_ball_array Array of ball pointers indexed by player (0-3)
+0x3A44 byte use_skydome 0 = skybox, 1 = skydome
+0x3A48 AthenaList visible_object_list Objects visible this frame (render bucket)
+0x3A4C byte shake_active 1 = camera shake / haptics currently active
+0x3AFC void* dynamic_object Object with vtable[8] render callback. Also called post_update_callback_obj
+0x3B00 void* trail_particles_ptr Trail particle system pointer
+0x3F18 void* water_ripple Water ripple renderer object
+0x3F1C byte path_follow_mode 1 = camera rides spline rails
+0x3F20 void* path_object Spline path data for camera rails
+0x3F24 float path_position Parametric t-position on spline
+0x3F2C int camera_snap_frames Frames remaining until snap-to-ball
+0x434C float camera_offset_x Camera offset X from ball
+0x4350 float camera_offset_y Camera offset Y from ball
+0x4354 float camera_offset_z Camera offset Z from ball
+0x4358 byte demo_timer_active 1 = demo countdown running
+0x435C int demo_countdown Frames remaining until demo popup
+0x4360 float demo_accumulator Popup timing accumulator
+0x4364 int demo_frame_counter Demo tick counter
+0x4368 byte demo_menu_suppressed 1 = block ESC menu during demo
+0x436C void* hammer_obj Bonk/hammer object (arena)
+0x4370 void* saw1_obj Saw blade 1 object (arena)
+0x4374 void* saw2_obj Saw blade 2 object (arena)
+0x43A0 float damage_amount Current damage value (set by E:BITE)
+0x43A8 int damage_timer Damage countdown timer
+0x43B8 void* catapult_list Catapult object list
+0x47D0 void* door_list Trapdoor/door object list
+0x4BBC void* judge_list Judge/score display list (arena)
+0x4BEC void* door_list_alt Alternate door list
+0x4FD4 void* bell_obj Bell object (arena extra time)

Scene Vtable Map (0x4D0260)

Index Address Name Description Calling Convention
[0x00] 0x425020 Scene_ctor Constructor __thiscall
[0x04] 0x419C00 Scene_Update Main game tick __thiscall
[0x08] 0x41A2E0 Scene_Render Render dispatch (1P/2P) __thiscall
[0x0C] 0x4692F0 Scene_dtor_thunk Destructor thunk __thiscall
[0x10] 0x469220 Scene_Release Release reference __thiscall
[0x14] 0x4130A0 Scene_SetupLevel Level setup __thiscall
[0x18] 0x469280 Scene_SceneObjThunk1 Thunk 1 __thiscall
[0x1C] 0x409D90 Scene_Init Initialize scene __thiscall
[0x20] 0x40B400 Scene_RunTick Run single tick __thiscall
[0x24] 0x44B840 Scene_NoOp1 No-op stub
[0x28] 0x44B840 Scene_NoOp2 No-op stub
[0x2C] 0x4692A0 Scene_SceneObjThunk2 Thunk 2 __thiscall
[0x30] 0x4692A0 Scene_SceneObjThunk3 Thunk 3 __thiscall
[0x34] 0x44B840 Scene_NoOp3 No-op stub
[0x38] 0x409DA0 Scene_LoadLevel Load level file __thiscall
[0x3C] 0x469430 Scene_SceneObjThunk4 Thunk 4 __thiscall
[0x40] 0x419740 Scene_CleanupScene Cleanup all state __thiscall
[0x44] 0x4692B0 Scene_SceneObjThunk5 Thunk 5 __thiscall
[0x48] 0x40B090 Scene_StartRace Start race countdown __thiscall
[0x4C] 0x41B130 Scene_HandleRaceEnd Check finish condition __thiscall
[0x50] 0x41B540 Scene_UpdateBallsAndState Ball physics + respawn __thiscall
[0x54] 0x40A040 Scene_NoOp_Collision Stub
[0x58] 0x41A540 Scene_ProcessRaceEnd Race countdown __thiscall
[0x5C] 0x409DE0 Scene_?? Unknown
[0x60] 0x40B420 Level_RenderDynamicObjects Sky/ripples/dynamic __thiscall
[0x64] 0x40B600 Level_UpdateAndRender Main level render __thiscall
[0x68] 0x40B570 Level_RenderObjects Transparent pass __thiscall
[0x6C] 0x41B710 Scene_RenderOverlay HUD/score overlay __thiscall
[0x70] 0x41BFD0 Scene_RenderPostEffects Fade/transition effects __thiscall
[0x74] 0x40C5D0 Scene_?? Unknown
[0x78] 0x44B840 Scene_NoOp4 No-op stub
[0x7C] 0x41AC70 Scene_LevelObjUpdate Level object tick __thiscall
[0x80] 0x41C5B0 Scene_?? Unknown
[0x84] 0x419750 Scene_?? Unknown
[0x88] 0x44B840 Scene_NoOp5 No-op stub
[0x8C] 0x41A9A0 Scene_ComputeInputForceDirection Computes 3D force vector from strongest player input (Ghidra label "ComputeLighting" is wrong) __thiscall

How to Iterate All Balls

Method 1: Array Access (Fastest)

// Get scene pointer
g_App = (char*)0x004FD680;
void* scene = *(void**)(g_App + 0x5DC);

// Player 1 balls
int p1_count = *(int*)((char*)scene + 0x29D8);
void** p1_balls = *(void***)((char*)scene + 0x2DE0);

for (int i = 0; i < p1_count; i++) {
    void* ball = p1_balls[i];
    int player_idx = *(int*)((char*)ball + 0x18);  // -1=AI, 0-3=human
    float x = *(float*)((char*)ball + 0x164);
    float y = *(float*)((char*)ball + 0x168);
    float z = *(float*)((char*)ball + 0x16C);
    // ... mod logic
}

// Player 2 balls (split-screen)
int p2_count = *(int*)((char*)scene + 0x3208);
void** p2_balls = *(void***)((char*)scene + 0x3610);

for (int i = 0; i < p2_count; i++) {
    void* ball = p2_balls[i];
    // ... mod logic
}

Method 2: Hook Scene_UpdateBallsAndState (0x41B540)

typedef void (__fastcall *UpdateBallsFunc)(void* scene);
UpdateBallsFunc orig = (UpdateBallsFunc)0x0041B540;

void __fastcall Hook_UpdateBallsAndState(void* scene) {
    int p1_count = *(int*)((char*)scene + 0x29D8);
    void** p1_balls = *(void***)((char*)scene + 0x2DE0);
    for (int i = 0; i < p1_count; i++) {
        void* ball = p1_balls[i];
        // ... pre-tick modifications
    }
    orig(scene);
}

Modding Recipes

Recipe 1: Get Current Player's Ball

void* GetPlayerBall(int player_index) {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    void** player_balls = *(void***)((char*)scene + 0x3A38);
    return player_balls[player_index];  // 0 = P1, 1 = P2
}

Recipe 2: Teleport All Balls to Position

void TeleportAllBalls(float x, float y, float z) {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    int p1_count = *(int*)((char*)scene + 0x29D8);
    void** p1_balls = *(void***)((char*)scene + 0x2DE0);
    
    for (int i = 0; i < p1_count; i++) {
        void* ball = p1_balls[i];
        *(float*)((char*)ball + 0x164) = x;
        *(float*)((char*)ball + 0x168) = y;
        *(float*)((char*)ball + 0x16C) = z;
        *(char*)((char*)ball + 0xC3C) = 1;  // teleport_flag
    }
    
    int p2_count = *(int*)((char*)scene + 0x3208);
    void** p2_balls = *(void***)((char*)scene + 0x3610);
    for (int i = 0; i < p2_count; i++) {
        void* ball = p2_balls[i];
        *(float*)((char*)ball + 0x164) = x;
        *(float*)((char*)ball + 0x168) = y;
        *(float*)((char*)ball + 0x16C) = z;
        *(char*)((char*)ball + 0xC3C) = 1;
    }
}

Recipe 3: Force Pause

void ForcePauseGame() {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    *(char*)((char*)scene + 0x220) = 1;  // paused_flag = true
}

Recipe 4: Read Camera State

void GetCameraState(float* orbit_angle, float* distance, float* offset) {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    *orbit_angle = *(float*)((char*)scene + 0x29BC);
    *distance = *(float*)((char*)scene + 0x29C0);
    offset[0] = *(float*)((char*)scene + 0x434C);
    offset[1] = *(float*)((char*)scene + 0x4350);
    offset[2] = *(float*)((char*)scene + 0x4354);
}

Recipe 5: Disable Demo Timer

void DisableDemoTimer() {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    *(char*)((char*)scene + 0x4358) = 0;  // demo_timer_active = false
    *(char*)((char*)scene + 0x4368) = 1;  // demo_menu_suppressed = true
}

Recipe 6: Trigger Camera Shake

void TriggerCameraShake(int magnitude) {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    *(int*)((char*)scene + 0x29B8) = magnitude;   // shake_magnitude
    *(char*)((char*)scene + 0x3A4C) = 1;          // shake_active
}

Recipe 7: Count Total Scene Objects

int CountSceneObjects() {
    void* scene = *(void**)((char*)0x004FD680 + 0x5DC);
    return *(int*)((char*)scene + 0x08BC);  // scene_object_count
}

Recipe 8: Activate All Trapdoors

void ActivateAllTrapdoors(void* scene) {
    void* door_list = *(void**)((char*)scene + 0x47D0);
    // Iterate door_list, call Trapdoor_Activate(door) for each
}

AthenaList Helper

AthenaList is the engine's linked list / array hybrid:

typedef struct {
    void** array;     // +0x00: pointer to element array
    int count;        // +0x04: number of elements
    int capacity;     // +0x08: allocated capacity
    int iterator;     // +0x0C: current iteration index
    void* head;       // +0x10: list head pointer
    void* tail;       // +0x14: list tail pointer
} AthenaList;

To iterate an AthenaList manually:

void IterateAthenaList(void* scene_list_ptr, void (*callback)(void*)) {
    AthenaList* list = (AthenaList*)scene_list_ptr;
    for (int i = 0; i < list->count; i++) {
        callback(list->array[i]);
    }
}

Key Functions

Function Address Parameters Description
Scene_Update 0x419C00 Scene* this Main game tick — hooks here see all frame state
Scene_UpdateBallsAndState 0x41B540 Scene* this Ball physics iteration — ALL balls update through here
Scene_Render 0x41A2E0 Scene* this Render dispatch (1P/2P split)
Scene_SetCamera 0x419FA0 Scene*, Ball*, int Camera positioning around ball
Scene_AddObject 0x469990 Scene*, SceneObject* Add object to scene
Scene_SpawnBallsAndObjects 0x41C5B0 Scene* Level startup spawner
Scene_CreateGameOverMenu 0x40A920 Scene*, int Pause/quit menu
Scene_CheckPath 0x457EC0 int start, int target 359-cell ring pathfinder
Gear_AdvanceAlongPath 0x418930 Gear*, float, float, float Spline path follower
TowerCollisionEvents 0x40DCD0 Scene*, Ball*, Collider* Level collision events
`ExpertCollisionEvents 0x40E6A0 Scene*, Ball*, Collider* Expert board collision events

Verification Notes

INT-INDEXED vs BYTE OFFSETS

Ghidra sometimes reports offsets as int indices (array indices into int[]). For this PE32 binary, sizeof(int) = 4. To convert:

Int Index Byte Offset
0x021D 0x0874
0x0220 0x0880
0x0A6C 0x29B0
0x0D88 0x3620
0x10D6 0x4358

The decompilations in decomp_scene_update.c use int-indexed notation in comments. The offsets in this document are already converted to byte addresses.

Verified Offsets (raw decompiled C, 19 functions scanned)

Automated verification via GhidraMCP REST API decompilation of these functions:
Scene_Update, Scene_dtor, Scene_SetCamera, Scene_Render, Scene_UpdateBallsAndState,
Scene_SpawnBallsAndObjects, Scene_LevelObjUpdate, Level_UpdateAndRender, Level_RenderDynamicObjects,
Level_RenderObjects, TowerCollisionEvents, ExpertCollisionEvents, Ball_Update, Ball_ctor,
Scene_StartRace, Scene_HandleRaceEnd, Scene_ProcessRaceEnd, Scene_CreateGameOverMenu, Scene_CheckPath.

Result: 56 offsets verified in raw C. 11 offsets not found in any decompiled function body.

Offset Field Verified In Count
0x0874 is_skydome Scene_CreateGameOverMenu, Scene_ProcessRaceEnd, Scene_Update 3
0x0878 scene_manager ExpertCollisionEvents, TowerCollisionEvents, Level_UpdateAndRender 3+
0x087C viewport_obj Scene_Render, Scene_SetCamera, Scene_dtor 3
0x0884 rumble_timer_1 Scene_Update, Scene_dtor 2
0x0898 rumble_timer_2 Scene_Update, Scene_dtor 2
0x08AC level_ptr Level_RenderObjects, Level_UpdateAndRender, Scene_SpawnBallsAndObjects 3
0x08B0 skydome_ptr Scene_dtor 1
0x08B8 scene_object_list ExpertCollisionEvents, Scene_ProcessRaceEnd, Scene_Update 3
0x08BC scene_object_count Scene_ProcessRaceEnd, Scene_Update, Scene_dtor 3
0x0CC4 scene_object_array Scene_ProcessRaceEnd, Scene_Update, Scene_dtor 3
0x1518 collision_list Scene_SpawnBallsAndObjects, Scene_dtor 2
0x2160 ripple_list Scene_dtor 1
0x29B0 ball_positions_dirty Scene_Update 1
0x29B8 shake_magnitude Scene_Update 1
0x29BC camera_orbit_angle Scene_SetCamera 1
0x29C0 camera_distance Scene_SetCamera 1
0x29D0 current_ball_ptr Scene_Render 1
0x29D4 ball_list_1 Level_UpdateAndRender, Scene_SpawnBallsAndObjects, Scene_Update 3
0x29D8 ball_list_1_count Level_UpdateAndRender, Scene_Update, Scene_UpdateBallsAndState 3
0x2DE0 ball_list_1_array Level_UpdateAndRender, Scene_Update, Scene_UpdateBallsAndState 3
0x3204 ball_list_2 Level_UpdateAndRender, Scene_UpdateBallsAndState, Scene_dtor 3
0x3208 ball_list_2_count Level_UpdateAndRender, Scene_UpdateBallsAndState, Scene_dtor 3
0x3610 ball_list_2_array Level_UpdateAndRender, Scene_UpdateBallsAndState, Scene_dtor 3
0x361C waypoint_arrow Level_UpdateAndRender, Scene_dtor 2
0x3620 frame_counter Scene_Update 1
0x362C player_list Scene_ProcessRaceEnd, Scene_Render, Scene_SpawnBallsAndObjects 3
0x3630 player_count Scene_ProcessRaceEnd, Scene_Render, Scene_SpawnBallsAndObjects 3
0x3A38 player_ball_array Scene_ProcessRaceEnd, Scene_Render, Scene_SpawnBallsAndObjects 3
0x3A48 visible_object_list Level_RenderObjects, Level_UpdateAndRender, Scene_dtor 3
0x3A4C shake_active Scene_Update 1
0x3AFC dynamic_object Scene_Update, Scene_dtor 2
0x3F18 water_ripple Scene_dtor 1
0x3F1C path_follow_mode Scene_SetCamera, Scene_Update 2
0x3F20 path_object Scene_SetCamera, Scene_Update, Scene_dtor 3
0x3F24 path_position Scene_SetCamera 1
0x3F2C camera_snap_frames Scene_SetCamera 1
0x434C camera_offset_x Scene_SetCamera 1
0x4350 camera_offset_y Scene_SetCamera 1
0x4354 camera_offset_z Scene_SetCamera 1
0x4358 demo_timer_active Scene_Update 1
0x435C demo_countdown Scene_Update 1
0x4360 demo_accumulator Scene_Update 1
0x4364 demo_frame_counter Scene_Update 1
0x4368 demo_menu_suppressed Scene_Update 1
0x436C hammer_obj ExpertCollisionEvents 1
0x4370 saw1_obj ExpertCollisionEvents 1
0x4374 saw2_obj ExpertCollisionEvents 1
0x43A0 damage_amount TowerCollisionEvents 1
0x43A8 damage_timer TowerCollisionEvents 1
0x43B8 catapult_list TowerCollisionEvents 1
0x47D0 door_list TowerCollisionEvents 1
0x4BBC judge_list ExpertCollisionEvents 1
0x4BEC door_list_alt TowerCollisionEvents 1
0x4FD4 bell_obj ExpertCollisionEvents 1

Unverified Offsets (NOT found in raw decompiled C)

These offsets are listed in the document but do not appear in any decompiled function body from the 19-function scan. They may be:

  • Embedded AthenaList sub-fields (count, iterator, capacity) that are accessed inline
  • Comment-only hypotheses from earlier RE sessions
  • Struct fields that exist but are never directly dereferenced in the scanned functions
Offset Field Note
0x08C0 scene_object_iterator Likely AthenaList embedded iterator at scene_object_list + 0x0C
0x151C collision_count Likely AthenaList embedded count at collision_list + 0x04
0x1520 collision_iterator Likely AthenaList embedded iterator at collision_list + 0x0C
0x1924 collision_array Likely AthenaList embedded array at collision_list + 0x00
0x29DC ball_list_1_iterator Likely AthenaList embedded iterator at ball_list_1 + 0x0C
0x320C ball_list_2_iterator Likely AthenaList embedded iterator at ball_list_2 + 0x0C
0x3634 player_iterator Likely AthenaList embedded iterator at player_list + 0x0C
0x3A44 use_skydome Field verified via is_skydome at 0x874; 0x3A44 may be a duplicate/alias
0x3B00 trail_particles_ptr Not found in any decompiled function from the scan set

Comment-Only Offsets

These appear only in the comment headers of decomp_scene_update.c (the raw C body is not saved in that file):

  • 0x3620frame_counter ✅ NOW VERIFIED (found in Scene_Update raw C)
  • 0x362Cplayer_list ✅ NOW VERIFIED (found in Scene_Render raw C)
  • 0x3630player_count ✅ NOW VERIFIED (found in Scene_Render raw C)
  • 0x3A38player_ball_array ✅ NOW VERIFIED (found in Scene_Render raw C)
  • 0x3A4Cshake_active ✅ NOW VERIFIED (found in Scene_Update raw C)
  • 0x43580x4368 — Demo timer fields ✅ ALL NOW VERIFIED (found in Scene_Update raw C)

Sources

All data verified via live GhidraMCP headless decompilation on Hamsterball.exe:

  • decomp_scene_update.c (0x419C00) — main game tick (comment-only header)
  • decomp_scene_updateballs.c (0x41B540) — ball physics iteration (comment-only header)
  • decomp_level_render.crendering pipeline with ball lists (comment-only header)
  • decomp_scene_spawnballs.c (0x41C5B0) — level startup + ball creation (comment-only header)
  • decomp_collision_events.c (0x40C5D0) — collision handler with scene offsets ✅ raw C
  • decomp_tower_collisionevents.c (0x40DCD0) — Tower board events ✅ raw C
  • decomp_expert_collisionevents.c (0x40E6A0) — Expert board events ✅ raw C
  • decomp_scene_setcamera.c (0x419FA0) — camera system ✅ raw C
  • decomp_scene_render.c (0x41A2E0) — render dispatch (comment-only header)
  • decomp_ball_vtable_0x408390.c — ball list iteration ✅ raw C
  • decomp_ball_vtable_0x409480.c — P2 ball list access ✅ raw C
  • SCENE_SYSTEM_DECOMP.md — vtable map, render pipeline, camera system
  • BALL_OBJECT_MODDING.md — ball structs for cross-reference
  • scene_struct.h — partial C struct from dtor analysis

Document generated: 2026-06-06
Method: Ghidra decompilation + cross-reference with existing scene docs + automated offset extraction from 29 raw decomp files
Confidence: High for ✅ verified offsets, Medium for ⚠️ comment-only offsets, Low for ❓ inferred fields


🔗 Related Documents

Scene Struct Deep Dive

types : objects
keywords :

📂 View source on GitHub


Scene Struct Deep Dive

Overview

The Scene struct is the central game object that holds all runtime state for a level.
It's created during level loading and contains references to all objects, physics state,
camera configuration, input handling, and scene graph data.

Scene Struct Layout (partial, from decompilation)

Offset Type Name Description
+0x00 vtable* vtable Scene vtable (0x4D0260)
+0x04 App* app Application pointer
+0x08 Scene* scene Self-reference (circular)
+0x0C-0x14 fields - Reserved / flags
+0x18 AthenaList gadget_list Main gadget list
+0x1C int gadget_count Number of gadgets
+0x20-0x28 int[] gadget_iters Iterator indices
+0x42C AthenaList - Secondary list
+0x424 AthenaList* data_ptr Data array pointer
+0x540 void* bonk_popup Bonk popup object pointer
+0x5410 void* bridge1_mesh BreakBridge 1 mesh
+0x5414 void* bridge2_mesh BreakBridge 2 mesh
+0x5418 BreakBridge* bridge1 Active BreakBridge 1
+0x541C BreakBridge* bridge2 Active BreakBridge 2
+0x5420 void* popcyl_mesh PopCylinder mesh
+0x5428 AthenaList popcyl_list PopCylinder list
+0x5840 void* blockdawg1_path "DAWGPATH1" path
+0x5844 void* blockdawg2_path "DAWGPATH2" path
+0x584C AthenaList catapult_list Catapult list
+0x607C void* gluebie_mesh Gluebie mesh
+0x6080 AthenaList gluebie_list Gluebie list
+0x858 AthenaList objects All scene objects
+0x864 void* active_input Currently active input gadget
+0x870 int input_state Input state flags
+0x878 App* app_ptr Application pointer (alternate)
+0x87C SoundChannel* sound Level sound channel
+0x8AC LevelMesh* level_mesh Level mesh data
+0x8B4 int object_count Incremented on spawn
+0x2578 AthenaList all_objects Master object list (all types)
+0x29BC float orbit_angle Camera orbit angle
+0x29C0 float orbit_distance Camera orbit distance
+0x3F1C int path_active Camera path rail flag
+0x3F20 Path* path_obj Camera path object
+0x3F24 float path_t Path parameter (0-1)
+0x3F28 int snap_flag Camera snap flag
+0x3F34 Vec3 snap_position Camera snap target
+0x3F38 int snap_flag2 Alternate snap flag
+0x3F2C int snap_countdown Camera snap countdown (frames)
+0x434C Vec3 cam_offset Camera offset from ball
+0x43AC Vec3 cam_initial Camera initial position (from CAMERALOOKAT)
+0x43BC Vec3 cam_target Camera target position (from CAMERALOOKAT)
+0x43C0 float cam_height Camera height (800 default)
+0x43C4 float cam_distance Camera distance (800 default)
+0x43C8 float cam_max_height Camera max height
+0x43CC int cam_mode Camera mode (1=orbit)
+0x4344 char* music_path Level music path
+0x436C void* bridge_mesh Bridge mesh reference
+0x4370 int bridge_collision Bridge collision flag
+0x437C Vec3 bridge_pos Bridge position
+0x4394 void* tipper_mesh Tipper mesh
+0x4398 void* tipper_visual TipperVisual mesh
+0x710 void*[8] render_passes 8 render pass slots

Key Scene Functions

Address Function Description
0x419FA0 Scene_SetCamera 5-mode camera system
0x41C5B0 Scene_SpawnBallsAndObjects Ball creation + object spawning
0x40ACA0 Level_SelectCameraProfile Camera profile per level
0x45E0E0 Scene_RenderAllObjects Render all scene objects
0x4692F0 Scene_HandleInput Dispatch input to gadgets

Scene Creation Flow

1. LoadRaceData (0x40A120)
   └─> MeshWorld_ctor: Parse MESHWORLD file
   └─> Level_InitScene (0x40B090)
       ├─ Create SoundChannel (0x80 bytes)
       ├─ Set volume (-50 dB)
       ├─ Level mesh reset
       ├─ Set random ambient colors
       ├─ Find "CAMERALOCUS" / "CAMERALOOKAT"
       ├─ Set camera path (orbit mode)
       ├─ Level_SelectCameraProfile
       └─ Play level music (2x speed / 4x intro)

2. Scene_SpawnBallsAndObjects (0x41C5B0)
   ├─ Create Ball(s) with start positions
   ├─ Scan for SAFESPOT/SAFEPOS objects
   ├─ CreateBadBall (tournament only)
   ├─ CreateMouseTrap (tournament only)
   ├─ CreateSecretObjects
   ├─ Scene_CreateFlags (goal markers)
   ├─ Scene_CreateSigns (direction arrows)
   └─ Scene_CreateDynamicObjects

3. CreateLevelObjects (0x4121D0)
   ├─ BRIDGE → bridge mesh reference
   ├─ TIPPER → Tipper object + TipperVisual
   ├─ BONK → Bonk popup
   ├─ BBRIDGE1/2 → BreakBridge
   ├─ POPCYLINDER → PopCylinder
   ├─ BLOCKDAWG1/2 → Blockdawg (with path)
   ├─ CATAPULT → Catapult
   └─ GLUEBIE → Gluebie sticky trap

🔗 Related Documents

Scene System Deep Documentatio

types : decompilation
keywords :

📂 View source on GitHub


Scene System Deep Documentation

Overview

The Scene system is the core game state manager for Hamsterball. Each game mode
(menus, arenas, tournament) has its own Scene instance with a vtable at 0x4D0260.
Scene controls the game tick, rendering, camera, physics pipeline, and object lifecycle.

Key Addresses

Function Address Purpose
Scene_Update 0x419C00 Main game tick (called each frame)
Scene_Render 0x41A2E0 Render dispatch (1P/2P split)
Scene_SetCamera 0x419FA0 Camera positioning and tracking
Scene_dtor 0x419770 Destructor (cleanup order reference)
Scene_AddObject 0x469990 Add SceneObject to scene
Scene_CreateGameOverMenu 0x40A920 Pause/quit menu creation
Gear_AdvanceAlongPath 0x418930 Spline path follower with collision avoidance

Scene Struct Layout (Key Offsets)

Offsets in hex (byte addresses), computed from Ghidra int-indexed fields.

Core State

+0x0000  vtable_ptr        (Scene vtable at 0x4D0260)
+0x0004  scene_base        (inherited fields from base class)
+0x0878  scene_manager     (D3D device/container, device at +0x174)
+0x087C  viewport_obj      (D3D viewport interface)
+0x3620  tick_count        (frame counter, incremented in Scene_Update)

Demo Timer

+0x4358  demo_timer_active (bool)
+0x435C  demo_countdown    (int, counts down each frame)
+0x4360  demo_accumulator  (float, popup timing)
+0x4364  demo_frame_counter
+0x4368  demo_menu_suppressed (bool)

Camera System

+0x434C  camera_offset     (Vec3: X/Y/Z offset from ball)
+0x4350  camera_offset_Y
+0x4354  camera_offset_Z
+0x3F1C  path_follow_mode  (bool, camera rails on spline)
+0x3F20  path_object       (Path* for camera rail)
+0x3F24  path_position     (float, parametric position)
+0x3F2C  camera_snap_frames (int, frames until snap-to-ball)
+0x29BC  camera_orbit_angle (float, Y-axis rotation)
+0x29C0  camera_distance   (float, orbit distance)

Player/Ball Tracking

+0x0870  app_ptr           (back-pointer to App singleton)
+0x2190  ball_positions_dirty (bool flag)
+0x2194  ball_list_iterator
+0x2198  ball_list_count
+0x219C  ball_list_current
+0x2DE0  ball_list_array   (Ball** array)
+0x2DC0  ball_count_alt    

Object Lists

+0x0884  rumble_timer_1    (ArenaBoard)
+0x0898  rumble_timer_2    (ArenaBoard)
+0x08B8  scene_object_list  (AthenaList<SceneObject*>)
+0x08BC  scene_object_count
+0x0CC4  scene_object_array
+0x362C  player_list        (AthenaList of player viewports)
+0x3630  player_count       (0=none, 1=single, 2=split)
+0x3A38  player_ball_array (Ball** array, indexed by player)
+0x29D0  current_ball_ptr   (Ball being tracked by camera)

Rumble/Haptics

+0x3A4C  rumble_active      (bool)
+0x29B8  rumble_intensity   (int, starts -800, decays by -10/frame)

Physics Pipeline vtable Calls

Called from Scene_Update via vtable when not in single-gear mode:

vtable[0x4C] = Scene_HandleRaceEnd    (0x41B130) - check race finish conditions
vtable[0x50] = Scene_UpdateBallsAndState (0x41B540) - ball physics + respawn + waypoints
vtable[0x54] = NoOp                   (0x40A040) - stubbed (collision handling is in Ball vtable)
vtable[0x58] = Scene_ProcessRaceEnd   (0x41A540) - race countdown timer
vtable[0x7C] = Scene_LevelObjUpdate   (0x41AC70) - level object tick (traps, moving platforms)

Level vtable Entries (from Scene vtable map)

Level_UpdateAndRender (0x40B600, vtable[0x64])

Main level render: two-pass system (opaque + alpha).

  1. Clear visible_object_list, append player balls
  2. SetRenderState(ALPHA_BLEND=FALSE) → opaque pass
  3. For each ball: ball->vtable0x1C = RenderOpaque
  4. SetRenderState(ALPHA_BLEND=TRUE) → alpha pass
  5. If race active: update waypoint arrow + render
  6. For each visible object: obj->vtable0x08 = Render
  7. Ball_RenderShadow for each ball (if shadow data exists)
    Scene offsets: +0x29D4=P1 balls, +0x3204=P2 balls, +0x3A48=visible objects, +0x361C=waypoint

Level_RenderObjects (0x40B570, vtable[0x68])

Transparent pass. Graphics_BeginFrame, scene_manager->vtable[0x4C]
for level geometry, then obj->vtable0x0C = RenderTransparent per visible object.

Level_RenderDynamicObjects (0x40B420, vtable[0x60])

Sky/dome + water ripples. If is_skydome_enabled (+0x3A44):
sky_dome_mesh->vtable0x48, else level_mesh->vtable0x48.
Then iterate ripple_list (+0x2160), render each:
Gfx_SetPositionAndRender→ScaleX→ScaleZ→SetPosition→FlagWaver_Render.
Finally dynamic_object->vtable8 callback.

Scene vtable Full Map (0x4D0260)

[0x00] 0x425020  Scene_ctor
[0x04] 0x419C00  Scene_Update          (main game tick)
[0x08] 0x41A2E0  Scene_Render           (1P/2P dispatch)
[0x0C] 0x4692F0  Scene_dtor_thunk
[0x10] 0x469220  Scene_Release
[0x14] 0x4130A0  Scene_SetupLevel
[0x18] 0x469280  Scene_SceneObjThunk1
[0x1C] 0x409D90  Scene_Init
[0x20] 0x40B400  Scene_RunTick
[0x24] 0x44B840  Scene_NoOp1
[0x28] 0x44B840  Scene_NoOp2
[0x2C] 0x4692A0  Scene_SceneObjThunk2
[0x30] 0x4692A0  Scene_SceneObjThunk3
[0x34] 0x44B840  Scene_NoOp3
[0x38] 0x409DA0  Scene_LoadLevel
[0x3C] 0x469430  Scene_SceneObjThunk4
[0x40] 0x419740  Scene_CleanupScene
[0x44] 0x4692B0  Scene_SceneObjThunk5
[0x48] 0x40B090  Scene_StartRace
[0x4C] 0x41B130  Scene_HandleRaceEnd
[0x50] 0x41B540  Scene_UpdateBallsAndState
[0x54] 0x40A040  Scene_NoOp_Collision (stub)
[0x58] 0x41A540  Scene_ProcessRaceEnd
[0x5C] 0x409DE0  Scene_?? (0x409DE0)
[0x60] 0x40B420  Level_RenderDynamicObjects
[0x64] 0x40B600  Level_UpdateAndRender
[0x68] 0x40B570  Level_RenderObjects
[0x6C] 0x41B710  Scene_RenderOverlay
[0x70] 0x41BFD0  Scene_RenderPostEffects
[0x74] 0x40C5D0  Scene_?? (0x40C5D0)
[0x78] 0x44B840  Scene_NoOp4
[0x7C] 0x41AC70  Scene_LevelObjUpdate
[0x80] 0x41C5B0  Scene_?? (0x41C5B0)
[0x84] 0x419750  Scene_?? (0x419750)
[0x88] 0x44B840  Scene_NoOp5
[0x8C] 0x41A9A0  Scene_ComputeInputForceDirection (NOT lighting — computes 3D force from player input)

Level Objects

+0x3624  level_object_list  (AthenaList of level objects)
+0x362C  level_object_count
+0x3630  level_object_iter
+0x3A38  level_object_array_ptr
+0x3AFC  post_update_callback_obj (vtable[1] called after all updates)

Game Tick Execution Order (Scene_Update)

1. Increment tick_count
2. Demo timer check:
   - If demo_timer_active: decrement countdown
   - On countdown=0: spawn "end of demo" popup via ScoreDisplay_CtorC
3. Pause/escape check:
   - If not menu mode (3/4) AND not pause_suppressed:
     - Check ESC key via Input_CheckKeyCombo(app, 2)
     - Create GameOverMenu if pressed
4. Ball position propagation:
   - If ball_positions_dirty flag set:
     - Iterate ball list, call Ball_SetTargetPos for each
     - Clear dirty flag
5. Gear path following:
   - If exactly 1 gear object AND gear_has_active_path:
     - Read camera position from app->camera_obj
     - Call Gear_AdvanceAlongPath(gear, camX, camY, camZ)
6. Rumble board timers:
   - Tick both ArenaBoard instances
7. Rumble intensity decay:
   - If rumble_active: decay intensity from -800 toward 0 by 10/frame
8. SceneObject update+render loop:
   - For each SceneObject in list:
     - Call vtable[1]() = Update
     - If render_flag set: Gfx_SetRenderState, then vtable[0]() = Render
9. Game state pipeline (4 vtable calls):
   - Scene_HandleRaceEnd -> Scene_UpdateBallsAndState -> NoOp -> Scene_ProcessRaceEnd
   - HandleRaceEnd: check if race won/time expired
   - UpdateBallsAndState: per-ball physics tick + OOB respawn + waypoint progression
   - NoOp: collision stub (collision is in Ball vtable update)
   - HandleCountdown: race start countdown timer
   - Skip if in single-gear camera-locked mode
10. Level object update:
    - For each level object: vtable[0x7C]()
11. Post-update callback

Render Pipeline (Scene_Render)

Single Player (player_count == 1)

Graphics_SetViewport(full_screen)
  -> Copy ball->pos to camera target
  -> Scene_SetCamera(scene, ball, apply_path=true)
  -> vtable[0x60] Level_RenderDynamicObjects (sky/far plane)
  -> vtable[0x64] Level_UpdateAndRender (level geometry + dynamics)
  -> vtable[0x68] Level_RenderObjects (transparent/glass objects)
Graphics_SetViewport(full_screen)
  -> vtable[0x70] Scene_RenderPostEffects  (fade, transitions)
  -> vtable[0x6C] Scene_RenderOverlay (HUD, score)

Split Screen (player_count == 2)

For each player:
  Graphics_SetViewport(half_screen_for_player)
  -> Scene_SetCamera(scene, player_ball, apply_path=true)
  -> vtable[0x60] RenderBackground
  -> vtable[0x64] RenderOpaqueObjects
  -> vtable[0x68] RenderTransparentObjects

Graphics_SetViewport(full_screen)
  -> vtable[0x70] RenderOverlay (shared HUD)
  -> vtable[0x6C] RenderPostEffects

Camera System (Scene_SetCamera)

Camera Modes

  1. Default Follow: Camera positioned at ball->camera_target_pos + camera_offset
  2. Path Following: Camera rails along spline, springs back if ball wanders
  3. Camera Shake: Random position jitter when ball->camera_shake_enabled
  4. Camera Snap: Instant reposition over N frames (teleport transitions)
  5. Orbit Mode: Rotates around ball using orbit_angle (menu screens)

Path Following Algorithm

  • When path_follow_mode is active:
    • Get current position on spline at path_position
    • Compute distance from camera to path center
    • If distance > threshold: apply spring force
      • Oscillation via sin wave (Wave_Sin)
      • Damping factor prevents infinite oscillation
      • Max pull strength: 700 units
    • Blend camera position toward path center
    • Update ball target position

App_Run Game Loop (0x46BD80)

while (!quit):
  Sleep(0)  // yield to OS
  tick_interval = 1000 / target_fps
  
  // FPS counter (1Hz update)
  if (time > fps_timer + 1000):
    update_fps_display()
    reset frame counter
  
  // Message pump
  while (PeekMessage()):
    TranslateMessage + DispatchMessage
    if (quit) break
  
  // Tick + Render
  do:
    elapsed = GetTickCount() - last_tick
    if (elapsed < tick_interval - 5) || (skip_count > 9):
      // Render only
      if (time > render_deadline):
        Graphics_BeginFrame()
        vtable[0x24] Scene_Render  // render-only
        vtable[0x28] Scene_Present
        vtable[0x2C] Scene_PostFrame
        Graphics_PresentOrEnd()
      break
    
    // Full tick
    frame_count++
    Graphics_BeginFrame()
    vtable[0x20] Scene_Update    // full game tick
    last_tick += tick_interval
    if (accumulated lag > 1000ms):
      last_tick = now - 1000  // prevent spiral of death
  while (updates_this_frame < 1)

Additional Scene Functions

Level_UpdateAndRender (0x40B600)

6-phase render for level geometry and objects. See RENDERING_PIPELINE.md for details.
Phases: Build visible list → Opaque pass → Alpha pass → Waypoint arrow → Visible list render → Ball shadows

Level_RenderObjects (0x40B570)

Transparent pass: BeginFrame → mesh render → BeginFrame → vtable[0x0C] per object in visible_list

Scene_CheckPath (0x57EC0)

Ring topology pathfinder on 359-cell circular grid. Two walkers (forward +1, backward -1)
search from start to target. Returns 1=forward, -1=backward, 0=unreachable.

Scene_SpawnBallsAndObjects (0x41C5B0)

Level startup spawner. Creates player balls at "START%d-%d" hash table positions,
then creates level objects (flags, signs, dynamic objects, secret objects).
Ball defaults: radius=26.0, max_speed=5.0, gravity=0.5, speed_scale=0.1

Scene_RenderAllObjects (0x45E0E0) — Detailed

3-pass render with object classification into buckets:

  • Pass 1 (Opaque): Objects with all alpha/decal flags=0, rendered immediately
  • Pass 2 (Translucent): Objects with flag 0x860=1, alpha blend ON
  • Pass 3 (Decal): Objects with flag 0x85F=1, stencil+depth bias enabled
  • Post-alpha: Iterate children calling vtable0x48

Scene_RenderFrame (0x60DA0) — Detailed

Per-frame: build vertex buffers with zigzag triangle strips (0x20 bytes/vertex),
render sprites, construct font→texture via Font_RenderToTextureComplex, free temp MeshWorld

Key insight: The game uses a fixed-timestep update with variable rendering.
If the game falls behind, it caps accumulated lag at 1000ms to prevent
the "spiral of death" where too many updates cause more lag.


🔗 Related Documents

Screenshot Comparison

types : analysis

📂 View source on GitHub


Hamsterball Screenshot Comparison - April 16, 2026

Test Environment

  • Wine 9.0 on Linux
  • Xvfb :99 (1024x768x24) — no real GPU
  • MinGW cross-compiled i686-w64-mingw32

Original Game (Hamsterball.exe)

  • File: original_fullscreen.png
  • Result: D3DERR_NOTAVAILABLE error dialog
  • Wine's D3D8 implementation requires a real GPU for the original game's
    fullscreen hardware vertex processing mode

Reimplementation (hamsterball.exe)

  • File: reimpl.png
  • Result: D3D8 device created successfully, window shows clear color
  • Our reimpl uses Windowed mode with software vertex processing fallback,
    so it initializes but shows blank (purple-gray) screen because the
    game loop rendering hasn't been wired up with actual geometry yet
  • Console logs: "[D3D8] Device created, 1024x768", "[Load] Arena-WarmUp: 17 objects",
    "[Load] Ball at (184.9, 26.4, 183.4)"

API Call Comparison (from Wine traces)

Original Game D3D8 Calls

  • Direct3DCreate8(D3D_SDK_VERSION)
  • GetAdapterDisplayMode(D3DADAPTER_DEFAULT)
  • CreateDevice(HAL, HARDWARE_VERTEXPROCESSING) → FAILS on Xvfb
  • Also calls: BASS_Init, BASS_MusicLoad, DirectSoundCreate8, DirectInput8Create

Our Reimplementation D3D8 Calls

  • Direct3DCreate8(D3D_SDK_VERSION) ✓
  • GetAdapterDisplayMode(D3DADAPTER_DEFAULT) ✓
  • CreateDevice(HAL, HARDWARE_VERTEXPROCESSING) → fails
  • CreateDevice(HAL, SOFTWARE_VERTEXPROCESSING) → succeeds ✓
  • SetRenderState calls (ZENABLE, ZWRITEENABLE, LIGHTING, AMBIENT, SHADEMODE, etc.) ✓
  • SetLight(0, directional) ✓
  • DirectInput8Create → keyboard + mouse ✓
  • DirectSoundCreate8 → fails gracefully (non-fatal) ✓

Next Steps

  1. Run on real GPU (not Xvfb) for full rendering comparison
  2. Add Wine D3D8 call tracing wrapper (WINEDEBUG=+d3d8) to capture exact call sequence
  3. Compare our CreateDevice params, SetRenderState sequence, and SetTransform calls
  4. Implement MESHWORLD geometry rendering to fill the blank screen

🔗 Related Documents

SpeedCylinder Global Spawner

types : tools
keywords :

📂 View source on GitHub


SpeedCylinder Global Spawner

Overview

Spawns a SpeedCylinder (Pendulum) at Player 1's position in any level, not just Level 6 (Up). Pure CE AutoAssembler script — no bass.dll proxy required.

Files

  • SpeedCylinderSpawn.CEA — CE AutoAssembler script (pure CEA, no Lua)

Installation

  1. Copy SpeedCylinderSpawn.CEA to your Cheat Engine scripts folder
  2. In CE: File → Load → select SpeedCylinderSpawn.CEA
  3. Enable the script

Usage

  1. Add the address SpawnFlag to your CE address list (Advanced → Add Address, or type "SpawnFlag" in the address bar after enabling the script)
  2. Enter a race or arena (any level works)
  3. Set SpawnFlag to 1 — a SpeedCylinder spawns at Player 1's current position
  4. The flag auto-resets to 0 after spawning
  5. Set to 1 again to spawn another

How It Works

The script hooks Scene_UpdateBallsAndState (0x41B540) which runs every frame. When SpawnFlag is set:

  1. Checks if a SpeedCylinder mesh is already loaded (Board+0x4788, only set on Up Race)
  2. If not, loads the mesh from levels\levelup-speedcylinder via MeshWorld_ctor
  3. Finds Player 1's ball in the all_balls_list and reads its position
  4. Allocates a Pendulum struct (0x150C bytes) and calls Pendulum_ctor
  5. Registers the new SpeedCylinder in the Board's mechanical objects list (Board+0x2578)

Verified Addresses (Ghidra, 2026-06-22)

Address Function Convention
0x41B540 Scene_UpdateBallsAndState __thiscall (ECX=Board)
0x4BA57B operator_new __cdecl (RET)
0x461510 MeshWorld_ctor __thiscall (RET 0x8, 2 params)
0x436A20 Pendulum_ctor __thiscall (RET 0x18, 6 params)
0x453810 AthenaList_Append __thiscall (RET 0x4, 1 param)
0x4D1140 "levels\levelup-speedcylinder" String constant
0x5341E0 App global pointer Runtime global

🔗 Related Documents

Startup API Replication Spec

types : project
keywords :

📂 View source on GitHub


Startup API Replication Spec

This document maps the original Hamsterball.exe startup sequence to our
open-source reimplementation. Each step is verified against Ghidra
decompilation of App_Initialize_Full (0x429530).

Original Flow: WinMain (0x4278E0)

WinMain → App_Initialize_Full → App_Run → App_Shutdown

App_Initialize_Full Steps (0x429530)

Step Original Our Equivalent Status
1 App_Initialize (0x46BB40) — 12 sub-steps: Registry, Window, DirectInput8, BASS audio, Graphics SDL_Init, window, GL context, audio, input
2 Set windowed mode flag (App+0x174->0x7d1=1) SDL_WINDOW_RESIZABLE
3 LoadCursorA(hInst, "BLANKCURSOR") SDL_ShowCursor(0) during gameplay
4 SetDisplayMode(800, 600) via vtable[0x8c] SDL_CreateWindow 800x600
5 D3D device check + SetRenderState(D3DRS_LIGHTING, TRUE, SHADEMODE=3) glEnable(GL_LIGHTING), glShadeModel(GL_SMOOTH)
6 Graphics_FindOrCreateTexture("shadow.png") texture_load("shadow")
7 MusicChannel_LoadAndAppend("music\music.mo3") Mix_LoadMUS (placeholder)
8 LoadJukebox("jukebox.xml") TODO: parse jukebox.xml
9-10 RegKeyList_CopyFromSibling (music config channels) N/A for open-source
11 Continue music setup N/A
12 RegKey_Open config_load()
13 RegKey_ReadDword("PlayCount") default=20 config_defaults()
14 Set initialized flag (App+0x200=1) g_state = GAME_STATE_MENU
15-16 InputDevice(0, type=1) → keyboard SDL keyboard state
17-18 InputDevice(1, type=2) → mouse SDL mouse events
19-20 InputDevice(2, type=4) → joystick1 SDL_JoystickOpen
21-22 InputDevice(3, type=5) → joystick2 SDL_JoystickOpen
23 RegKey_Close N/A
25 vtable0xa0 = ShowWindow/MainMenu ui_render_title()
26 load_assets() (our addition)

App_Run Game Loop (0x46BD80)

Original Our Equivalent Status
PeekMessage → DispatchMessage SDL_PollEvent
Scene_Update physics_update(dt)
Scene_Render → BeginScene glClear + glMatrixMode set
Scene_RenderAllObjects render_level_objects + render_ball
Scene_RenderScoreHUD ui_render_hud
Graphics_PresentOrEnd → EndScene + Present SDL_GL_SwapWindow
Sleep(targetFrameTime - elapsed) SDL_Delay
Target: 33ms/frame (~30fps) 1000/30 = 33ms

App_Shutdown (0x46DB10)

Original Our Equivalent Status
BASS_Free Mix_CloseAudio
DirectInput8_Release N/A (SDL handles)
Graphics_Release glDeleteTextures + SDL_GL_DeleteContext
DestroyWindow SDL_DestroyWindow
UnregisterClassEx SDL_Quit

Key Constants (from binary data section)

Address Name Value Usage
0x4CF4C0 FRICTION 0.95f Per-frame velocity damping
0x4CF434 Y_DAMP 0.8f Vertical bounce damping
0x4CF4B8 SPEED_FRICTION 0.99f Continuous velocity decay
Ball+0x284 Ball radius 35.0f (0x420C0000) Collision sphere
Ball+0x188 max_speed 5000.0f Velocity clamp

Status Key

  • ✅ Implemented and matching original behavior
  • ⏳ Partially implemented / placeholder
  • ⬜ Not yet implemented

🔗 Related Documents

Struct Definitions

types : analysis
keywords :

📂 View source on GitHub


Hamsterball - Struct Definitions

This directory contains C header files defining the core game structures
extracted from decompiler analysis of Hamsterball.exe. These are also applied
as Ghidra data types for improved decompilation.

Ghidra Structs (Applied)

Struct Size Fields Source
Ball 3148 bytes 38 named fields Ball_ctor2, Ball_Update, Ball_dtor2
Vec3 12 bytes 3 float fields Vec3_Init
App 2328 bytes 22 fields App_Initialize_Full, App_Run
Gadget 2156 bytes 16 fields Gadget_ctor
SceneObject 212 bytes 15 fields SceneObject_ctor
ArenaBoard 16 bytes 4 fields ToggleTimer_Init
ArenaBoardObj 18380 bytes 6 fields ArenaBoard_ctor

C Header Files

File Struct Description
ball_struct.h Ball Player ball physics object (~0xC98 bytes)
gadget_struct.h Gadget Base class for scene objects (0x870 bytes)
scene_struct.h Scene Main game state container (~0x1000 bytes)
board_struct.h Board Level/board state (inherits Gadget+Scene, ~0x4368)
app_struct.h App Global app singleton (g_App at 0x4FD680)
rumbleboard_struct.h ArenaBoard Haptic feedback timer (0x14 bytes)

Inheritance Hierarchy

SceneObject (vtable 0x4D934C) - base 3D object with position/rotation/scale
  └─ Gadget (+0x000, vtable 0x4D9170, size 0x870) - adds lists, app_ptr, name
       └─ Board (+0x870 base, vtable 0x4D0260, size ~0x4368) - level state
            └─ ArenaBoard (vtable 0x4D1358, size ~0x47D4) - rumble mode board

Ball (vtable 0x4CF3A0, size 0xC98) - player ball (NOT in Gadget hierarchy)
  - Created by Board_ctor, stored at Board+0x361C

Key Vtables

Name Address Functions
Ball 0x4CF3A0 9 methods (+0x27F0, +0x5100, +0x2DE0, +0x2A70, +0x8390, +0x1590, +0x2650, +0x2C10, +0x9480)
Scene/Board 0x4D0260 Scene_DeletingDtor
SceneObject 0x4D9170 SceneObject_ScalarDtor (Gadget base)
SceneObject 0x4D934C SceneObject_dtor (standalone)
ArenaBoard 0x4D1358 ArenaBoard_dtor
GameObject 0x4CF314 sub2_dtor (Ball ctor initial vtable)

Ball Field Map (Key Offsets)

Offset Ghidra Name Type Value/Description
0x000 dwVtable uint 0x4CF3A0
0x008 nCollision_result int collision system result
0x00C nString_timer int Countdown (200=show), frees display_string
0x010 dwApp_state uint App state ptr
0x014 dwScene uint Scene ptr
0x018 nPlayer_index int -1=AI, 0+=player
0x01C dwRender_callback uint Render/audio vtable
0x108 dwTimer uint Timer_Init target
0x150 flAccumulated_time float Frame delta accumulator
0x158 flPrev_pos_x/y/z float[3] Previous frame position
0x164 flCur_pos_x/y/z float[3] Current position
0x170 flVel_x/y/z float[3] Velocity
0x188 flMax_speed float 5000.0f
0x18C flSpeed_scale float 1.0f
0x1A4 dwCollision_mesh uint CollisionMesh ptr
0x1A8 flGravity_vec_x/y/z float[3] Gravity vector
0x1B8 dwRender_ctx_1 uint RenderContext 1
0x1C8 flRender_alpha float 0.75f
0x208 dwRender_ctx_2 uint RenderContext 2
0x20C flColor_a/r/g/b float[4] RGBA (1.0f init)
0x254 bUses_alpha byte color_a != 1.0f
0x264 pRumble_timer1 byte[20] ArenaBoard timer 1
0x278 flGravity_scale float 0.1f
0x281 bUnused_init_flag byte 1=init (DEAD: never read)
0x284 flRadius float 27.0f
0x290 pRumble_timer2 byte[20] ArenaBoard timer 2
0x2A4 - float 5.0f
0x2FC flTimer_bf float 1.0f, countdown
0x768 bCam_active byte Camera follow on
0x764 flCam_follow float 1.0f cam lerp
0xC28 dwDisplay_string uint Timer string ptr
0xC3C bTeleport_active byte Teleport flag
0xC40 flTeleport_x/y/z float[3] Teleport destination

Applying Structs

The create_struct MCP tool requires JSON format for the fields parameter:

[{"name": "field_name", "offset": 0, "type": "uint"}]

Ghidra ECX this parameters cannot be retyped via API. Plate comments document intended types.
To fully apply: manually retype this in Ghidra GUI (right-click -> Retype Variable -> Ball*).


🔗 Related Documents

Structures and Types

types : objects
keywords :

📂 View source on GitHub


Hamsterball - Structures and Types

Game Data Types (INFERRED from binary and asset analysis)

3D Vector Types

// CONFIRMED - observed in MESH and MESHWORLD files
typedef struct {
    float x, y, z;
} Vec3;

typedef struct {
    float x, y, z, w;  // w often = 1.0 or 0.0
} Vec4;

typedef struct {
    float m[3][3];  // or m[4][3] with padding
} Matrix3x3;

MESH File Header (HIGH CONFIDENCE)

// Based on analysis of Sphere.MESH, Bell.MESH, Hamster.MESH
typedef struct {
    uint32_t version;          // Always 1
    uint32_t name_length;      // Length of mesh name string
    char     name[];            // Null-terminated, padded to name_length
    // Followed by transform/material data
} MeshHeader;

// After header, per-object data:
// Position: Vec3 (3 floats)
// Scale: Vec3 (3 floats, typically 1.0)
// Bounding sphere/box data
// Material properties:
//   - Diffuse color: Vec3 (RGB floats)
//   - Ambient color: Vec3 (RGB floats)  
//   - Specular color: Vec3 (RGB floats)
//   - Specular power: float (typically 25.0)
// Texture name: length-prefixed string (e.g., "HamsterBall.png")
// Then vertex/index data

MESH Vertex Data (CONFIRMED from string references)

*MESH_NUMVERTEX - vertex count
*MESH_VERTEX - vertex position data
*MESH_NUMTVERTEX - texture vertex count  
*MESH_TVERT - texture coordinate data
*MESH_NUMFACES - face count
*MESH_FACE - face indices
*MESH_FACENORMAL - face normals
*MESH_VERTEXNORMAL - vertex normals

These are 3DS Max ASE format strings — the MESH binary format contains
equivalent data in binary form.

MESHWORLD Level Format (HIGH CONFIDENCE)

// Based on Arena-SpawnPlatform.MESHWORLD and Arena-Beginner.MESHWORLD analysis
typedef struct {
    uint32_t object_count;  // Number of objects in level
    // Followed by object_count objects
} MeshWorldHeader;

// Each object starts with:
// uint32_t type_string_length;
// char type_string[type_string_length];  // Null-terminated/padded
//   Known types: START1-1, START2-1, START2-2, FLAG02, FLAG04,
//   FLAG06, FLAG07, SAFESPOT, CAMERALOOKAT, PLATFORM,
//   N:SINKPLATFORM, E:NODIZZY<TIME>50</TIME>
//
// Position data: Vec3 (3 floats)
// Orientation: Vec3 (3 floats, Euler angles or direction)  
// Scale: float (1.0 = normal)
// Additional per-type data:
//   - PLATFORM: texture name, mesh reference, physics properties
//   - CAMERALOOKAT: position, look-at target
//   - FLAG: position, checkpoint type
//   - SAFESPOT: position, safe zone properties

// Object types with embedded XML properties:
// N:SINKPLATFORM = platform that sinks after player steps on it
// E:NODIZZY<TIME>N</TIME> = disables dizzy effect for N seconds
// E:GROWSOUND = sound trigger for growth effect

Game Configuration (HS.CFG) - SPECULATIVE

// Based on hex analysis of HS.CFG (1300 bytes)
// Fixed-size records
typedef struct {
    char name[64];      // Null-padded player name
    float scores[?];    // High scores per race
    int32_t flags[?];   // Unlock flags
} PlayerRecord;

// File appears to contain 5 player slots:
// "R. FINK", "SQUEAKS", "HAMMSTURABBI", "PEEPUMS", "MR. RAPTIS"

Race Configuration (RaceData.xml) - CONFIRMED

<!-- Per-race timing data -->
<RACENAME>
    <TIME>time_limit_seconds</TIME>
    <PAR>par_time_seconds</PAR>
    <WEASEL>weasel_time</WEASEL>      <!-- Best possible time -->
    <GOLD>gold_time</GOLD>            <!-- Gold medal threshold -->
    <SILVER>silver_time</SILVER>      <!-- Silver medal threshold -->
    <BRONZE>bronze_time</BRONZE>      <!-- Bronze medal threshold -->
    <CAM>camera_param</CAM>           <!-- Camera behavior parameter -->
</RACENAME>

Music Configuration (Jukebox.xml) - CONFIRMED

<SONG>
    <NAME>display_name</NAME>
    <HEX>mo3_position_hex</HEX>  <!-- Pattern position in MO3 file -->
</SONG>

Font Description Format - CONFIRMED

typedef struct {
    uint32_t version;          // = 1
    uint32_t height;           // e.g., 111 (for ShowcardGothic72)
    uint32_t base_char;        // First character code (e.g., 32 = space)
    uint32_t texture_count;    // Number of texture pages
    // ... per-glyph entries with:
    //   char_code (uint32)
    //   x_position (float)     // X in texture atlas
    //   width (float)          // Glyph width in texture
    //   x_offset (float)       // Horizontal offset
    //   y_offset (float)       // Vertical offset
    //   advance (float)        // X advance
    //   texture_index (uint32) // Which texture page
} FontDescription;

Import Table Structures (Addresses)

D3D8 (d3d8.dll):
  Direct3DCreate8 @ IAT 0x4CF050

DINPUT8 (DINPUT8.dll):
  DirectInput8Create @ IAT 0x4CF29C

DSOUND (DSOUND.dll):
  DirectSoundCreate @ IAT 0x4CF01C (ordinal import)

BASS (BASS.dll):
  BASS_Init @ IAT 0x4CF04C (string ref 0x4D9238 -> string at VA 0x4D9238)
  BASS_Stop @ IAT 0x4CF050 (same as D3D8 entry area?)
  BASS_Free
  BASS_Start
  BASS_SetConfig
  BASS_ErrorGetCode
  BASS_MusicLoad
  BASS_MusicPlayEx
  BASS_ChannelSetAttributes
  BASS_ChannelStop

Key Global Addresses (from string cross-references)

Address (VA) Description Confidence
0x4D9384 App::Initialize debug strings CONFIRMED
0x4D8A98 Graphics::Initialize debug strings CONFIRMED
0x4D88A8 Graphics::Defaults debug strings CONFIRMED
0x4D0634 BoardLevel3::BoardLevel3 debug strings CONFIRMED
0x4D5DB0 "BallPath" string CONFIRMED
0x4D8708 "HAMSTERBALL TOURNAMENT!" string CONFIRMED
0x4D3EAC "CLICK HERE TO PLAY!" string CONFIRMED
0x4D03D0 "RACE TIME:" string CONFIRMED
0x4D2659 "DATA\HS.CFG" path string CONFIRMED
0x4D8F9E "%s.mesh" format string CONFIRMED

SceneObject Structure (vtable 0x4D934C, CONFIRMED from Ghidra decompilation)

// Size: 0xD4 (212 bytes)
// Constructor: SceneObject_ctor at 0x46B4F0
typedef struct {
    void*    vtable;          // +0x000 - Always 0x4D934C
    int      gfxContext;      // +0x004 - Graphics context pointer (param_1 of ctor)
    int      field_08;        // +0x008 - Zeroed
    int      field_0C;        // +0x00C - Zeroed
    int      field_10;        // +0x010 - Zeroed
    int      field_14;        // +0x014 - Zeroed
    int      field_18;        // +0x018 - Zeroed
    int      field_1C;        // +0x01C - Zeroed
    // +0x20 to +0x87: padding / unknown
    char     visible;         // +0x088 - Visibility flag (1=visible, 0=hidden)
    int      zOrder;          // +0x08C - Z-order / object ID (-1 = unregistered)
    float    baseScale[5];   // +0x094 - 4x4 base scale matrix (initialized to identity 1.0,1.0,1.0,1.0)
    float    rotation[5];    // +0x0A8 - 4x4 rotation matrix (initialized to zero)
    float    worldMatrix[5]; // +0x0BC - 4x4 world transform matrix (initialized to zero)
    float    radius;         // +0x0CC - Bounding radius = sqrt(global_constant)
    int      type;            // +0x0D0 - Object type (3 = default)
} SceneObject; // Total: 0xD4

// Scene registration:
// Scene_RegisterObject (0x453BD0): obj->zOrder = id; obj->vtable[3](); scene[0x710 + id*4] = obj;

Vec3 Structure (vtable 0x4CF300, CONFIRMED)

// Size: 20 bytes (0x14)
// Constructor: Vec3_Init at 0x453180
typedef struct {
    void*  vtable;    // +0x000 - 0x4CF300
    float  x;         // +0x004
    float  y;         // +0x008
    float  z;         // +0x00C
    float  w;         // +0x010 - Default 1.0 (0x3F800000)
} Vec3;

Scene Object Offsets (CONFIRMED from Scene_dtor and Scene_Update)

// Scene object (large, ~0xFC8+ bytes based on dtor iteration)
// Scene_dtor at 0x419770 shows these lists:
#define SCENE_STATIC_OBJECTS    0x22E   // Static objects (persistent, updated each frame)
#define SCENE_DYNAMIC_OBJECTS   0x335   // Dynamic objects (moving platforms etc.)
#define SCENE_SUB_OBJECTS       0x43B   // Sub-objects (linked to dynamic, removable)
#define SCENE_LEVEL_ORIG        0x22B   // Original Level object pointer
#define SCENE_LEVEL_CLONE       0x22C   // Cloned Level object pointer
#define SCENE_RESOURCE1          0x21F   // Resource pointer 1
#define SCENE_RESOURCE2         0xE92   // Resource pointer 2
#define SCENE_BALL_LIST         0xA75   // Ball/character list (position updates)
#define SCENE_EXTRA_LIST        0xC81   // Additional object list
#define SCENE_RENDER_LIST       0xD8B   // Render update list
// Scene_Update at 0x419C00:
#define SCENE_FRAME_COUNTER    0xD88   // Incremented each tick
#define SCENE_GAME_STATE        0x708   // 3 = racing
#define SCENE_SCREEN_OFFSET    0xA6E   // Screen offset (decrements 10/frame)
#define SCENE_DEMO_TIMER_ON    0x10D6  // Demo expiration timer active
#define SCENE_DEMO_COUNTDOWN   0x10D7  // Demo countdown frames
#define SCENE_UNPAUSE_FLAG     0x10DA  // Unpause gate

Key Global Addresses (continued)

Address (VA) Description Confidence
0x4D8D80 "Direct3DCreate8" import name CONFIRMED
0x4D0260 Scene vtable (36 virtual method entries) CONFIRMED
0x4D934C SceneObject vtable (10 virtual method entries) CONFIRMED
0x4CF300 Vec3 vtable CONFIRMED
0x4D9368 SceneObject secondary vtable (set by BaseDtor) CONFIRMED

Scene Vtable Constants

// Scene vtable at 0x4D0260 - slot offsets
#define SCENE_VT_DELETING_DTOR    0x00  // 0x425020: ~Scene + free
#define SCENE_VT_UPDATE           0x04  // 0x419C00: Scene_Update
#define SCENE_VT_RENDER           0x08  // 0x41A2E0: Scene_Render
#define SCENE_VT_HANDLE_INPUT     0x0C  // 0x4692F0: Menu item input handling
#define SCENE_VT_ACTIVATE_ITEM    0x10  // 0x469220: Activate current item
#define SCENE_VT_SELECT_ITEM      0x18  // 0x469280: Select current item
#define SCENE_VT_CLEAR_ITEM       0x2C  // 0x4692A0: Clear current item ptr
#define SCENE_VT_SAVE_CLEANUP     0x44  // 0x4692B0: Save and cleanup
#define SCENE_VT_INIT_SCENE       0x48  // 0x40B090: Level_InitScene
#define SCENE_VT_NOOP             0x24  // 0x44B840: Default empty stub

// SceneObject vtable at 0x4D934C - slot offsets
#define SOBJ_VT_DTOR              0x00  // 0x46B650: ~SceneObject
#define SOBJ_VT_SET_POSITION      0x04  // 0x46B490: Set position + update
#define SOBJ_VT_SET_SCALE         0x08  // 0x46B4B0: Set scale + update
#define SOBJ_VT_RENDER            0x0C  // 0x46B670: Build world matrix + D3D
#define SOBJ_VT_SET_VISIBLE       0x10  // 0x46B4D0: Toggle visibility
#define SOBJ_VT_DELETING_DTOR     0x1C  // 0x46B9F0: BaseDtor + free

// Scene current item pointer (used by vtable slots 3-6, 11-12)
#define SCENE_CURRENT_ITEM        0x864  // ptr to current menu/scene item

Rumble Board Arena Paths

// 15 arena level paths loaded by ArenaBoard_*_Init functions
"levels\\arena-WarmUp"       // 0x413C20
"levels\\arena-beginner"     // 0x413CE0
"levels\\arena-intermediate" // 0x414180
"levels\\arena-dizzy"       // 0x414240
"levels\\arena-tower"        // 0x4144B0
"levels\\arena-up"          // 0x414960
"levels\\arena-expert"      // 0x414B10
"levels\\arena-Odd"         // 0x414CE0
"levels\\arena-Toob"        // 0x414F00
"levels\\arena-Wobbly"     // 0x4153A0
"levels\\arena-Sky"         // 0x4158C0
"levels\\arena-Master"      // 0x416080
"levels\\arena-neon"        // 0x416F40
"levels\\arena-glass"       // 0x417DF0
"levels\\arena-impossible"  // 0x418540

// Tournament sub-level paths (Board subclasses)
"Levels\\Level8-Spinny"       // Toob Board "Rodenthood"
"Levels\\Level8-Saw"         // Toob Board "Rodenthood"
"Levels\\Level8-Fallout"     // Toob Board "Rodenthood"
"Levels\\Level8-Blockdawg1"  // Toob Board "Rodenthood"
"Levels\\Level8-Blockdawg2"  // Toob Board "Rodenthood"

UIListItem (0x444 bytes, created by UIListItem_ctor 0x4490A0)

struct UIListItem {
    char*       display_text;    // +0x00 (strdup'd)
    char*       subtext;         // +0x04 (strdup'd, used as key for UIList_SetTextByName)
    Vec3        position;        // +0x08..0x14 (Vec3 with init) [padding/unused?]
    AthenaList  children;        // +0x28..0x40 (AthenaList for sub-elements)
    uint32_t    color_r;         // +0x0C*4 (red component)
    uint32_t    color_g;         // +0x0D*4 (green component)
    uint32_t    color_b;         // +0x0E*4 (blue component)
    uint32_t    color_a;         // +0x0F*4 (alpha component)
    SceneObject* icon;           // +0x1C*4 (+0x70) — SceneObject for icon row
    int         linked_obj;      // +0x20*4 (+0x80) — linked scene object
    int         height;          // +0x24*4 (+0x90) — row height in pixels
    AthenaList  icon_list;       // +0x2C*4+0x10... (icon list for sub-icons)
    uint8_t     is_icon_row;     // +0x110 (0x441) — if 1, render as icon instead of text
    uint8_t     pad;             // +0x441
};

SimpleMenu / UIList (vtable 0x4D6A70)

struct SimpleMenu {          // extends Scene
    // ... Scene base fields ...
    char*       name;           // +0x868 ("Simple Menu")
    App*        app;            // +0x878
    Font*       normal_font;    // +0x87C
    Font*       selected_font;  // +0x880
    int         has_header_icon;// +0x888
    AthenaList  items;          // +0x88C (list of UIListItem*)
    int         item_count;     // +0x890
    // ... index tracking ...
    int         total_width;    // +0xCB0
    int         total_height;   // +0xCB4
    int         needs_layout;   // +0xCBC (flag: 1 = re-layout needed)
    UIListItem* selected_item;  // +0xCC0
    int         scroll_pos;     // +0xCCC
    SceneObject* up_scroller;    // +0xCA4
    SceneObject* dn_scroller;    // +0xCA8
    int         can_scroll;     // +0xCD0
    int         scroll_up_cooldown;  // +0xCD4
    int         scroll_dn_cooldown;  // +0xCD8
};

ArenaBoard (vtable PTR 0x4D1358, extends Board extends Scene)

struct ArenaBoard {
    // ... Board/Scene base fields ...
    int         base_score;     // +0x47AC = 6000
    uint8_t     is_tie_breaker; // +0x47C5
    uint8_t     tie_active;     // +0x47CC
    int         max_rounds;     // +0x47D0 = 0x19 = 25
    // Timer fields at +0x47C8 (ToggleTimer_Init/CleanupTimer/TickTimer)
    // Player scores at +0x11ED..+0x11F0 (4 players)
    int         round_end_timer;// +0x11EB (countdown after round ends)
    int         game_over_flag; // +0x11F1
};

Sprite (vtable 0x4D8F84)

struct Sprite {
    VTable*     vtable;         // +0x00 (0x4D8F84)
    Graphics*   gfx;            // +0x04
    RenderContext rc;           // +0x08..0x60
    Texture*    texture;        // +0x50
    uint8_t     pad;            // +0x54
    float       width;          // +0xC8 (= texture+0x14)
    float       height;         // +0xCC (= texture+0x18)
    uint8_t     visible;        // +0xD0 = 1
    uint8_t     pad2;           // +0xD1
    // Color/material fields: +0x60..0xC4
};

PRNG State (RNG_Rand uses this as 'this')

struct RNGState {
    VTable*     vtable;         // +0x00
    int         read_ptr;       // +0x04 (wraps at 55)
    int         write_ptr;      // +0x08 (wraps at 55)
    uint32_t    buffer[55];     // +0x0C..0xE4 (circular buffer)
};
// Algorithm: buf[read] = (buf[read] + buf[write]) & 0x3FFFFFFF
// Return: (result >> 6) % range
// Signed mode: if param_2==1 && Rand(2)==0, negate

AthenaString Class (0x473500 family)

struct AthenaString {
    char*       buffer;         // +0x00: pointer to char buffer
    uint32_t    capacity;       // +0x04: allocated size (includes null terminator)
    uint32_t    length;         // +0x08: string length (not including null)
    uint32_t    null_flag;      // +0x0C: 1 if string was set to NULL originally
    // Total: 0x10 bytes
};

// Key methods:
// - AthenaString_AssignCStr (0x473500): assigns C string, frees old, allocates new
// - AthenaString_dtor (0x4736b0): frees buffer, zeros fields, sets vtable to base
// - AthenaString_AssignCRLF (0x473a50): assigns "\r\n"
// - AthenaString_SprintfToBuffer (0x4bae43): sprintf into caller buffer
// - AthenaString_Assign (0x4737f0): full assign with formatting
// - AthenaString_Format (0x466c70): format string (98 xrefs)
// - AthenaString_Sprintf (0x4bbdfd): sprintf to internal buffer

// VTable at 0x4D290C (base dtor vtable)

🔗 Related Documents

The [[39040799821588|ball object]]

types : objects
keywords :

📂 View source on GitHub


Hamsterball — The Ball Object: Complete Modding Reference

Scope: Original Hamsterball.exe (PE32, i386, Athena engine)
Last Updated: 2026-06-25
Target Audience: Modders, DLL injectors, reverse-engineers


Table of Contents

  1. What Is the ball object?
  2. Memory Layout (0xC98 bytes)
  3. PhysicsObject / CollisionMesh (at +0x1A4, size 0xCB0)
  4. Vtable Methods
  5. Key Modifiable Fields
  6. Physics Pipeline (Ball_Update)
  7. Collision & Events
  8. Rendering
  9. AI & Special Modes
  10. Modding Hook Points
  11. Quick Reference

What Is the Ball Object?

The Ball is the player-controlled hamsterball. It is the first parameter of Ball_Update (0x405E00) and the core physics actor in every race and arena mode. The Ball derives from GameObject and has a vtable at 0x4CF3A0 with 65+ entries (not 9 — see Vtable Methods).

Constructor chain:

Ball_ctor (0x40AFE0)      → allocate 0xC98 bytes, set base vtable
Ball_ctor2 (0x4039E0)     → init physics defaults, radius, gravity, timers
Scene_SpawnBallsAndObjects → place at START object, set player_index

Constructor field overrides: Ball_ctor2 sets initial defaults. Then Ball_InitPhysicsDefaults (vtable[1], 0x405100) is called during setup and overrides many fields with different values. The runtime values differ from the raw ctor2 defaults:

Field ctor2 value InitPhysicsDefaults value
+0x188 max_speed 5000.0 6.0
+0x1A0 speed_scale 1.0 0.2
+0x278 gravity_scale 0.1 0.5
+0x27C (unknown) 0.0 0.2
+0x284 radius 27.0 35.0

Always reference the InitPhysicsDefaults column for actual in-game defaults.

Destructor: Ball_dtor (0x4027F0) → Ball_dtor2GameObject_dtor → optionally free(this).


Memory Layout

Total size: 0xC98 bytes (3,224 bytes)
Base class: GameObject (vtable 0x4CF314)
Own vtable: 0x4CF3A0

Critical Fields (Modder-Useful Offsets)

Offset Type Name Default (runtime) Description
0x000 void** vtable 0x4CF3A0 Ball vtable (65+ entries)
0x00C int32 string_timer 0 Countdown to free display_string
0x010 void* app_state App pointer (from param_1+0x878)
0x014 void* scene Pointer to parent Scene/Board object
0x018 int32 player_index -1 -1 = AI / none, 0 = Player 1, 1 = Player 2, etc.
0x014 void* render_callback UITimer sub-object start
0x150 float accumulated_time 0.0 Delta-time accumulator per frame
0x154 int32 rng_seed Random seed (set by RNG_Rand)
0x158 float prev_pos_x 0.0 Previous frame X
0x15C float prev_pos_y 0.0 Previous frame Y
0x160 float prev_pos_z 0.0 Previous frame Z
0x164 float pos_x 0.0 Current position X
0x168 float pos_y 0.0 Current position Y
0x16C float pos_z 0.0 Current position Z
0x170 float force_x 0.0 Force accumulator X (cleared each frame, filled by ApplyForce)
0x174 float force_y 0.0 Force accumulator Y
0x178 float force_z 0.0 Force accumulator Z
0x17C float accel_x 0.0 Acceleration X
0x180 float accel_y 0.0 Acceleration Y
0x184 float accel_z 0.0 Acceleration Z
0x188 float max_speed 6.0 Speed cap (ctor2=5000.0, overridden to 6.0 by InitPhysicsDefaults)
0x18C float speed_scale 1.0 Global speed multiplier
0x190 float unknown_190 -1.0 Unknown (set to -1.0 in ctor2)
0x194 float unknown_194 -1.0 Unknown (set to -1.0 in ctor2)
0x19C byte unknown_19C 0 Unknown flag
0x1A0 float speed_cap_multiplier 0.2 Secondary cap multiplier (ctor2=1.0, overridden to 0.2)
0x1A4 PhysicsObject* physics_object PhysicsObject (CollisionMesh) pointer, size 0xCB0 — see PhysicsObject
0x1A8 float[3] gravity_vec (0,1,0) Gravity direction vector
0x1B8 render_ctx_1 RenderContext sub-object
0x1C8 float render_alpha 0.75 Render context #1 alpha
0x1BC float render_scale_x 0.25 Render context scale
0x1C0 float render_scale_y 0.25 Render context scale
0x1C4 float render_scale_z 0.25 Render context scale
0x204 byte render_flag_204 1 Unknown render flag
0x208 render_ctx_2 RenderContext sub-object #2
0x20C float color_r 1.0 RGBA red
0x210 float color_g 1.0 RGBA green
0x214 float color_b 1.0 RGBA blue
0x218 float color_a 1.0 RGBA alpha
0x23C float render2_r 1.0 Render context #2 RGBA
0x240 float render2_g 1.0 Render context #2 RGBA
0x244 float render2_b 1.0 Render context #2 RGBA
0x248 float render2_a 1.0 Render context #2 RGBA
0x254 uint8 uses_alpha 0 True if color_a != 1.0
0x25C float unknown_25C 0.0 Unknown (modified in Ball_Update spin friction)
0x260 uint8 boost_hit_flag 0 Set on boost pad contact
0x264 uint8[0x14] rumble_timer1 ArenaBoard timer sub-object
0x26C int32 unknown_26C 20 Unknown int (0x14)
0x278 float gravity_scale 0.5 Gravity multiplier (ctor2=0.1, overridden to 0.5)
0x27C float unknown_27C 0.2 Unknown (ctor2=0.0, overridden to 0.2)
0x280 uint8 unknown_280 0 Unknown flag
0x281 uint8 unused_init_flag 1 DEAD: set to 1 in ctor, 0 on spawn; never read by any function
0x284 float radius 35.0 Ball radius (ctor2=27.0, overridden to 35.0; shrunk to 13.0 on odd race shrink)
0x288 float unknown_288 0.0 Unknown
0x290 uint8[0x14] rumble_timer2 Second ArenaBoard timer
0x29C float unknown_29C 1.0 Unknown (modified in Ball_Update)
0x2A4 float spin_rate 5.0 Angular spin factor
0x2A8 float[3] speed_modifier (0,0,0) Vec3 speed modifier (init by Vec3_Init)
0x2B8 float[3] accel_vec (0,0,0) Acceleration vector
0x2C0 float force_x_2C0 0.0 Secondary force accumulator X
0x2C4 float force_y_2C4 0.0 Secondary force accumulator Y
0x2C8 float force_z_2C8 0.0 Secondary force accumulator Z
0x2CC uint8 force_disable 0 If 1, Ball_ApplyForce is skipped
0x2D5 uint8 unknown_2D5 0 Unknown (cleared by timer decay)
0x2D8 int32 unknown_2D8 0 Unknown
0x2DC float lgp_x 0.0 Last Grounded Position X (LGP)
0x2E0 float lgp_y 0.0 Last Grounded Position Y (LGP)
0x2E4 float lgp_z 0.0 Last Grounded Position Z (LGP)
0x2E8 uint8 event_flag 0 Checkpoint-hit event marker
0x2E9 uint8 impact_shatter 0 NOT on_ramp/ground flag! Sticky limit/trajectory flag (E:LIMIT + type-5 collision). Never cleared within Ball_Update. See docs/agent-knowledge/ball-ground-detection.md
0x2EC int32 unknown_2EC 0 Unknown
0x2F0 uint32 force_count 0 Number of forces applied this frame
0x2F4 int32 unknown_2F4 0 Unknown
0x2F8 uint8 update_in_progress 0 Set 1 during Ball_Update
0x2F9 uint8 frozen 0 Stuck on surface (velocity zeroed)
0x2FC float freeze_timer 1.0 Countdown while frozen (ctor2=1.0, not 150 as previously documented)
0x300 uint32 freeze_val 0 (ctor2=0, not 150 as previously documented)
0x30A char* display_string_ptr NULL Floating text string (freed when string_timer hits 0)
0x30F uint8 teleport_flag 0 Teleport pending flag
0x310 uint8 state_active 1 General active flag
0x311 float teleport_x 0.0 Teleport destination X
0x312 float teleport_y 0.0 Teleport destination Y
0x313 uint8 unknown_313 0 Camera/limit-related flag
0x314 float ambient_sound_timer 0.0 Timer for ambient sound decay
0x324 uint8 in_tube 0 If true, Ball_Update returns immediately — no physics!
0x328 int32 unknown_328 -1 Unknown (set to 0xFFFFFFFF)
0x32C AthenaList trail_list_32C Trail particle list
0x700 int32 sound_3d_handle 3D sound effect handle
0x744 int32 unknown_744 0 Unknown
0x748 int32 unknown_748 0 Unknown
0x74C int32 unknown_74C 0 Unknown
0x750 int32 unknown_750 0 Unknown
0x754 int32 unknown_754 0 Unknown
0x768 uint8 cam_active 1 Camera follow enabled
0x769 uint8 cam_flag_769 0 Camera snap flag
0x76A uint8 cam_flag_76A 0 Cleared on surface snap
0x764 float cam_follow_factor 1.0 Camera follow lerp factor
0x778-0x784 float[4] unknown_778 0.0 Unknown camera-related fields
0x788 float[16] matrix_2 zeros Second matrix (4×4, zeroed in ctor2)
0x7C8 float[16] matrix_1 identity 4×4 transform matrix
0x808 float[2] unknown_808 (0,0) Unknown (NOT freeze_counter — see freeze_timer at 0x2FC)
0x810 AthenaList list_810 Unknown list
0xC28 char* display_string NULL Floating text above ball
0x0C38 int32 unknown_C38 -1 Unknown (set to 0xFFFFFFFF)
0xC3C uint8 teleport_active 0 Teleport in progress
0xC40 float teleport_x 0.0 Teleport destination X
0xC44 float teleport_y 0.0 Teleport destination Y
0xC48 float teleport_z 0.0 Teleport destination Z
0xC4C uint8 is_shrunk 0 odd race shrunk state (E:SHRINK=1, E:GROW=0)
0xC50 int32 unknown_C50 0 Unknown
0xC54 int32 unknown_C54 0 Unknown (AthenaList data pointer)
0xC58 uint8 unknown_C58 0 Unknown
0xC5C int32 unknown_C5C 0 Unknown (alternate_state flag used by ApplyForce)
0xC88 float[16] world_matrix identity 4×4 world transform for rendering

Field naming note: Fields at +0x170/+0x174/+0x178 were previously documented as "vel_x/y/z". They are actually force accumulators — cleared to zero at the start of each Ball_Update frame, then populated by Ball_ApplyForce. The actual per-frame velocity is the delta between pos (0x164) and prev_pos (0x158), computed internally by Ball_Update and stored temporarily.


Physics Object

The field at +0x1A4 is a pointer to a PhysicsObject (internally called CollisionMesh), allocated as operator_new(0xCB0) (3264 bytes) and constructed via CollisionMesh_ctor (0x456D80).

This is NOT just a collision mesh — it stores the ball's runtime physics state including gravity normal, computed velocity, collision entries, and AI parameters.

Construction

CollisionMesh_ctor(this, ball_ptr)    // 0x456D80
  → Sets vtable to 0x4D8E10 (Mesh_DeletingDtor)
  → Stores ball back-reference at +0x10
  → Inits 3 AthenaLists at +0x18, +0x430, +0x848
  → Calls Ball_InitBattleMode(this)   // 0x456CD0

PhysicsObject Internal Layout (size 0xCB0)

Offset Type Default Description
+0x000 void** 0x4D8E10 Vtable (Mesh_DeletingDtor)
+0x010 void* ball ptr Back-reference to owner Ball
+0x018 AthenaList Collision entry list (type 1=ball-ball, 2=wall, 5=floor entries)
+0x430 AthenaList Second collision node list
+0x848 AthenaList Third list (event collision objects)
+0xC60 int32 3 Battle mode state (3 = arena default)
+0xC64 float (not set in ctor) — written by Ball_Update as speed
+0xC68 float 0.555 Battle mode friction parameter
+0xC6C float 1.0 → 600.0 CHASE distance (overridden to 600.0 by InitPhysicsDefaults)
+0xC70 float 1000.0 → 1200.0 HOME distance (overridden to 1200.0 by InitPhysicsDefaults)
+0xC74 float 0 Speed value (written by Ball_SetSpeed)
+0xC78 float 25.0 → 0.0 spin_angle (overridden to 0.0 by InitPhysicsDefaults)
+0xC7C byte 1 Unknown flag
+0xC80 float[3] (0,0,0) Unknown velocity vector
+0xC8C float[3] (0,-1.0,0) Gravity normal (default points down)
+0xC98 float[3] Computed velocity (written every frame by Ball_Update)
+0xCA4 float[3] (0,0,0) Direction vector (surface normal cache)

Collision Entry Structure

Each entry in the PhysicsObject+0x18 AthenaList is a struct accessed by Ball_Update as int*. Known fields:

Index Byte Offset Type Description
[0] +0x00 int32 Type: 1=ball-ball, 2=wall, 5=floor
[1] +0x04 int32 Unknown (not read by Ball_Update — verify before using)
[3] +0x0C Ball* Other ball (for type 1 = ball-ball collision)
[4] +0x10 Scene* Board pointer (used for +0x434, +0x43C lookups)
[8] +0x20 float Collision normal vector X
[9] +0x24 float Collision normal vector Y
[10] +0x28 float Collision normal vector Z
[12] +0x30 float Secondary vector X
[13] +0x34 float Secondary vector Y
[14] +0x38 float Secondary vector Z
[21] +0x54 float Collision depth/distance
[25] +0x64 PhysicsObject* Owner physics object (compared against ball's physics)

Note: The currCollision[1] == 4 check used in some mod frameworks targets offset +0x04, which is not read by Ball_Update. This field may be set during collision creation in Ball_AdvancePositionOrCollision (0x4564C0), but its meaning is unverified. Test with MessageBoxA dumps before relying on it.


Vtable Methods

The Ball vtable lives at 0x4CF3A0 with 65+ entries (not 9 as previously documented). The vtable is NOT terminated at index 9 — the NULL at index 9 is a valid entry, not an end marker.

Documented Vtable Entries

Vtable Offset Address Name Description
+0x00 0x4027F0 Ball_dtor Destructor — calls Ball_dtor2, optionally frees memory
+0x04 0x405100 Ball_InitPhysicsDefaults Sets runtime physics defaults (overrides ctor2 values)
+0x08 0x402DE0 Ball_CollisionCheck Per-frame collision against level mesh + planes
+0x0C 0x402A70 Ball_OnCollision Collision response dispatcher
+0x10 0x408390 Ball_AI_ChaseNearest AI opponent steering (finds nearest ball, applies force)
+0x14 0x401590 Ball_vtable5 Unknown (called from collision path)
+0x18 0x402650 Ball_ApplyForceWithMultipliers Apply directional force vector to velocity accumulators
+0x1C 0x402C10 Ball_vtable7 Unknown (render-related)
+0x20 0x409480 Ball_SplitAndExplode Called on E:BREAK collision events
... ... ... (many more entries up to 65+)
+0x104 0x408830 Ball_FallUpdate Fall animation + respawn logic

Base GameObject vtable (0x4CF314) provides shared destructor logic used before Ball-specific cleanup.


Key Modifiable Fields

Position / Velocity (Direct Write Safe)

Writing to these offsets from a DLL hook is safe during Ball_Update or Scene_UpdateBallsAndState:

// Instant teleport (no animation)
*(float*)(ball + 0x164) = newX;
*(float*)(ball + 0x168) = newY;
*(float*)(ball + 0x16C) = newZ;

// Zero force accumulators (emergency stop)
*(float*)(ball + 0x170) = 0.0f;
*(float*)(ball + 0x174) = 0.0f;
*(float*)(ball + 0x178) = 0.0f;

Note: +0x170/+0x174/+0x178 are force accumulators, NOT persistent velocity. They are cleared to zero at the start of each Ball_Update frame. The actual per-frame velocity is computed internally as pos - prev_pos.

Physics Constants (Game Data Section)

These global floats at 0x4CF3xx affect ALL balls. Patch once, affects every frame:

Address Value Effect
0x4CF368 0.0 Float epsilon
0x4CF36C 0.75 Force multiplier when is_shrunk (odd race)
0x4CF374 0.2 Force multiplier on ice
0x4CF378 0.0 Force multiplier in tube (0 = no control!)
0x4CF380 0.25 Force multiplier after first frame of input
0x4CF39C 0.037 Collision radius multiplier
0x4CF3E8 6.0 Ice friction factor
0x4CF3F0 0.95 Damping constant
0x4CF484 40.0 Collision mesh distance threshold
0x4CF48C 2.0 Y offset threshold (radius + epsilon)

Per-Ball Physics Overrides

Field Offset Runtime Default What It Does
radius 0x284 35.0 Collision + visual size. Shrunk to 13.0 on odd race shrink.
max_speed 0x188 6.0 Hard velocity cap. ctor2=5000.0, InitPhysicsDefaults=6.0.
speed_scale 0x18C 1.0 Global multiplier on ALL velocity changes.
gravity_scale 0x278 0.5 Gravity strength multiplier. ctor2=0.1, InitPhysicsDefaults=0.5.
speed_cap_multiplier 0x1A0 0.2 Secondary cap multiplier. ctor2=1.0, InitPhysicsDefaults=0.2.
in_tube 0x324 0 If non-zero, Ball_Update returns immediately — no physics!
force_disable 0x2CC 0 If non-zero, Ball_ApplyForce is skipped entirely.

Mod idea: Setting speed_scale = 2.0f gives a permanent speed boost. Setting gravity_scale = 0.0f makes the ball weightless (but collision snapping still applies).


Physics Pipeline

Ball_Update (0x405E00) — The Main Tick

Called once per frame for every active ball from Scene_UpdateBallsAndState (0x41B540).

Phase 1 — Reset & Timer Decay:

ambient_sound_timer *= decay_factor
Various timer decrements (trail, boost, sound cooldowns)
string_timer decrements (frees display_string at 0xC28 when it hits 0)

Phase 2 — Trail Particles:
If trail timer active, spawn ArenaScoreParticle particle at ball position + random offset.

Phase 3 — Force Accumulator Save & Clear:

prev_pos = pos                    // Save current position
saved_force = force_accumulators   // Copy +0x170/+0x174/+0x178
force_accumulators = (0,0,0)       // Clear for next frame

Phase 4 — Spatial Tree Build:
Build a temporary SpatialTree from the scene's collision mesh for this frame's collision queries.

Phase 5 — Collision Iteration:
Iterate PhysicsObject+0x18 AthenaList entries:

  • Type 1 (ball-ball): Compute collision response, apply forces, play sound, award points
  • Type 2 (wall): Reflect velocity, update surface normal, apply friction
  • Type 5 (floor): Set ground flag, snap to surface, trigger limit/trajectory events

Phase 6 — Spin & Roll Physics:
3 iterations of spin friction computation using gravity normal and surface velocity.

Phase 7 — Position Integration:

pos += computed_velocity           // Apply final velocity
display_pos = lerp(display_pos, pos, follow_factor)

Phase 8 — Teleport Override:
If teleport_flag (0x30F) is set, override position with teleport destination.

Ball_AdvancePositionOrCollision (0x4564C0) — Core Physics

Called from the scene update pipeline. 6-phase physics:

  1. Free Lists — Release trail points and collision markers from last frame
  2. Input Velocity — Add input force to velocity, clamp to max_speed
  3. Dampingvelocity *= (1-dt) + (1-damping)*dt
  4. Collision — If collision flag → vtable[0x1C] dispatch
  5. Gravity — Apply gravity_vec * gravity_scale with 0.95 damping
  6. Trail Recording — Record trail point on non-collision frames

How to Use Ball_ApplyForce (0x402650)

Ball_ApplyForce is the primary way to externally influence ball movement. It is called from input code (Ball_GetInputForce at 0x46EC30), AI code (Ball_AI_ChaseNearest at 0x408390), and event handlers (E:TRAJECTORY, E:JUMP).

Function Signature

void __thiscall Ball_ApplyForce(
    void* ball,      // ECX: Ball* (this)
    float force_x,   // EDX: X component of force direction
    float force_y,   // Stack: Y component
    float force_z,   // Stack: Z component
    float magnitude  // Stack: scalar multiplier
);

What it does:

  1. Normalize the (force_x, force_y, force_z) direction vector
  2. Multiply by magnitude and several conditional multipliers
  3. Accumulate the result into force accumulators at ball+0x170
  4. Increment force_count at ball+0x2F0

Conditional Multipliers (Applied in Order)

Condition Field / Global Multiplier Effect
force_count == 0 (first frame) DAT_004CF380 1.0 Full force on first press
force_count > 0 (held) DAT_004CF380 0.25 Quarter force while held
in_tube (ball+0x324) DAT_004CF378 0.0 Zero force — completely disabled
on_ice flag DAT_004CF374 0.2 Ice: 20% force + friction 6.0
is_shrunk flag (ball+0xC4C) DAT_004CF36C 0.75 Shrunk (odd race): 75% force
alternate_state (ball+0xC5C) DAT_004CF374 0.2 Alternate physics mode
Direction tweak DAT_004CF3E8 6.0 Scales the final direction vector

Early-Out Checks

Ball_ApplyForce returns immediately (does nothing) if ANY of these are true:

Condition Offset Value
frozen ball+0x2F9 1 — ball is snapped to a surface
force_disable ball+0x2CC 1 — force application globally disabled
freeze_counter ball+0x2FC > 0 — input freeze timer active

Writing Your Own Force Calls

Example 1: Push the ball forward instantly

// From a DLL hook — ball is the Ball* pointer
typedef void (__thiscall *tApplyForce)(void* ball, float fx, float fy, float fz, float mag);
tApplyForce ApplyForce = (tApplyForce)0x402650;

// Push forward (+Z) with strength 50.0
ApplyForce(ball, 0.0f, 0.0f, 1.0f, 50.0f);

Example 2: Reverse gravity burst

// Strong upward force — overrides gravity briefly
ApplyForce(ball, 0.0f, -1.0f, 0.0f, 200.0f);
// Note: -Y is "up" because gravity is +Y down

Example 3: Homing force toward a target

float dx = targetX - *(float*)(ball + 0x164);
float dy = targetY - *(float*)(ball + 0x168);
float dz = targetZ - *(float*)(ball + 0x16C);

// Normalize
float len = sqrtf(dx*dx + dy*dy + dz*dz);
if (len > 0.0f) {
    dx /= len; dy /= len; dz /= len;
    ApplyForce(ball, dx, dy, dz, 30.0f);
}

Forcing a Call Through the Vtable

If you want to be binary-compatible with potential mods that hook Ball_ApplyForce, call through the vtable instead of the raw address:

void** vtable = *(void***)ball;          // vtable at ball+0x00
tApplyForce pfn = (tApplyForce)vtable[6]; // vtable[0x18] = ApplyForce
pfn(ball, fx, fy, fz, magnitude);

Pitfall: Writing directly to ball+0x170 (force accumulators) bypasses the multipliers, clamping, and surface-snap logic. This is fine for teleport-style hacks, but for gameplay-compatible movement, always use Ball_ApplyForce so the engine handles ice, tubes, is_shrunk state, and max_speed correctly.

Ball_ApplyForceV2 (0x4016F0)

There is an alternate force application at 0x4016F0 that is gravity-plane-aware. It applies the same multipliers but respects the current gravity plane when computing facing angle. Use this if you are building a mod that supports tilted-gravity levels (Plane 1 or 2).


Collision & Events

Two-Tier Collision

Tier 1 — Planes: Ball_CheckCollisionPlanes (0x402810)

  • Tests ball position against 6 collision planes stored at ball+0x0C
  • Each plane: ax + by + cz + d = 0
  • Quick boundary rejection before expensive mesh tests

Tier 2 — Mesh: Mesh_FindClosestCollision (0x465D90)

  • Octree traversal (Collision_TraverseSpatialTree at 0x465EF0)
  • AABB tests per triangle
  • Returns closest hit point with 0.01 precision

3-Tier Event Dispatch

When the ball hits a collision object, the event string (at object+0x864) is parsed:

Scene vtable determines which handler runs:
  ├─ ExpertCollisionEvents (0x40E6A0)   ← Rumble arenas
  │    └─→ DispatchCollisionEvents (0x40C5D0)   ← Shared base (ALL events)
  └─ TowerCollisionEvents (0x40DCD0)   ← Race levels
       └─→ DispatchCollisionEvents (0x40C5D0)   ← Shared base (ALL events)

Note: Arena and Level handlers are parallel, not chained. Ball_AdvancePositionOrCollision (0x4564C0) handles geometric collision only; event dispatch happens from the ball update chain.

Event prefixes:

  • N: = Named physical object (bumpers, trapdoors, water)
  • E: = Event trigger (score, jump, limit, action)

Key events affecting the Ball:

Event Effect on Ball
E:JUMP Bounce + 3D sound + +200pts. Sets impact=10, force=0.025.
E:BREAK Calls ball vtable[0x20] (0x409480)
E:TRAJECTORY Reads <X>, <Y>, <Z> XML tags, calls Ball_SetTrajectory
N:WATER Sets in-water flag + 10 frame timer
N:TARPIT 3D sound, tar state, clears velocity
N:GOAL Race finish — plays music, sets finish flags
N:NOCONTROL 10 frame freeze (input disabled)
N:MOUSETRAP Deflects ball: normalize direction × speed
DROPIN Pipe drop-in: sound + +200pts if speed > threshold
PIPEBONK Random sound + +100pts, 10 frame cooldown
POPOUT Sound + +100pts, 50 frame cooldown

Ball State Used by Events

Ball Offset Event Using It
0x164–0x16C (pos) 3D sound positioning
0x170–0x178 (force) Cleared by TARPIT, modified by JUMP
0x2DC–0x2E4 (checkpoint) Ball_FindClosestRespawnPoint respawns here
0xC2C (section_filter) E:SAFESWITCH copies data here
0xC4C (is_shrunk) Set by odd race E:SHRINK/E:GROW

Rendering

Ball_Render (0x402860)

Sets D3D8 states and draws the ball mesh:

SetRenderState(D3DRS_ZENABLE, 1)
SetRenderState(D3DRS_CULLMODE, 1)
SetRenderState(D3DRS_SPECULARENABLE, 1)
SetTexture(0, HamsterBall.png)
SetRenderState(D3DRS_LIGHTING, flag from ball+0x700)
DrawPrimitiveUP(triangle_list, vertices)

Ball_RenderShadow (0x401920)

Renders a shadow quad scaled by radius * constant, positioned at ball XYZ.

Ball_RenderWithCollision (0x402C10)

Full render pass: check collision planes → render shadow → apply scaling → end frame.

Ball_RenderWithMaterial (0x45D8F0)

Used for 3D text (Font_DrawGlyph3D). Renders ball mesh with a custom material override.

Ball_CreateTrailParticles (0x401DD0)

Spawns 9 trail particles in a ring around the ball:

  • Each particle is a ArenaScoreParticle object (0x28 bytes)
  • Position = ball_pos + (radius × camera-right × sinθ) − (radius × camera-up × cosθ)
  • Velocity = offset × random_scale
  • Appended to scene particle list at scene+0x3B00

AI & Special Modes

Ball_AI_ChaseNearest (0x408390)

AI steering for computer-controlled balls:

  1. Find nearest opponent ball (iterates ball list)
  2. Compute direction vector toward target
  3. Call Ball_ApplyForce toward target
  4. If no target found: sine-wave wandering fallback

Hook tip: To make AI balls target the player, patch the nearest-ball search to always return the player ball index.

Ball_FallUpdate (0x408830)

Called when the ball falls off the track:

  1. Shrink radius from 35.0 → 13.0
  2. Handle scale change smoothly
  3. Clean up trail particles
  4. After fall animation completes, respawn at last checkpoint (0x2DC)

Ball_Shrink (0x402200)

Marks ball as fallen:

  • airborne = 1
  • radius = 13.0
  • Play 3D sound at ball position

Ball_Grow (0x402270)

Resets from fallen state:

  • airborne = 0
  • radius = 26.0 (note: different from runtime default 35.0!)
  • Restore physics defaults

Split Ball Mechanics

Function Address What It Does
Ball_Split_ctor 0x408D10 Constructor for split ball (vtable 0x4CF560)
Ball_Shatter 0x408D70 Arena 8-ball mechanic: marks parent ball for despawn, spawns 3 AI split balls (called from FollowBall_Update 0x43ECC0)
Ball_SplitAndExplode 0x409480 Creates 2 split balls + circular ArenaScoreParticle explosion (0–360°)

Split balls are temporary physics objects that scatter from the original ball position and expire after a timer.

Ball_InitBattleMode (0x456CD0)

Initializes ball for Rodent Rumble arena mode:

  • Friction = 0.555
  • Bounciness = 1.0
  • Radius = 400.0 (much larger than race mode!)
  • Speed scale adjusted for arena physics

Modding Hook Points

High-Value Hooks for Ball Modding

# Target Address When to Hook What You Can Do
1 Ball_Update 0x405E00 Every frame Override entire physics, teleport, noclip
2 Ball_ApplyForce 0x402650 On input Custom forces, reverse gravity, zero gravity
3 Ball_GetInputForce 0x46EC30 On input Add custom actions (brake, jump, camera snap)
4 Ball_CollisionCheck 0x402DE0 Every frame Disable collision (noclip), custom bounce logic
5 Ball_Render 0x402860 Every frame Custom visual effects, wireframe, size changes
6 Ball_Shrink 0x402200 On odd race Prevent shrink, teleport instead
7 Scene_UpdateBallsAndState 0x41B540 Every frame Modify ball list iteration, add/remove balls
8 Ball_AI_ChaseNearest 0x408390 AI tick Change AI behavior, make AI friendly/hostile
9 Scene_dtor 0x419770 Scene destroy Null out cached ball/scene pointers (use-after-free prevention)
10 Board_ctor 0x419030 Scene create Detect new level load, acquire scene pointer

Scene Lifecycle Hook Pattern

For mods that cache ball/scene pointers across frames, hook both construction and destruction to avoid use-after-free:

// In Ball_Update hook (acquire pointers)
void Hooked_BallUpdate(Ball* ball) {
    if (ball->player_index == 0) {
        g_Player = ball;
        if (g_Scene == nullptr) {
            g_Scene = ball->scene;  // nullptr→addr transition = new level loaded
        }
    }
    Original_BallUpdate(ball);
}

// In Scene_dtor hook (null pointers BEFORE calling original)
void Hooked_SceneDtor(Scene* scene) {
    if (scene == g_Scene) {
        g_Scene = nullptr;       // null FIRST, before balls get freed
        g_Player = nullptr;
        // g_Player2/3/4 = nullptr;
        // g_Enemies.clear();
    }
    Original_SceneDtor(scene);   // now safe — original frees balls
}

Destruction order inside Scene_dtor:

  1. Vtable pointer overwritten (data fields still intact)
  2. Ball list iterated — every ball gets Ball_dtor(this, 1)free()
  3. Effect/object lists iterated and freed
  4. AthenaList containers freed
  5. SceneObject_dtor base cleanup
  6. Returns → caller calls free(scene)

The scene's own inline fields (camera, timers, race state) remain readable throughout, but every pointer the scene holds becomes dangling partway through. Always null your cached pointers at the top of your hook before calling the original.

Example: Speed Hack (Single Write)

// Multiply max_speed for all balls by 2x
*(float*)(ball + 0x188) = 12.0f;  // 6.0 default × 2

Example: Noclip (Hook Ball_CollisionCheck)

typedef void (__thiscall *tCollisionCheck)(void* ball);
tCollisionCheck oCollisionCheck;

void __fastcall hkCollisionCheck(void* ball) {
    // Do nothing — skip ALL collision
    // Ball will fly through walls and floors
}

Example: Custom Action in Ball_GetInputForce

// After calling original, check your custom key
oBallGetInputForce(ball, outForce);

if (g_CustomBrakeKey.isDown) {
    float* force = (float*)((char*)ball + 0x170);
    force[0] *= 0.85f;
    force[1] *= 0.85f;
    force[2] *= 0.85f;
}

Example: Gravity Modifier

// Change gravity direction (default is +Y down)
float* gravity = (float*)((char*)ball + 0x1A8);
gravity[0] = 0.0f;   // X
gravity[1] = -1.0f;  // Y (upward gravity = anti-gravity!)
gravity[2] = 0.0f;   // Z

Quick Reference

All Ball Functions by Address

Address Name Lines Description
0x401590 Ball_vtable5 Unknown vtable slot
0x401920 Ball_RenderShadow Shadow quad renderer
0x401DD0 Ball_CreateTrailParticles 9-particle trail ring
0x402200 Ball_Shrink odd race shrink ball, reduce radius
0x402270 Ball_Grow Exit odd race shrink state
0x402650 Ball_ApplyForceWithMultipliers 47 Apply force vector to velocity accumulators
0x4027F0 Ball_dtor Destructor
0x402810 Ball_CheckCollisionPlanes 6-plane collision test
0x402860 Ball_Render D3D8 ball mesh render
0x402A70 Ball_OnCollision Collision response
0x402C10 Ball_vtable7 Render-related vtable slot
0x402DE0 Ball_CollisionCheck Per-frame collision entry
0x4030B0 Ball_ResetCollisionMesh Reset collision state
0x403100 Ball_SetTiltedGravity Set gravity plane = 1
0x403150 Ball_SetFlatGravity Set gravity plane = 2
0x4039E0 Ball_ctor2 Secondary constructor (physics init)
0x405100 Ball_InitPhysicsDefaults vtable[1] — overrides ctor2 physics defaults
0x405E00 Ball_Update 400+ Main physics tick
0x408390 Ball_AI_ChaseNearest 60 AI opponent steering
0x408830 Ball_FallUpdate 40 Fall animation + respawn
0x408D10 Ball_Split_ctor 14 Split ball constructor
0x408D70 Ball_Shatter 50 Arena: split parent ball into 3 AI balls
0x409480 Ball_SplitAndExplode 70 Split + ArenaScoreParticle ring
0x40AFE0 Ball_ctor 30 Primary allocator + base init
0x40AF90 Ball_GetTransform Read transform into struct
0x4564C0 Ball_AdvancePositionOrCollision 6-phase physics pipeline
0x456CD0 Ball_InitBattleMode Arena physics defaults
0x456D80 CollisionMesh_ctor PhysicsObject constructor (0xCB0 bytes)
0x46EC30 Ball_GetInputForce 7 Convert input to force vector

Scene Lifecycle Functions

Address Name Description
0x419030 Board_ctor Base board constructor (calls Gadget_ctor → Scene init)
0x419770 Scene_dtor Scene destructor (frees balls, objects, lists)
0x425020 Scene_DeletingDtor Calls Scene_dtor + free(this)
0x458CE0 Scene_Destroy Calls Scene_ScalarDtor (alternate dtor path)

Ball Struct C Header

A machine-readable C header is maintained at:

analysis/ghidra/structs/ball_struct.h

It contains the full 0xC98 layout with verified offsets from Ball_ctor2 decompilation.


See Also

  • docs/INPUT_SYSTEM.md — Input device, key remapping, DIK codes
  • docs/COLLISION_EVENT_SYSTEM.md — Full event dispatch chain
  • docs/COLLISION_SYSTEM_DEEP.md — Octree, AABB, mesh collision
  • docs/MODDING_NEW_CONTROLS.md — Step-by-step DLL hook guide
  • docs/SCENE_SYSTEM_DECOMP.md — Scene update order, camera modes
  • docs/BALL_PHYSICS_DECOMP.md — Legacy physics documentation
  • analysis/ghidra/structs/ball_struct.h — C struct definition

Document compiled from 3,700+ documented functions, live Ghidra decompilation, and cross-referenced struct analysis. All offsets verified against PE32 Hamsterball.exe loaded at 0x00400000. PhysicsObject layout verified via CollisionMesh_ctor (0x456D80) and Ball_InitBattleMode (0x456CD0) decompilation. Runtime defaults verified via Ball_InitPhysicsDefaults (0x405100) decompilation.


🔗 Related Documents

The App Object

types : objects
keywords :

📂 View source on GitHub


Hamsterball — The App Object: A Modder's Guide

What This Document Is

This is a reverse-engineering reference for anyone who wants to hook into,
read from, or modify the running Hamsterball game. The App object is the
global singleton that holds every subsystem pointer, every game-state flag,
and every config value. If you can get its address, you can reach every
other object in the game.


Getting the App Pointer

Method 1: Direct Global Address (Easiest)

The game allocates the App object as a global static in .data.

  • Address: 0x004FD680 (VA in Hamsterball.exe)
  • Symbol: g_App
  • Type: App* (pointer to a ~0xA00-byte structure)
// In your DLL / injector code
App* g_App = *(App**)0x004FD680;

This address is valid from the moment WinMain calls App_Ctor (0x0046DC40)
until process exit. The constructor runs before the game loop starts, so you
can safely read it any time after CreateWindow returns.

Method 2: Hook WinMain (Guaranteed Early Access)

If you need the address before any frame runs, hook the beginning of
WinMain (0x004278E0). The first instruction after entry is:

WinMain:
  push ebp
  mov ebp, esp
  ...
  call App_Ctor        ; creates App at 0x4FD680
  call App_Initialize_Full
  call App_Run          ; game loop
  call LoadOrSaveConfig

Hook address: 0x004278E0
At this point g_App is already constructed (the CRT .data init ran
before WinMain). You can read 0x004FD680 immediately.

Method 3: Hook App_Initialize_Full (All Subsystems Ready)

If you want to intercept the game after all subsystems are initialized
(Graphics, Audio, Input, Registry) but before the first frame renders:

  • Address: 0x00429530
  • Why: Step 25 calls vtable[0xA0]() which shows the title screen. If
    you hook after step 24 and before step 25, every subsystem pointer is
    filled in.

Method 4: Hook App_Run (Per-Frame Access)

For frame-by-frame mods (trainers, overlays, TAS tools):

  • Address: 0x0046BD80
  • Hook point: The outer while (!quit_flag) loop. Replace the
    app->vtable[0x20]() call (Update) or app->vtable[0x28]() call (Render)
    with your own dispatcher.
// Original dispatch in App_Run:
app->vtable[0x20]();   // Update  — game logic
app->vtable[0x24]();   // Pre-render
app->vtable[0x28]();   // Render  — draw everything
app->vtable[0x2C]();   // Post-render / HUD

Replacing any of these four vtable calls lets you run custom code every
frame while keeping the original game running.

Method 5: Hook App_FrameUpdate (Convenient Single Point)

  • Address: 0x0046C170
  • What it does: Polls input, runs collision, calls GameUpdate, handles
    cursor capture.
  • Why hook it: One single hook gives you input, physics, and game-state
    access every frame without touching the render pipeline.

App Vtable Layout

The App vtable lives at 0x004CE400. Key slots:

Slot Offset Address Name Description
0 +0x00 0x46DC20 App_ScalarDtor Destructor + free if flag&1
2 +0x08 0x46BA10 App_Shutdown Cleanup on exit
8 +0x20 Update Game logic (Scene_Update)
9 +0x24 PreRender Camera setup
10 +0x28 Render Draw scene
11 +0x2C PostRender HUD / menus
35 +0x8C SetDisplayMode Called with (800,600) during init
40 +0xA0 0x4280E0 App_ShowMainMenu Creates MainMenu object

Hooking any vtable slot is the cleanest way to intercept behavior without
patching function bodies. Just overwrite the pointer at
*(void**)(g_App + 0x00) + slot_offset.


App Structure Layout (Offsets)

The App struct is the root of the game. Everything else hangs off it.

Core Identity & Window

Offset Type Name Description
+0x000 void** vtable App vtable = 0x004CE400
+0x004 HINSTANCE hInstance WinMain param_1
+0x008 int cmdShow WinMain nCmdShow
+0x054 RegKey* registryKey Registry handle (ADVAPI32)

Timing & Frame Control

Offset Type Name Description
+0x05C int targetFPS Target frame rate (usually 30)
+0x168 int msPerFrame 1000 / targetFPS
+0x170 int fpsDivisor Backup of target FPS
+0x18C int updateCount Total physics updates
+0x194 int frameCounter Frames rendered this second
+0x1AC bool showFPS 1 = draw FPS counter
+0x210 char* phaseName "Background" / "Update" / "Render"
+0x159 bool quitFlag 1 = exit game loop
+0x15A bool activeFlag 1 = window focused
+0x158 bool minimizedFlag 1 = window minimized
+0x156 bool updateDisabled 1 = pause all updates

Subsystem Pointers (The Big Ones)

Offset Type Name Description
+0x174 Graphics* graphics D3D8 Graphics engine
+0x17C AudioSystem* audioSystem BASS audio wrapper
+0x180 InputHandler* inputHandler DirectInput8 handler
+0x184 void* gameUpdateObj Passed to App_TickGameUpdate
+0x5D void* renderTarget D3D render target surface

Display Settings

Offset Type Name Description
+0x15C int width Window width (default 800)
+0x160 int height Window height (default 600)
+0x158 bool windowed 1 = windowed, 0 = fullscreen

Audio / Music

Offset Type Name Description
+0x534 HMUSIC musicHandle BASS music handle for music.mo3
+0x538 HCHANNEL musicChannel1 BASS channel 1
+0x53C HCHANNEL musicChannel2 BASS channel 2

Game Mode Objects (vtable 0x8C call creates these)

Offset Type Name Description
+0x550 void* gameMode1 1-player mode object
+0x554 void* gameMode2 2-player mode object
+0x558 void* gameMode3 4-player mode object
+0x55C void* gameMode4 Tournament mode object

Game State & Scenes

Offset Type Name Description
+0x178 Scene* currentScene Active scene (menu or level)
+0x184 Scene* loadingScene Loading screen
+0x1DC SoundChannel* inputSound Click/beep channel
+0x1E4 InputDevice* player1Device P1 input (keyboard default)
+0x1E8 InputDevice* player2Device P2 input
+0x224 void* mainMenuObj MainMenu instance
+0x228 void* resultsScreen Race results screen
+0x708 int gameState 3 = racing, other = menu/loading

Progress / Unlock Flags

Offset Type Name Description
+0x851 bool unlock_DizzyRace 1 = Dizzy race unlocked
+0x852 bool unlock_TowerRace 1 = Tower race unlocked
+0x853 bool unlock_UpRace 1 = Up race unlocked
+0x854 bool unlock_ExpertRace 1 = Expert race unlocked
+0x855 bool unlock_OddRace 1 = odd race unlocked
+0x856 bool unlock_ToobRace 1 = Toob race unlocked
+0x857 bool unlock_WobblyRace 1 = Wobbly race unlocked
+0x858 bool unlock_SkyRace 1 = Sky race unlocked
+0x859 bool unlock_MasterRace 1 = Master race unlocked
+0x85A bool unlock_DizzyArena 1 = Dizzy arena unlocked
+0x85B bool unlock_TowerArena 1 = Tower arena unlocked
+0x85C bool unlock_UpArena 1 = Up arena unlocked
+0x85D bool unlock_ExpertArena 1 = Expert arena unlocked
+0x85E bool unlock_OddArena 1 = Odd arena unlocked
+0x85F bool unlock_ToobArena 1 = Toob arena unlocked
+0x860 bool unlock_WobblyArena 1 = Wobbly arena unlocked
+0x861 bool unlock_SkyArena 1 = Sky arena unlocked
+0x862 bool unlock_MasterArena 1 = Master arena unlocked
+0x863 bool unlock_NeonRace 1 = Neon race unlocked
+0x864 bool unlock_GlassRace 1 = Glass race unlocked
+0x865 bool unlock_ImpossibleRace 1 = Impossible race unlocked
+0x866 bool unlock_NeonArena 1 = Neon arena unlocked
+0x867 bool unlock_GlassArena 1 = Glass arena unlocked
+0x868 bool unlock_ImpossibleArena 1 = Impossible arena unlocked
+0x86C uint8[0x50] bestTimes Per-level best times (raw binary)
+0x8BC uint8[0x50] medals Per-level medal status (0=none..3=gold)
+0x914 int playCount Total launches from registry
+0x850 bool mirrorMode Tournament mirror tracks
+0x84C float mouseSensitivity 0.0 – 1.0 range
+0x238 bool rightButtonPause Toggle pause on right-click

Input / Controller Config

Offset Type Name Description
+0xB28 DWORD p2Controller1 DirectInput device index
+0xB2C DWORD p2Controller2 DirectInput device index
+0xB30 DWORD p2Controller3 DirectInput device index
+0xB34 DWORD p2Controller4 DirectInput device index

Misc

Offset Type Name Description
+0x1B4 char* versionString ProductVersion from Version API
+0x1CC int loadedCount Objects loaded counter
+0x200 bool initialized 1 after App_Initialize_Full finishes
+0x208 char* initStep Debug string: "Initialize(1)".."(26)"
+0x240 HCURSOR cursor "BLANKCURSOR" handle
+0x278 Texture* shadowTexture Loaded shadow.png

What You Can Do With the App Object

1. Instant Unlock Everything

// Set every unlock flag to 1
for (int i = 0x851; i <= 0x868; i++) {
    *(bool*)(g_App + i) = true;
}

This unlocks every race and arena instantly. No registry editing, no file
patching. The game reads these flags every time it draws a menu.

2. Force Fullscreen / Windowed

// Toggle windowed mode
bool* windowed = (bool*)(g_App + 0x158);
*windowed = !*windowed;
// Then call App_SetFullScreen(0x0046C7C0) to apply

3. Change Mouse Sensitivity

float* sens = (float*)(g_App + 0x84C);
*sens = 2.0f;   // Double default sensitivity

4. Skip Intro / Instant Menu

Hook App_Initialize_Full and replace the call at step 25:

Original:
  call vtable[0xA0]   ; Shows title screen
Replace with:
  call App_ShowMainMenu  ; 0x004280E0 — jumps straight to menu

5. Frame-By-Frame TAS Hook

Hook App_Run and replace vtable[0x20]() with your own function that
reads input from a file instead of DirectInput:

void MyUpdate() {
    // Read next frame of inputs from TAS movie file
    // Write them into InputDevice+0x50C..0x518 (DIK codes)
    // Call original Scene_Update
    OriginalSceneUpdate();
}

6. Force Quit / Safe Exit

// Set quit flag — game exits cleanly at next loop iteration
*(bool*)(g_App + 0x159) = true;

7. FPS Unlock / Frame Limiter Override

// Change target FPS (default is 30)
*(int*)(g_App + 0x05C) = 60;
*(int*)(g_App + 0x168) = 1000 / 60;

8. No-Clip / Disable Collision

The currentScene pointer at +0x178 leads to the Scene object, which
has a collision handler at vtable+0x29 (0x40C5D0). Replacing that vtable
slot with a no-op disables all collision events.

Scene* scene = *(Scene**)(g_App + 0x178);
void** scene_vt = *(void***)scene;
scene_vt[0x29] = (void*)0x44B840;   // NoOp stub

9. Instant Level Load

App_StartRace (0x004287C0) takes a level path string and loads it
immediately. Call it from your hook to warp to any level:

typedef void (*App_StartRace_t)(App*, const char*);
App_StartRace_t StartRace = (App_StartRace_t)0x004287C0;
StartRace(g_App, "levels\\level10");

10. Music Speed / Tempo Hack

The BASS music handle is at App+0x534. Use BASS's BASS_ChannelSetAttribute
with BASS_ATTRIB_FREQ to change playback speed without touching game code.


Key Functions for Modding

Address Name Why You Care
0x004278E0 WinMain Earliest hook point
0x00429530 App_Initialize_Full All systems ready, pre-menu
0x0046BD80 App_Run Per-frame hook point
0x0046C170 App_FrameUpdate Input+physics every frame
0x0046C7C0 App_SetFullScreen Toggle display mode
0x004280E0 App_ShowMainMenu Jump to menu
0x004287C0 App_StartRace Load any level
0x00425F90 App_CompleteRace Finish race instantly
0x00428C50 App_StartPracticeRace Start practice mode
0x004288B0 App_StartTournamentRace Start tournament
0x0046CB70 App_SetTitleString Change window title
0x0046C050 App_CreateInputDevice Add custom input device
0x0046CB00 App_CreateScoreDisplay Inject HUD element
0x0046BCA0 App_WriteDisplaySettings Save res to registry
0x0046BD00 App_ReadDisplaySettings Load res from registry
0x4284C0 App_SaveAllConfig Force save all settings

Registry Persistence

The game saves all unlock flags and settings to the Windows registry on exit.
If you set flags in memory but want them to survive a restart, either:

  1. Call App_SaveAllConfig(0x004284C0) with g_App as the argument.
  2. Let the game exit normally — it calls this automatically in LoadOrSaveConfig.

Registry path (inferred): HKEY_CURRENT_USER\Software\Raptisoft\Hamsterball


C Header for Injection

#ifndef HB_APP_H
#define HB_APP_H
#include <stdint.h>
#include <windows.h>

typedef struct {
    void**    vtable;          // +0x000
    HINSTANCE hInstance;       // +0x004
    int       cmdShow;         // +0x008
    uint8_t   pad_00C[0x48];   // +0x00C..0x053
    void*     registryKey;     // +0x054
    uint8_t   pad_058[0x04];
    int       targetFPS;       // +0x05C
    int       frameTimeMs;     // +0x5A (alias, verify)
    int       fpsDenominator;  // +0x5B
    void*     renderTarget;    // +0x5D
    int       frameCounter;    // +0x65
    uint8_t   pad_069[0xEF];
    uint8_t   updateDisabled;  // +0x156
    uint8_t   windowed;        // +0x158
    uint8_t   minimizedFlag;   // +0x15A
    uint8_t   quitFlag;        // +0x159
    int       width;           // +0x15C
    int       height;          // +0x160
    uint8_t   pad_164[0x10];
    void*     graphics;        // +0x174
    void*     audioSystem;     // +0x17C
    void*     inputHandler;    // +0x180
    void*     gameUpdateObj;   // +0x184
    uint8_t   pad_188[0x2C];
    char*     versionString;   // +0x1B4
    int       loadedCount;     // +0x1CC
    uint8_t   initialized;     // +0x200
    char*     initStep;        // +0x208
    uint8_t   pad_20C[0x34];
    HCURSOR   cursor;          // +0x240
    uint8_t   pad_244[0x34];
    void*     shadowTexture;   // +0x278
    uint8_t   pad_27C[0x2B8];
    HMUSIC    musicHandle;     // +0x534
    HCHANNEL  musicChannel1;   // +0x538
    HCHANNEL  musicChannel2;   // +0x53C
    uint8_t   pad_540[0x10];
    void*     gameMode1;       // +0x550
    void*     gameMode2;       // +0x554
    void*     gameMode3;       // +0x558
    void*     gameMode4;       // +0x55C
    uint8_t   pad_560[0x3B4];
    void*     currentScene;    // +0x178 (RELOCATE — verify in Ghidra)
    // ... continue from docs above
} App;

// Global singleton
static App** const g_ppApp = (App**)0x004FD680;
#define g_App (*g_ppApp)

#endif

Note: The header above uses approximate offsets. For production code,
verify every offset in Ghidra against the current binary build. The
App_Initialize_Full decompilation is the authoritative source.


Quick Reference: Offsets at a Glance

0x004FD680  g_App                App*  (global)
0x004CE400  App_vtable           void**
0x004D0260  Scene_vtable         void**
0x004D934C  SceneObject_vtable   void**
0x004CF300  Vec3_vtable          void**

Files Referenced

File Description
analysis/ghidra/structs/app_struct.h Ghidra C struct export
analysis/ghidra/decompilations/app/decomp_app_initialize.c App_Initialize_Full decomp
analysis/ghidra/decompilations/app/decomp_app_run.c App_Run decomp
docs/FUNCTION_MAP.md Full function listing
docs/SAVE_CONFIG_REGISTRY_SYSTEM.md Registry details
docs/GAME_LOOP_WINDOW_MANAGEMENT.md Frame timing
docs/STRUCTS_AND_TYPES.md Other structures

Document version: 2026-06-04
Based on Hamsterball.exe analysis via Ghidra 12.0.4 + GhidraMCP
Phase 1 structs complete — App struct verified against decompilation


🔗 Related Documents

Timer/HUD System & Asset Manif

types : ui
keywords :

📂 View source on GitHub


Timer/HUD System & Asset Manifest

Architecture Overview

The game's resource loader (misnamed TimerDisplay at 0x42A8C0) creates a LoadingScreenGadget (0x3628 bytes) which serves as both:

  1. Loading screen with progress indicator
  2. Asset manager for the entire game

The timer/HUD system draws on top of the 3D scene during gameplay:

  • Time pool display (with timerblot.png background)
  • Race name
  • Safe circle indicator (safecircle.png)
  • Medal/rank icons (bronze/silver/gold/goldenweasel)

Resource Loading Pipeline

LoadingScreenGadget

Created once at startup (0x3628 bytes). Loads ALL game assets via vtable dispatch:

Vtable Offset Function Parameter
+0x48 LoadTexture(destination, filename, mirror_flag) .png/.bmp → Direct3D texture
+0x4C LoadMesh(destination, filename) .x mesh → D3DX mesh
+0x50 LoadLevel(destination, filename) Level → MeshWorld
+0x54 AttachLevel(destination, source) Attach secondary level to primary
+0x58 LoadTextureSimple(destination, filename) Texture without mirror variant
+0x5C LoadFont(destination, fontname) Font from fonts/ directory
+0x60 LoadSound(destination, filename, channel_count) Sound with channel pool count

Loading Flow

App_ResourceLoader(app):
  1. LoadingScreenGadget_Ctor(0x3628 bytes)
  2. app->loading_screen = gadget  // App+0x22C
  
  PHASE 1: Fonts
    - fonts\showcardgothic28 (title text, large)
    - fonts\arialnarrow12bold (small text)
    - fonts\showcardgothic14 (medium text)
    - fonts\showcardgothic72 (huge numbers / timer display)
    - fonts\showcardgothic16 (HUD text)
  
  PHASE 2: UI Textures (titletext, hammy, blueblot, bluecircle)
  PHASE 3: Race Textures (signs, goals, locktiles, arrows, checkers, bricks)
  PHASE 4: Ball Meshes (Sphere, SphereBreak, Hamster anims, 8Ball, FunBall, Bell, Dizzy)
  PHASE 5: Level Prefabs (MouseTrap, Secret, Secret-Unlock, Trapdoors, PopupSign)
  PHASE 6: Object Meshes (tarbubble, fanblades, sawblade, dawgshoe, mace)
  PHASE 7: HUD Textures (weaselbox, ballborder, timerblot, chrome, medals)
  PHASE 8: Tournament Textures (tourney-* for each race)
  PHASE 9: Result Textures (Burst, Lost, Winner, Winner2p, title1-4)
  PHASE 10: Sound Effects (55 sounds with channel counts)
  PHASE 11: Endgame Textures (settext, gotext)
  12. Menu_MergeAllLists()
  13. Scene_AddObject(app->scene, gadget)

Complete Asset Manifest

Fonts (5 fonts)

App Offset Font Name Usage
+0x318 showcardgothic28 Title screens
+0x31C showcardgothic14 Medium dialogue
+0x320 showcardgothic16 HUD text
+0x324 arialnarrow12bold Small labels
+0x328 showcardgothic72 Timer display

Ball Meshes (14 meshes)

App Offset Mesh Description
+0x244 Sphere Standard ball
+0x248 SphereBreak1 Ball breaking animation 1
+0x24C SphereBreak2 Ball breaking animation 2
+0x250 Hamster-Waiting Hamster idle inside ball
+0x254 Hamster-trot1 Walk animation 1
+0x258 Hamster-trot2 Walk animation 2
+0x25C Hamster-trot3 Walk animation 3
+0x260 RBGlare Light reflection on ball
+0x264 Sphere+Tar Ball with tar coating
+0x268 8Ball 8-ball penalty ball
+0x26C FunBall Fun/bonus ball
+0x270 Bell Bell object
+0x274 Dizzy Dizzy animation

Race Textures (16 categories × 2-3 variants each)

Each race has a checker + brick texture pair:

  • pink, blue, green, yellow, grey-outline, red, orange, bright-green
  • toob (tube race), sky, purple, brown, black

Goal textures: goal.png, goal-lit.png, goal-mirrored.png, goal-lit-mirrored.png
Round goal variants: goal-round*.png

Tournament Race Thumbnails (14 races)

App Offset Race Name
+0x3B4 Beginner
+0x3B8 Cascade
+0x3BC Intermediate
+0x3C0 Dizzy
+0x3C4 Tower
+0x3C8 Up
+0x3CC Neon
+0x3D0 Expert
+0x3D4 Odd
+0x3D8 Toob
+0x3DC Wobbly
+0x3E0 Glass
+0x3E4 Sky
+0x3E8 Master
+0x3EC Impossible

Sound Effects (55 sounds)

App Offset Sound Channels
+0x43C sounds\collide 10
+0x440 sounds\roll 10
+0x444 sounds\whistle 1
+0x448 sounds\bumper 10
+0x44C sounds\ballbreak 5
+0x450 sounds\ballbreaksmall 5
+0x454 sounds\thwomp 2
+0x458 sounds\snap 2
+0x45C sounds\popup 2
+0x460 sounds\dropin 2
+0x464 sounds\dropinshort 2
+0x468 sounds\popout 2
+0x46C sounds\pipebump1 10
+0x470 sounds\pipebump2 10
+0x474 sounds\pipebump3 10
+0x478 sounds\gearclank 20
+0x47C sounds\bridgeslam 2
+0x480 sounds\platformtick 5
+0x484 sounds\gluestuck 5
+0x488 sounds\bubble1 5
+0x48C sounds\bubble2 5
+0x490 sounds\wheelcreak 2
+0x494 sounds\catapult 2
+0x498 sounds\trapdoor 2
+0x49C sounds\fwing 2
+0x4A0 sounds\clink 3
+0x4A4 sounds\whoosh 3
+0x4A8 sounds\chomp 1
+0x4AC sounds\fan-start 10
+0x4B0 sounds\fan-blow 10
+0x4B4 sounds\crack 2
+0x4B8 sounds\crumble 2
+0x4BC sounds\sawstartup 2
+0x4C0 sounds\sawcut 2
+0x4C4 sounds\minipop 5
+0x4C8 sounds\bell 3
+0x4CC sounds\zip 2
+0x4D0 sounds\ting 20
+0x4D4 sounds\shrink 3
+0x4D8 sounds\grow 3
+0x4DC sounds\tweet 3
+0x4E0 sounds\creakyplatform 20
+0x4E4 sounds\wubba 5
+0x4E8 sounds\saw 2
+0x4EC sounds\sawspeedy 2
+0x4F0 sounds\dawgstep1 10
+0x4F4 sounds\dawgstep2 10
+0x4F8 sounds\dawgsmash 10
+0x4FC sounds\sizzle 2
+0x500 sounds\explode 3
+0x504 sounds\vac-o-sux 3
+0x508 sounds\speedcylinder 2
+0x50C sounds\bonuspop 5
+0x510 sounds\buzzbonus 1
+0x514 sounds\breakbridge 1
+0x518 sounds\unlock 1
+0x51C sounds\NeonRide 1
+0x520 sounds\NeonFlicker 50
+0x524 sounds\ZoopDown 2
+0x528 sounds\LightsOff 2
+0x52C sounds\GlassBonus 2

Timer System

ArenaBoard Timer (Ball+0x264)

Each ball has a ArenaBoard timer component (16 bytes):

  • Used in rumble/arena mode for round timing
  • ToggleTimer_Init resets the timer
  • Timer counts down during gameplay
  • When timer reaches zero: round ends

Tournament Time Pool

  • PlayerProfile+0x5E4 = total accumulated time (float, for ranking)
  • App+0x5E8 = active time pool (float)
  • App+0x5F4 = (unknown timing float)
  • Time pool decreases during race; when it hits zero = "LOST TOURNAMENT"
  • Warm-up race (race 1) does NOT affect time pool

Timer Display

  • Font: showcardgothic72 (large digits)
  • Background: timerblot.png
  • Positioned in HUD overlay layer (alpha pass)
  • Displayed as minutes:seconds remaining

HUD Elements

Element Texture Location
Timer background timerblot.png Top-center
Safe circle safecircle.png Progress indicator
Ball border ballborder.png Ball highlight ring
Ball burner ballburner.png Speed effect overlay
Sweat drop sweat.png Ball strain indicator
Star burst star.png Achievement effect
Dust cloud dust.png Impact effect
Chrome ball chrome.png / chromeshadow.png Premium ball skin
Medal (bronze) bronze-small.png, bronze-icon.png Tournament score
Medal (silver) silver-small.png, silver-icon.png
Medal (gold) gold-small.png, gold-icon.png
Golden weasel goldenweasel.png, goldenweasel-icon.png Best medal
Lock icon lock.png Locked content
Rank badge textures\ranks%d.jpg Post-race rank display

Key Address Map

Address Function Description
0x42A8C0 App_ResourceLoader Load ALL game assets
0x458E60 ToggleTimer_Init Reset round timer

Reimplementation Notes (SDL2)

Asset Loading

  • Use a manifest file (JSON/YAML) instead of hardcoded loading
  • SDL2: IMG_Load() for textures, TTF_OpenFont() for fonts
  • For meshes: custom mesh format or GLTF2
  • For sounds: Mix_LoadWAV() / Mix_Music

Timer System

class TimerSystem {
    float time_pool;        // Tournament time remaining
    float race_elapsed;     // Current race time
    bool is_warmup;         // Race 1 doesn't affect pool
    
    void Update(float dt) {
        race_elapsed += dt;
        if (!is_warmup) time_pool -= dt;
        if (time_pool <= 0) OnTournamentLost();
    }
    
    void Render() {
        // Render timer display with showcardgothic72 font
        // Background with timerblot.png
        // Format as MM:SS
    }
};

🔗 Related Documents

Tower & Dizzy Object System

types : docs
keywords :

📂 View source on GitHub


Hamsterball Tower & Dizzy Object System — Complete Reverse Engineering

Factory Functions

CreateDizzyObjects @ 0x0040A5F0

Dizzy Race level-specific factory. Dispatches via __strnicmp on object names from MESHWORLD data.

Object Match String VA Alloc Constructor Board Mesh AthenaList Difficulty
TIPPER "TIPPER", 6 0x4CF69C 0x1104 Tipper_ctor +0x4394 +0x2578 YES
WATERWHEEL "WATERWHEEL", 10 0x4CF690 NONE NONE (Board fields) +0x4BA8/+0x4BAC NO NO
SWIRL "SWIRL", 5 0x4CF688 NONE NONE (Board fields) +0x4BC4/+0x4BC8 NO NO
GLUEBIE "GLUEBIE", 7 0x4CF680 0x110C Gluebie_ctor +0x4374 +0x2578/+0x6080 YES

CreateTowerObjects @ 0x0040D7C0

Tower Race level-specific factory. Handles 7 object types.

Object Match String VA Alloc Constructor Board Mesh AthenaList Difficulty
CATAPULT "CATAPULT", 8 0x4CF99C 0x1108 Catapult_ctor @ 0x437E10 +0x436C +0x2578/+0x584C NO
MACE "MACE", 4 0x4CF994 0x110C 0x438750 +0x4378 +0x2578 NO
DRAWBRIDGE "DRAWBRIDGE", 10 0x4CF988 0x113C Glass_Level_ctor @ 0x4384A0 +0x4370 +0x2578 (×2) NO
WINDMILL "WINDMILL", 8 0x4CF97C 0x10D0 CollisionLevel_ctorWithLevel @ 0x465080 +0x437C NO NO
TRAPDOOR "TRAPDOOR", 8 0x4CF970 0x10F8 0x438290 N/A +0x2578 (×6) NO
CHOMPER "CHOMPER", 7 0x4CF968 NONE NONE (Board fields) +0x4390 NO NO
TURRET "TURRET", 6 0x4CF960 0x10D0 Stands_ctor @ 0x462850 +0x43B4 NO (vtable) NO

Mesh Path Strings

VA String Board Offset Used By
0x4D0794 Levels\Level3-WaterWheel +0x4BA8 Dizzy WATERWHEEL
0x4D099C Levels\Level4-Drawbridge +0x4370 Tower DRAWBRIDGE
0x4D0974 Levels\Level4-Mace +0x4378 Tower MACE
0x4D095C Levels\Level4-Windmill +0x437C Tower WINDMILL
0x4D094C Meshes\Chomper +0x4390 Tower CHOMPER
0x4D0932 Levels\Level4-Turret +0x43B4 Tower TURRET

MESHWORLD Placement

File Objects Found
Level3-WaterWheel.MESHWORLD WATERWHEEL
Level4.MESHWORLD CHOMPER, DRAWBRIDGE, WINDMILL, MACE, TURRET
Level4-Trapdoor1.MESHWORLD TRAPDOOR
Level4-Trapdoor2.MESHWORLD TRAPDOOR
Arena-Tower.MESHWORLD MACE, TURRET
Level9.MESHWORLD TRAPDOOR

Spawnability Verdict

Object Spawn? Reason
WaterWheel NO No alloc, no ctor, no vtable — just Board field write. Static mesh only.
Chomper NO No alloc, no ctor — MeshNode visual only. No collision or game logic.
Drawbridge YES Alloc 0x113C, Glass_Level_ctor, appended to +0x2578. Needs Board+0x4370 mesh.
Mace YES Alloc 0x110C, ctor 0x438750, appended to +0x2578. Needs Board+0x4378 mesh.
Windmill PARTIAL CollisionLevel only (collision, no game object/render). Not in +0x2578.
Trapdoor YES Alloc 0x10F8, ctor 0x438290, appended to +0x2578. No mesh dependency.
Turret PARTIAL Stands_ctor but no AthenaList_Append. Uses vtable dispatch for rendering.

Key Functions

  • MeshWorld_ctor: 0x00461510 — alloc(0x10D0, App+0x174, "path")
  • CollisionLevel_ctorWithLevel: 0x00465080
  • Stands_ctor: 0x462850
  • AthenaList_Append: 0x00453810
  • operator_new: 0x4BA57B (jmp to malloc)
  • __strnicmp: 0x4C7677

Global Spawn Pattern (for functional objects)

For objects that CAN be spawned (Drawbridge, Mace, Trapdoor):

  1. Get Board pointer: ball+0x14 → Board
  2. Get App pointer: Board+0x878 → App
  3. Load required mesh: MeshWorld_ctor(0x10D0, App+0x174, meshPath) → store at Board+offset
  4. Allocate object: operator_new(size)
  5. Call constructor: Ctor(alloc, Board, mesh) — thiscall (ecx=alloc, push Board+mesh)
  6. Set position: obj+0x10D4/+0x10D8/+0x10DC = param_block pos
  7. Append to list: AthenaList_Append(Board+0x2578, obj)

🔗 Related Documents

UI & Menu System

types : ui
keywords :

📂 View source on GitHub


UI & Menu System

Overview

Hamsterball's menu system uses a class hierarchy:

  • SimpleMenu (base, 0x870 bytes) — title, UIList, button handling
  • MainMenu — vtable 0x4D3F30, title "Main Menu"
  • DifficultyMenu — vtable 0x4D40A8, title "CHOOSE A DIFFICULTY!"
  • PracticeMenu, TimeTrialMenu, PartyMenu — level selection
  • TourneyMenu — tournament progression
  • OptionsMenu — settings with control rendering
  • PauseMenu — in-race pause
  • QuitRaceMenu — quit confirmation
  • HighScoreMenu — score display
  • ConfirmMenu — generic confirmation dialog

Menu Class Hierarchy

Gadget (0x870 bytes)
  └─ SimpleMenu (derived from Gadget)
       ├─ MainMenu (vtable 0x4D3F30)
       ├─ DifficultyMenu (vtable 0x4D40A8)
       ├─ PracticeMenu
       ├─ TimeTrialMenu
       ├─ PartyMenu
       ├─ TourneyMenu
       ├─ OptionsMenu
       ├─ PauseMenu
       └─ ConfirmMenu

MainMenu Items (0x42DE50)

Menu items are added via UIList_AddItem(menu, "Display Text", "ID_CODE", ...):

Display Text ID Code Condition Scale Factor
LET'S PLAY! PLAY Always (0.75, 1.0, 0.75, 1.0)
(spacer 10) - Always -
HIGH SCORES HS Always (1.0, 1.0, 1.0, 1.0)
OPTIONS OP Always (1.0, 1.0, 1.0, 1.0)
CREDITS CR Always (1.0, 1.0, 1.0, 1.0)
REGISTER GAME RG If App+0x200==0 (not registered) (1.0, 1.0, 0, 1.0)
(spacer 10) - Always -
(mini-game) MG If App+0x918 && App+0xB24 (width, height)
(spacer 10) - If mini-game shown -
EXIT TO DESKTOP EXIT Always (1.0, 0.75, 0.75, 1.0)

DifficultyMenu Items (0x42E220)

Display Text ID Code Scale Factor
PIPSQUEAK EASY (0.7, 1.0, 0.7, 1.0)
NORMAL NORMAL (1.0, 1.0, 0.7, 1.0)
FRENZIED! HARD (1.0, 0.5, 0.5, 1.0)
(spacer 10) - -
PREVIOUS BACK (0.75, 0.75, 1.0, 1.0)

Difficulty Mapping

  • EASY (0): MouseSensitivity default, slower AI
  • NORMAL (1): Default game speed
  • HARD (2): Frenzied mode — faster objects, harder tracks

SimpleMenu Layout (0x870 bytes, extends Gadget)

Key offsets specific to SimpleMenu:

Offset Type Description
+0x44C AthenaList Font string list
+0x868 char* Menu title string
+0x86C AthenaList Item list (alternate)
+0x88C AthenaList UIListItem list
+0xCAC int Min width
+0xCB0 int Max width
+0xCB4 int Total height
+0xCBC byte Needs recalculation flag

UIList_AddItem (0x449820)

Creates UIListItem (0x444 bytes) per menu entry:

  1. Allocate UIListItem (0x444 bytes)
  2. Copy display text string → item+0x00 (strdup)
  3. Copy ID code string → item+0x04 (strdup)
  4. Store matrix transform → item+0x0C..0x18
  5. Create AthenaString (0x1C bytes) for rendering → item+0x1C
  6. Store font reference → item+0x20
  7. Append to font list (SimpleMenu+0x44C)
  8. Append to item list (SimpleMenu+0x88C)
  9. Calculate text width via Font_MeasureText
  10. Update menu total height (SimpleMenu+0xCB4)
  11. Update menu max width (SimpleMenu+0xCB0)

UIListItem Structure (0x444 bytes)

Offset Type Description
+0x00 char* Display text (e.g., "LET'S PLAY!")
+0x04 char* ID code (e.g., "PLAY")
+0x0C float[4] Matrix scale row (X1, X2, Y1, Y2)
+0x1C AthenaString* Rendered text object
+0x20 int Font data reference
+0x24 int Item height
+0x110 byte Hidden flag
+0x441 byte Disabled flag

Menu Command Dispatch

When a menu item is selected, the ID code string is dispatched through
App_ShowMainMenu (0x4280E0) which uses a strcmp chain:

"PLAY"    → Show difficulty selection
"HS"      → Show high scores
"OP"      → Show options menu
"CR"      → Show credits
"RG"      → Show registration
"MG"      → Launch mini-game
"EXIT"    → Quit to desktop
"EASY"    → Start practice race (difficulty 0)
"NORMAL"  → Start practice race (difficulty 1)
"HARD"    → Start practice race (difficulty 2)
"BACK"    → Return to previous menu

Pause System

PauseMenu_Ctor (0x42E4B0)

In-race pause menu with:

  • RESUME → continue racing
  • QUIT → quit race menu

QuitRaceMenu (0x42E6F0)

Confirmation dialog:

  • YES, QUIT → end race
  • NO, CONTINUE → resume racing

TourneyMenu System

TourneyMenu_Advance (0x42E210)

Handles tournament progression after race completion.

TourneyMenu_GetRaceName (0x4264A0)

Returns race name string for current tournament position.

TourneyMenu_WriteSave (0x4264B0)

Saves tournament progress to registry.

TourneyMenu_LoadSaveAndShow (0x4265A0)

Loads saved tournament state and displays current race.

GraphicsOptionsMenu (0x42E840)

OptionsMenu_RenderControls renders control mapping:

  • Lists all DirectInput devices
  • Shows key bindings (up/down/left/right/action)
  • Supports keyboard, mouse, joystick remapping

LoadingScreenGadget (0x3628 bytes)

Created by TimerDisplay (0x4298C0 — actually App_ResourceLoader).
Manages the loading screen during initial asset loading.
See ASSET_MANIFEST.md for complete list of loaded resources.

Menu Input Handling

Scene_HandleInput (0x4692F0):

  1. Checks each gadget in scene's gadget list (Scene+0x858)
  2. If gadget is input active (+0x16 != 0):
    • Dispatches input through gadget vtable[1]
    • If gadget captures input (+0x14 != 0): marks as active
  3. Player 1 uses App+0x1E4 input channel
  4. Player 2 uses App+0x1E8 input channel
  5. Plays sound effect via App+0x1DC sound channel

Key Menu Functions

Address Function Description
0x42DE50 MainMenu_ctor "Main Menu" — PLAY/HS/OP/CR/RG/MG/EXIT
0x42E220 DifficultyMenu_ctor "CHOOSE A DIFFICULTY!" — EASY/NORMAL/HARD/BACK
0x42EA30 PracticeMenu_ctor Level selection for practice mode
0x42F810 TimeTrialMenu_ctor Level selection for time trials
0x42FC10 PartyMenu_ctor Level selection for party/multiplayer
0x42E4B0 PauseMenu_Ctor In-race pause: RESUME/QUIT
0x42E6F0 QuitRaceMenu Quit confirmation: YES/NO
0x42B190 ConfirmMenu_ctor Generic yes/no dialog (6 xrefs)
0x42B890 HighScoreMenu_Render High score display
0x449820 UIList_AddItem Add menu item with display text + ID code
0x4497F0 UIList_AddSpacer Add vertical spacing
0x4280E0 App_ShowMainMenu Menu command dispatcher
0x4284C0 App_SaveAllConfig Save settings to registry on menu exit
0x4279F0 LoadOrSaveConfig Cleanup + save on game exit

🔗 Related Documents

UI Text Elements

types : modding
keywords :

📂 View source on GitHub


Hamsterball UI Text Elements: Drawing On-Screen Text

Scope: Original Hamsterball.exe (PE32, i386, Athena engine, VS2003).
Method: Direct Ghidra decompilation and disassembly verification.
Last Updated: 2026-06-20 (revision 2 — vtable params, Color struct passing, troubleshooting)


1. Overview

This document explains how to draw text on screen in Hamsterball using the
engine's built-in bitmap-font rendering pipeline. It covers the correct this
(Font*) pointer chain, function signatures, color/transform struct layout, and
practical DLL-mod examples.

The old version of this doc incorrectly described the this pointer as
"GraphicsDevice/App" and gave wrong parameter descriptions. All findings below
are verified against raw Ghidra decompilation and disassembly, cross-referenced
with actual call sites in the game code.

Revision 2 adds: critical finding that params 6 and 11 are overwritten
internally by UI_DrawTextCentered/UI_DrawTextShadow_Wrapper, troubleshooting
for Color struct passing, and a concrete failing-call analysis.


2. The Font Object

All text drawing functions are __thiscall with this = a Font object
pointer
, NOT the App, GraphicsDevice, or Scene.

2.1 Font* pointer chain from App

The global App pointer is at 0x005341E0:

void* g_App = *(void**)0x005341E0;

the app object stores pointers to loaded Font objects at these offsets
(verified via decomp_resource_manifest.c — the master asset loader at
0x0042A8C0):

App Offset Font Path Usage
+0x318 fonts\showcardgothic28 Main title / UI text
+0x31C fonts\showcardgothic14 Small label text
+0x324 fonts\arialnarrow12bold UI detail text
+0x328 fonts\showcardgothic72 Race timer digits (large)
+0x320 fonts\showcardgothic16 Info text

2.2 How to get the Font* in a DLL mod

// Global App pointer (Athena engine)
void* g_App = *(void**)0x005341E0;

// Get font pointers
void* font_title    = *(void**)((char*)g_App + 0x318);  // showcardgothic28
void* font_small    = *(void**)((char*)g_App + 0x31C);  // showcardgothic14
void* font_detail   = *(void**)((char*)g_App + 0x324);  // arialnarrow12bold
void* font_timer    = *(void**)((char*)g_App + 0x328);  // showcardgothic72
void* font_info     = *(void**)((char*)g_App + 0x320);  // showcardgothic16

2.3 Font struct layout (from LoadFont at 0x00457130)

Offset Type Field
+0x00 void* vtable
+0x04 void* Graphics device ptr
+0x08 AthenaList Glyph texture list
+0x420 int Space width
+0x424 int Max line height
+0x428 float Scale (default 1.0 = 0x3f800000)
+0x42C char[0x500] Per-glyph table (0x14 bytes × 128 entries)

Each glyph entry at font + char × 0x14 + 0x42C:

Offset Type Field
+0x00 char Valid flag
+0x04 int advance_width
+0x08 int offset_x
+0x0C int offset_y
+0x10 int width
+0x14 void* sprite ptr

Note: The glyph entry offsets are approximate based on LoadFont and
Font_DrawGlyph decompilation. The key fields used by the renderer are:
valid flag (+0x00), advance width (+0x04+0x430 via index math),
and sprite pointer (+0x43C).


3. Color / Transform Struct

The text-drawing functions use a 5-DWORD struct for color/transform data.
This is NOT a simple RGBA int — it's a struct with a vtable pointer followed
by 4 floats (R, G, B, A).

3.1 Static identity transform

A pre-built static struct exists at 0x004CF300:

Offset Value Meaning
+0x00 0x00401070 vtable (Vec3_dtor)
+0x04 255.0f R (white)
+0x08 (garbage) G
+0x0C 1.45f B / scale

In practice, most callers use Matrix_Scale4x4 to build a fresh 4×4 matrix on
the stack, then pass its address. The first DWORD of the matrix is treated as a
vtable pointer by the rendering code.

3.2 How the game builds color structs

From TourneyContinueDialog_Render (0x00445F50):

// The game creates two 20-byte structs on the stack
// (one for text color, one for shadow color)
// via Matrix_Scale4x4, then passes them to UI_DrawTextShadow.

float text_color[5];    // [vtable, R, G, B, A]
float shadow_color[5];   // [vtable, R, G, B, A]

// Matrix_Scale4x4 fills the struct with a scale matrix
Matrix_Scale4x4(text_color, 0, 0, 0, 1.0f);    // identity
Matrix_Scale4x4(shadow_color, 1.0f, 1.0f, 1.0f, 1.0f); // white shadow

Practical shortcut: Use 0x004CF300 as the vtable pointer value and fill
the rest with floats. Or better yet, use the wrapper function (§4.3) which
auto-fills the vtable.


4. Text Drawing Functions

4.1 UI_DrawTextShadow — Full control (15 stack params + this)

Address: 0x004012C0
Calling convention: __thiscall (ECX = Font*)
RET: 0x3C (15 × 4 = 60 bytes cleaned)

void __thiscall UI_DrawTextShadow(
    Font* this,           // ECX
    char* text,           // [ESP+0x00] param_1
    int x,                // [ESP+0x04] param_2
    int y,                // [ESP+0x08] param_3
    int shadow_dx,        // [ESP+0x0C] param_4
    int shadow_dy,        // [ESP+0x10] param_5
    void* text_xform_vtbl,// [ESP+0x14] param_6  — vtable ptr for text color
    float text_r,         // [ESP+0x18] param_7
    float text_g,         // [ESP+0x1C] param_8
    float text_b,         // [ESP+0x20] param_9
    float text_a,         // [ESP+0x24] param_10
    void* shdw_xform_vtbl,// [ESP+0x28] param_11 — vtable ptr for shadow color
    float shadow_r,       // [ESP+0x2C] param_12
    float shadow_g,       // [ESP+0x30] param_13
    float shadow_b,       // [ESP+0x34] param_14
    float shadow_a        // [ESP+0x38] param_15
);

This is the lowest-level text function. It draws text with a drop-shadow.
The color params are part of 5-DWORD transform structs (vtable + 4 floats).

This is the most complex function and hardest to call directly.
Prefer the wrapper (§4.3) or Font_DrawCentered (§4.4) instead.

4.2 UI_DrawTextCentered — Centered with shadow (15 params + this)

Address: 0x00409C60
Calling convention: __thiscall (ECX = Font*)
RET: 0x3C (15 × 4 = 60 bytes cleaned)

Same signature as UI_DrawTextShadow. Internally:

  1. Builds two 5-DWORD color structs on the stack, using 0x4CF300 as the
    vtable pointer (params 6 and 11 from the caller are ignored/overwritten)
  2. Calls Font_MeasureText(this, text) to get text width
  3. Subtracts width/2 from x to center
  4. Calls UI_DrawTextShadow with the two internally-built structs

The this pointer MUST be a Font*, obtained via *(App + 0x318) etc.

CRITICAL (verified from disassembly): Params 6 and 11 are overwritten
with the hardcoded absolute address 0x4CF300 inside the function body:

00409c7f: MOV dword ptr [EAX],0x4cf300   ; overwrites param_6 slot
00409cae: MOV dword ptr [EAX],0x4cf300   ; overwrites param_11 slot

This means you do NOT need to pass a valid vtable pointer for params 6
and 11 — they are replaced internally. Pass 0, NULL, or any value; it
will be overwritten. The actual color data lives in params 7–10 (text
RGBA) and 12–15 (shadow RGBA) as individual floats.

4.3 UI_DrawTextShadow_Wrapper — Easiest high-level (15 params + this)

Address: 0x00409B90
Calling convention: __thiscall (ECX = Font*)
RET: 0x3C (15 × 4 = 60 bytes cleaned)

void __thiscall UI_DrawTextShadow_Wrapper(
    Font* this,           // ECX
    char* text,           // param_1  — text to draw
    int x,                // param_2  — screen X
    int y,                // param_3  — screen Y
    int shadow_dx,        // param_4  — shadow pixel offset X (typically 2-5)
    int shadow_dy,        // param_5  — shadow pixel offset Y (typically 2-5)
    void* unused_6,       // param_6  — IGNORED (replaced with &0x4CF300 internally)
    float text_r,         // param_7  — text red   (0.0-1.0)
    float text_g,         // param_8  — text green (0.0-1.0)
    float text_b,         // param_9  — text blue  (0.0-1.0)
    float text_a,         // param_10 — text alpha (0.0-1.0)
    void* unused_11,      // param_11 — IGNORED (replaced with &0x4CF300 internally)
    float shadow_r,      // param_12 — shadow red
    float shadow_g,       // param_13 — shadow green
    float shadow_b,       // param_14 — shadow blue
    float shadow_a        // param_15 — shadow alpha
);

This is the RECOMMENDED function for DLL mods. It auto-fills the vtable
pointers (0x4CF300) for both color structs, so you only pass raw RGBA floats.
Pass 0 (NULL) for params 6 and 11 — they're overwritten internally.

4.4 Font_DrawCentered — No shadow, auto-centered (8 params + this)

Address: 0x0042C870
Calling convention: __thiscall (ECX = Font*)
RET: 0x20 (8 × 4 = 32 bytes cleaned)

void __thiscall Font_DrawCentered(
    Font* this,           // ECX
    char* text,           // param_1
    int x,                // param_2  — center X
    int y,                // param_3  — Y
    void* unused_4,       // param_4  — IGNORED (overwritten with &0x4CF300)
    float r,              // param_5  — red   (0.0-1.0)
    float g,              // param_6  — green (0.0-1.0)
    float b,              // param_7  — blue  (0.0-1.0)
    float a               // param_8  — alpha (0.0-1.0)
);

This is the simplest text function — no shadow, auto-centered, auto-fills
the transform vtable. Internally calls Font_MeasureText then Font_DrawGlyph.

Note on color: When font scale (Font+0x428) == 1.0f (the default), the
color params are IGNORED — each glyph sprite is drawn via Sprite_DrawRect
which uses Color_RandomRGBA() internally. To get colored text, you must
either set Font+0x428 to something other than 1.0 (which triggers the
Scene_CreateObject4f path that uses the color params), or accept the default
white text from the sprite textures.

4.5 Font_DrawGlyph — Raw glyph rendering (8 params + this)

Address: 0x00457440
Calling convention: __thiscall (ECX = Font*)
RET: 0x20 (8 × 4 = 32 bytes cleaned)

void __thiscall Font_DrawGlyph(
    Font* this,           // ECX
    char* text,           // param_1  — text (iterates each char)
    int x,                // param_2  — start X (left-aligned)
    int y,                // param_3  — start Y
    void* xform_vtbl,     // param_4  — vtable ptr (use 0x4CF300)
    float r,              // param_5
    float g,              // param_6
    float b,              // param_7
    float a               // param_8
);

This is the lowest-level text function. Iterates each character, looks up its
glyph at Font + char × 0x14 + 0x42C, and draws it via Sprite_DrawRect
(scale 1.0) or Scene_CreateObject4f (scaled). Same color caveat as
Font_DrawCentered — colors only work when scale ≠ 1.0.

4.6 Font_MeasureText — Measure string width

Address: 0x00456E20
Calling convention: __thiscall (ECX = Font*)
RET: 0x04 (1 × 4 = 4 bytes cleaned)

int __thiscall Font_MeasureText(Font* this, char* text);

Returns total advance width in pixels for the string. Uses Font+0x428 (scale)
and each glyph's advance width at Font + char × 0x14 + 0x430.

4.7 Font_DrawGlyph3D — World-space text (18 params + this)

Address: 0x00457690
Calling convention: __thiscall (ECX = Font*)
RET: 0x48 (18 × 4 = 72 bytes cleaned)

Draws text in 3D world space with arbitrary orientation vectors. Too complex
for simple HUD use. Not recommended for DLL mods.


5. Why 0x409C60 May Not Work

The function at 0x409C60 (UI_DrawTextCentered) is correct and works,
but there are several reasons it may fail in practice:

5.1 Wrong this pointer

The #1 cause of failure. The old version of this doc said this was
"GraphicsDevice/App" — this is wrong. The this pointer MUST be a Font*
obtained from App+0x318 (or another font offset). Passing the App, Scene,
or GraphicsDevice pointer will crash inside Font_DrawGlyph when it tries to
access this+0x42C (the glyph table).

Correct:

void* app  = *(void**)0x005341E0;
void* font = *(void**)((char*)app + 0x318);
UI_DrawTextCentered(font, "Hello", 400, 300, ...);

Wrong (will crash):

void* app = *(void**)0x005341E0;
UI_DrawTextCentered(app, "Hello", 400, 300, ...);  // CRASH!

5.2 Too many parameters

UI_DrawTextCentered takes 15 stack params + ECX (this). If you're calling it
from C/C++ with __thiscall, you must push exactly 15 DWORDs. Many callers
get the count wrong. Use Font_DrawCentered (8 params) or
UI_DrawTextShadow_Wrapper (15 params but auto-fills vtable) instead.

5.3 Color struct vtable not set — DEBUNKED

Previous doc said: "Params 6 and 11 must be a valid vtable pointer
(0x4CF300) or the function will crash."

WRONG. Verified from disassembly: UI_DrawTextCentered (0x409C60)
overwrites params 6 and 11 with the hardcoded immediate 0x4CF300:

00409c7f: MOV dword ptr [EAX],0x4cf300   ; param_6 overwritten
00409cae: MOV dword ptr [EAX],0x4cf300   ; param_11 overwritten

The same is true for UI_DrawTextShadow_Wrapper (0x409B90).
Params 6 and 11 are always replaced internally — you can pass 0, NULL,
or garbage. The actual color data is the 4 floats after each vtable slot
(params 7–10 and 12–15).

5.4 Font not loaded yet

The font pointers at App+0x318 etc. are only valid after the resource
loader (0x0042A8C0) has completed. If you call during early initialization,
the pointer will be NULL. Hook after the loading screen completes.

5.5 Color struct not expanded to individual floats

If your mod API wrapper (CallMethod or similar) takes a Color struct
(e.g. struct Color { float r, g, b, a; }) as a single parameter, it may
pass it as a pointer (1 DWORD) instead of pushing 4 individual floats
onto the stack. This results in only 9 DWORDs on the stack instead of 15,
causing stack corruption and a crash when the function executes RET 0x3C
(it tries to clean 60 bytes but only 36 were pushed).

Fix: Pass each color component as a separate float argument:

// WRONG — Color struct may be passed as pointer (1 DWORD):
CallMethod(0x409C60, font, "text", x, y, 5, 5,
    0, Color(0.5f, 0.5f, 0.5f, 0.8f),
    0, Color(0.0f, 0.0f, 0.0f, 1.0f));

// CORRECT — 15 individual DWORDs on the stack:
CallMethod(0x409C60, font, "text", x, y, 5, 5,
    0,                           // param_6 (ignored, overwritten)
    0.5f, 0.5f, 0.5f, 0.8f,     // params 7-10: text RGBA
    0,                           // param_11 (ignored, overwritten)
    0.0f, 0.0f, 0.0f, 1.0f);    // params 12-15: shadow RGBA

5.6 Y coordinate at screen edge

Setting y=0 places text at the very top of the screen. Depending on the
font's glyph baseline offset and the viewport, the text may be partially or
fully clipped. Use y=20 or higher to ensure visibility.

5.7 Failing call analysis (real example)

This call was reported as not working:

DWORD vtable = baseAddr + 0xCF300;
void* font = *(void**)((char*) api->GetApp() + 0x318);
CallMethod(0x409C60, font, (char*)"69420", 437, 0, 5, 5,
    vtable, Color(.5f, .5f, .5f, .8f),
    vtable, Color(0.0f, 0.0f, 0.0f, 1.0f));

Issues identified:

  1. vtable param is unnecessary. Params 6 and 11 are overwritten with
    0x4CF300 inside the function. The baseAddr + 0xCF300 calculation is
    wasted effort and irrelevant.

  2. Color() struct passing. If CallMethod passes Color() as a struct
    by pointer (1 DWORD) instead of expanding to 4 floats (4 DWORDs), the
    stack is misaligned. This is the most likely cause of failure.
    Total params would be 9 instead of 15.

  3. y=0 may cause text to be clipped at the top of the screen.

  4. No null check on font. If App+0x318 is NULL (font not loaded yet),
    the call will crash.

Recommended fix — use Font_DrawCentered (8 params, simplest):

void* font = *(void**)((char*)api->GetApp() + 0x318);
if (!font) return;
CallMethod(0x42C870, font, (char*)"69420", 437, 20,
    0,                    // ignored (overwritten with 0x4CF300)
    0.5f, 0.5f, 0.5f, 0.8f);  // RGBA (note: only works if font scale ≠ 1.0)

Or use UI_DrawTextShadow_Wrapper (15 params, with shadow):

void* font = *(void**)((char*)api->GetApp() + 0x318);
if (!font) return;
CallMethod(0x409B90, font, (char*)"69420", 437, 20, 3, 3,
    0,                        // ignored
    0.5f, 0.5f, 0.5f, 0.8f,   // text RGBA
    0,                        // ignored
    0.0f, 0.0f, 0.0f, 1.0f);  // shadow RGBA

6. Recommended Approach for DLL Mods

6.1 Best: Use UI_DrawTextShadow_Wrapper (0x409B90)

This is the easiest function because it auto-fills the color struct vtable:

typedef void (__thiscall *DrawTextShadowWrapper_t)(
    void* font, char* text, int x, int y,
    int sx, int sy, void* unused1,
    float tr, float tg, float tb, float ta,
    void* unused2, float sr, float sg, float sb, float sa);

// At address 0x409B90 in the original EXE
DrawTextShadowWrapper_t DrawTextShadowWrapper =
    (DrawTextShadowWrapper_t)0x00409B90;

void DrawHUDText(const char* text, int x, int y) {
    void* app  = *(void**)0x005341E0;
    void* font = *(void**)((char*)app + 0x318);  // showcardgothic28

    if (!font) return;  // font not loaded yet

    DrawTextShadowWrapper(
        font,                           // ECX = Font*
        (char*)text,                     // text
        x, y,                            // position
        3, 3,                            // shadow offset (3px right, 3px down)
        (void*)0,                        // unused (auto-filled)
        1.0f, 1.0f, 1.0f, 1.0f,         // text color: white, opaque
        (void*)0,                        // unused (auto-filled)
        0.0f, 0.0f, 0.0f, 1.0f          // shadow color: black, opaque
    );
}

6.2 Alternative: Use Font_DrawCentered (0x0042C870)

Simpler signature (8 params), no shadow, auto-centered:

typedef void (__thiscall *FontDrawCentered_t)(
    void* font, char* text, int x, int y,
    void* unused, float r, float g, float b, float a);

FontDrawCentered_t FontDrawCentered =
    (FontDrawCentered_t)0x0042C870;

void DrawCenteredText(const char* text, int x, int y) {
    void* app  = *(void**)0x005341E0;
    void* font = *(void**)((char*)app + 0x318);

    if (!font) return;

    FontDrawCentered(
        font,                   // ECX = Font*
        (char*)text,             // text
        x, y,                    // center position
        (void*)0,                // unused (auto-filled with 0x4CF300)
        1.0f, 1.0f, 1.0f, 1.0f  // RGBA (only used if font scale != 1.0)
    );
}

6.3 Direct assembly call (for inline asm in DLL)

If calling from inline assembly in a bass.dll proxy:

; Example: Call UI_DrawTextShadow_Wrapper(font, "Hello", 400, 300, 3, 3, ...)
; ECX = Font*, 15 stack params

push_immediate 1.0          ; param_15: shadow_alpha (0x3f800000)
push_immediate 0.0          ; param_14: shadow_blue  (0x0)
push_immediate 0.0          ; param_13: shadow_green (0x0)
push_immediate 0.0          ; param_12: shadow_red   (0x0)
push_immediate 0x4cf300     ; param_11: shadow vtable (or 0 for wrapper)
push_immediate 1.0          ; param_10: text_alpha
push_immediate 1.0          ; param_9:  text_blue
push_immediate 1.0          ; param_8:  text_green
push_immediate 1.0          ; param_7:  text_red
push_immediate 0x4cf300     ; param_6:  text vtable (or 0 for wrapper)
push_immediate 3            ; param_5:  shadow_dy
push_immediate 3            ; param_4:  shadow_dx
push_immediate 300          ; param_3:  y
push_immediate 400          ; param_2:  x
push_offset hello_str       ; param_1:  text
mov  ecx, [font_ptr]       ; ECX = Font*
call dword ptr [0x00409B90] ; UI_DrawTextShadow_Wrapper
; No add esp needed — function cleans 0x3C bytes via RET 0x3C

7. Font Object Access Paths (for reference)

Different game contexts access the font through different object chains:

7.1 From Gadget-derived objects (Board, Menu, etc.)

All Gadget-derived objects store the App pointer at +0x878:

this+0x878 = App
Font = *(App + 0x318)   // or 0x31C, 0x324, 0x328, 0x320

Verified call sites:

  • ArenaBoard_Render (0x421910): MOV ECX,[ESI+0x878]; MOV ECX,[ECX+0x328]
  • TourneyMenu_Render (0x00450AF0): UI_DrawTextShadow(*([this+0x878]+0x318), ...)
  • HighScoreEntry_Render (0x0042BD40): *([this+0x878]+0x318)
  • TourneyContinueDialog_Render (0x00445F50): *([this+0x878]+0x318)
  • OkayDialog_ctor (0x00440E70): *([this+0x878]+0x318)

7.2 From SimpleMenu-derived objects

SimpleMenu caches the font pointer at +0x87C:

this+0x87C = *(App + 0x318)  // set in SimpleMenu_ctor (0x00448F20)

Verified: UIList_Render (0x449D40) line: MOV EBP,[EDI+0x87C]

7.3 From ConfirmMenu_Ctor objects (RaceGoalReached)

this+0x0C = [parent+0x878] = App
Font = *(App + 0x318)

Verified: ConfirmMenu_Render (0x44CD10): MOV EAX,[ESI+0x0C]; MOV ECX,[EAX+0x318]

7.4 From CreditsScreen

this+0xCDC = App  (stored in ConfirmMenu_ctor)
Font = *(App + 0x318)

7.5 Direct from global App

void* g_App = *(void**)0x005341E0;
Font = *(void**)((char*)g_App + 0x318);

This is the most reliable approach for DLL mods — no need to trace through
object hierarchies.


8. Changing Font Scale for Colored Text

By default, font scale (Font+0x428) = 1.0f, which causes Font_DrawGlyph
to use the fast Sprite_DrawRect path that ignores color params. To get
colored text:

// Temporarily change font scale to enable colored rendering
float* font_scale = (float*)((char*)font + 0x428);
float old_scale = *font_scale;
*font_scale = 1.2f;  // any value != 1.0 triggers the colored path

FontDrawCentered(font, text, x, y, 0, r, g, b, a);

*font_scale = old_scale;  // restore

The game itself does this in HighScoreEntry_Render (0x0042BD40):

*(float*)(font + 0x428) = 0x3f400000;  // 0.75f — shrink for subtitle
// ... draw text ...
*(float*)(font + 0x428) = 0x3f800000;  // 1.0f — restore

9. String Formatting

Use AthenaString_SprintfToBuffer (0x004BAE43) for safe formatting:

typedef void (*AthenaSprintf_t)(char* buffer, const char* fmt, ...);
AthenaSprintf_t AthenaSprintf = (AthenaSprintf_t)0x004BAE43;

char buf[256];
AthenaSprintf(buf, "Score: %d", score);
DrawHUDText(buf, 10, 10);

10. Practical Example: HUD Timer Overlay

// In bass.dll proxy — hook during render frame
void OnRenderFrame() {
    void* app  = *(void**)0x005341E0;
    if (!app) return;

    void* font = *(void**)((char*)app + 0x318);  // showcardgothic28
    if (!font) return;

    // Get scene timer (arena mode: Scene+0x47AC)
    void* scene = *(void**)((char*)app + 0x184);
    if (!scene) return;

    int timer = *(int*)((char*)scene + 0x47AC);
    float seconds = timer / 60.0f;

    char buf[64];
    typedef void (*Sprintf_t)(char*, const char*, ...);
    Sprintf_t Sprintf = (Sprintf_t)0x004BAE43;
    Sprintf(buf, "TIME: %.1f", seconds);

    // Draw using wrapper (simplest API)
    typedef void (__thiscall *DrawText_t)(void*, char*, int, int, int, int,
        void*, float, float, float, float, void*, float, float, float, float);
    DrawText_t DrawText = (DrawText_t)0x00409B90;

    DrawText(font, buf, 400, 20, 3, 3,
        0, 1.0f, 1.0f, 1.0f, 1.0f,    // white text
        0, 0.0f, 0.0f, 0.0f, 1.0f);   // black shadow
}

11. Quick Reference

Function Address Params (stack+this) RET Purpose
UI_DrawTextShadow_Wrapper 0x00409B90 15+1 0x3C Best for mods — auto-fills vtable
UI_DrawTextCentered 0x00409C60 15+1 0x3C Centered + shadow
UI_DrawTextShadow 0x004012C0 15+1 0x3C Raw shadowed text
Font_DrawCentered 0x0042C870 8+1 0x20 Simplest — centered, no shadow
Font_DrawGlyph 0x00457440 8+1 0x20 Raw per-char rendering
Font_MeasureText 0x00456E20 1+1 0x04 Measure string width
Font_DrawGlyph3D 0x00457690 18+1 0x48 3D world-space text (complex)
LoadFont 0x00457130 2+1 Load a font from disk
AthenaString_SprintfToBuffer 0x004BAE43 Safe printf
UIList_SetTextByName 0x0044A8B0 Change menu item text
UIList_AddItem 0x004497F0 Add menu item

Font offsets from App (0x005341E0)

Offset Font Purpose
+0x318 showcardgothic28 Main title / UI text
+0x31C showcardgothic14 Small label text
+0x320 showcardgothic16 Info text
+0x324 arialnarrow12bold UI detail text
+0x328 showcardgothic72 Race timer digits

Font struct key offsets

Offset Type Field
+0x428 float Scale (1.0 = no color, ≠1.0 = colored)
+0x424 int Line height
+0x42C char[] Per-glyph table start

Static identity transform

Address Value Purpose
0x004CF300 vtable+floats Default color/transform struct

12. Common Pitfalls

  1. Wrong this pointer. UI_DrawTextCentered and all text functions expect
    a Font* as this (ECX), obtained via *(App + 0x318). Passing the App,
    Scene, GraphicsDevice, or any other pointer will crash in Font_DrawGlyph.

  2. Too many/few params. UI_DrawTextCentered/UI_DrawTextShadow take 15
    stack params (RET 0x3C). Font_DrawCentered/Font_DrawGlyph take 8 (RET 0x20).
    Mismatching the count corrupts the stack.

  3. Color struct vtable not set — DEBUNKED. Params 6 and 11 of
    UI_DrawTextShadow/UI_DrawTextCentered are overwritten internally with
    0x4CF300. You can pass 0 for these params. They are NOT read from the
    caller's stack. (See §5.3 for disassembly proof.)

  4. Colors ignored at scale 1.0. When Font+0x428 == 1.0f, the fast path
    (Sprite_DrawRect) is used and color params are ignored. Set scale ≠ 1.0
    temporarily for colored text (see §8).

  5. Color struct passed as pointer instead of 4 floats. If your CallMethod
    wrapper receives a Color struct, it may push 1 pointer instead of 4 floats.
    This gives 9 stack params instead of 15 → stack corruption via RET 0x3C.
    Always expand Color into 4 individual float arguments. (See §5.5.)

  6. Font not loaded. Font pointers at App+0x318 etc. are NULL until the
    resource loader completes. Always null-check before drawing.

  7. Calling convention. These are __thiscall — Font* goes in ECX, params
    go on stack right-to-left, callee cleans the stack (RET N). From C, use
    __thiscall typedefs or inline assembly.


🔗 Related Documents

UI/Menu System

types : ui
keywords :

📂 View source on GitHub


UI/Menu System

Hamsterball's UI is a hierarchical menu system built on top of the SceneObject
framework. All menus are SceneObjects added to the scene graph. The system uses
a SimpleMenu base class with UIList widget for vertical scrolling lists.

Menu Class Hierarchy

SceneObject (base)
└── SimpleMenu (0x448F20) — base menu with UIList
    ├── MainMenu (0x4D3F30) — "LET'S PLAY!", HIGH SCORES, OPTIONS, CREDITS, EXIT
    ├── DifficultyMenu (0x4D40A8) — PIPSQUEAK, NORMAL, FRENZIED!
    ├── PauseMenu (0x4D4120) — RESUME, OPTIONS, ABORT RACE
    ├── PauseArenaMenu (0x430330) — pause for ArenaBoard modes
    ├── PracticeMenu (0xD18 bytes) — level select for practice
    ├── TimeTrialMenu (0xD18 bytes) — level select for time trial
    ├── ArenaMenu (0x42FC40) — ArenaBoard arena select
    ├── MPMenu (0x4D3F30, 0xCDC bytes) — multiplayer party setup
    ├── HighScoreMenu (0x42B290) — high scores display
    ├── OptionsMenu — graphics/sound/controls
    ├── KeyRemapMenu (0x4D4340) — key binding screen
    ├── GraphicsOptionsMenu (0x441E70) — quality settings
    └── GameSelectionScreen (0x42E060) — tournament results

Dialog Classes

SceneObject (base)
├── OkayDialog (0x440E70, 0x8A8 bytes) — simple message + OK button
├── ConfirmMenu (0x4C780, 0x8A8 bytes) — YES/NO confirmation
├── QuitDialog (0x43E30) — quit race confirmation + continue tourney
├── QuitAbortDialog (0x45320) — abort race dialog
├── QuitToDesktopDialog (0x44790) — exit to desktop confirmation
├── SaveTourneyDialog (0x4FD60) — save tournament progress
├── TourneyContinueDialog (0x45E60, 0x8BC bytes) — resume saved tournament?
└── RegisterDialog (0x476B0) — serial key registration

SimpleMenu Base (0x448F20)

The fundamental menu class. All game menus inherit from this.

Key Offsets (SimpleMenu and subclasses)

Offset Type Field
+0x868 char* Title text (e.g. "Main Menu", "Pause Menu")
+0x878 App* App pointer (set via SimpleMenu_ctor param)

SimpleMenu_ctor Flow

  1. Call SceneObject base constructor
  2. Set vtable pointer for subclass
  3. Set title text at this+0x868
  4. Add menu items via UIList_AddItem(this, display_text, command_text, colors...)
  5. Add spacers via UIList_AddSpacer(this, pixel_height)

UIList Widget System

UIList Functions

Address Function Description
0x4492D0 UIList_AddItem Add selectable item to list
0x4497F0 UIList_AddItem (alt) Add item with format string
0x4490F0 UIList_AddItemWithFormat Add formatted item
0x449430 UIList_AddSpacer Add vertical spacer
0x449750 UIList_ActivateCurrentItem Click current selection
0x449C20 UIList_HandleKeyNav Keyboard/gamepad navigation
0x44A570 UIList_Layout Recalculate item positions
0x449D40 UIList_Render Draw all items
0x4494D0 UIList_ScrollUpdate Handle scroll animation
0x44A970 UIList_SetColorsByName Set item colors by command
0x44A8B0 UIList_SetTextByName Set item text by command

UIListItem (0x4490A0)

Each item in the list stores:

  • Display text (shown on screen)
  • Command text (dispatched on click, e.g. "PLAY", "BACK", "1PT")
  • Color matrix (4x4 Scale for tinting)
  • Spacer flag + height

Menu Dispatch Tables

Main Menu (0x42DE50)

Display Command Handler
"LET'S PLAY!" "PLAY" GameSelectionManager_ctor
"HIGH SCORES" "HS" HighScoreMenu_ctor
"OPTIONS" "OP" OptionsMenu
"CREDITS" "CR" Credits screen
"REGISTER GAME" "RG" RegisterDialog (only if not registered, App+0x200==0)
(dynamic) "MG" Mini-game button (only if App+0x918 && App+0xB24)
"EXIT TO DESKTOP" "EXIT" QuitToDesktopDialog

Difficulty Menu (0x42E220)

Display Command Difficulty Value
"PIPSQUEAK" "EASY" 0 (easiest, +1000ms bonus)
"NORMAL" "NORMAL" Standard
"FRENZIED!" "HARD" 1 (hardest, +500ms bonus)
(spacer)
"PREVIOUS" "BACK" Return to previous menu

Title: "CHOOSE A DIFFICULTY!"

Pause Menu (0x42E4B0)

Display Command Handler
"RESUME" "RESUME" Unpause game
"OPTIONS" "OP" Options submenu
(spacer)
"ABORT RACE" "BACK" Quit current race

Title: "Pause Menu"

ArenaBoard Menu (0x433AC0 — GameSelectionManager)

See RUMBLEBOARD_SYSTEM.md for full dispatch table.

Play Mode Selection (from GameSelectionManager)

Display Command Mode
1-Player Tournament "1PT" Tournament, is_mirror=0
1-Player Mirror Tournament "1PMT" Tournament, is_mirror=1
1-Player Practice "1PP" Practice, is_tournament=0
1-Player Time Trial "1PTT" TimeTrial, is_tournament=0
Multiplayer Party "PARTY" MP mode

Menu Color System

Each UIList_AddItem takes a 4x4 matrix for color tinting. Common color patterns:

Pattern Hex Values Usage
Highlight (bright) 1.0, 1.0, 0.7, 1.0 Selected item
Normal 1.0, 1.0, 1.0, 1.0 Standard item
Dimmed 0.75, 0.75, 0.5, 1.0 Less important item
Warning (red) 1.0, 0.5, 0.5, 1.0 Destructive action (Abort/Exit)
Dim red 0.75, 0.5, 0.5, 1.0 Less important destructive
Pipsqueak 0.7, 1.0, 0.7, 1.0 Green tint (easiest)
Frenzied 1.0, 0.5, 0.5, 1.0 Red tint (hardest)
Register 1.0, 1.0, 0.0, 1.0 Yellow (limited time)

App Menu State

App Offset Type Field
+0x184 void* scene object list (for Scene_AddObject)
+0x224 void* Current main menu pointer
+0x235 byte is_tournament (0=practice/trial, 1=tournament)
+0x236 byte is_mirror (0=normal tracks, 1=mirror tracks)
+0x237 byte tournament_complete flag
+0x200 byte is_registered (1=registered, show "REGISTER" if 0)
+0x5E8 int P1 time display
+0x688 int P2 time display
+0x728 int P3 time display
+0x7C8 int P4 time display
+0x918 byte has_mini_game (show "MG" button)
+0xB24 char* mini_game_label (dynamic button text)

App Frame Lifecycle

App_FrameTick (0x46C9E0)

Called each frame when game is NOT paused (App+0x159 == 0):

  1. vtable[0x60]() — SceneObject::Tick (update current scene objects)
  2. MeshWorld_CallVtable34(scene, param) — Update mesh world
  3. MusicDevice_MuteToggle(param) — Handle audio focus

App_CompleteRace (0x425F90)

Called when a race finishes:

  1. If App+0x704 != 0 (race in progress):
    • Increment App+0x7C8 (race completion counter)
    • Call scene->vtable[0xFC](0, 13, 1) — D3D surface operation 13
    • Call scene->vtable[0xFC](0, 14, 1) — D3D surface operation 14
    • Clear App+0x704 (race no longer in progress)

App_Is2PMode (0x427910)

Checks if 2-player mode is active:

bool App_Is2PMode(App *app) {
    return ((app[app->p1_offset + 0xC] | app[app->p2_offset + 0xC]) >> 7) & 1;
}

Dialog Details

RegisterDialog (0x476B0)

Serial key registration dialog. Features:

  • Text input for serial key
  • RegisterDialog_ValidateSerial(0x46B80) — key validation
  • RegisterDialog_HandleInput(0x475A0) — character input processing
  • RegisterDialog_HandleKey(0x48890) — keyboard handler
  • "REGISTER GAME" appears on Main Menu only when App+0x200 == 0

OkayDialog (0x440E70)

Simple message display dialog:

OkayDialog_ctor(new(0x8A8), app, title_text, message_text, display_time_ms);

Used for error messages, unlock notifications, etc.

TourneyContinueDialog (0x45E60)

Appears when tournament save file exists (DATA\TOURNAMENT.SAV):

  • "Continue saved tournament?" prompt
  • On continue: loads saved state
  • On new game: starts fresh tournament

Related Functions

Address Name Purpose
0x448F20 SimpleMenu_ctor Base menu constructor
0x42DE50 MainMenu_ctor Main menu setup
0x42E220 DifficultyMenu_ctor Difficulty selection
0x42E4B0 PauseMenu_Ctor In-game pause
0x433AC0 GameSelectionManager Title screen dispatcher
0x4492D0 UIList_AddItem Add menu item
0x449430 UIList_AddSpacer Add vertical gap
0x449C20 UIList_HandleKeyNav Input navigation
0x449D40 UIList_Render Draw menu
0x440E70 OkayDialog_ctor Message dialog
0x443E30 QuitDialog_ctor Race quit dialog
0x44790 QuitToDesktopDialog_Ctor Exit confirmation
0x45E60 TourneyContinueDialog_Ctor Resume tournament
0x476B0 RegisterDialog_ctor Serial key entry
0x46B80 RegisterDialog_ValidateSerial Key validation

🔗 Related Documents

Unbreakable + 6x Size Mod

types : mods
keywords :

📂 View source on GitHub


Unbreakable + 6x Size Mod

Info

  • File: bass.dll (proxy)
  • Effects: No fall damage + 6x ball size + no pause
  • Android-safe: No IAT hooks, no GetTickCount, no threads

What it does

Merges three mods into one DLL:

1. No Pause (3 patches)

Disables ESC/right-click pause via 3 single-byte JZ→JMP patches.

2. 6x Ball Size (3 patches)

  • Ball_ctor2 default radius: 27.0 → 162.0
  • Player ball spawn radius: 26.0 → 156.0
  • CreateBadBall SIZE: code cave multiplies by 6.0f (pure FPU asm, no C calls)

3. Unbreakable Ball (8 patches from XRow's CEA)

  • 3× early RET (Ball_Shatter, variant, Ball_FallDeath)
  • 5× NOP (prevent shatter flag + is_active + fall timer writes)

Winlator Safety

  • No IAT hooks (GetTickCount crashes Android)
  • No background threads — all patches applied in DllMain
  • Code cave is pure FPU assembly (FSTP/FLD/FMUL/RET)
  • All patches restored on DLL_PROCESS_DETACH

Build

i686-w64-mingw32-gcc -shared -o bass.dll unbreakable_6x.c \
  -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy mod bass.dll to game folder
  3. On Android/Wine: set DLL override to native for bass.dll

🔗 Related Documents

Unbreakable Ball Mod

types : mods
keywords :

📂 View source on GitHub


Unbreakable Ball Mod

Info

  • File: bass.dll (proxy)
  • Effect: Ball never shatters — no fall damage
  • Android-safe: No IAT hooks, no code caves, no threads for byte patches
  • Translated from: XRow's CEA script

What it does

Patches 8 points in Hamsterball.exe to prevent the ball from shattering:

# Address Original Patch Effect
1 0x408D70 6A FF 64 A1 (SEH frame) C3 90 90 (RET) Ball_Shatter early return
2 0x409050 6A FF 64 A1 (SEH frame) C3 90 90 (RET) Shatter variant early return
3 0x409480 6A FF 64 A1 (SEH frame) C3 90 90 (RET) Ball_FallDeath early return
4 0x40C761 88 85 68 07 00 00 6× NOP Prevent is_active=0 write
5 0x40C767 C6 85 E9 02 00 00 01 7× NOP Prevent shatter flag=1 write
6 0x40F226 C6 86 68 07 00 00 00 7× NOP Prevent is_active=0 write
7 0x40F22D C6 86 E9 02 00 00 01 7× NOP Prevent shatter flag=1 write
8 0x4075C9 FF 86 EC 02 00 00 6× NOP Prevent fall timer increment

How it works

  • Uses VirtualProtect to make code pages writable
  • Writes byte patches directly (NOP or RET)
  • Verifies original bytes before patching (won't double-patch)
  • Restores original bytes on DLL unload
  • All pointer accesses guarded by IsBadReadPtr

Build

i686-w64-mingw32-gcc -shared -o bass.dll unbreakable_ball.c -lwinmm \
  -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

Installation

  1. Rename original bass.dll to bass_real.dll
  2. Copy mod bass.dll to game folder
  3. On Android/Wine: set DLL override to native for bass.dll

🔗 Related Documents

Universal Ref Loader

types : mods
keywords :

📂 View source on GitHub


Universal Ref Loader — Warm-Up Slot Swap Test

Date: 2026-06-23
Mod: universal-ref-loader v3 (bass.dll proxy, 94747 bytes)
Test method: Each race level's MESHWORLD file was copied into Level1.MESHWORLD (Warm-Up race slot), then the Warm-Up Time Trial race was loaded. The game process was checked for survival 8 seconds after race start.

Important limitation

Screenshots are black on Wine/llvmpipe (the mod DLL causes rendering to not produce visible output on software rendering). The mod DLL's hook at 0x0040C4BA interferes with D3D8 rendering on llvmpipe — the game window stays black. However, the game process continues running, which means the ref loading code (Scene_CreateDynamicObjects → vtable[33] hook) executes without crashing.

On real Windows with a GPU, rendering works normally — the hook only intercepts the factory dispatch, not the rendering pipeline.

Results

Level Race Object Refs Result
L3 Intermediate BRIDGE, MOUSETRAP ✅ ALIVE
L4 Dizzy GLUEBIE, TIPPER, WATERWHEEL, SWIRL, TARBUBBLE, SIGN-TARPIT ✅ ALIVE
L5 Tower CATAPULT, TRAPDOOR, DRAWBRIDGE, MACE, WINDMILL, CHOMPER, TURRET ✅ ALIVE
L6 Up SPEEDCYLINDER, LIFTER, TIMEBUTTON ✅ ALIVE
L7 Neon DFLOOR, TRODE, NEONPLATFORM ✅ ALIVE
L8 Expert BONK, FAN, SAWBLADE, BRIDGE, JUDGE, BELL ✅ ALIVE
L9 Odd LIFTER, LAUNCH ✅ ALIVE
L10 Toob SPINNY, SAW, FALLOUT1, BLOCKDAWG ✅ ALIVE
L11 Wobbly WOBBLY, WAVY ✅ ALIVE
L12 Glass SMASHER ✅ ALIVE
L13 Sky PILLAR, MAGNIFYER, POPCYLINDER, TRAPDOOR ✅ ALIVE
L14 Master BBRIDGE, BLOCKDAWG, BONK, BRIDGE, CATAPULT, GLUEBIE, POPCYLINDER, TIPPER ✅ ALIVE
L15 Impossible LOOPER, GEAR, BIGGEAR, ROTATOR, PENDULUM ✅ ALIVE

13/13 levels passed — no crashes.

All 46 unique object types across 13 race levels loaded without crashing when loaded in the Warm-Up race slot. The Warm-Up board constructor does not load any object meshes (it has no object refs), so the mod's universal factory dispatch hook was responsible for loading all objects via the Arena factory fallback + JIT mesh injection.

What "ALIVE" means

  • Game process survived 8+ seconds after entering the race
  • No crash during Scene_CreateDynamicObjects (where the hook fires)
  • No crash during factory dispatch for each ref point
  • No crash during JIT mesh loading (MeshWorld_ctor, CollisionLevel_ctorWithLevel)
  • No crash during clone-on-return for static-mesh objects

What we couldn't verify on Wine

  • Visual rendering (black screen on llvmpipe)
  • Whether objects appear at correct positions
  • Whether object behaviors work (catapults, tippers, etc.)
  • These require testing on real Windows with a GPU

🔗 Related Documents

Universal Safespots

types : mods
keywords :

📂 View source on GitHub


Universal Safespots

A bass.dll proxy mod that adds a "universal" SAFESPOT type to Hamsterball.

What It Does

Place a SAFESPOT(*) reference point in your level's MESHWORLD file. When the ball
has an active checkpoint filter (e.g. (B) from an E:SAFESWITCH(B) trigger), normal
SAFESPOTs with non-matching letters are rejected by the respawn search. This mod
intercepts that rejection and checks if the SAFESPOT name contains (*). If found,
the SAFESPOT is accepted regardless of the current filter — it competes with
matching SAFESPOTs purely on distance.

How It Works

In Ball_FindClosestRespawnPoint (0x405190), the filter check at 0x405894 does:

jne 0x4058C2    ; reject if strnicmp != 0 (letters don't match)

The mod replaces this 2-byte jne (plus 3 bytes of the accept path that follow it)
with a 5-byte JMP to a code cave. The code cave:

  1. If strnicmp matched (eax==0): accept (original behavior)
  2. If strnicmp didn't match: calls strstr(safespot_name, "(*)")
    • If found: accept (override — universal safespot)
    • If not found: reject (original behavior)

The strstr function (0x4BAC20) is statically linked in the EXE and preserves edi.

Hook Details

Address Original Bytes Description
0x405894 75 2C jne 0x4058C2 (reject non-matching filter)
0x405896 8B 07 mov eax, [edi] (accept path: load safespot name)
0x405898 68 90 F4 4C 00 push 0x4CF490 (accept path: push "[Z]" string)

5 bytes at 0x405894 are replaced with E9 XX XX XX XX (JMP to code cave).

Usage

  1. Rename the original bass.dll to bass_real.dll in the game folder
  2. Copy this mod's bass.dll into the game folder
  3. Add SAFESPOT(*) reference points in your level's MESHWORLD file
  4. The universal SAFESPOTs will be accepted regardless of active checkpoint filter

Safety

  • No threads, no IAT hooks
  • No C function calls from the code cave (pure assembly, calls statically linked strstr)
  • [Z]/[X] gravity checks still apply after acceptance
  • Multiplayer proximity checks (2P mode) still apply

Compilation

i686-w64-mingw32-gcc -shared -o bass.dll universal_safespots.c \
  -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
  -Wl,--add-stdcall-alias

🔗 Related Documents

unlimited_tris

types : mods

📂 View source on GitHub


unlimited_tris

Removes triangle count limit on custom levels

Files

  • unlimited_tris_bass.c — C source code
  • bass_unlimited.dll — Compiled DLL (PE32 i386)

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

Unreleased & Hidden Features

types : docs
keywords :

📂 View source on GitHub


Hamsterball Unreleased & Hidden Features

Reverse-engineered from Hamsterball.exe (V3.6.c, PE32 i386) via Ghidra decompilation and string analysis.


1. Party Race / Local 2-Player Split-Screen

Status: Fully coded, menu-accessible in retail but possibly cut from some distributions

A complete 2-player local mode exists in the binary:

  • Functions: App_Is2PMode (0x00427910), App_Start2PRace (0x00429230), PartyMenu_ctor (0x0042fc10)
  • Strings: "PARTY GAMES!", "CHOOSE A PARTY RACE!", "PARTY RACE (2P ONLY)", "THE PARTY RACE REQUIRES THAT PLAYER 1 AND PLAYER 2 BE HUMAN PLAYERS!"
  • Controls: 2PController1 through 2PController4 — 4 separate control profiles for local multiplayer
  • Level asset: Levels\\Level10-2PBridge — a dedicated 2P bridge level for the Master race
  • Textures: partyrace.png, Winner2p.png
  • Menu flag: Sets App+0x237=1 (is_2p_mode), App+0x234=1, and enables player slots at +0x677, +0x717, +0x7b7
  • Player 2 status: "PLAYER 2: %s", "PLAYER 2: COMPUTER", "PLAYER 2: OFF" — P2 can be human, AI, or disabled

The 2P mode checks player slots (+0xB2C, +0xB30, +0xB34 against value 100) to determine if players 3/4 are active.


2. Mirror Tournament

Status: Unlockable in retail, but hidden behind win condition

A mirrored (reversed) version of the tournament mode:

  • Strings: "MIRROR TOURNAMENT", "MirrorTournament", "THE MIRROR TOURNAMENT ISN'T UNLOCKED YET! TO UNLOCK THE MIRROR TOURNAMENT, YOU NEED TO WIN A TOURNAMENT AT NORMAL OR FRENZIED DIFFICULTY!"
  • Mirror textures: arrow1-mirrored.png, goal-lit-mirrored.png, goal-mirrored.png, goal-round-lit-mirrored.png, goal-round-mirrored.png, mirror.png, sign-bewarethetar-mirrored.png
  • Render functions: Level_RenderWithMirror4 (0x00413fc0), Level_RenderWithMirror5 (0x004151e0) — render scenes with mirrored projection matrices (flips projection on one axis then restores)
  • Unlock check: GameSelectionManager (0x00433ac0) checks for "LOCKED" string to show the unlock dialog

The mirror rendering works by adjusting projection matrix offsets (+0x790/+0x794) by a delta value, rendering 4 mirror planes, then restoring.


3. "NetworkConnection" — Actually an InputDevice (Not Networking)

Status: Misleading name — this is an InputDevice class, NOT network multiplayer

  • Function: NetworkConnection_Ctor (0x0046dfa0) — base InputDevice constructor
  • Struct layout (20 bytes / 0x14):
    • +0x00: char* name ("Keyboard", "Mouse/Trackball", "Not Connected")
    • +0x04: InputHandler* parent
    • +0x08: int type (0=none, 1=keyboard, 2=mouse, 4-7=gamepad 1-4)
    • +0x0C: float sensitivity (1.0f default, 0.0 when not connected)
    • +0x10: void* device_data (keyboard buffer, joystick state, etc.)
  • Created in: App_Initialize_Full — 4 instances at App+0x550..0x55C, then InputDevice_SetType configures each as keyboard/mouse/gamepad1/gamepad2
  • "Not Connected" is just the default name when no gamepad/joystick is physically plugged in — NOT a network status

WS2_32.dll imports (socket, connect, send, recv, etc.) exist in the binary but are used exclusively by:

  1. eSellerate DRM — HTTP connections to store.esellerate.net for activation/serial verification
  2. Raptisoft crash reporter — sends error reports to Raptisoft's server

There is no online multiplayer code in Hamsterball. The MPMenu ("MP Menu" / App_StartMPRace) stands for Multiplayer Party (local), offering "PARTY RACE (2P ONLY)" and "RODENT RUMBLE (1-4P)" — both local modes.


4. Secret Objects & Arena Unlock System

Status: Fully implemented, part of tournament progression

Hidden collectibles in race levels that unlock arenas:

  • Function: CreateSecretObjects (0x0040baa0) — factory for SECRET and SECRETUNLOCK objects
  • Types:
    • N:SECRET — marks a secret spot found (calls Rotator_MarkTriggered at 0x004371f0, sets +0x10E4=1)
    • N:UNLOCKSECRET / SECRETUNLOCK — triggers arena unlock (calls CheckArenaUnlock at 0x0040aba0)
  • Level files: Levels\Secret, Levels\Secret-Unlock
  • Unlock mechanism: CheckArenaUnlock uses a switch on player profile index (profile+8, cases 4-15) to set unlock flags at offsets +0x85A through +0x868 on the App struct
  • Unlock text: "THIS ARENA ISN'T UNLOCKED YET! TO UNLOCK %s ARENA, YOU NEED TO FIND THE SECRET UNLOCK SPOT IN THE %s RACE DURING A NORMAL OR FRENZIED TOURNAMENT GAME!"
  • Gate: Only works when App+0x23C != 0 (not Pipsqueak difficulty) AND App+0x234 == 0 (in tournament mode) AND profile +0x11 == 0

Secret_ctor (0x0043dfb0) inherits from Stands (static mesh object), allocates a CollisionLevel, and sets up timer-based update/render callbacks.


5. Tournament System (Full Campaign)

Status: Fully implemented, retail feature

A complete tournament mode with progression, saving, and multiple difficulty levels:

  • Manager: GameSelectionManager (0x00433ac0) — handles menu routing: BACK → MainMenu, LOCKED → unlock dialog, 1PT → single player tournament
  • Save file: DATA\TOURNAMENT.SAV / DATA\tournament.sav
  • Save/Load: TourneyMenu_LoadSaveAndShow (0x004265a0), TourneyMenu_WriteSave (0x004264b0), Tourney_SaveTournament (0x00446730)
  • Continue dialog: TourneyContinueDialog_Ctor (0x00445e60) — "CONTINUE TOURNAMENT?" prompt
  • Rollback: "YOU HAVE LOST THE TOURNAMENT. IN ORDER TO RESUME, YOU NEED TO SELECT 'ROLLBACK' ON THE MENU!"
  • Difficulty: Pipsqueak / Normal / Frenzied — affects time bonuses and unlock eligibility
  • Race unlock text: "THIS RACE ISN'T UNLOCKED YET! TO UNLOCK %s RACE, YOU NEED TO REACH IT WHILE PLAYING A NORMAL OR FRENZIED TOURNAMENT GAME!"
  • Rankings: Final ranking by score, with rank textures at textures\ranks\%d.jpg
  • Tournament textures: Per-race preview images (tourney-beginner.png through tourney-impossible.png)
  • Music: Tournament adjusts music tempo (MusicPlayer_SetTempoScale 1.0 for main, 0.5 for secondary)

6. Demo/Trial Limitations

Status: Embedded DRM — retail had a demo version

  • Free play counter: "You have %d free play remaining!" — limited number of plays before purchase required
  • Demo end: "You have reached the end of the demo version of Hamsterball! But, if you buy now, you can continue, right here, right now! Or, click cancel to return to the main menu."
  • Purchase prompts: "BUY HAMSTERBALL AND YOU CAN SAVE YOUR HIGH SCORES!", "CLICK HERE TO BUY!", "CLICK HERE TO REGISTER HAMSTERBALL!"
  • Demo slowdown: "DEMO, THE SLOWER THE GAME" — demo version intentionally slows gameplay
  • Tournament save lock: "DID YOU KNOW THAT IF YOU BUY HAMSTERBALL, YOU CAN CONTINUE YOUR TOURNAMENT LATER? YOU CAN BUY NOW, AND PICK UP WHERE YOU LEFT OFF NEXT TIME YOU PLAY!"
  • High score lock: Demo users cannot save high scores — "BUY HAMSTERBALL AND YOU CAN SAVE YOUR HIGH SCORES!"
  • Demo textures: demo.png

7. eSellerate DRM System

Status: Embedded third-party DRM (eSellerate by Esellerate)

A full e-commerce/DRM system is embedded in the binary:

  • Activation: Activate, ActivateSerialNumber, Activate using another computer with web access
  • Serial/Key: Enter Activation Key, Activation Key:, Activation URL:, ManualActivation.txt
  • Manual activation: URL http://activate.esellerate.net, "Click the URL below to direct your web browser to the manual product activation web site"
  • Server communication: cmd=DoHandshake&encryptedClientData=, cmd=GetServerPublicKey
  • Encryption: "We encrypt any serial number transmissions and/or product downloads"
  • Reinstall: "hold down the Alt key and click the Reinstall button"
  • Purchase flow: Purchase, Software\eSellerate\Common\PurchaseInfo, Billing Information (Preview Mode)
  • Coupons: COUPON, COUPONERRMSG, COUPONSAVINGS, COUPONTYPE, CROSSSELLGROUPDESC

8. Raptisoft Crash Reporter / Bug Tracker

Status: Fully implemented error reporting system

An embedded crash reporter that sends data to Raptisoft:

  • Strings: "RaptisoftBugTracker", "RaptisoftCrashWindow", "Raptisoft Utility", "*** BEGIN RAPTISOFT SESSION ***", "*** END RAPTISOFT SESSION ***"
  • Error report: "Sending Error Report...", "Error report sent successfully!", "Could not send error report:", "Error: Could not contact Raptisoft reporting server!"
  • User prompt: "%s has encountered an unexpected error! Pressing the 'Send Report' button will send this error report to Raptisoft via the web (an internet connection is required)."
  • Report format: XML-based — <ERRORREPORT>, </ERRORREPORT>, <MODULE>, <description>
  • Safe mode: "SAFE MODE: %s" / "SAFE MODE: OFF"SAFEMODE flag, OptionsMenu_UpdateSafeModeText (0x00442630)
  • Debug: ASTART-DEBUG string, DebugSetMute (D3D8 function)

9. Auto-Update System

Status: Fully implemented update checker

  • Strings: "Check For Update", "CheckForUpdate", "Checking for new updater software", "CHECKFORUPDATEMSGTEXT"
  • Update URL pattern: ?selector=UpdaterStub&vers= — queries Raptisoft server for version updates
  • Mechanism: Separate updater stub process (UpdaterStub)

10. Medal / Rank System (Gold/Silver/Bronze/Weasel)

Status: Fully implemented — time-based ranking per level

Each race track has medal time thresholds:

  • Tiers: Gold, Silver, Bronze, and a special "Weasel" rank
  • Strings: "GOLD TIME:", "SILVER TIME:", "BRONZE TIME:", "WEASEL'S TIME:", "BEST RACE TIME:"
  • Icons: gold-icon.png, silver-icon.png, bronze-icon.png, gold-small.png, silver-small.png, bronze-small.png
  • Golden Weasel: Special top rank — goldenweasel-icon.png, goldenweasel.png, textures\ranks\weasel.png, weaselbox.png
  • Rank textures: textures\ranks\%d.jpg (numbered rank images)
  • Display: "YOUR RANK:", "FINAL RANKING:", "FINAL RANKING BY SCORE:", "FINAL SCORE: %.0f"
  • Registry: Medals key

11. Bonus Systems

11a. Glass Break Bonus

  • Meshes: Meshes\GlassBonus, Meshes\GlassBonus-Smashed — a glass panel that breaks
  • Sound: sounds\GlassBonus, sounds\bonuspop
  • Description: "IF YOU CAN MANAGE TO BREAK SOME GLASS HERE, YOU'LL GET A NICE TIME BONUS!"
  • Mechanism: Glass level has breakable panels that grant time bonus when shattered

11b. Time Bonus Events

  • N:TENBONUS1 / N:TENBONUS2 — bonus time triggers (10-second bonuses)
  • N:EXTRATIME — extra time pickup
  • Strings: "Bonus +%d!", "Bonus 20 - %d = %d", "EXTRA TIME:", "SUPER BONUS:", "SURVIVAL BONUS:"
  • Bell bonus: "RING THE BELL ON THE BIG JUMP FOR AN EXTRA FIVE SECONDS!" (Expert race)

11c. Survival Bonus (Arena/Rumble)

  • "SURVIVAL BONUS:" — points for surviving in Rodent Rumble arena mode

12. Hidden Game Object Types (N: Events)

All named object types found in the binary. Many are well-known, but some are more obscure:

N: Tag Level(s) Description
N:SECRET Secret levels Secret collectible (marks triggered)
N:UNLOCKSECRET Secret-Unlock Arena unlock trigger
N:TENBONUS1 Various 10-second time bonus
N:TENBONUS2 Various Second 10-second time bonus type
N:EXTRATIME Various Extra time pickup
N:GLASS Glass race Glass breakable surface
N:BOUNCE Impossible arena Bounce pad
N:JUMPFIRST Tower First jump trigger
N:JUMPSECOND Tower Second jump trigger
N:NOCONTROL Various Disable player control (cutscene)
N:ONGEAR Master Gear riding surface
N:ONROTATOR Various Rotating platform surface
N:SAWTEETH Expert Saw blade hazard
N:SPEEDCYLINDER Up Speed boost cylinder
N:SPINNY Expert Spinning platform
N:SQUAREWOBBLY Wobbly Square wobbling platform
N:TRAPDOOR Tower/Toob Trapdoor (falls away)
N:WHEELEMBED Various Embedded wheel mechanism
N:NEONPLATFORM Neon Glowing platform
N:WATERWHEEL Toob Water wheel
N:SWIRL Toob Swirling flag wave effect
N:TARPIT Toob Tar pit (slows ball)
N:MOUSETRAP Intermediate Mouse trap hazard
N:WAVY Various Wavy platform
N:WATER Toob Water surface

Hidden E: (Event) Triggers

Event triggers that fire gameplay effects. Some appear unused or very rare:

E: Tag Likely Purpose
E:OPENSESAME Secret door opening (Sesame reference)
E:SAFESWITCH Safety switch toggle
E:VACPOPOUT Vacuum pop-out effect
E:ZOOP Quick movement/teleport effect
E:SHRINK Shrink ball (odd race gravity mechanics)
E:GROW Grow ball
E:GROWSOUND Grow sound effect
E:GRAVITY Gravity flip (odd race)
E:SWALLOW Swallow ball (despawn/kill)
E:TRAJECTORY Set ball trajectory
E:SHRINKCENTER Shrink toward center
E:LIGHTSOFF / E:LIGHTSON Neon race lighting toggle
E:HEATOFF / E:HEATON Heat effect toggle
E:CATAPULTBOTTOM Catapult bottom trigger
E:DROPLIFT Drop lift platform
E:CALLHAMMER Summon BONK (hammer)
E:HAMMERCHASE Activate BONK chase mode
E:MACETRIGGER Mace swing trigger
E:ALERTJUDGES Alert judge objects
E:SAFESWITCH Safe mode switch
E:PIPERANDOM Random pipe selection
E:TRAPPOP Trap pop effect
E:ACTIVATESAW1 / E:ACTIVATESAW2 Activate saw blades
E:ALERTSAW1 / E:ALERTSAW2 Alert saw blades
E:PEGS / E:NOPEGS Show/hide pegs
E:NODIZZY Disable dizzy effect
E:BRANCH Branch path selection
E:BREAK Break object
E:LIMIT / E:LIMITX / E:LIMITZ / E:LIMITPIPE1 / E:LIMITPIPE2 Movement limits
E:HELPINERTIA / E:UNHELPINERTIA Assist/impede ball inertia

13. BounceBall / FollowBall System

Status: Fully implemented — appears in specific levels

A ball-spawning system where certain objects spawn chasing balls:

  • BounceBall_Update (0x00440840): Timer-based spawner. After a countdown (initial 120.0 seconds = 0x42F00000), spawns a FollowBall:
    • Searches for "BallPath" object in the scene via Level_FindObjectByName
    • Reads "FOLLOWBALLSPOT" from hash table for spawn position
    • Creates FollowBall with FollowBall_Ctor (0x0043ebc0)
    • Sets ball velocity to (-3.0, 10.0, 0) — launches upward
    • Sets FollowBall state to +0x80C = 0xF (chase mode 15)
    • Adds to ball list (board+0x29D4)
    • Plays 3D spawn sound
  • FollowBall_Update (0x0043ecc0): AI follow/chase logic for spawned ball

The BounceBall has two phases: active (counting down) and inactive (counting up). State stored at +0x439 (timer) and +0x43A (phase flag).


14. Hamster Character Animations

Status: Mesh assets referenced — may be unused or for menu/cutscene

Hamster character model meshes beyond the ball:

  • Meshes\Hamster-Waiting — idle/standing hamster
  • Meshes\Hamster-trot1 — trot animation frame 1
  • Meshes\Hamster-trot2 — trot animation frame 2
  • Meshes\Hamster-trot3 — trot animation frame 3
  • Meshes\FunBall — unknown ball variant (possibly bouncy/fun mode)
  • Meshes\RBGlare — ball glare effect
  • Meshes\YellowLink — unknown (possibly chain/link visual)
  • Meshes\dawgshadow / dawgshoe / dawgshoe2 — Block Dawg (enemy) shadow and shoe meshes

15. DispatchCollisionEvents Object

Status: Implemented but obscure

  • Function: DispatchCollisionEvents (0x0040c5d0) — dispatches collision events (N:GOAL, N:TARPIT, E:JUMP, E:NODIZZY, etc.)
  • Event: E:NODIZZY — triggered when ball enters this zone
  • Likely used on specific levels where the player would otherwise get dizzy (e.g., after spinning platforms)

16. Rodent Rumble (Arena) — Full 4-Player Mode

Status: Retail feature, fully implemented

Arena combat mode for 1-4 players:

  • Strings: "RODENT RUMBLE (1-4P)", "THE RODENT RUMBLE REQUIRES AT LEAST TWO HUMAN OR COMPUTER PLAYERS!"
  • 13 arenas: Warmup, Beginner, Intermediate, Dizzy, Tower, Up, Expert, Odd, Toob, Wobbly, Neon, Glass, Impossible, Master, Sky
  • AI players: "PLAYER 2: COMPUTER" — CPU-controlled opponents
  • Scoring: Survival bonus, knockoff count tracking
  • Pause: PauseArenaMenu_ctor (0x00430330) — "Pause Rumble Menu"
  • Special: ImpossibleArenaCollisionEvents (0x00418600) has unique N:BOUNCE handling not in other arenas

17. Registry / Config Persistence

Status: Retail — standard config system

Registry keys for persistent game state:

  • CONTROL1 through CONTROL4 — per-player control mappings
  • 2PController1 through 2PController4 — 2P mode control mappings
  • BestTime — per-level best times
  • Medals — medal/rank achievement tracking
  • SafeMode — safe rendering mode flag
  • MirrorTournament — mirror tournament unlock flag
  • SAFEMODE — safe mode toggle

Summary Table

Feature Status Notes
Party Race (2P split-screen) ✅ Implemented Requires 2 human players, dedicated level asset
Mirror Tournament ✅ Unlockable Win tournament at Normal/Frenzied
Online Multiplayer ❌ Not present NetworkConnection is InputDevice, WS2_32 is for DRM/crash reporter
Secret Collectibles ✅ Implemented Unlock arenas in tournament mode
Tournament Campaign ✅ Retail Full save/load, 3 difficulties, rollback
Demo/Trial Mode ✅ Embedded Free play counter, purchase prompts
eSellerate DRM ✅ Embedded Full activation/serial system
Crash Reporter ✅ Implemented Sends reports to Raptisoft server
Auto-Update ✅ Implemented UpdaterStub version check
Medal/Rank System ✅ Retail Gold/Silver/Bronze/Weasel per level
Glass Break Bonus ✅ Implemented Glass level time bonus
BounceBall/FollowBall ✅ Implemented Timed ball spawner with chase AI
Hamster Character Meshes ⚠️ Referenced Trot animation frames — may be unused
NoDizzy Zone ✅ Implemented Disables dizzy effect
Safe Mode ✅ Retail Graphics fallback mode

Document generated via Ghidra decompilation + string analysis of Hamsterball.exe V3.6.c (md5: 7d25019366b8d7f55906325bd630d7fe). All addresses are RVAs in the loaded image.


🔗 Related Documents

Verified MESHWORLD Ref Points

types : docs

📂 View source on GitHub


Verified MESHWORLD Ref Points by Race Level

Source: Parsed directly from the original game's MESHWORLD binary files
Location: originals/installed/extracted/Levels/*.MESHWORLD
Method: Binary parser using the official Raptisoft meshworld format spec
Date: 2026-06-23

What This Document Is

This is the ground-truth list of Section 1 ref point names found in each race level's MESHWORLD file. Every name below was extracted by parsing the actual binary data — not inferred from decompilation or guesswork.

46 unique object types exist across the 15 race levels. This corrects earlier documentation that claimed "75 refs" — that number was inflated by incorrectly counting Section 6 entity names (N:/E:/T: geometry behavior modifiers) as if they were Section 1 ref points, and by inventing names that do not exist in any file.

MESHWORLD Format — Two Separate Systems

A MESHWORLD file has two distinct name systems that should not be conflated:

Section 1: Ref Points (Object Spawn Points)

  • Stored at the top of the file as [u32 count][count × ref_entry]
  • Each entry: [string name][float pos.x,z,y][float rot.x,z,y][u32 has_material][material?]
  • These are object spawn markers — the game reads them and creates game objects via Board->vtable[33] factory dispatch
  • Examples: SPEEDCYLINDER, BONK, GEAR, TIPPER, CATAPULT
  • Names are stored without N:/E: prefix — bare names only

Section 6: Entity Names (Geometry Behavior Modifiers)

  • Stored inside the octree leaf nodes, attached to individual geometry strips
  • Format: [string name] per geom inside a leaf cube
  • These are not objects — they modify the behavior of the mesh geometry they're attached to
  • Examples: N:GOAL, E:LIMIT, E:JUMP, T:NEONARROW(NOCOLLIDE), E:LIGHTSOFF
  • Only Neon Race has Section 6 entity names in its race MESHWORLD file. Other race levels have empty Section 6 entity lists (entity names are more common in Arena MESHWORLD files).

Utility Refs (Present in Most/All Levels, Not Game Objects)

These refs appear in Section 1 but are not game objects — they are structural/utility markers:

Utility Ref Purpose
START1-1, START2-1, START2-2 Ball spawn points (player 1, player 2 positions)
FLAG02FLAG18 Checkpoint flags along the race track
SAFESPOT / SAFEPOS Respawn safe spots (with variants like SAFESPOT(A), SAFESPOT(B), SAFESPOT[X], SAFESPOT[Z])
SAFESPOT\t Tab-terminated safespots (same purpose, formatting variant)
SECRET / SECRETUNLOCK Secret level unlock triggers
BADBALL <CHASE>...</CHASE><HOME>...</HOME> AI ball spawn with XML parameters
START-DEBUG0106 / START-DEBUGX Debug spawn points
NOTHING Null placeholder ref
CameraLocus1 Camera focus point
FollowBallSpot Camera follow target
SIGN / SIGN-* Sign objects (handled by separate Scene_CreateSigns dispatch)

Verified Object Refs Per Race Level

L1 — Warm-up Race (Level1.MESHWORLD)

(no object refs — only START, SAFESPOT, and utility refs)

L2 — Beginner Race (LevelCascade.MESHWORLD)

(no object refs — only START, SAFESPOT, FLAG, BADBALL)

L3 — Intermediate Race (Level2.MESHWORLD)

  • BRIDGE
  • MOUSETRAP

L4 — Dizzy Race (Level3.MESHWORLD)

  • SIGN-TARPIT
  • TARBUBBLE
  • GLUEBIE
  • TIPPER
  • WATERWHEEL
  • SWIRL

L5 — Tower Race (Level4.MESHWORLD)

  • CATAPULT
  • TRAPDOOR
  • DRAWBRIDGE
  • MACE
  • WINDMILL
  • CHOMPER
  • TURRET

L6 — Up Race (LevelUp.MESHWORLD)

  • SPEEDCYLINDER
  • LIFTER
  • TIMEBUTTON

L7 — Neon Race (LevelDark.MESHWORLD)

  • DFLOOR
  • TRODE
  • NEONPLATFORM

L8 — Expert Race (Level5.MESHWORLD)

  • BONK
  • FAN
  • FANSLOW
  • SAWBLADE
  • BRIDGE
  • SAW-BREAK
  • JUDGE
  • BELL

L9 — Odd Race (Level6.MESHWORLD)

  • LIFTER
  • LAUNCH

L10 — Toob Race (Level8.MESHWORLD)

  • SPINNY
  • SAW
  • FALLOUT1
  • SAW2
  • BLOCKDAWG

L11 — Wobbly Race (Level7.MESHWORLD)

  • WOBBLY
  • WAVY

L12 — Glass Race (LevelGlass.MESHWORLD)

  • SMASHER

L13 — Sky Race (Level9.MESHWORLD)

  • PILLAR
  • MAGNIFYER
  • POPCYLINDER
  • TRAPDOOR

L14 — Master Race (Level10.MESHWORLD)

  • BBRIDGE
  • BLOCKDAWG
  • BONK
  • BRIDGE
  • CATAPULT
  • GLUEBIE
  • MOUSETRAP
  • POPCYLINDER
  • TIPPER
  • TARBUBBLE

L15 — Impossible Race (LevelImpossible.MESHWORLD)

  • LOOPER
  • GEAR
  • BIGGEAR
  • ROTATOR
  • PENDULUM

Reverse Index — Object → Levels

Object Race Levels Found In Count
BBRIDGE Master 1
BELL Expert 1
BIGGEAR Impossible 1
BLOCKDAWG Toob, Master 2
BONK Expert, Master 2
BRIDGE Intermediate, Expert, Master 3
CATAPULT Tower, Master 2
CHOMPER Tower 1
DFLOOR Neon 1
DRAWBRIDGE Tower 1
FALLOUT1 Toob 1
FAN Expert 1
FANSLOW Expert 1
GEAR Impossible 1
GLUEBIE Dizzy, Master 2
JUDGE Expert 1
LAUNCH Odd 1
LIFTER Up, Odd 2
LOOPER Impossible 1
MACE Tower 1
MAGNIFYER Sky 1
MOUSETRAP Intermediate, Master 2
NEONPLATFORM Neon 1
PENDULUM Impossible 1
PILLAR Sky 1
POPCYLINDER Sky, Master 2
ROTATOR Impossible 1
SAW Toob 1
SAW-BREAK Expert 1
SAW2 Toob 1
SAWBLADE Expert 1
SIGN-TARPIT Dizzy 1
SMASHER Glass 1
SPEEDCYLINDER Up 1
SPINNY Toob 1
SWIRL Dizzy 1
TARBUBBLE Dizzy, Master 2
TIMEBUTTON Up 1
TIPPER Dizzy, Master 2
TRAPDOOR Tower, Sky 2
TRODE Neon 1
TURRET Tower 1
WATERWHEEL Dizzy 1
WAVY Wobbly 1
WINDMILL Tower 1
WOBBLY Wobbly 1

Total: 46 unique object types across 15 race levels.

Refs That Do NOT Exist in Any Race Level File

The following names were listed in earlier documentation as ref points but do not exist in any original race MESHWORLD Section 1 data. Some are Section 6 entity names (geometry modifiers), others were fabricated:

  • BUMP, N:BUMP, N:BUMPER — do not exist as Section 1 refs (BUMPER is not a ref point; bumper behavior is set via Section 6 entity names)
  • N:GLASS, N:TENBONUS1, N:TENBONUS2 — Section 6 entity names, not Section 1 refs
  • N:SPINNER, N:ONGEAR, N:ONROTATOR, N:BOUNCE — Section 6 entity names
  • N:SQUAREWOBBLY, N:WAVY, N:SPINNY, N:SAWTEETH — Section 6 entity names
  • N:JUMPFIRST, N:JUMPSECOND — do not exist in any file
  • N:WATER, N:TARPIT, N:GOAL — Section 6 entity names (N:GOAL appears only in Neon Race Section 6)
  • E:LAUNCH, E:JUMP, E:ACTION, E:LIMIT, E:BREAK — Section 6 entity names
  • EDGECYLINDER — does not exist in any race file
  • T:SPEEDARROW — does not exist as a Section 1 ref
  • FLICKNING — does not exist in any race file (appears in Arena Neon only)
  • N:NEONPLATFORM — Section 6 entity name
  • VAC-IN, VAC-OUT, VAC-VEC — utility refs for camera/vacuum zones in Up Race, not game objects

🔗 Related Documents

Wall Bumpers Mod

types : mods
keywords :

📂 View source on GitHub


Wall Bumpers Mod

All walls act as pinball bumpers — the ball bounces off walls with amplified force.

Installation

  1. Rename original bass.dllbass_real.dll in your Hamsterball folder
  2. Copy the modded bass.dll into the game folder
  3. Launch the game

Controls

Key Action
F8 Toggle mod on/off (default: ON)
F9 Cycle bumper force (40 → 60 → 80 → 120 → 200)

Log File

The mod writes bumper_mod.log in the game directory. Check this file to confirm:

  • Hook installed successfully
  • Per-frame ball tracking data
  • Bumper hit events with velocity details

How It Works (v3)

The mod hooks Ball_AI_ChaseNearest (0x408390) — ball vtable[4], called every frame for all balls including the player in race mode.

Detection: Two Methods

  1. Velocity Reversal Detection (primary, works in all modes):
    Tracks ball position each frame and computes velocity from position delta. If the dot product of consecutive velocity vectors is negative (velocity reversed), the ball hit something — amplify the bounce.

  2. Collision Entry Scanning (secondary, arena mode only):
    Scans the PhysicsObject's collision entry list for type==2 (wall) entries and reads surface normals.

History

  • v1: Hooked Ball_FallUpdate (0x408830) — dead code, never called for player
  • v2: Hooked Ball_AI_ChaseNearest (0x408390) but scanned collision entries which are EMPTY for player in race mode (Ball_Update never called). Also had BASS proxy stack corruption (void WINAPI name() with zero params).
  • v3: Fixed BASS proxy signatures + velocity-reversal detection that works in all modes.

Build

i686-w64-mingw32-gcc -shared -o bass.dll wall_bumper_mod.c \
    -lwinmm -Wl,--enable-stdcall-fixup -O2 -static -static-libgcc \
    -Wl,--add-stdcall-alias

🔗 Related Documents

Water Physics Mod

types : tools

📂 View source on GitHub


Hamsterball Water Physics Mod

Custom water physics for Hamsterball via bass.dll proxy.

Installation

  1. In your Hamsterball game folder, rename the original bass.dll to bass_real.dll
  2. Copy the mod bass.dll and hamsterball_water.ini into the game folder
  3. Place E:WATER collision planes in custom levels (see below)
  4. Run Hamsterball.exe normally

Uninstall

  1. Delete the mod bass.dll
  2. Rename bass_real.dll back to bass.dll

Or run uninstall_water_mod.bat.

How Water Planes Work

Level Setup

In the Raptisoft level editor, add a collision mesh object named E:WATER.
The object needs at least one face (triangle). The Y coordinate of the first
vertex of the first face determines the water surface height.

The mod scans both the collision MeshWorld and visual MeshWorld for objects
named E:WATER when a level loads.

Fallback: INI Water Planes

If your level's collision data doesn't expose E:WATER objects (or you want
to test without editing a level), you can specify water plane Y coordinates
in hamsterball_water.ini:

[WaterPlanes]
Count=1
Y0=100.0

Physics Behavior

When the ball touches a water plane, the following physics apply:

  1. Entry Damping: On first contact while falling, vertical speed is
    reduced by 30% (configurable via EntryDamping).

  2. Drag: A small per-frame velocity reduction on all axes (Drag).

  3. Buoyancy: An upward force proportional to how deep the ball is
    submerged. The force increases linearly from 0 (just touching surface)
    to 2× gravity (fully submerged).

  4. Equilibrium: At half-submerged, buoyancy exactly cancels gravity,
    so the ball floats with zero net vertical force at the surface.

  5. Horizontal Dampening: Extra drag on X/Z axes, slightly lowering
    the maximum horizontal speed in water (HorizontalDrag).

  6. No Vertical Speed Cap: The engine's own max velocity remains
    unchanged — no artificial cap is added.

Configuration

See hamsterball_water.ini for all options with descriptions.

Debug Log

With Debug=1 in the INI, the mod writes a log file (<a href="#14995561611717" title="water_mod" class="record-link ">water_mod</a>.log or
Hamsterball_water_mod.log) showing water plane discovery and per-frame
physics data.


🔗 Related Documents

water_mod

types : mods

📂 View source on GitHub


water_mod

Water visual effect mod

Files

  • hamsterball_water_mod.c — C source code
  • bass.dll — Compiled DLL (PE32 i386)
  • hamsterball-water-mod.zip — Packaged zip

Proxy Type

BASS.dll proxy. Installation:

  1. Rename original bass.dllbass_real.dll in the Hamsterball game folder
  2. Copy the mod's bass.dll (or renamed DLL) into the game folder
  3. Launch Hamsterball

🔗 Related Documents

Windmill & Judge System (Tower

types : docs
keywords :

📂 View source on GitHub


Windmill & Judge System (Tower + Expert Races)

Complete reverse-engineering analysis of the Windmill (Tower Race) and Hammy Judge (Expert Race) systems.


WINDMILL (Tower Race)

Overview

The Windmill is a CollisionLevel + static visual mesh. It has no game object vtable — instead, its collision mesh contains N:SWIRL events that rotate the ball on contact. The visual windmill mesh is rendered as part of the level's static geometry.

Binary Addresses

Symbol Address Purpose
"Levels\Level4-Windmill" 0x4D095C Mesh file path string
"N:SWIRL" 0x4CF928 Collision event on windmill triangles
CollisionLevel_ctorWithLevel 0x465080 Creates collision level from mesh
Level_LoadMeshes 0x465200 Loads mesh vertex/index data
SceneObject_SetupCallback 0x45DD60 Registers collision with scene manager (stdcall, ret 8)
Stands_ctor 0x462850 Creates visual mesh object (thiscall, ret 4)
Rotator_AddBall 0x43B6F0 N:SWIRL handler — applies rotation to ball

Creation Flow (Scene_LoadLevel4 @ 0x40D6D0)

// 1. Find "WINDMILL" mesh ref
mesh_ref = Level_FindObjectByName("WINDMILL");
if (mesh_ref == NULL) skip;

// 2. Get mesh from board+0x437C (pre-loaded by Tower ctor)
mesh_ptr = *(board + 0x437C);  // Level4-Windmill

// 3. Create CollisionLevel
coll = operator_new(0x10D0);
CollisionLevel_ctorWithLevel(coll, mesh_ptr);  // thiscall, ret 4

// 4. Load mesh data
Level_LoadMeshes(coll);

// 5. Copy position from mesh ref
coll->pos = mesh_ref->pos;

// 6. Register with scene manager for collision detection
SceneObject_SetupCallback(0x4F7360, 0x168, 0);  // global scene + callback type

// 7. Store angle at board+0x438C
board->windmill_angle = (float)result;

// 8. Also creates a Trapdoor
trapdoor_mesh = Level_FindObjectByName("TRAPDOOR");
trapdoor = operator_new(0x10F8);
GlassStands_Ctor(trapdoor, mesh_ptr);

How N:SWIRL Works

The windmill mesh's collision triangles have N:SWIRL event names embedded in them. When the ball intersects these triangles:

// In collision dispatch (Tower Arena handler @ 0x414570):
if (stricmp(eventName, "N:SWIRL") == 0) {
    Rotator_AddBall(board, ball);  // 0x43B6F0
}

Rotator_AddBall registers the ball on a rotator AthenaList with a 10-frame tick counter. Each frame, a rotation matrix is applied to the ball's position and velocity, spinning it around the windmill center.

Tower Constructor Mesh Pre-loading

board+0x437C = MeshWorld_ctor("Levels\\Level4-Windmill");  // VA 0x4D095C

Why It Needs Both Collision + Visual

  • CollisionLevel: Provides N:SWIRL collision events that spin the ball
  • Visual mesh: The windmill mesh is normally part of the level's static geometry (spatial octree). When spawned globally, a separate Stands_ctor object is needed to render it.

JUDGE / HAMMY JUDGE (Expert Race)

Overview

The Hammy Judge is a standalone game object with its own vtable, update, and render functions. It appears as a hamster judge character that scores balls passing through gates. Fully spawnable as a standalone object.

Binary Addresses

Function Address Calling Convention Purpose
Judge_Ctor 0x43A150 thiscall(obj, board) ret 4 Constructor — calls Level_ctor, sets vtable
Judge_Update 0x434B60 thiscall(obj) vtable[11] — animation (trig oscillation)
Judge_Render 0x43A270 thiscall(obj) vtable[18] — render judge mesh
Judge_scalar_dtor 0x43A250 thiscall(obj) vtable[0] — destructor
Level_ctor 0x461740 thiscall(obj, D3D) Base class constructor (loads mesh internally)

Object Structure (0x1100 bytes = 4352)

Offset Size Type Description
+0x0000 4 ptr Vtable (0x4D52B8)
+0x10D0 4 ptr Board pointer
+0x10D4 4 float Position X
+0x10D8 4 float Position Y
+0x10DC 4 float Position Z
+0x10E4 4 float Animation parameter

Vtable (0x4D52B8)

Index Offset Address Function
0 0x00 0x43A250 scalar_dtor
11 0x2C 0x434B60 Judge_Update (trig oscillation animation)
18 0x48 0x43A270 Judge_Render

Creation Flow

Factory (inside CreateSawblade @ 0x40E590):

// 1. Find "JUDGE" mesh ref
mesh_ref = Level_FindObjectByName("JUDGE", 5, board);
if (mesh_ref != NULL) skip;  // Already exists in Race mode

// 2. Allocate
obj = operator_new(0x1100);  // 4352 bytes

// 3. Copy position from mesh ref to stack
push_pos(mesh_ref->pos);

// 4. Construct
// Judge_Ctor(this=obj, board) — thiscall, ret 4
// Inside: Level_ctor(obj, D3D_device) → loads "meshes\hammyjudge" mesh
//         Sets vtable = 0x4D52B8
push board;
mov ecx, obj;
call Judge_Ctor;

// 5. Register
AthenaList_Append(board+0x4BBC, obj);  // judge list

Constructor (Judge_Ctor @ 0x43A150):

Judge_Ctor(this, board) {
    App = board+0x878;
    D3D = App+0x174;
    Level_ctor(this, D3D);          // loads "meshes\hammyjudge" mesh
    vtable = 0x4D52B8;
    // copy position from param
}

Expert Constructor Mesh Loading

LevelBoard_Expert_ctor loads 3 copies of the HammyJudge mesh:

board+0x4BB0 = MeshWorld_ctor("meshes\\hammyjudge");  // judge 1
board+0x4BB4 = MeshWorld_ctor("meshes\\hammyjudge");  // judge 2
board+0x4BB8 = MeshWorld_ctor("meshes\\hammyjudge");  // judge 3

Update Function (vtable[11] @ 0x434B60)

void Judge_Update(this) {
    FLD [this+0x10D8]  // pos X
    FLD [this+0x10DC]  // pos Y
    CALL 0x459860      // trig function (same as Pendulum — sin/cos oscillation)
    CALL 0x4BA754      // some function
    FLD [this+0x10E4]  // animation parameter
    // ... animation logic
}

The Judge animates using the same trig oscillation pattern as the Pendulum/Mace — it bobs/rotates around its position using sin/cos functions.

Collision Events

Event Handler Action
E:ALERTJUDGES ExpertCollisionEvents (0x40E6A0) Iterates board+0x4BBC, activates all judges
E:BELL ExpertCollisionEvents Rings bell sound
E:SCORE ExpertCollisionEvents Awards points

Judge List

Judges are registered to board+0x4BBC (dedicated judge list). The E:ALERTJUDGES event iterates this list to activate/deactivate judges.

Texture Files

File Purpose
textures\hammy1.png Judge texture 1
textures\hammy2.png Judge texture 2
textures\hammy3.png Judge texture 3

Global Spawn Approach

Judge

Fully spawnable — standalone game object with vtable:

  1. operator_new(0x1100)
  2. Judge_Ctor(obj, board) — loads "meshes\hammyjudge" internally via Level_ctor
  3. Set position at obj+0x10D4/+0x10D8/+0x10DC
  4. AthenaList_Append(board+0x4BBC, obj) — judge list
  5. AthenaList_Append(board+0x2578, obj) — general list
  6. Per-frame: call vtable[11] (Judge_Update) for animation

No JIT mesh injection needed — Level_ctor loads the mesh internally.

Windmill

Requires spawning BOTH collision + visual mesh:

  1. Load Levels\Level4-Windmill mesh via MeshWorld_ctor
  2. Create visual: operator_new(0x10D0)Stands_ctor(obj, mesh) → visible mesh
  3. Set position, register to board+0x2578
  4. Create collision: operator_new(0x10D0)CollisionLevel_ctorWithLevel(coll, mesh)
  5. Level_LoadMeshes(coll) — load collision data
  6. Set position on collision
  7. SceneObject_SetupCallback(0x4F7360, 0x168, 0) — register collision
  8. N:SWIRL events fire automatically when ball touches collision mesh

🔗 Related Documents

XML Data Formats Specification

types : project

📂 View source on GitHub


XML Data Formats Specification

RaceData.xml — Race Configuration

Full game race parameters, loaded at startup.

Format

<RACENAME>
    <TIME>seconds</TIME>       <!-- Time pool for tournament -->
    <PAR>seconds</PAR>          <!-- Par time (target completion) -->
    <WEASEL>time</WEASEL>       <!-- Golden weasel (best) medal time -->
    <GOLD>time</GOLD>           <!-- Gold medal time -->
    <SILVER>time</SILVER>       <!-- Silver medal time -->
    <BRONZE>time</BRONZE>       <!-- Bronze medal time -->
    <CAM>float</CAM>            <!-- Camera profile parameter -->
</RACENAME>

Complete Race Data (from game files)

Race Time Pool Par Weasel Gold Silver Bronze CAM
BEGINNERRACE 60 47.0 6.6 15.0 10.3 7.6 2.57
CASCADERACE 50 25.0 15.9 17.5 24.5 30.3 0.64
INTERMEDIATERACE 45 35.0 23.0 26.5 35.2 46.7 0.0
DIZZYRACE 40 35.0 37.2 41.4 48.0 58.8 0.88
TOWERRACE 35 35.0 36.5 40.0 47.8 59.2 0.03
UPRACE 30 25.0 29.7 32.0 35.1 40.8 0.33
NEONRACE 30 25.0 37.7 46.0 55.3 65.8 0.0
EXPERTRACE 30 20 34.0 39.5 48.0 61.2 0.0
ODDRACE 30 20 44.6 48.0 61.8 80.7 1.28
TOOBRACE 25 20 42.3 45.2 53.5 60.6 0.13
WOBBLYRACE 25 20 37.0 44.0 52.1 63.8 0.0
GLASSRACE 25 10 36.0 43.5 52.1 65.0 0.71
SKYRACE 25 5 40 46.0 53.5 60.0 0.44
MASTERRACE 55 2 65 73.4 88.8 112.4 0.0
IMPOSSIBLERACE 50 2 44 60.0 80.3 100.4 0.5

Notes

  • TIME is tournament time pool (decreasing), not race duration limit
  • PAR is the expected completion time; used for tournament scoring
  • Medal times: WEASEL < GOLD < SILVER < BRONZE (lower is better)
  • CAM value selects camera profile: 0.0 = default, >0 = alternate
  • No warm-up race in RaceData.xml (race indices 1-2 aren't competitive)
  • Silver in BEGINNERRACE (10.3) < Gold (15.0) — data file has SILVER/GOLD swapped for that race
  • Internal vs display names: XML tags do NOT match display names.
    BEGINNERRACE = Warm-up Race (Level 1), CASCADERACE = Beginner Race (Level 2).
    The game's internal naming is offset by one from the display names.

Jukebox.xml — Music Track Mapping

Maps track names to MO3 pattern indices.

Format

<SONG> * comment *
    <NAME>display name</NAME>
    <HEX>pattern_index</HEX>
</SONG>

Complete Track List

Context Name HEX (Pattern)
Title Screen Main Theme 0x02
Title Screen (no intro) Main Theme - No Intro 0x03
Beginner Race Hamster Nation 0x50
Beginner Race (no intro) Hamster Nation - No Intro 0x50
Cascade Race Cascade Race 0x7F
Cascade Race (no intro) Cascade Race - No Intro 0x81
Intermediate Race Gerbil Groove 0x26
Intermediate Race (no intro) Gerbil Groove - No Intro 0x28
Dizzy Race Dizzy! 0x47
Dizzy Race (no intro) Dizzy! - No Intro 0x47
Tower Race Happy Rush 0x2F
Tower Race (no intro) Happy Rush - No Intro 0x2F
Up Race Up Race 0x8B
Up Race (no intro) Up Race - No Intro 0x8B
Neon Race Neon Theme 0xA8
Expert Race Fight! 0x08
Expert Race (no intro) Fight! - No Intro 0x09
odd race Ninja Hamster 0x38
odd race (no intro) Ninja Hamster - No Intro 0x3A
Toob Race Rodenthood 0x55
Toob Race (no intro) Rodenthood - No Intro 0x56
Wobbly Race Hamster Chase 0x19
Wobbly Race (no intro) Hamster Chase - No Intro 0x1B
Sky Race Bucky Break 0x5C
Glass Race Glass Theme 0x9F
Sky Race (no intro) Bucky Break - No Intro 0x5C
Master Race Master Theme 0x71
Master Race (no intro) Master Theme - No Intro 0x71
Impossible Race Impossible Theme 0x94
Game Over Game Over 0x62
Tournament Overview Tournament 0x63
Goal Reached Goal! 0x6B
High Scores High Scores 0x13
Loading Loading 0x6F

Notes

  • HEX is a BASS_MusicPlayEx position/pattern index into Music.mo3
  • "No Intro" variants start from a later pattern in the same song, skipping the intro section
  • Some songs share the same HEX for both variants (Hamster Nation 0x50, Dizzy! 0x47, Happy Rush 0x2F, Up Race 0x8B) — these probably have the intro as part of the same pattern or skip it differently
  • The MO3 file contains ALL music in a single module — different HEX values select different pattern/sub-song positions

Font Description Format

Binary font metric file at Fonts/<name>/font.description, paired with texture atlas PNGs.

Structure

[4 bytes]  version_or_count = 2
[4 bytes]  glyph_height = 11 (for ShowcardGothic72)
[4 bytes]  char_height = 48 (pixel height)

Then per-glyph entries (mixed int/float, approximately 20 bytes each):

  • Character code
  • X position in atlas (float)
  • Character width (float)
  • UV coordinates for texture mapping
  • Atlas page index (Data0.png vs Data1.png)

Font Atlas

Each font has texture atlas PNG files:

  • Data0.png — Primary glyph atlas
  • Data1.png — Secondary atlas (for larger fonts needing more glyphs)

Known Fonts

Directory Height (px) Atlases Usage
ShowcardGothic72 72 Data0.png Timer display (large numbers)
ShowcardGothic28 28 Data0.png Title screens
ShowcardGothic16 16 Data0.png HUD text
ShowcardGothic14 14 Data0.png Medium dialogue
ArialNarrow12Bold 12 Data0.png Small labels

Rendering

// Font render from decompiled code
Graphics_DrawText(font, text, x, y):
  For each character in text:
    Lookup glyph in font.description → UV coords + width
    Draw textured quad from atlas at (x, y)
    x += glyph.width

Reimplementation Notes (SDL2)

RaceData

  • Same XML format, load with tinyxml2 or pugixml
  • Medal times determine rank display after race
  • CAM value maps to camera profile selection

Jukebox

  • MO3 format: use BASS library with libbass, or convert to individual .ogg/.wav tracks
  • For SDL_mixer: extract each pattern as separate .ogg during asset conversion
  • Pattern index → track file mapping table

Fonts

  • Replace with TTF fonts (Showcard Gothic is a standard Windows font, freely available)
  • SDL2: TTF_RenderText_Blended() for anti-aliased text
  • For pixel-perfect match: render Showcard Gothic TTF at same sizes

🔗 Related Documents

Help

Click here to access Cosma's documentation

Shortcuts

Space Re-run the force-layout algorithm
S Move the cursor to Search
Alt + click (on a record type) Deselect other types
R Reset zoom
Alt + R Reset the display
C Zoom in on the selected node
F Switch to Focus mode
Escape Close the active record

Hamsterball RE — Knowledge Map

rsks & contributors

Interactive mind map of all Hamsterball reverse engineering documentation, mods, and tools


Version 2.6.1 • License GPL-3.0-or-later

  • Arthur Perret
  • Guillaume Brioudes
  • Olivier Le Deuff
  • Clément Borel
  • ANR research programme HyperOtlet
D3 v4.13.0
Mike Bostock (BSD 3-Clause)
Nunjucks v3.2.3
James Long (BSD 2-Clause)
Js-yaml v4.1.0
Vitaly Puzrin (MIT License)
Markdown-it v12.3.0
Vitaly Puzrin, Alex Kocharin (MIT License)
Citeproc v2.4.62
Frank Bennett (CPAL, AGPL)
Fuse-js v6.4.6
Kiro Risk (Apache License 2.0)