Files
yaze/docs/dungeon_editing_implementation_plan.md

30 KiB

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
  2. ZScream's Working Approach
  3. ROM Data Structure Reference
  4. Implementation Tasks
  5. Technical Architecture
  6. Testing Strategy
  7. 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)

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<Room_Object> tilesObjects;  // Objects in room
    public List<Room_Object> tilesLayoutObjects; // Layout/structural objects
    public List<Sprite> sprites;            // Sprites (separate system)
    public List<Chest> chest_list;          // Chests in room
    public byte[] blocks;                    // 16 GFX blocks for room
}

Object Data Structure (ZScream)

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<Tile> 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]
<Layer 1 Objects>
0xFF 0xFF  <- Layer 1 End
<Layer 2 Objects>
0xFF 0xFF  <- Layer 2 End
<Layer 3 Objects>
[Optional Door Section]
0xF0 0xFF  <- Door Section Marker
<Door Objects (2 bytes each)>
0xFF 0xFF  <- End of Room

Object Loading Process (ZScream)

// 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)

public byte[] getTilesBytes()
{
    List<byte> objectsBytes = new List<byte>();
    List<byte> doorsBytes = new List<byte>();
    
    // 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:

// 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)

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:

// 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:

// Add to room.h
struct LayeredObjects {
  std::vector<RoomObject> layer1;
  std::vector<RoomObject> layer2;
  std::vector<RoomObject> layer3;
  std::vector<DoorObject> 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:

// 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<std::vector<gfx::Tile16>> 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:

// Enhanced rendering
absl::Status RenderObjectWithTiles(const RoomObject& object,
                                     const gfx::SnesPalette& palette,
                                     std::vector<uint8_t>& bg1_buffer,
                                     std::vector<uint8_t>& bg2_buffer);

// Compute object dimensions
void ComputeObjectDimensions(RoomObject& object);

// Draw individual tile
void DrawTileToBuffer(const gfx::Tile16& tile, int x, int y,
                      std::vector<uint8_t>& 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:

// Add to room.h
struct SerializedRoomData {
  std::vector<uint8_t> data;
  int size;
  int original_size;
};

// Serialize all room objects to ROM format
absl::StatusOr<SerializedRoomData> SerializeRoomObjects();

// Encode objects for one layer
std::vector<uint8_t> EncodeLayer(const std::vector<RoomObject>& objects);

// Encode door section
std::vector<uint8_t> EncodeDoors(const std::vector<DoorObject>& 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<RoomObject> (tile objects)
│   │   ├── RoomObject (base)
│   │   ├── Subtype1 (Type1 objects)
│   │   ├── Subtype2 (Type2 objects)
│   │   └── Subtype3 (Type3 objects)
│   ├── std::vector<DoorObject> (doors)
│   ├── std::vector<Sprite> (sprites)
│   └── std::vector<chest_data> (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

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

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

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

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

TEST(RoomTest, ParseObjectsWithLayers) {
  // Create test data with 3 layers
  std::vector<uint8_t> 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

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


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

  • 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