title: Implement Sonic 2 Object/Badnik description: Guide for implementing Sonic 2 objects and badniks with ROM-accurate art, behavior, subtypes, and disassembly validation.
Implement Sonic 2 Object/Badnik
Implement a Sonic 2 object or badnik with complete ROM accuracy. This skill guides complete implementation including art, animation, sound effects, all subtypes, and cross-validation against the disassembly.
Inputs
$ARGUMENTS: Object name or ID (e.g., "Masher", "0x5C", "Crawl badnik")
Related Skills
When delegating agents to explore the disassembly, instruct them to use the s2disasm-guide skill for:
- Directory structure and file locations
- Compression types (.nem, .kos, .eni, .sax, .bin)
- Label naming conventions (ArtNem_, Pal_, Obj_, etc.)
- RomOffsetFinder tool commands
- Object system reference (status table offsets, routine patterns)
- Zone abbreviations and IDs
Implementation Process
Phase 1: Research & Discovery
Delegate multiple agents to explore the disassembly. Include this instruction in each agent prompt:
Use the s2disasm-guide skill (
.agents/skills/s2disasm-guide/skill.md) for reference on disassembly structure, label conventions, RomOffsetFinder commands, and object system patterns.
Agents should:
Identify the object - Parse $ARGUMENTS to determine the object ID and name
- Search
Sonic2ObjectIds.javaandSonic2ObjectRegistryData.javafor ID/name mapping - If ambiguous, search the disassembly in
docs/s2disasm/for object references
- Search
Locate disassembly source - Find the object's ASM file in
docs/s2disasm/_anim/anddocs/s2disasm/directories:find docs/s2disasm -name "Obj*.asm" | xargs grep -l "OBJECT_NAME"Common patterns:
ObjXX.asm,ObjXX - Name.asmAnalyze the disassembly to understand:
- All routines (indexed by
objoff_xxoffsets) - State machines and transitions
- All subtypes and their behaviors (from subtype byte interpretation)
- Movement physics (velocities, timers, ranges)
- Collision handling (size index, touch response type)
- Animation sequences and frame timing
- Sound effects triggered (search for
sfxorSndID) - Art/mappings references
- All routines (indexed by
Find art and mappings:
- Search for
ArtNem_orArtKos_references in the disassembly - Use RomOffsetFinder to get ROM addresses:
mvn exec:java -Dexec.mainClass="com.openggf.tools.disasm.RomOffsetFinder" -Dexec.args="search ObjectName" -q - Search results now show PLC cross-references - which PLCs load this art
- Use
plc <name>command to see all art entries in a PLC:mvn exec:java -Dexec.mainClass="com.openggf.tools.disasm.RomOffsetFinder" -Dexec.args="plc PlrList_Ehz1" -q - The ObjectDiscoveryTool checklist also shows PLC IDs per object per zone
- Check existing art keys in
Sonic2ObjectArtKeys.java - Check if art is zone-specific or shared
- Search for
Phase 2: Implementation
2.1 Constants (if needed)
Add to Sonic2Constants.java:
// Object art ROM addresses
public static final int ART_NEM_OBJECTNAME_ADDR = 0xXXXXX;
Add to Sonic2ObjectIds.java (if not present):
public static final int OBJECT_NAME = 0xXX;
2.2 Art Loading (if needed)
PLC note: S2 art is loaded via ArtLoadCues (PLCs) in the ROM. The shared PlcParser utility handles parsing. See plc-system skill. Use RomOffsetFinder plc <name> to inspect PLC contents from the CLI. The ObjectDiscoveryTool checklist shows PLC IDs per object.
If the object needs new art:
Add art key to
Sonic2ObjectArtKeys.java:public static final String OBJECT_NAME = "objectname";Add loader method to
Sonic2ObjectArt.java:public ObjectSpriteSheet loadObjectNameSheet() { Pattern[] patterns = safeLoadNemesisPatterns( Sonic2Constants.ART_NEM_OBJECTNAME_ADDR, "ObjectName"); if (patterns.length == 0) return null; List<SpriteMappingFrame> mappings = createObjectNameMappings(); return new ObjectSpriteSheet(patterns, mappings, paletteIndex, 1); }Create mappings method matching disassembly:
private List<SpriteMappingFrame> createObjectNameMappings() { // Parse from Map_ObjectName in disassembly // Each piece: x_offset, y_offset, width_tiles, height_tiles, pattern_index, flags }Register in
Sonic2ObjectArtProvider.loadArtForZone():registerSheet(Sonic2ObjectArtKeys.OBJECT_NAME, artLoader.loadObjectNameSheet());
For zone-specific graphics: Create separate loader methods and register conditionally based on zone.
2.3 Object Instance Class
Create the instance class following existing patterns. Choose the appropriate pattern based on the object's behavior:
Pattern 1: Simple Object (Single Entity)
For objects that exist as a single collision/render entity:
package com.openggf.game.sonic2.objects;
public class ObjectNameObjectInstance extends AbstractObjectInstance
implements SolidObjectProvider, SolidObjectListener {
// Single object with its own collision and rendering
}
Examples: SpringObjectInstance, MonitorObjectInstance, MTZPlatformObjectInstance
Pattern 2: Multi-Piece Solid (Single State Machine, Multiple Collision Pieces)
For objects with multiple collision surfaces calculated from a single state machine:
public class ObjectNameObjectInstance extends AbstractObjectInstance
implements MultiPieceSolidProvider, SolidObjectListener {
@Override
public int getPieceCount() { return NUM_PIECES; }
@Override
public int getPieceX(int pieceIndex) {
// Calculate piece position from single state (e.g., rotation angle)
return baseX + calculateOffset(pieceIndex);
}
@Override
public SolidObjectParams getPieceParams(int pieceIndex) {
return PIECE_PARAMS; // Shared or per-piece collision
}
}
Use when:
- All pieces move together based on shared state (rotation, interpolation)
- Pieces are calculated, not independent entities
- Single
update()method calculates all piece positions
Examples:
ARZRotPformsObjectInstance- 3 orbiting platforms + 9 chain links, all rotating around a single center pointCPZStaircaseObjectInstance- 4 platform pieces interpolating together as a staircase
Pattern 3: Parent-Child Spawning (Independent Child Objects)
For objects that spawn separate, independent child objects:
public class ObjectNameObjectInstance extends AbstractObjectInstance
implements SolidObjectProvider {
private void spawnChildren() {
// Preferred: use spawnChild() helper (inherited from AbstractObjectInstance)
ObjectSpawn childSpawn = new ObjectSpawn(childX, childY, objectId, childSubtype, ...);
ChildObjectInstance child = spawnChild(() -> new ChildObjectInstance(childSpawn, "ChildName"));
}
}
Use when:
- Children have independent state machines
- Children can be destroyed/activated separately
- Children move independently (not calculated from parent state)
- ROM uses
AllocateObjectAfterCurrentto spawn children
Examples:
MCZRotPformsObjectInstance- Parent (subtype 0x18) spawns 2 child platforms with independent movementBubbleGeneratorObjectInstance- Spawns individual bubble objectsEggPrisonObjectInstance- Spawns button, lock, and animals as separate objectsAbstractBadnikInstance- Spawns explosion + animal on destruction
Pattern 4: Badnik (Enemy with AI)
For enemies with touch response and destruction behavior:
package com.openggf.game.sonic2.objects.badniks;
import com.openggf.level.objects.AbstractBadnikInstance;
public class ObjectNameBadnikInstance extends AbstractBadnikInstance {
@Override
protected void updateMovement(int frameCounter, AbstractPlayableSprite player) {
// Movement AI from disassembly
}
@Override
protected void updateAnimation(int frameCounter) {
// Animation state machine
}
@Override
protected int getCollisionSizeIndex() {
return COLLISION_SIZE_INDEX; // From disassembly collision_flags
}
}
Examples: BuzzerBadnikInstance, GrabberBadnikInstance, MasherBadnikInstance
Pattern 5: Boss (Zone Act 2 Boss Fights)
Use the dedicated /s2-implement-boss skill (.agents/skills/s2-implement-boss/skill.md) for boss implementations.
Bosses differ significantly from regular objects:
- Dynamic spawning via
Sonic2LevelEventManager(not level layout) - Camera arena locking with min/max boundaries
- 8 hits with invulnerability and palette flash
- Multi-component architecture with
AbstractBossChild - Defeat sequences (explosions, flee, EggPrison spawn)
- Music transitions (fade, boss music, resume)
Detect a boss when disassembly shows:
- Object spawned by
LevEvents_XXXroutines collision_flagsset to$C0 | size_index(boss category)- Uses
Boss_HandleHitspattern
Choosing the Right Pattern
| Disassembly Pattern | Engine Pattern | Key Indicator |
|---|---|---|
| Single object, single collision | Simple Object | No child allocation, single width_pixels/y_radius |
| Loop calculating piece positions | Multi-Piece Solid | Pieces calculated from shared angle/state |
AllocateObjectAfterCurrent calls |
Parent-Child Spawning | Children get independent routine/subtype |
Obj25 (enemy) base routines |
Badnik | Uses touch response, spawns explosion on death |
LevEvents_XXX spawning, Boss_HandleHits |
Use /s2-implement-boss | Spawned by level events, 8 hits, camera lock |
2.4 Reusable Engine Utilities
IMPORTANT: Before writing any physics, movement, or collision code, check these existing utilities. Do NOT reimplement functionality that already exists.
Movement & Physics
| Utility | Location | Use When |
|---|---|---|
SubpixelMotion.moveSprite(state, gravity) |
com.openggf.level.objects.SubpixelMotion |
16:8 fixed-point position update with gravity (ROM's ObjectFall/SpeedToPos). Create a SubpixelMotion.State field, sync before call, read back after. |
SubpixelMotion.moveSprite2(state) |
com.openggf.level.objects.SubpixelMotion |
Same without gravity (ROM's SpeedToPos). |
SubpixelMotion.moveX(state) |
com.openggf.level.objects.SubpixelMotion |
X-only movement. |
SwingMotion.update() |
com.openggf.physics.SwingMotion |
Object oscillates/bobs/swings (ROM's Swing_UpAndDown). |
PatrolMovementHelper |
com.openggf.level.objects.PatrolMovementHelper |
Left-right patrol with turn-at-edge detection. |
PlatformBobHelper |
com.openggf.level.objects.PlatformBobHelper |
Sine-based standing-nudge displacement for platforms. |
ObjectTerrainUtils.checkFloorDist() |
com.openggf.physics |
Single-point floor/ceiling/wall detection for objects. |
TrigLookupTable.calcAngle() / sinHex() / cosHex() |
com.openggf.physics |
ROM-accurate angle calculation and trig. |
Base Classes
| Base Class | Location | Use When |
|---|---|---|
AbstractBadnikInstance |
com.openggf.level.objects |
All badniks. Provides touch response, destruction with DestructionEffects, debug rendering. S2 badniks pass Sonic2BadnikConfig.DESTRUCTION. Objects receive ObjectServices via injection — use services() to access camera, audio, level, game state. |
AbstractProjectileInstance |
com.openggf.level.objects |
Fire-and-forget projectiles. Handles motion, gravity, off-screen destroy, HURT collision. |
AbstractSpikeObjectInstance |
com.openggf.level.objects |
Spike objects with retract/extend behavior. |
AbstractMonitorObjectInstance |
com.openggf.level.objects |
Monitor objects. Shared icon-rise physics. Override applyPowerup(). |
AbstractPointsObjectInstance |
com.openggf.level.objects |
Floating score popups. Override getFrameForScore(). |
GravityDebrisChild |
com.openggf.level.objects |
Debris/fragment children with gravity. Override appendRenderCommands(). |
Collision & Touch Response
| Pattern | When to Use |
|---|---|
TouchResponseAttackable + TouchResponseProvider |
Destroyable enemies. Player jump/roll destroys them. |
TouchResponseProvider only (no Attackable) |
Non-destroyable hazards. Return 0x80 | sizeIndex for HURT. |
DestructionEffects.destroyBadnik() |
Explosion + animal + points on badnik defeat. |
SpringBounceHelper |
com.openggf.level.objects.SpringBounceHelper — shared spring bounce physics. |
Rendering & Animation
| Utility | Use When |
|---|---|
getRenderer(artKey) |
Inherited from AbstractObjectInstance. Returns ready PatternSpriteRenderer or null. Use instead of manual render manager access. |
services().renderManager() |
Returns ObjectRenderManager for manual render control when needed. |
AnimationTimer |
com.openggf.util.AnimationTimer — cyclic frame animation timer. |
LazyMappingHolder |
com.openggf.util.LazyMappingHolder — lazy-loading holder for sprite mappings. |
PatternDecompressor |
com.openggf.util.PatternDecompressor — bytes→Pattern[] conversion. |
S2SpriteDataLoader.loadMappingFrames() |
Parse S2 mappings from ROM at runtime. Prefer over hardcoded mappings. |
Object Lifecycle
| Utility | Use When |
|---|---|
buildSpawnAt(x, y) |
Inherited from AbstractObjectInstance. Use in getSpawn() overrides instead of constructing new ObjectSpawn(...) manually. |
isPlayerRiding() |
Inherited from AbstractObjectInstance. Safe null-check chain for platform riding detection. |
isOnScreen(margin) |
Inherited from AbstractObjectInstance. Off-screen visibility check. |
DebugRenderContext |
com.openggf.debug.DebugRenderContext — use for appendDebugRenderCommands(). |
2.5 Implementation Requirements
Engine Extensions: If the ROM uses functionality that the engine doesn't expose, you MUST extend the engine rather than working around it or documenting it as a limitation. Examples:
- ROM reads button state (up/down/left/right) → Engine must expose
isUpPressed(),isDownPressed(), etc. - ROM uses a RAM variable for inter-object communication → Engine must provide equivalent manager/service
- ROM accesses player state not currently exposed → Add the getter/setter to
AbstractPlayableSprite
Never accept "engine limitation" as a reason for incomplete behavior. The engine exists to support ROM-accurate implementations.
When extending the engine:
- Search for similar existing functionality to follow established patterns
- Add fields/methods to the appropriate class (e.g.,
AbstractPlayableSpritefor player state) - Update any input/update pipelines that need to populate the new state (e.g.,
SpriteManagerfor input) - Make the extension general-purpose so other objects can use it
- Document the extension with ROM references in comments
Constants: Extract all magic numbers as named constants with disassembly comments:
// From disassembly: move.w #$180,x_vel(a0)
private static final int X_VELOCITY = 0x180;
Subtypes: Implement ALL subtypes from the subtype byte interpretation:
int subtype = spawn.subtype();
int behaviorBits = (subtype >> 4) & 0x0F;
int configBits = subtype & 0x0F;
Sound effects: Use constants from Sonic2AudioConstants.java:
services().audioManager().playSfx(Sonic2AudioConstants.SFX_SPRING);
Reference Sonic2SmpsLoader for SFX name → ID mapping:
| ID | Name |
|---|---|
| 0xA0 | Jump |
| 0xA1 | Checkpoint |
| 0xA3 | Hurt |
| 0xB4 | Bumper |
| 0xB9 | Smash |
| 0xC1 | Explosion |
| 0xCC | Spring |
| ... | (see Sonic2SmpsLoader for full list) |
Debug visualization: Implement when debug enabled:
@Override
public void appendDebugRenderCommands(List<GLCommand> commands) {
if (!SonicConfigurationService.getInstance().isDebugViewEnabled()) return;
// Draw collision bounds, state info, sensor rays, etc.
}
2.6 Factory Registration
Register in Sonic2ObjectRegistry.registerDefaultFactories():
registerFactory(Sonic2ObjectIds.OBJECT_NAME,
(spawn, registry) -> new ObjectNameObjectInstance(spawn));
For badniks:
registerFactory(Sonic2ObjectIds.OBJECT_NAME,
(spawn, registry) -> new ObjectNameBadnikInstance(spawn));
Phase 3: Code Quality
Ensure the implementation:
- Has no TODOs or placeholder code
- Has no "engine limitation" workarounds - if the ROM does it, the engine must support it
- Uses explicit disassembly references in comments for non-trivial logic
- Handles object creation and cleanup correctly
- Properly manages object lifecycle (spawning, despawning)
- Follows existing code patterns in the codebase
- Any engine extensions are clean, well-documented, and usable by other objects
Phase 4: Cross-Validation
Delegate to a review agent to cross-validate against the disassembly. Include this instruction in the agent prompt:
Use the s2disasm-guide skill (
.agents/skills/s2disasm-guide/skill.md) for reference on disassembly structure, label conventions, and object system patterns.
Review the implementation of [ObjectName] (0xXX) against the Sonic 2 disassembly.
Reference: Use the s2disasm-guide skill for disassembly navigation guidance.
Files to review:
- [List all created/modified files]
Disassembly reference: docs/s2disasm/...
Validation checklist:
1. Code quality: clean, concise, well-commented
2. Art implementation: patterns, mappings, palette
3. All subtypes implemented with correct behavior
4. Animation frames and timing match disassembly
5. Sound effects match disassembly SFX IDs
6. Movement/physics values match disassembly
7. Collision handling matches disassembly
8. Debug visualization present
9. No TODOs or simplifications
10. Object lifecycle handled correctly
11. No "engine limitation" workarounds - any missing engine functionality was added
Report any discrepancies with specific line references from both code and disassembly.
If issues are found:
- Fix all identified issues
- Delegate another review agent
- Repeat until validation passes
Phase 5: Finalization
Once cross-validation is confirmed bug-free:
Add to IMPLEMENTED_IDS in
Sonic2ObjectProfile.java(theIMPLEMENTED_IDSset):0xXX, // ObjectName (brief description)Keep the list sorted numerically.
Build and test:
mvn packageReport completion with summary of implementation details.
Reference Files
| Purpose | Location |
|---|---|
| Disassembly guide | .agents/skills/s2disasm-guide/skill.md |
| Boss skill | .agents/skills/s2-implement-boss/skill.md |
| Object IDs | src/.../game/sonic2/constants/Sonic2ObjectIds.java |
| ROM offsets | src/.../game/sonic2/constants/Sonic2Constants.java |
| Art keys | src/.../game/sonic2/Sonic2ObjectArtKeys.java |
| Art loader | src/.../game/sonic2/Sonic2ObjectArt.java |
| Art provider | src/.../game/sonic2/Sonic2ObjectArtProvider.java |
| Registry | src/.../game/sonic2/objects/Sonic2ObjectRegistry.java |
| Base badnik | src/.../level/objects/AbstractBadnikInstance.java |
| SFX mapping | src/.../game/sonic2/audio/smps/Sonic2SmpsLoader.java |
| SFX constants | src/.../game/sonic2/constants/Sonic2AudioConstants.java |
| Disassembly | docs/s2disasm/ |
| Implemented IDs | src/.../tools/Sonic2ObjectProfile.java (IMPLEMENTED_IDS set) |
Example Implementations
Study these for patterns:
Simple Objects (Single Entity)
SpringObjectInstance.java- Object with subtypes (red/yellow, direction variants)MTZPlatformObjectInstance.java- Multi-subtype platform with 12 movement typesCNZRectBlocksObjectInstance.java- Animated platform with state machine
Multi-Piece Solid (Single State, Multiple Collision Pieces)
ARZRotPformsObjectInstance.java- 3 orbiting platforms + 9 chain links rotating togetherCPZStaircaseObjectInstance.java- 4 platform pieces interpolating as staircase
Parent-Child Spawning (Independent Children)
MCZRotPformsObjectInstance.java- Parent spawns 2 independent moving platforms (subtype 0x18)BubbleGeneratorObjectInstance.java- Spawns individual bubble objects on timerEggPrisonObjectInstance.java- Spawns button, lock, and animals as separate entities
Badniks (Enemies with AI)
BuzzerBadnikInstance.java- Flying badnik with projectile spawningGrabberBadnikInstance.java- Complex multi-state spider badnik with grabbingMasherBadnikInstance.java- Simple jumping fish badnik