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
- Current State Analysis
- ZScream's Working Approach
- ROM Data Structure Reference
- Implementation Tasks
- Technical Architecture
- Testing Strategy
- 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
- Object Data Format: The 3-byte object encoding/decoding is incomplete
- Layer Separation: Objects on BG1, BG2, BG3 not properly separated
- Object Rendering: Object tile data not being loaded and drawn
- Editor Integration: No working UI flow for object placement
- 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
-
Basic Object Loading
- Load room with various object types
- Verify all objects appear
- Check object positions match ZScream
-
Object Placement
- Place new objects in empty room
- Move existing objects
- Delete objects
- Verify changes persist
-
Layer Management
- Place objects on different layers
- Toggle layer visibility
- Verify layer separation
-
Save/Load
- Edit multiple rooms
- Save changes
- Reload ROM
- Verify all changes present
-
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/savingRoom_Object.cs- Object class with encoding/decodingDungeonObjectData.cs- Object tile data tablesGFX.cs- Graphics management
Yaze (Implementation)
src/app/zelda3/dungeon/room.h/cc- Room classsrc/app/zelda3/dungeon/room_object.h/cc- RoomObject classsrc/app/zelda3/dungeon/object_parser.h/cc- Object parsingsrc/app/zelda3/dungeon/object_renderer.h/cc- Object renderingsrc/app/editor/dungeon/dungeon_editor.h/cc- Main editorsrc/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
- 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
- Start with Task 1.1 - Implement object encoding/decoding
- Create unit tests - Test all three object types
- Verify with ZScream - Compare results with known working implementation
- 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