diff --git a/docs/dungeon_editing_implementation_plan.md b/docs/dungeon_editing_implementation_plan.md new file mode 100644 index 00000000..86cf7af0 --- /dev/null +++ b/docs/dungeon_editing_implementation_plan.md @@ -0,0 +1,1172 @@ +# Dungeon Editing Implementation Plan for Yaze + +**Status**: Planning Phase +**Goal**: Implement fully functional dungeon room editing based on ZScream's proven approach +**Created**: October 4, 2025 + +## Executive Summary + +The dungeon object system in yaze currently does not work properly. ZScream's dungeon editing is fully functional and provides a proven reference implementation. This document outlines a comprehensive plan to implement working dungeon editing in yaze by analyzing ZScream's approach and adapting it to yaze's C++ architecture. + +## Table of Contents + +1. [Current State Analysis](#current-state-analysis) +2. [ZScream's Working Approach](#zscreams-working-approach) +3. [ROM Data Structure Reference](#rom-data-structure-reference) +4. [Implementation Tasks](#implementation-tasks) +5. [Technical Architecture](#technical-architecture) +6. [Testing Strategy](#testing-strategy) +7. [References](#references) + +--- + +## Current State Analysis + +### What Works in Yaze + +- ✅ Room header loading (basic properties: blockset, palette, spriteset) +- ✅ Room class structure exists +- ✅ RoomObject class with basic properties +- ✅ Graphics loading system (blocks, palettes) +- ✅ Room layout system for structural elements +- ✅ Sprite loading (separate from objects) +- ✅ UI framework with canvas, selectors, and viewers + +### What Doesn't Work + +- ❌ **Object Parsing**: Objects are loaded but not properly decoded +- ❌ **Object Drawing**: Objects don't render correctly on canvas +- ❌ **Object Placement**: No working system to place/edit objects +- ❌ **Object Encoding**: Can't save objects back to ROM format +- ❌ **Object Selection**: Can't select/move/delete objects properly +- ❌ **Layer Management**: Layer separation not working correctly +- ❌ **Door Objects**: Door section (0xF0 FF) not handled +- ❌ **Object Types**: Type1, Type2, Type3 distinctions not properly implemented +- ❌ **Tile Loading**: Objects don't load their tile data correctly +- ❌ **Collision/Properties**: Object-specific properties not tracked + +### Key Problems Identified + +1. **Object Data Format**: The 3-byte object encoding/decoding is incomplete +2. **Layer Separation**: Objects on BG1, BG2, BG3 not properly separated +3. **Object Rendering**: Object tile data not being loaded and drawn +4. **Editor Integration**: No working UI flow for object placement +5. **Save System**: No encoding of edited objects back to ROM format + +--- + +## ZScream's Working Approach + +### Room Data Structure (ZScream) + +```csharp +public class Room +{ + // Header Data (14 bytes at header_location) + public byte layout; // Byte 1 (bits 2-4) + public byte floor1; // Byte 0 (bits 0-3) + public byte floor2; // Byte 0 (bits 4-7) + public byte blockset; // Byte 2 + public byte spriteset; // Byte 3 + public byte palette; // Byte 1 (bits 0-5) + public Background2 bg2; // Byte 0 (bits 5-7) + public CollisionKey collision; // Byte 0 (bits 2-4) + public EffectKey effect; // Byte 4 + public TagKey tag1; // Byte 5 + public TagKey tag2; // Byte 6 + public byte holewarp; // Byte 9 + public byte[] staircase_rooms; // Bytes 10-13 (4 stairs) + public byte[] staircase_plane; // Byte 7-8 (plane info) + + // Object Data + public List tilesObjects; // Objects in room + public List tilesLayoutObjects; // Layout/structural objects + public List sprites; // Sprites (separate system) + public List chest_list; // Chests in room + public byte[] blocks; // 16 GFX blocks for room +} +``` + +### Object Data Structure (ZScream) + +```csharp +public class Room_Object +{ + // ROM Data (encoded in 3 bytes) + public byte X; // Position X (0-63) + public byte Y; // Position Y (0-63) + public byte Size; // Size parameter (0-15) + public ushort id; // Object ID (0x000-0xFFF) + public LayerType Layer; // BG1, BG2, or BG3 + + // Rendering Data + public List tiles; // Tile data for drawing + public int width, height; // Computed object size + public int offsetX, offsetY; // Drawing offset + + // Editor Data + public string name; // Object name for UI + public ObjectOption options; // Flags: Door, Chest, Block, Torch, Stairs + public DungeonLimits LimitClass; // Object limits tracking + public int uniqueID; // Unique ID for undo/redo +} +``` + +### Object Encoding Format (Critical!) + +ZScream uses **three different encoding formats** based on object ID range: + +#### Type 1 Objects (ID 0x000-0x0FF) - Standard Objects + +**Encoding**: `xxxxxxss yyyyyyss iiiiiiii` + +``` +Byte 1: (X << 2) | ((Size >> 2) & 0x03) +Byte 2: (Y << 2) | (Size & 0x03) +Byte 3: Object ID (0x00-0xFF) +``` + +**Decoding**: +``` +X = (Byte1 & 0xFC) >> 2 // Bits 2-7 = X position +Y = (Byte2 & 0xFC) >> 2 // Bits 2-7 = Y position +Size = ((Byte1 & 0x03) << 2) | (Byte2 & 0x03) // Size 0-15 +ID = Byte3 // Object ID +``` + +#### Type 2 Objects (ID 0x100-0x1FF) - Special Objects + +**Encoding**: `111111xx xxxxyyyy yyiiiiii` + +``` +Byte 1: 0xFC | ((X & 0x30) >> 4) // 0xFC-0xFF +Byte 2: ((X & 0x0F) << 4) | ((Y & 0x3C) >> 2) +Byte 3: ((Y & 0x03) << 6) | (ID & 0x3F) +``` + +**Decoding**: +``` +X = ((Byte1 & 0x03) << 4) | ((Byte2 & 0xF0) >> 4) // X position +Y = ((Byte2 & 0x0F) << 2) | ((Byte3 & 0xC0) >> 6) // Y position +ID = (Byte3 & 0x3F) | 0x100 // Object ID +Size = 0 // No size parameter +``` + +#### Type 3 Objects (ID 0xF00-0xFFF) - Extended Objects + +**Encoding**: `xxxxxxii yyyyyyii 11111iii` + +``` +Byte 1: (X << 2) | (ID & 0x03) +Byte 2: (Y << 2) | ((ID >> 2) & 0x03) +Byte 3: (ID >> 4) & 0xFF // 0xF0-0xFF +``` + +**Decoding**: +``` +X = (Byte1 & 0xFC) >> 2 +Y = (Byte2 & 0xFC) >> 2 +ID = ((Byte3 << 4) & 0xF00) | ((Byte2 & 0x03) << 2) | (Byte1 & 0x03) | 0x80 +``` + +### Layer and Door Markers + +Objects are organized in **layers** separated by special markers: + +``` +[Floor/Layout byte] +[Layout configuration byte] + +0xFF 0xFF <- Layer 1 End + +0xFF 0xFF <- Layer 2 End + +[Optional Door Section] +0xF0 0xFF <- Door Section Marker + +0xFF 0xFF <- End of Room +``` + +### Object Loading Process (ZScream) + +```csharp +// 1. Get room object pointer (3 bytes) +int object_pointer = ROM at (room_object_pointer + room_id * 3) +int objects_location = SnesToPc(object_pointer) + +// 2. Read floor/layout +byte floor1 = data[objects_location] & 0x0F +byte floor2 = (data[objects_location] >> 4) & 0x0F +byte layout = (data[objects_location + 1] >> 2) & 0x07 + +// 3. Parse objects +int pos = objects_location + 2 +int layer = 0 +bool door = false + +while (true) { + byte b1 = data[pos] + byte b2 = data[pos + 1] + + // Check for layer end + if (b1 == 0xFF && b2 == 0xFF) { + layer++ + pos += 2 + if (layer == 3) break + continue + } + + // Check for door section + if (b1 == 0xF0 && b2 == 0xFF) { + door = true + pos += 2 + continue + } + + byte b3 = data[pos + 2] + + if (!door) { + // Decode object based on type + RoomObject obj = DecodeObject(b1, b2, b3, layer) + tilesObjects.Add(obj) + pos += 3 + } else { + // Decode door (2 bytes) + door_pos = b1 >> 3 + door_dir = b1 & 0x07 + door_type = b2 + // Create door object + pos += 2 + } +} +``` + +### Object Saving Process (ZScream) + +```csharp +public byte[] getTilesBytes() +{ + List objectsBytes = new List(); + List doorsBytes = new List(); + + // 1. Write floor/layout + byte floorbyte = (byte)((floor2 << 4) + floor1); + byte layoutbyte = (byte)(layout << 2); + objectsBytes.Add(floorbyte); + objectsBytes.Add(layoutbyte); + + // 2. Write Layer 1 + foreach (object in layer0_objects) { + byte[] encoded = EncodeObject(object); + objectsBytes.AddRange(encoded); + } + objectsBytes.Add(0xFF); + objectsBytes.Add(0xFF); + + // 3. Write Layer 2 + foreach (object in layer1_objects) { + byte[] encoded = EncodeObject(object); + objectsBytes.AddRange(encoded); + } + objectsBytes.Add(0xFF); + objectsBytes.Add(0xFF); + + // 4. Write Layer 3 + foreach (object in layer2_objects) { + byte[] encoded = EncodeObject(object); + objectsBytes.AddRange(encoded); + } + + // 5. Write Doors if any + if (has_doors) { + objectsBytes.Add(0xF0); + objectsBytes.Add(0xFF); + objectsBytes.AddRange(doorsBytes); + } + + objectsBytes.Add(0xFF); + objectsBytes.Add(0xFF); + + return objectsBytes.ToArray(); +} +``` + +### Object Tile Loading (ZScream) + +Each object references tile data from ROM tables: + +```csharp +// Get tile pointer from subtype table +int tile_ptr = GetSubtypeTablePointer(object_id); +int tile_pos = ROM[tile_ptr] + ROM[tile_ptr+1] << 8; + +// Load tiles (each tile is 2 bytes: ID and properties) +for (int i = 0; i < tile_count; i++) { + byte tile_id = ROM[tile_pos + i*2]; + byte tile_prop = ROM[tile_pos + i*2 + 1]; + + Tile tile = new Tile(tile_id, tile_prop); + object.tiles.Add(tile); +} +``` + +### Object Drawing (ZScream) + +```csharp +public void Draw() +{ + // Calculate object dimensions based on size + width = basewidth + (sizewidth * Size); + height = baseheight + (sizeheight * Size); + + // Draw each tile at computed position + foreach (Tile tile in tiles) { + draw_tile(tile, tile_x * 8, tile_y * 8); + } +} + +// Tile drawing writes to background buffers +public void draw_tile(Tile t, int xx, int yy) +{ + int buffer_index = ((xx / 8) + offsetX + nx) + + ((ny + offsetY + (yy / 8)) * 64); + + if (Layer == BG1) { + tilesBg1Buffer[buffer_index] = t.GetTileInfo(); + } + if (Layer == BG2) { + tilesBg2Buffer[buffer_index] = t.GetTileInfo(); + } +} +``` + +--- + +## ROM Data Structure Reference + +### Key ROM Addresses (from ALTTP Disassembly) + +``` +# Room Headers +room_object_pointer = 0x874C # Long pointer to room object data +room_object_layout_pointer = 0x882D # Pointer to layout data +kRoomHeaderPointer = 0xB5DD # Long pointer to room headers +kRoomHeaderPointerBank = 0xB5E7 # Bank byte + +# Graphics +gfx_groups_pointer = 0x6237 # Graphics groups +blocks_pointer1-4 = 0x15AFA-0x15B0F # Block data pointers +dungeons_palettes = 0xDD734 # Palette data +sprite_blockset_pointer = 0x5B57 # Sprite blocksets + +# Sprites +rooms_sprite_pointer = 0x4C298 # 2-byte bank pointer +sprites_data = 0x4D8B0 # Sprite data +sprites_end_data = 0x4EC9E # End of sprite data + +# Chests +chests_length_pointer = 0xEBF6 # Chest count +chests_data_pointer1 = 0xEBFB # Chest data pointer + +# Doors +door_gfx_up/down/left/right = 0x4D9E-0x4EC6 # Door graphics +door_pos_up/down/left/right = 0x197E-0x19C6 # Door positions + +# Torches, Blocks, Pits +torch_data = 0x2736A # Torch data +torches_length_pointer = 0x88C1 # Torch count +blocks_length = 0x8896 # Block count +pit_pointer = 0x394AB # Pit data +pit_count = 0x394A6 # Pit count + +# Messages +messages_id_dungeon = 0x3F61D # Message IDs + +# Object Subtypes (Tile Data Tables) +kRoomObjectSubtype1 = 0x8000 # Type 1 table +kRoomObjectSubtype2 = 0x83F0 # Type 2 table +kRoomObjectSubtype3 = 0x84F0 # Type 3 table +kRoomObjectTileAddress = 0x1B52 # Base tile address +``` + +### Room Header Format (14 bytes) + +``` +Offset | Bits | Description +-------|-----------|------------------------------------------ +0 | 0-0 | Is Light (1 = light room, 0 = dark) +0 | 2-4 | Collision type +0 | 5-7 | Background2 type +1 | 0-5 | Palette ID +2 | 0-7 | Blockset ID +3 | 0-7 | Spriteset ID +4 | 0-7 | Effect +5 | 0-7 | Tag 1 +6 | 0-7 | Tag 2 +7 | 2-3 | Staircase 1 plane +7 | 4-5 | Staircase 2 plane +7 | 6-7 | Staircase 3 plane +8 | 0-1 | Staircase 4 plane +9 | 0-7 | Holewarp room +10 | 0-7 | Staircase 1 room +11 | 0-7 | Staircase 2 room +12 | 0-7 | Staircase 3 room +13 | 0-7 | Staircase 4 room +``` + +### Object Data Format + +``` +[Floor/Layout - 2 bytes] + Byte 0: floor1 (bits 0-3), floor2 (bits 4-7) + Byte 1: layout (bits 2-4) + +[Layer 1 Objects] + For each object: 3 bytes (Type1/Type2/Type3 encoding) + +[Layer 1 End Marker] + 0xFF 0xFF + +[Layer 2 Objects] + For each object: 3 bytes (Type1/Type2/Type3 encoding) + +[Layer 2 End Marker] + 0xFF 0xFF + +[Layer 3 Objects] + For each object: 3 bytes (Type1/Type2/Type3 encoding) + +[Optional: Door Section] + 0xF0 0xFF (Door section marker) + For each door: 2 bytes + Byte 1: door_pos (bits 3-7), door_dir (bits 0-2) + Byte 2: door_type + +[End Marker] + 0xFF 0xFF +``` + +### Tile Data Format + +``` +Each Tile16 (metatile) consists of 4 Tile8s (8x8 tiles): + Word 0: Top-left tile + Word 1: Top-right tile + Word 2: Bottom-left tile + Word 3: Bottom-right tile + +Each Word encodes: + Bits 0-9: Tile ID (0-1023) + Bit 10: Horizontal flip + Bit 11: Vertical flip + Bits 12-14: Palette (0-7) + Bit 15: Priority +``` + +--- + +## Implementation Tasks + +### Phase 1: Core Object System (HIGH PRIORITY) + +#### Task 1.1: Object Encoding/Decoding ✅ CRITICAL + +**Files**: `src/app/zelda3/dungeon/room_object.cc`, `room_object.h` + +**Objectives**: +- Implement `DecodeObjectFromBytes(byte b1, byte b2, byte b3, layer)` +- Implement `EncodeObjectToBytes(RoomObject) -> [byte, byte, byte]` +- Handle Type1, Type2, Type3 encoding schemes correctly +- Add unit tests for encoding/decoding round-trips + +**Dependencies**: None + +**Estimated Time**: 3-4 hours + +**Implementation Details**: + +```cpp +// Add to room_object.h +struct ObjectBytes { + uint8_t b1; + uint8_t b2; + uint8_t b3; +}; + +// Decode object from 3-byte ROM format +static RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t layer); + +// Encode object to 3-byte ROM format +ObjectBytes EncodeObjectToBytes() const; + +// Determine object type from bytes +static int DetermineObjectType(uint8_t b1, uint8_t b3); +``` + +#### Task 1.2: Enhanced Object Parsing ✅ CRITICAL + +**Files**: `src/app/zelda3/dungeon/room.cc`, `room.h` + +**Objectives**: +- Refactor `ParseObjectsFromLocation()` to handle all 3 layers +- Implement door section parsing (0xF0 FF marker) +- Track objects by layer properly +- Handle layer end markers (0xFF FF) + +**Dependencies**: Task 1.1 + +**Estimated Time**: 4-5 hours + +**Implementation Details**: + +```cpp +// Add to room.h +struct LayeredObjects { + std::vector layer1; + std::vector layer2; + std::vector layer3; + std::vector doors; +}; + +// Enhanced parsing with layer separation +absl::Status ParseLayeredObjects(int objects_location, + LayeredObjects& out_objects); +``` + +#### Task 1.3: Object Tile Loading ✅ CRITICAL + +**Files**: `src/app/zelda3/dungeon/room_object.cc`, `object_parser.cc` + +**Objectives**: +- Implement proper tile loading from subtype tables +- Load tiles based on object ID and subtype +- Cache tile data for performance +- Handle animated tiles + +**Dependencies**: Task 1.1 + +**Estimated Time**: 4-5 hours + +**Implementation Details**: + +```cpp +// Enhance RoomObject::EnsureTilesLoaded() +absl::Status LoadTilesFromSubtypeTable(int object_id); + +// Get subtype table info +SubtypeTableInfo GetSubtypeTable(int object_id); + +// Load tile data from ROM +absl::StatusOr> LoadTileData(int tile_ptr); +``` + +#### Task 1.4: Object Drawing System ⚠️ HIGH PRIORITY + +**Files**: `src/app/zelda3/dungeon/object_renderer.cc` + +**Objectives**: +- Implement proper object rendering with tiles +- Draw objects at correct positions with proper offsets +- Handle object sizing based on Size parameter +- Render to correct background layer (BG1/BG2/BG3) + +**Dependencies**: Task 1.3 + +**Estimated Time**: 5-6 hours + +**Implementation Details**: + +```cpp +// Enhanced rendering +absl::Status RenderObjectWithTiles(const RoomObject& object, + const gfx::SnesPalette& palette, + std::vector& bg1_buffer, + std::vector& bg2_buffer); + +// Compute object dimensions +void ComputeObjectDimensions(RoomObject& object); + +// Draw individual tile +void DrawTileToBuffer(const gfx::Tile16& tile, int x, int y, + std::vector& buffer); +``` + +--- + +### Phase 2: Editor UI Integration (MEDIUM PRIORITY) + +#### Task 2.1: Object Placement System + +**Files**: `src/app/editor/dungeon/dungeon_object_interaction.cc` + +**Objectives**: +- Implement click-to-place object system +- Snap objects to grid +- Show preview of object before placement +- Validate object placement (limits, collisions) + +**Dependencies**: Phase 1 complete + +**Estimated Time**: 4-5 hours + +#### Task 2.2: Object Selection System + +**Files**: `src/app/editor/dungeon/dungeon_object_interaction.cc` + +**Objectives**: +- Click to select objects +- Drag to move objects +- Multi-select with box selection +- Show selection highlight + +**Dependencies**: Task 2.1 + +**Estimated Time**: 3-4 hours + +#### Task 2.3: Object Properties Editor + +**Files**: `src/app/editor/dungeon/dungeon_object_selector.cc` + +**Objectives**: +- Show object properties in sidebar +- Edit object properties (X, Y, Size, Layer) +- Change object type +- Real-time preview of changes + +**Dependencies**: Task 2.2 + +**Estimated Time**: 3-4 hours + +#### Task 2.4: Layer Management UI + +**Files**: `src/app/editor/dungeon/dungeon_editor.cc` + +**Objectives**: +- Layer visibility toggles +- Active layer selection +- Layer opacity controls +- Show layer indicators + +**Dependencies**: Task 2.1 + +**Estimated Time**: 2-3 hours + +--- + +### Phase 3: Save System (HIGH PRIORITY) + +#### Task 3.1: Room Object Encoding ✅ CRITICAL + +**Files**: `src/app/zelda3/dungeon/room.cc` + +**Objectives**: +- Implement `SerializeRoomObjects() -> byte[]` +- Encode objects by layer with proper markers +- Handle door section separately +- Maintain floor/layout bytes + +**Dependencies**: Task 1.1 + +**Estimated Time**: 4-5 hours + +**Implementation Details**: + +```cpp +// Add to room.h +struct SerializedRoomData { + std::vector data; + int size; + int original_size; +}; + +// Serialize all room objects to ROM format +absl::StatusOr SerializeRoomObjects(); + +// Encode objects for one layer +std::vector EncodeLayer(const std::vector& objects); + +// Encode door section +std::vector EncodeDoors(const std::vector& doors); +``` + +#### Task 3.2: ROM Writing System + +**Files**: `src/app/rom.cc`, `src/app/zelda3/dungeon/room.cc` + +**Objectives**: +- Write serialized object data back to ROM +- Update room pointers if size changed +- Handle room size expansion +- Validate written data + +**Dependencies**: Task 3.1 + +**Estimated Time**: 5-6 hours + +#### Task 3.3: Save Validation + +**Files**: `src/app/zelda3/dungeon/room.cc` + +**Objectives**: +- Verify encoded data matches original format +- Check object limits (sprites, doors, chests) +- Validate room size constraints +- Round-trip testing (load -> save -> load) + +**Dependencies**: Task 3.2 + +**Estimated Time**: 3-4 hours + +--- + +### Phase 4: Advanced Features (LOW PRIORITY) + +#### Task 4.1: Undo/Redo System + +**Files**: `src/app/zelda3/dungeon/dungeon_editor_system.cc` + +**Objectives**: +- Track object changes +- Implement undo stack +- Implement redo stack +- Handle multiple operations + +**Dependencies**: Phase 2, Phase 3 + +**Estimated Time**: 5-6 hours + +#### Task 4.2: Copy/Paste Objects + +**Files**: `src/app/editor/dungeon/dungeon_object_interaction.cc` + +**Objectives**: +- Copy selected objects +- Paste with offset +- Duplicate objects +- Paste across rooms + +**Dependencies**: Task 2.2 + +**Estimated Time**: 2-3 hours + +#### Task 4.3: Object Library/Templates + +**Files**: `src/app/editor/dungeon/dungeon_object_selector.cc` + +**Objectives**: +- Save common object configurations +- Load object templates +- Share templates across rooms +- Import/export templates + +**Dependencies**: Phase 2 + +**Estimated Time**: 4-5 hours + +#### Task 4.4: Room Import/Export + +**Files**: `src/app/zelda3/dungeon/room.cc` + +**Objectives**: +- Export room to JSON/binary +- Import room from file +- Batch export/import +- Room comparison tools + +**Dependencies**: Phase 3 + +**Estimated Time**: 5-6 hours + +--- + +## Technical Architecture + +### Class Hierarchy + +``` +Rom +├── Room (296 rooms) +│ ├── RoomHeader (14 bytes) +│ ├── RoomLayout (structural elements) +│ ├── std::vector (tile objects) +│ │ ├── RoomObject (base) +│ │ ├── Subtype1 (Type1 objects) +│ │ ├── Subtype2 (Type2 objects) +│ │ └── Subtype3 (Type3 objects) +│ ├── std::vector (doors) +│ ├── std::vector (sprites) +│ └── std::vector (chests) +└── DungeonEditor + ├── DungeonEditorSystem (editing logic) + ├── DungeonObjectEditor (object manipulation) + ├── ObjectRenderer (rendering) + ├── ObjectParser (parsing) + ├── DungeonCanvasViewer (display) + ├── DungeonObjectSelector (object palette) + └── DungeonObjectInteraction (mouse/keyboard) +``` + +### Data Flow + +``` +ROM File + ↓ +[LoadRoomFromRom] + ↓ +Room Header → Room Properties + ↓ +[ParseObjectsFromLocation] + ↓ +Object Bytes (3-byte format) + ↓ +[DecodeObjectFromBytes] + ↓ +RoomObject instances + ↓ +[LoadTilesFromSubtypeTable] + ↓ +Tile data loaded + ↓ +[RenderObjectWithTiles] + ↓ +Display on Canvas + ↓ +User Edits + ↓ +[SerializeRoomObjects] + ↓ +Object Bytes (3-byte format) + ↓ +[WriteToRom] + ↓ +ROM File Updated +``` + +### Key Algorithms + +#### Object Type Detection + +```cpp +int DetermineObjectType(uint8_t b1, uint8_t b3) { + // Type 3: b3 >= 0xF8 or b1 >= 0xFC with b3 >= 0xF0 + if (b3 >= 0xF8) return 3; + + // Type 2: b1 >= 0xFC + if (b1 >= 0xFC) return 2; + + // Type 1: default + return 1; +} +``` + +#### Object Decoding + +```cpp +RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t layer) { + int type = DetermineObjectType(b1, b3); + + uint8_t x, y, size; + uint16_t id; + + switch (type) { + case 1: // Type1: xxxxxxss yyyyyyss iiiiiiii + x = (b1 & 0xFC) >> 2; + y = (b2 & 0xFC) >> 2; + size = ((b1 & 0x03) << 2) | (b2 & 0x03); + id = b3; + break; + + case 2: // Type2: 111111xx xxxxyyyy yyiiiiii + x = ((b1 & 0x03) << 4) | ((b2 & 0xF0) >> 4); + y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6); + size = 0; + id = (b3 & 0x3F) | 0x100; + break; + + case 3: // Type3: xxxxxxii yyyyyyii 11111iii + x = (b1 & 0xFC) >> 2; + y = (b2 & 0xFC) >> 2; + size = 0; + id = ((b3 & 0xFF) << 4) | ((b2 & 0x03) << 2) | (b1 & 0x03) | 0x80; + break; + } + + return RoomObject(id, x, y, size, layer); +} +``` + +#### Object Encoding + +```cpp +ObjectBytes EncodeObjectToBytes(const RoomObject& obj) { + ObjectBytes bytes; + + if (obj.id_ >= 0xF00) { // Type 3 + bytes.b1 = (obj.x_ << 2) | (obj.id_ & 0x03); + bytes.b2 = (obj.y_ << 2) | ((obj.id_ >> 2) & 0x03); + bytes.b3 = (obj.id_ >> 4) & 0xFF; + } + else if (obj.id_ >= 0x100) { // Type 2 + bytes.b1 = 0xFC | ((obj.x_ & 0x30) >> 4); + bytes.b2 = ((obj.x_ & 0x0F) << 4) | ((obj.y_ & 0x3C) >> 2); + bytes.b3 = ((obj.y_ & 0x03) << 6) | (obj.id_ & 0x3F); + } + else { // Type 1 + bytes.b1 = (obj.x_ << 2) | ((obj.size_ >> 2) & 0x03); + bytes.b2 = (obj.y_ << 2) | (obj.size_ & 0x03); + bytes.b3 = obj.id_; + } + + return bytes; +} +``` + +--- + +## Testing Strategy + +### Unit Tests + +#### Encoding/Decoding Tests + +```cpp +TEST(RoomObjectTest, EncodeDecodeType1) { + // Create Type1 object + RoomObject obj(0x42, 10, 20, 3, 0); + + // Encode + auto bytes = obj.EncodeObjectToBytes(); + + // Decode + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + // Verify + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x_, obj.x_); + EXPECT_EQ(decoded.y_, obj.y_); + EXPECT_EQ(decoded.size_, obj.size_); +} + +TEST(RoomObjectTest, EncodeDecodeType2) { + // Type2 object + RoomObject obj(0x125, 15, 30, 0, 1); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); + + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x_, obj.x_); + EXPECT_EQ(decoded.y_, obj.y_); +} + +TEST(RoomObjectTest, EncodeDecodeType3) { + // Type3 object (chest) + RoomObject obj(0xF99, 5, 10, 0, 0); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x_, obj.x_); + EXPECT_EQ(decoded.y_, obj.y_); +} +``` + +#### Room Parsing Tests + +```cpp +TEST(RoomTest, ParseObjectsWithLayers) { + // Create test data with 3 layers + std::vector data = { + 0x12, // floor1/floor2 + 0x04, // layout + // Layer 1 + 0x28, 0x50, 0x10, // Object 1 + 0xFF, 0xFF, // Layer 1 end + // Layer 2 + 0x30, 0x60, 0x20, // Object 2 + 0xFF, 0xFF, // Layer 2 end + 0xFF, 0xFF // End + }; + + Room room; + auto status = room.ParseObjectsFromData(data); + EXPECT_TRUE(status.ok()); + EXPECT_EQ(room.GetTileObjects().size(), 2); +} +``` + +### Integration Tests + +#### Round-Trip Tests + +```cpp +TEST(IntegrationTest, LoadSaveLoadRoom) { + Rom rom("test_rom.sfc"); + + // Load room + Room room1 = LoadRoomFromRom(&rom, 0x001); + size_t original_objects = room1.GetTileObjects().size(); + + // Save room + auto serialized = room1.SerializeRoomObjects(); + EXPECT_TRUE(serialized.ok()); + + // Write to ROM + auto write_status = rom.WriteRoomObjects(0x001, serialized.value()); + EXPECT_TRUE(write_status.ok()); + + // Load again + Room room2 = LoadRoomFromRom(&rom, 0x001); + + // Verify same objects + EXPECT_EQ(room2.GetTileObjects().size(), original_objects); + for (size_t i = 0; i < original_objects; i++) { + EXPECT_EQ(room2.GetTileObject(i).id_, room1.GetTileObject(i).id_); + EXPECT_EQ(room2.GetTileObject(i).x_, room1.GetTileObject(i).x_); + EXPECT_EQ(room2.GetTileObject(i).y_, room1.GetTileObject(i).y_); + } +} +``` + +### Manual Testing + +#### Test Cases + +1. **Basic Object Loading** + - Load room with various object types + - Verify all objects appear + - Check object positions match ZScream + +2. **Object Placement** + - Place new objects in empty room + - Move existing objects + - Delete objects + - Verify changes persist + +3. **Layer Management** + - Place objects on different layers + - Toggle layer visibility + - Verify layer separation + +4. **Save/Load** + - Edit multiple rooms + - Save changes + - Reload ROM + - Verify all changes present + +5. **Edge Cases** + - Full rooms (many objects) + - Empty rooms + - Rooms with only doors + - Large objects (max size) + +--- + +## References + +### Documentation + +- **ZScream Source Code**: `/Users/scawful/Code/ZScreamDungeon/ZeldaFullEditor/Rooms/` +- **Yaze Source Code**: `/Users/scawful/Code/yaze/src/app/zelda3/dungeon/` +- **ALTTP Disassembly**: `/Users/scawful/Code/yaze/assets/asm/usdasm/` +- **This Document**: `/Users/scawful/Code/yaze/docs/dungeon_editing_implementation_plan.md` + +### Key Files + +#### ZScream (Reference) +- `Room.cs` - Main room class with object loading/saving +- `Room_Object.cs` - Object class with encoding/decoding +- `DungeonObjectData.cs` - Object tile data tables +- `GFX.cs` - Graphics management + +#### Yaze (Implementation) +- `src/app/zelda3/dungeon/room.h/cc` - Room class +- `src/app/zelda3/dungeon/room_object.h/cc` - RoomObject class +- `src/app/zelda3/dungeon/object_parser.h/cc` - Object parsing +- `src/app/zelda3/dungeon/object_renderer.h/cc` - Object rendering +- `src/app/editor/dungeon/dungeon_editor.h/cc` - Main editor +- `src/app/editor/dungeon/dungeon_object_interaction.h/cc` - Object interaction + +### External Resources + +- **ALTTP Hacking Resources**: https://github.com/spannerisms/z3doc +- **ROM Map**: https://www.zeldacodes.org/ +- **Disassembly**: https://github.com/camthesaxman/zelda3 + +--- + +## Appendix: Object ID Reference + +### Type 1 Objects (0x00-0xFF) + +Common objects with size parameter: +- 0x00-0x09: Walls (ceiling, top, bottom) +- 0x0A-0x20: Diagonal walls +- 0x21: Platform stairs +- 0x22: Rail +- 0x23-0x2E: Pit edges +- 0x2F-0x30: Rail walls +- 0x33: Carpet +- 0x34: Carpet trim +- 0x36-0x3E: Drapes, statues, columns +- 0x3F-0x45: Water edges +- 0x50-0x5F: Dungeon-specific decorations + +### Type 2 Objects (0x100-0x1FF) + +Special fixed-size objects: +- 0x100-0x10F: Corners (concave, convex) +- 0x110-0x11F: Kinked corners +- 0x120-0x12F: Decorative objects (braziers, statues) +- 0x130-0x13F: Functional objects (blocks, stairs, chests) +- 0x140-0x14F: Interactive objects (switches, torches) + +### Type 3 Objects (0xF00-0xFFF) + +Extended special objects: +- 0xF99: Small chest +- 0xFB1: Large chest +- 0xF80-0xF8F: Various stairs +- 0xF90-0xF9F: Special effects +- 0xFA0-0xFAF: Interactive elements + +--- + +## Status Tracking + +### Current Progress + +- [x] Document created +- [ ] Phase 1: Core Object System + - [ ] Task 1.1: Object Encoding/Decoding + - [ ] Task 1.2: Enhanced Object Parsing + - [ ] Task 1.3: Object Tile Loading + - [ ] Task 1.4: Object Drawing System +- [ ] Phase 2: Editor UI Integration +- [ ] Phase 3: Save System +- [ ] Phase 4: Advanced Features + +### Next Steps + +1. **Start with Task 1.1** - Implement object encoding/decoding +2. **Create unit tests** - Test all three object types +3. **Verify with ZScream** - Compare results with known working implementation +4. **Proceed to Task 1.2** - Once encoding/decoding is solid + +### Timeline Estimate + +- **Phase 1**: 16-20 hours (CRITICAL - Must be done first) +- **Phase 2**: 12-16 hours (Required for usability) +- **Phase 3**: 12-15 hours (Required for saving) +- **Phase 4**: 16-20 hours (Nice to have) + +**Total**: 56-71 hours (7-9 full working days) + +--- + +*Document Version: 1.0* +*Last Updated: October 4, 2025* +*Author: AI Assistant with scawful* +