title: Implement Sonic 1 Object/Badnik description: Guide for implementing Sonic 1 objects and badniks with ROM-accurate art, behavior, subtypes, and disassembly validation.
Implement Sonic 1 Object/Badnik
Implement a Sonic 1 object or badnik with complete ROM accuracy. This skill guides complete implementation including art, animation, sound effects, all subtypes, and cross-validation against the Sonic 1 disassembly.
Inputs
$ARGUMENTS: Object name or ID (e.g., "Crabmeat", "0x1F", "Motobug badnik")
Related Skills
When delegating agents to explore the disassembly, instruct them to use the s1disasm-guide skill for:
- Directory structure and file locations (
_incObj/,_anim/,_maps/,artnem/) - Compression types and label prefixes (
Nem_,Map_,Ani_) - RomOffsetFinder tool commands (all require
--game s1flag) - Object system reference (S1 field names:
obRoutine,obX,obY, etc.) - Zone abbreviations and IDs (GHZ, LZ, MZ, SLZ, SYZ, SBZ, FZ)
Implementation Process
Phase 1: Research & Discovery
Delegate multiple agents to explore the disassembly. Include this instruction in each agent prompt:
Use the s1disasm-guide skill (
.agents/skills/s1disasm-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
Sonic1ObjectIds.javafor ID/name mapping - Search
Sonic1ObjectRegistry.javafor existing registration - If ambiguous, search the disassembly in
docs/s1disasm/_incObj/for object files
- Search
Locate disassembly source - Find the object's files:
- Object code:
docs/s1disasm/_incObj/HEX Name.asm(e.g.,_incObj/1F Crabmeat.asm) - Animation:
docs/s1disasm/_anim/Name.asm(e.g.,_anim/Crabmeat.asm) - Mappings:
docs/s1disasm/_maps/Name.asm(e.g.,_maps/Crabmeat.asm) - Some objects have multiple parts:
_incObj/11 Bridge (part 1).asm,(part 2).asm, etc.
- Object code:
Analyze the disassembly to understand:
- All routines (indexed by
obRoutinevalues: 0, 2, 4, ...) - State machines and transitions via
obRoutineandob2ndRout - All subtypes and their behaviors (from
obSubtypebyte interpretation) - Movement physics (velocities, timers, ranges)
- Collision handling (
obColType= type + size index) - Animation sequences and frame timing from
_anim/file - Sound effects triggered (search for
sfxor sound command references) - Art references (
Nem_labels)
- All routines (indexed by
Find art and mappings:
- Search for
Nem_references in the object's ASM file - Use RomOffsetFinder to get ROM addresses:
mvn exec:java -Dexec.mainClass="com.openggf.tools.disasm.RomOffsetFinder" -Dexec.args="--game s1 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="--game s1 plc PLC_GHZ" -q - The ObjectDiscoveryTool checklist also shows PLC IDs per object per zone
- Parse mappings from
_maps/Name.asm(5-byte piece format, byte header) - Check if art is zone-specific or shared
- Search for
Check for boss objects - If the object is a boss (IDs: 0x3D GHZ, 0x73 MZ, 0x75 SYZ, 0x77 LZ, 0x7A SLZ, 0x7D SBZ, 0x85 FZ):
- Redirect to
/s1-implement-bossskill instead
- Redirect to
Phase 2: Implementation
2.1 Constants (if needed)
Add ROM address to Sonic1Constants.java:
// Object art ROM addresses
public static final int ART_NEM_OBJECTNAME_ADDR = 0xXXXXX;
Add to Sonic1ObjectIds.java (if not present):
public static final int OBJECT_NAME = 0xXX;
2.2 Art Loading
PLC note: S1 loads object art via ArtLoadCues (PLCs) during level init. 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. For dedicated object art that shouldn't overwrite level pattern tiles, use standalone decompression: PlcParser.decompressAll(rom, plc) returns independent Pattern[] arrays per PLC entry.
S1 art infrastructure exists: Sonic1ObjectArt.java and Sonic1ObjectArtProvider.java are established. Art keys use the shared ObjectArtKeys class (com.openggf.level.objects.ObjectArtKeys). Follow the existing pattern:
Add art key to
ObjectArtKeys.java:public static final String OBJECT_NAME = "objectname";Add loader method to
Sonic1ObjectArt.java:public ObjectSpriteSheet loadObjectNameSheet() { Pattern[] patterns = safeLoadNemesisPatterns( Sonic1Constants.ART_NEM_OBJECTNAME_ADDR, "ObjectName"); if (patterns.length == 0) return null; List<SpriteMappingFrame> mappings = createObjectNameMappings(); return new ObjectSpriteSheet(patterns, mappings, paletteIndex, 1); }Create S1 mappings method - Parse from
_maps/Name.asm:private List<SpriteMappingFrame> createObjectNameMappings() { // S1 format: byte piece_count, then per piece: // byte y_offset, byte size, word pattern, byte x_offset // Note: x_offset is a signed BYTE (not word like S2) }
2.3 Object Instance Class
Create the instance class following existing Sonic 2 patterns but in the Sonic 1 package.
Choose the appropriate pattern based on behavior:
Pattern 1: Simple Object
package com.openggf.game.sonic1.objects;
public class ObjectNameObjectInstance extends AbstractObjectInstance
implements SolidObjectProvider, SolidObjectListener {
// Single object with its own collision and rendering
}
Pattern 2: Badnik (Enemy with AI)
package com.openggf.game.sonic1.objects.badniks;
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 from _anim/Name.asm
}
@Override
protected int getCollisionSizeIndex() {
return COLLISION_SIZE_INDEX; // From disassembly obColType
}
}
Pattern 3: Boss
Use the dedicated /s1-implement-boss skill (.agents/skills/s1-implement-boss/skill.md) for boss implementations.
Detect a boss when:
- Object file is named
Boss - Zone Name.asm - Object IDs: 0x3D (GHZ), 0x73 (MZ), 0x75 (SYZ), 0x77 (LZ), 0x7A (SLZ), 0x7D (SBZ), 0x85 (FZ)
- Uses
obColPropfor hit counter - Has camera lock and arena setup behavior
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/MoveSprite). Create a SubpixelMotion.State field, sync positions/velocities before call, read back after. |
SubpixelMotion.moveSprite2(state) |
com.openggf.level.objects.SubpixelMotion |
Same without gravity (ROM's MoveSprite2). |
SubpixelMotion.moveX(state) |
com.openggf.level.objects.SubpixelMotion |
X-only movement (horizontal projectiles). |
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 (enemies). Provides touch response, destruction with DestructionEffects, debug rendering. S1 badniks pass S1-specific DestructionConfig. Objects receive ObjectServices via injection — use services() to access camera, audio, level, game state. |
AbstractProjectileInstance |
com.openggf.level.objects |
Fire-and-forget projectiles (missiles, fireballs). Handles motion, gravity, off-screen destroy, HURT collision. |
AbstractMonitorObjectInstance |
com.openggf.level.objects |
Monitor objects. Shared icon-rise physics and state machine. Override applyPowerup(). |
AbstractPointsObjectInstance |
com.openggf.level.objects |
Floating score popups. Shared rise-and-fade physics. 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) |
Static method on AbstractObjectInstance. Returns ready PatternSpriteRenderer or null. Use instead of manual render manager access. |
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. |
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.
Never accept "engine limitation" as a reason for incomplete behavior.
Constants: Extract all magic numbers as named constants with disassembly comments:
// From disassembly: move.w #$100,obVelX(a0)
private static final int X_VELOCITY = 0x100;
S1 field name mapping: When translating disassembly, use these S1→engine mappings:
| S1 Field | Engine Method/Field |
|---|---|
obX |
getX() / setX() (center coords) |
obY |
getY() / setY() (center coords) |
obVelX |
X velocity |
obVelY |
Y velocity |
obRoutine |
routine state variable |
ob2ndRout |
secondary routine |
obSubtype |
spawn.subtype() |
obColType |
collision flags (type + size) |
obColProp |
collision property (hit count for bosses) |
obFrame |
current mapping frame |
obAnim |
current animation ID |
Subtypes: Implement ALL subtypes from the subtype byte:
int subtype = spawn.subtype();
int behaviorBits = (subtype >> 4) & 0x0F;
int configBits = subtype & 0x0F;
Sound effects: Use constants from Sonic1AudioProfile.java:
services().audioManager().playSfx(Sonic1AudioProfile.SFX_SPRING);
Key Sonic 1 SFX IDs:
| ID | Name | Constant |
|---|---|---|
| 0xA0 | Jump | SFX_JUMP |
| 0xA1 | Lamppost | SFX_LAMPPOST |
| 0xA3 | Death/Hurt | SFX_DEATH |
| 0xAC | Hit Boss | SFX_HIT_BOSS |
| 0xB4 | Bumper | SFX_BUMPER |
| 0xB5 | Ring | SFX_RING |
| 0xC1 | Break Item | SFX_BREAK_ITEM |
| 0xCC | Spring | SFX_SPRING |
Debug visualization: Implement when debug enabled:
@Override
public void appendDebugRenderCommands(List<GLCommand> commands) {
if (!SonicConfigurationService.getInstance().isDebugViewEnabled()) return;
// Draw collision bounds, state info, etc.
}
2.6 Factory Registration
Register in Sonic1ObjectRegistry.registerDefaultFactories():
registerFactory(Sonic1ObjectIds.OBJECT_NAME,
(spawn, registry) -> new ObjectNameObjectInstance(spawn));
Phase 3: Code Quality
Ensure the implementation:
- Has no TODOs or placeholder code
- Has no "engine limitation" workarounds
- 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 s1disasm-guide skill (
.agents/skills/s1disasm-guide/skill.md) for reference on disassembly structure, label conventions, and object system patterns.
Review the implementation of [ObjectName] (0xXX) against the Sonic 1 disassembly.
Reference: Use the s1disasm-guide skill for disassembly navigation guidance.
Files to review:
- [List all created/modified files]
Disassembly references:
- Object code: docs/s1disasm/_incObj/XX Name.asm
- Animation: docs/s1disasm/_anim/Name.asm
- Mappings: docs/s1disasm/_maps/Name.asm
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 _anim/ file
5. Sound effects match disassembly SFX IDs
6. Movement/physics values match disassembly
7. Collision handling matches disassembly (obColType)
8. Debug visualization present
9. No TODOs or simplifications
10. Object lifecycle handled correctly
11. No "engine limitation" workarounds
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:
Verify registration in
Sonic1ObjectRegistry- ensure the factory is registered and the object is no longer returned as aPlaceholderObjectInstance.Add to IMPLEMENTED_IDS in
Sonic1ObjectProfile.java(theIMPLEMENTED_IDSset):Sonic1ObjectIds.OBJECT_NAMEKeep the set entries sorted logically with the other entries.
Update S1_OBJECT_CHECKLIST.md:
- Move the object from the "Unimplemented Objects" table to the "Implemented Objects" table
- Update the summary counts (Implemented/Unimplemented numbers and percentages)
- In the "By Zone" section, change
[ ]to[x]for every zone/act entry of this object - Update the per-act "Implemented" and "Unimplemented" counts in each affected act header
Build and test:
mvn packageReport completion with summary of implementation details.
Reference Files
| Purpose | Location |
|---|---|
| Disassembly guide | .agents/skills/s1disasm-guide/skill.md |
| Boss skill | .agents/skills/s1-implement-boss/skill.md |
| Object IDs | src/.../game/sonic1/constants/Sonic1ObjectIds.java |
| ROM offsets | src/.../game/sonic1/constants/Sonic1Constants.java |
| Registry | src/.../game/sonic1/objects/Sonic1ObjectRegistry.java |
| Audio profile | src/.../game/sonic1/audio/Sonic1AudioProfile.java |
| Base badnik | src/.../game/sonic2/objects/badniks/AbstractBadnikInstance.java (shared) |
| Disassembly objects | docs/s1disasm/_incObj/ |
| Disassembly animations | docs/s1disasm/_anim/ |
| Disassembly mappings | docs/s1disasm/_maps/ |
| Disassembly art | docs/s1disasm/artnem/ |
| Implemented IDs | src/.../tools/Sonic1ObjectProfile.java (IMPLEMENTED_IDS set) |
| Object checklist | S1_OBJECT_CHECKLIST.md (update after each implementation) |
S1 vs S2 Key Differences Summary
| Aspect | Sonic 1 | Sonic 2 |
|---|---|---|
| Object ASM location | _incObj/HEX Name.asm |
Inline in main ASM |
| Animation files | _anim/Name.asm |
Inline |
| Mapping files | _maps/Name.asm |
mappings/sprite/ |
| Mapping format | 5-byte pieces, byte header | 6-byte pieces, word header |
| Art label prefix | Nem_ |
ArtNem_ |
| Object field names | obRoutine, obX, obY |
routine, x_pos, y_pos |
| Block size | 256x256 | 128x128 |
| RomOffsetFinder | --game s1 required |
Default (no flag) |
| Constants file | Sonic1Constants.java |
Sonic2Constants.java |
| Object IDs file | Sonic1ObjectIds.java |
Sonic2ObjectIds.java |
| SFX constants | Sonic1AudioProfile.java |
Sonic2AudioConstants.java |
| Registry | Sonic1ObjectRegistry.java |
Sonic2ObjectRegistry.java |
| Art infrastructure | Established (Sonic1ObjectArt/Provider/Keys) |
Fully established |