refactor: Replace deprecated dungeon editor guide with updated documentation

- Deleted the old D1-dungeon-editor-guide.md and F1-dungeon-editor-guide.md files to streamline documentation.
- Introduced a new DUNGEON_EDITOR_GUIDE.md that consolidates features, architecture, and usage instructions for the Dungeon Editor.
- Updated the development guide to include new naming conventions and clarified rendering processes.
- Enhanced the overall structure and content of the documentation to reflect the current production-ready status of the Dungeon Editor.
This commit is contained in:
scawful
2025-10-09 20:48:07 -04:00
parent 6c7f301177
commit c512dd7f35
11 changed files with 1026 additions and 3358 deletions

View File

@@ -1,897 +0,0 @@
# Dungeon Editor Complete Guide
**Last Updated**: October 9, 2025
**Status**: EXPERIMENTAL - Core features implemented but requires thorough testing
---
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Implemented Features](#implemented-features)
- [Technical Details](#technical-details)
- [Usage Guide](#usage-guide)
- [Troubleshooting](#troubleshooting)
- [Next Steps](#next-steps)
- [Reference](#reference)
---
## Overview
The Dungeon Editor uses a modular card-based architecture for editing dungeon rooms in The Legend of Zelda: A Link to the Past.
**WARNING**: This editor is currently experimental. While core features are implemented, thorough testing is still required before production use.
### Key Capabilities
- Visual room editing with 512x512 canvas
- Object placement with pattern-based rendering
- Live palette editing with instant preview
- Independent dockable UI cards
- Multi-room editing support
- Automatic graphics loading
- Error recovery system
---
## Architecture
### Component Hierarchy
```
DungeonEditorV2 (Coordinator)
├── Toolbar (Toolset)
│ ├── Open Room
│ ├── Toggle Rooms List
│ ├── Toggle Room Matrix
│ ├── Toggle Entrances List
│ ├── Toggle Room Graphics
│ ├── Toggle Object Editor
│ └── Toggle Palette Editor
├── Independent Cards (all dockable)
│ ├── Rooms List Card
│ ├── Entrances List Card
│ ├── Room Matrix Card (16x19 grid)
│ ├── Room Graphics Card
│ ├── Object Editor Card
│ ├── Palette Editor Card
│ └── Room Cards (dynamic, auto-dock together)
└── Per-Room Rendering
└── Room
├── bg1_buffer_ (BackgroundBuffer)
├── bg2_buffer_ (BackgroundBuffer)
└── DungeonCanvasViewer
```
### Independent Card Architecture
**Key Principle**: Each card is a top-level ImGui window with NO table layout or window hierarchy inheritance.
```cpp
// Each card is completely independent
void DungeonEditorV2::DrawLayout() {
// Room Selector (persistent)
{
static bool show = true;
gui::EditorCard card("Room Selector", ICON_MD_LIST, &show);
if (card.Begin()) {
room_selector_.Draw();
}
card.End();
}
// Room Cards (closable, auto-dock)
for (int room_id : active_rooms_) {
bool open = true;
gui::EditorCard card(MakeCardTitle(room_id), ICON_MD_GRID_ON, &open);
if (card.Begin()) {
DrawRoomTab(room_id);
}
card.End();
if (!open) RemoveRoom(room_id);
}
}
```
**Benefits**:
- ✅ Full freedom to drag, dock, resize
- ✅ No layout constraints or inheritance
- ✅ Can be arranged however user wants
- ✅ Session-aware card titles
- ✅ ImGui handles all docking logic
---
## Implemented Features
### 1. Rooms List Card
```cpp
Features:
- Filter/search functionality
- Format: [HEX_ID] Room Name
- Click to open room card
- Double-click for instant focus
- Shows all 296 rooms (0x00-0x127)
```
### 2. Entrances List Card (ZScream Parity)
```cpp
Configuration UI:
- Entrance ID, Room ID, Dungeon ID
- Blockset, Music, Floor
- Player Position (X, Y)
- Camera Trigger (X, Y)
- Scroll Position (X, Y)
- Exit value
- Camera Boundaries (quadrant & full room)
List Features:
- Format: [HEX_ID] Entrance Name -> Room Name
- Shows entrance-to-room relationship
- Click to select and open associated room
```
### 3. Room Matrix Card (16x19 Grid)
```cpp
Layout:
- 16 columns × 19 rows = 304 cells
- Displays all 296 rooms (0x00-0x127)
- 24px cells with 1px spacing (optimized)
- Window size: 440x520
Visual Features:
- Instant loading with deterministic HSV colors
- Color calculated from room ID (no palette loading)
- Light green outline: Currently selected room
- Green outline: Open rooms
- Gray outline: Inactive rooms
Interaction:
- Click to open room card
- Hover for tooltip (room name)
- Auto-focuses existing cards
```
**Performance**:
- Before: 2-4 seconds (lazy loading 296 rooms)
- After: < 50ms (pure math, no I/O)
### 4. Room Graphics Card
```cpp
Features:
- Shows blockset graphics for selected room
- 2-column grid layout
- Auto-loads when room changes
- Up to 16 graphics blocks
- Toggleable via toolbar
```
### 5. Object Editor Card (Unified)
```cpp
Improved UX:
- Mode controls at top: None | Place | Select | Delete
- Current object info always visible
- 2 tabs:
- Browser: Object selection with previews
- Preview: Emulator rendering with controls
Object Browser:
- Categorized objects (Floor/Wall/Special)
- 32x32 preview icons
- Filter/search functionality
- Shows object ID and type
```
### 6. Palette Editor Card
```cpp
Features:
- Palette selector dropdown (20 dungeon palettes)
- 90-color grid (15 per row)
- Visual selection with yellow border
- Tooltips: color index, SNES BGR555, RGB values
- HSV color wheel picker
- Live RGB display (0-255)
- SNES format display (15-bit BGR555)
- Reset button
Live Updates:
- Edit palette all open rooms re-render automatically
- Callback system decouples palette editor from rooms
```
### 7. Room Cards (Auto-Loading)
```cpp
Improvements:
- Auto-loads graphics when properties change
- Simple status indicator ( Loaded / Not Loaded)
- Auto-saves with main Save command
- Removed manual "Load Graphics" buttons
Docking Behavior:
- ImGuiWindowClass for automatic tab grouping
- New room cards auto-dock with existing rooms
- Can be undocked independently
- Maintains session state
```
### 8. Object Drawing System
```cpp
ObjectDrawer (Native C++ Rendering):
- Pattern-based tile placement
- Fast, no emulation overhead
- Centralized pattern logic
Supported Patterns:
- 1x1 Solid (0x34)
- Rightward 2x2 (0x00-0x08) - horizontal walls
- Downward 2x2 (0x60-0x68) - vertical walls
- Diagonal Acute (0x09-0x14) - / walls
- Diagonal Grave (0x15-0x20) - \ walls
- 4x4 Blocks (0x33, 0x70-0x71) - large structures
Integration:
// Simplified from 100+ lines to 3 lines
ObjectDrawer drawer(rom_);
drawer.DrawObjectList(tile_objects_, bg1_buffer_, bg2_buffer_);
```
### 9. Cross-Editor Navigation
```cpp
From Overworld Editor:
editor_manager->JumpToDungeonRoom(room_id);
From Dungeon Editor:
- Click in Rooms List opens/focuses room card
- Click in Entrances List opens associated room
- Click in Room Matrix opens/focuses room card
EditorCard Focus System:
- Focus() method brings window to front
- Works with docked and floating windows
- Avoids duplicate cards
```
### 10. Error Handling & Recovery
```cpp
Custom ImGui Assertion Handler:
- Catches UI assertion failures
- Logs errors instead of crashing
- After 5 errors:
1. Backs up imgui.ini imgui.ini.backup
2. Deletes imgui.ini (reset workspace)
3. Resets error counter
4. Application continues running
Benefits:
- No data loss from UI bugs
- Automatic recovery
- User-friendly error handling
```
---
## Technical Details
### Rendering Pipeline
```
1. Room::CopyRoomGraphicsToBuffer()
→ Loads tile graphics into current_gfx16_ [128×N indexed pixels]
2. BackgroundBuffer::DrawFloor()
→ Fills tilemap buffer with floor tile IDs
3. ObjectDrawer::DrawObjectList()
→ Writes wall/object tiles to BG1/BG2 buffers
4. BackgroundBuffer::DrawBackground()
→ For each tile in tilemap:
- Extract 8×8 pixels from gfx16_data
- Apply palette offset (palette_id * 8 for 3BPP)
- Copy to bitmap (512×512 indexed surface)
→ Sync: memcpy(surface->pixels, bitmap_data)
5. Bitmap::SetPalette()
→ Apply 90-color dungeon palette to SDL surface
6. Renderer::RenderBitmap()
→ Convert indexed surface → RGB texture
→ SDL_CreateTextureFromSurface() applies palette
7. DungeonCanvasViewer::RenderRoomBackgroundLayers()
→ Draw texture to canvas with ImGui::Image()
```
### SNES Graphics Format
**8-bit Indexed Color (3BPP for dungeons)**:
```cpp
// Each pixel is a palette index (0-7)
// RGB color comes from applying dungeon palette
bg1_bmp.SetPalette(dungeon_pal_group[palette_id]); // 90 colors
Renderer::Get().RenderBitmap(&bitmap); // indexed → RGB
```
**Color Format: 15-bit BGR555**
```
Bits: 0BBB BBGG GGGR RRRR
││││ ││││ ││││ ││││
│└──┴─┘└──┴─┘└──┴─┘
│ Blue Green Red
└─ Unused (always 0)
Each channel: 0-31 (5 bits)
Total colors: 32,768 (2^15)
```
**Palette Organization**:
- 20 total palettes (one per dungeon color scheme)
- 90 colors per palette (full SNES BG palette)
- ROM address: `kDungeonMainPalettes` (0xDD734)
### Critical Math Formulas
**Tile Position in Tilesheet (128px wide)**:
```cpp
int tile_x = (tile_id % 16) * 8;
int tile_y = (tile_id / 16) * 8;
int pixel_offset = (tile_y * 128) + tile_x;
```
**Tile Position in Canvas (512×512)**:
```cpp
int canvas_x = (tile_col * 8);
int canvas_y = (tile_row * 8);
int pixel_offset = (canvas_y * 512) + canvas_x;
// CRITICAL: For NxN tiles, advance by (tile_row * 8 * width)
int dest_offset = (yy * 8 * 512) + (xx * 8); // NOT just (yy * 512)!
```
**Palette Index Calculation**:
```cpp
// 3BPP: 8 colors per subpalette
int final_index = pixel_value + (palette_id * 8);
// NOT 4BPP (× 16)!
// int final_index = pixel_value + (palette_id << 4); // WRONG
```
### Per-Room Buffers (Critical for Multi-Room Editing)
**Old way (broken)**: Multiple rooms shared `gfx::Arena::Get().bg1()` and corrupted each other.
**New way (fixed)**: Each `Room` has its own buffers:
```cpp
// In room.h
gfx::BackgroundBuffer bg1_buffer_;
gfx::BackgroundBuffer bg2_buffer_;
// In room.cc
bg1_buffer_.DrawFloor(...);
bg1_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
Renderer::Get().RenderBitmap(&bg1_buffer_.bitmap());
```
### Color Format Conversions
```cpp
// ImGui → SNES BGR555
int r_snes = (int)(imgui_r * 31.0f); // 0-1 → 0-31
int g_snes = (int)(imgui_g * 31.0f);
int b_snes = (int)(imgui_b * 31.0f);
uint16_t bgr555 = (b_snes << 10) | (g_snes << 5) | r_snes;
// SNES BGR555 → RGB (for display)
uint8_t r_rgb = (snes & 0x1F) * 255 / 31; // 0-31 → 0-255
uint8_t g_rgb = ((snes >> 5) & 0x1F) * 255 / 31;
uint8_t b_rgb = ((snes >> 10) & 0x1F) * 255 / 31;
```
---
## Usage Guide
### Opening Rooms
1. **Rooms List**: Search and click room
2. **Entrances List**: Click entrance to open associated room
3. **Room Matrix**: Visual navigation with color-coded grid
4. **Toolbar**: Use "Open Room" button
### Editing Objects
1. Toggle **Object Editor** card
2. Select mode: **Place** / **Select** / **Delete**
3. Browse objects in **Browser** tab
4. Click object to select
5. Click on canvas to place
6. Use **Select** mode for multi-select (Ctrl+drag)
### Editing Palettes
1. Toggle **Palette Editor** card
2. Select palette from dropdown (0-19)
3. Click color in 90-color grid
4. Adjust with HSV color wheel
5. See live updates in all open room cards
6. Reset color if needed
### Configuring Entrances
1. Toggle **Entrances List** card
2. Select entrance from list
3. Edit properties in configuration UI
4. All changes auto-save
5. Click entrance to jump to associated room
### Managing Layout
- All cards are dockable
- Room cards automatically tab together
- Save layout via imgui.ini
- If errors occur, layout auto-resets with backup
---
## Troubleshooting
### Common Bugs & Fixes
#### Empty Palette (0 colors)
**Symptom**: Graphics render as solid color or invisible
**Cause**: Using `palette()` method (copy) instead of `operator[]` (reference)
```cpp
// WRONG:
auto pal = group.palette(id); // Copy, may be empty
// CORRECT:
auto pal = group[id]; // Reference
```
#### Bitmap Stretched/Corrupted
**Symptom**: Graphics only in top portion, repeated/stretched
**Cause**: Wrong offset in DrawBackground()
```cpp
// WRONG:
int offset = (yy * 512) + (xx * 8); // Only advances 512 per row
// CORRECT:
int offset = (yy * 8 * 512) + (xx * 8); // Advances 4096 per row
```
#### Black Canvas Despite Correct Data
**Symptom**: current_gfx16_ has data, palette loaded, but canvas black
**Cause**: Bitmap not synced to SDL surface
```cpp
// FIX: After DrawTile() loop
SDL_LockSurface(surface);
memcpy(surface->pixels, bitmap_data.data(), bitmap_data.size());
SDL_UnlockSurface(surface);
```
#### Wrong Colors
**Symptom**: Colors don't match expected palette
**Cause**: Using 4BPP offset for 3BPP graphics
```cpp
// WRONG (4BPP):
int offset = palette_id << 4; // × 16
// CORRECT (3BPP):
int offset = palette_id * 8; // × 8
```
#### Wrong Bitmap Depth
**Symptom**: Room graphics corrupted, only showing in small portion
**Cause**: Depth parameter wrong in CreateAndRenderBitmap
```cpp
// WRONG:
CreateAndRenderBitmap(0x200, 0x200, 0x200, data, bitmap, palette);
// ^^^^^ depth should be 8!
// CORRECT:
CreateAndRenderBitmap(0x200, 0x200, 8, data, bitmap, palette);
// ^ 8-bit indexed
```
#### Emulator Preview "ROM Not Loaded"
**Symptom**: Preview shows error despite ROM being loaded
**Cause**: Emulator initialized before ROM loaded
```cpp
// WRONG (in Initialize()):
object_emulator_preview_.Initialize(rom_); // Too early!
// CORRECT (in Load()):
if (!rom_ || !rom_->is_loaded()) return error;
object_emulator_preview_.Initialize(rom_); // After ROM confirmed
```
### Debugging Tips
If objects don't appear:
1. **Check console output**:
```
[ObjectDrawer] Drawing object $34 at (16,16)
[DungeonCanvas] Rendered BG1/BG2 to canvas
```
2. **Verify tiles loaded**:
- Object must have tiles (`EnsureTilesLoaded()`)
- Check `object.tiles().empty()`
3. **Check buffer writes**:
- Add logging in `WriteTile16()`
- Verify `IsValidTilePosition()` isn't rejecting writes
4. **Verify buffer rendering**:
- Check `RenderRoomBackgroundLayers()` renders BG1/BG2
- May need `bg1.DrawBackground(gfx16_data)` after writing
### Floor & Wall Rendering Debug Guide
**What Should Happen**:
1. `DrawFloor()` writes 4096 floor tile IDs to the buffer
2. `DrawBackground()` renders those tiles + any objects to the bitmap
3. Bitmap gets palette applied
4. SDL texture created from bitmap
**Debug Output to Check**:
When you open a room and load graphics, check console for:
```
[BG:DrawFloor] tile_address=0xXXXX, tile_address_floor=0xXXXX, floor_graphics=0xXX, f=0xXX
[BG:DrawFloor] Floor tile words: XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX
[BG:DrawFloor] Wrote 4096 floor tiles to buffer
[BG:DrawBackground] Using existing bitmap (preserving floor)
[BG:DrawBackground] gfx16_data size=XXXXX, first 32 bytes: XX XX XX XX...
```
**Common Floor/Wall Issues**:
#### Issue 1: Floor tiles are all 0x0000
**Symptom**: `Floor tile words: 0000 0000 0000 0000 0000 0000 0000 0000`
**Cause**: `tile_address` or `tile_address_floor` is wrong, or `floor_graphics` is wrong
**Fix**: Check that room data loaded correctly
#### Issue 2: gfx16_data is all 0x00
**Symptom**: `gfx16_data size=16384, first 32 bytes: 00 00 00 00 00 00 00 00...`
**Cause**: `CopyRoomGraphicsToBuffer()` failed to load graphics
**Fix**: Check `current_gfx16_` is populated in Room
#### Issue 3: "Creating new bitmap" instead of "Using existing bitmap"
**Symptom**: DrawBackground creates a new zero-filled bitmap
**Cause**: Bitmap wasn't active when DrawBackground was called
**Fix**: DrawBackground now checks `bitmap_.is_active()` before recreating
#### Issue 4: Buffer has tiles but bitmap is empty
**Symptom**: DrawFloor reports writing tiles, but canvas shows nothing
**Cause**: DrawBackground isn't actually rendering the buffer's tiles
**Fix**: Check that `buffer_[xx + yy * tiles_w]` has non-zero values
**Rendering Flow Diagram**:
```
Room::RenderRoomGraphics()
├─1. CopyRoomGraphicsToBuffer()
│ └─> Fills current_gfx16_[16384] with tile pixel data (3BPP)
├─2. bg1_buffer_.DrawFloor()
│ └─> Writes floor tile IDs to buffer_[4096]
│ └─> Example: buffer_[0] = 0x00EE (tile 238, palette 0)
├─3. RenderObjectsToBackground()
│ └─> ObjectDrawer writes wall/object tile IDs to buffer_[]
│ └─> Example: buffer_[100] = 0x0060 (wall tile, palette 0)
├─4. bg1_buffer_.DrawBackground(current_gfx16_)
│ └─> For each tile ID in buffer_[]:
│ ├─> Extract tile_id, palette from word
│ ├─> Read 8x8 pixels from current_gfx16_[128-pixel-wide sheet]
│ ├─> Apply palette offset (palette * 8 for 3BPP)
│ └─> Write to bitmap_.data()[512x512]
├─5. bitmap_.SetPalette(dungeon_palette[90 colors])
│ └─> Applies SNES BGR555 colors to SDL surface palette
└─6. Renderer::RenderBitmap(&bitmap_)
└─> Creates SDL_Texture from indexed surface + palette
└─> Result: RGB texture ready to display
```
**Expected Console Output (Working)**:
```
[BG:DrawFloor] tile_address=0x4D62, tile_address_floor=0x4D6A, floor_graphics=0x00, f=0x00
[BG:DrawFloor] Floor tile words: 00EE 00EF 01EE 01EF 02EE 02EF 03EE 03EF
[BG:DrawFloor] Wrote 4096 floor tiles to buffer
[ObjectDrawer] Drew 73 objects, skipped 0
[BG:DrawBackground] Using existing bitmap (preserving floor)
[BG:DrawBackground] gfx16_data size=16384, first 32 bytes: 3A 3A 3A 4C 4C 3A 3A 55 55 3A 3A 55 55 3A 3A...
```
✅ Floor tiles written ✅ Objects drawn ✅ Graphics data present ✅ Bitmap preserved
**Quick Diagnostic Tests**:
1. **Run App & Check Console**:
- Open Dungeon Editor
- Select a room (try room $00, $02, or $08)
- Load graphics
- Check console output
2. **Good Signs**:
- `[BG:DrawFloor] Wrote 4096 floor tiles` ← Floor data written
- `Floor tile words: 00EE 00EF...` ← Non-zero tile IDs
- `gfx16_data size=16384, first 32 bytes: 3A 3A...` ← Graphics data present
- `[ObjectDrawer] Drew XX objects` ← Objects rendered
3. **Bad Signs**:
- `Floor tile words: 0000 0000 0000 0000...` ← No floor tiles!
- `gfx16_data size=16384, first 32 bytes: 00 00 00...` ← No graphics!
- `[BG:DrawBackground] Creating new bitmap` ← Wiping out floor!
**If Console Shows Everything Working But Canvas Still Empty**:
The issue is in **canvas rendering**, not floor/wall drawing. Check:
1. Is texture being created? (Check `Renderer::RenderBitmap`)
2. Is canvas displaying texture? (Check `DungeonCanvasViewer::DrawDungeonCanvas`)
3. Is texture pointer valid? (Check `bitmap.texture() != nullptr`)
**Quick Visual Test**:
If you see **pink/brown rectangles** but no floor/walls:
- ✅ Canvas IS rendering primitives
- ❌ Canvas is NOT rendering the bitmap texture
This suggests the bitmap texture is either:
1. Not being created
2. Being created but not displayed
3. Being created with wrong data
---
## Next Steps
### Remaining Issues
#### Issue 1: Room Layout Not Rendering (COMPLETED ✅)
- **Solution**: ObjectDrawer integration complete
- Walls and floors now render properly
#### Issue 2: Entity Interaction
**Problem**: Can't click/drag dungeon entities like overworld
**Reference**: `overworld_entity_renderer.cc` lines 23-91
**Implementation Needed**:
```cpp
// 1. Detect hover
bool IsMouseHoveringOverEntity(const Entity& entity, canvas_p0, scrolling);
// 2. Handle dragging
void HandleEntityDragging(Entity* entity, ...);
// 3. Double-click to open
if (IsMouseHoveringOverEntity(entity) && IsMouseDoubleClicked()) {
// Open entity editor
}
// 4. Right-click for context menu
if (IsMouseHoveringOverEntity(entity) && IsMouseClicked(Right)) {
ImGui::OpenPopup("Entity Editor");
}
```
**Files to Create/Update**:
- `dungeon_entity_interaction.h/cc` (new)
- `dungeon_canvas_viewer.cc` (integrate)
#### Issue 3: Multi-Select for Objects
**Problem**: No group selection/movement
**Status**: Partially implemented in `DungeonObjectInteraction`
**What's Missing**:
1. Multi-object drag support
2. Group movement logic
3. Delete multiple objects
4. Copy/paste groups
**Implementation**:
```cpp
void MoveSelectedObjects(ImVec2 delta) {
for (size_t idx : selected_object_indices_) {
auto& obj = room.GetTileObjects()[idx];
obj.x_ += delta.x;
obj.y_ += delta.y;
}
}
```
#### Issue 4: Context Menu Not Dungeon-Aware
**Problem**: Generic canvas context menu
**Solution**: Use `Canvas::AddContextMenuItem()`:
```cpp
// Setup before DrawContextMenu()
canvas_.ClearContextMenuItems();
if (!selected_objects.empty()) {
canvas_.AddContextMenuItem({
ICON_MD_DELETE " Delete Selected",
[this]() { DeleteSelectedObjects(); },
"Del"
});
}
canvas_.AddContextMenuItem({
ICON_MD_ADD " Place Object",
[this]() { ShowObjectPlacementMenu(); }
});
```
### Future Enhancements
1. **Object Preview Thumbnails**
- Replace "?" placeholders with rendered thumbnails
- Use ObjectRenderer for 64×64 bitmaps
- Cache for performance
2. **Room Matrix Bitmap Preview**
- Hover shows actual room bitmap
- Small popup with preview
- Rendered on-demand
3. **Palette Presets**
- Save/load favorite combinations
- Import/export between dungeons
- Undo/redo for palette changes
4. **Custom Object Editor**
- Let users create new patterns
- Visual pattern editor
- Save to ROM
5. **Complete Object Coverage**
- All 256+ object types
- Special objects (stairs, chests, doors)
- Layer 3 effects
---
## Reference
### File Organization
**Core Rendering**:
- `src/app/zelda3/dungeon/room.{h,cc}` - Room state, buffers
- `src/app/gfx/background_buffer.{h,cc}` - Tile → bitmap drawing
- `src/app/core/renderer.cc` - Bitmap → texture conversion
- `src/app/editor/dungeon/dungeon_canvas_viewer.cc` - Canvas display
**Object Drawing**:
- `src/app/zelda3/dungeon/object_drawer.{h,cc}` - Native C++ patterns
- `src/app/gui/widgets/dungeon_object_emulator_preview.{h,cc}` - Research tool
**Editor UI**:
- `src/app/editor/dungeon/dungeon_editor_v2.{h,cc}` - Main coordinator
- `src/app/gui/widgets/editor_card.{h,cc}` - Independent card system
- `src/app/editor/dungeon/dungeon_object_interaction.{h,cc}` - Object selection
**Palette System**:
- `src/app/gfx/snes_palette.{h,cc}` - Palette loading
- `src/app/gui/widgets/palette_editor_widget.{h,cc}` - Visual editor
- `src/app/gfx/bitmap.cc` - SetPalette() implementation
### Quick Reference: Key Functions
```cpp
// Load dungeon palette
auto& pal_group = rom->palette_group().dungeon_main;
auto palette = pal_group[palette_id]; // NOT .palette(id)!
// Apply palette to bitmap
bitmap.SetPalette(palette); // NOT SetPaletteWithTransparent()!
// Create texture from indexed bitmap
Renderer::Get().RenderBitmap(&bitmap); // NOT UpdateBitmap()!
// Tile sheet offset (128px wide)
int offset = (tile_id / 16) * 8 * 128 + (tile_id % 16) * 8;
// Canvas offset (512px wide)
int offset = (tile_row * 8 * 512) + (tile_col * 8);
// Palette offset (3BPP)
int offset = palette_id * 8; // NOT << 4 !
```
### Performance Metrics
**Matrix Loading**:
- Load time: < 50ms (pure calculation, no I/O)
- Memory allocations: ~20 per matrix draw (cached colors)
- Frame drops: None
**Room Loading**:
- Lazy loading: Rooms loaded on-demand
- Graphics caching: Reused across room switches
- Texture batching: Up to 8 textures processed per frame
---
## Status Summary
### Implemented Features
**Rendering**:
- Floor rendering with tile graphics and palettes
- Object drawing via ObjectDrawer with pattern-based rendering
- Live palette editing with HSV picker
- Per-room background buffers (no shared state corruption)
**UI**:
- Independent dockable cards
- Room matrix for visual navigation
- Entrance configuration
- Cross-editor navigation (jump between overworld/dungeon)
- Error recovery system
### In Progress
**Interaction**:
- Entity click/drag for sprites and objects
- Multi-select drag for group movement
- Context-aware right-click menu
**Enhancement**:
- Object thumbnails in selector
- Room layout visual editor
- Auto-tile placement
- Object snapping grid
---
## Build Instructions
```bash
cd /Users/scawful/Code/yaze
cmake --build build_ai --target yaze -j12
./build_ai/bin/yaze.app/Contents/MacOS/yaze
```
---
**Status**: EXPERIMENTAL
The dungeon editor provides core editing capabilities but requires thorough testing before production use. Users should save backups before editing ROMs.
### Critical Rendering Pipeline Details
#### Bitmap Data Synchronization
When updating bitmap pixel data, two memory locations must stay synchronized:
1. `data_` - C++ std::vector<uint8_t>
2. `surface_->pixels` - SDL raw pixel buffer used for texture creation
**Always use**:
- `set_data()` for bulk updates (updates both vector AND surface via memcpy)
- `WriteToPixel()` for single pixel changes
- **Never** assign directly to `mutable_data()` for replacement operations
#### Texture Update Queue
Texture operations are queued and processed in batches for performance:
```cpp
// Queue texture operation
gfx::Arena::Get().QueueTextureCommand(
gfx::Arena::TextureCommandType::UPDATE, &bitmap);
// Process queue every frame (required!)
gfx::Arena::Get().ProcessTextureQueue(renderer_);
```
#### Graphics Sheet System
All 223 graphics sheets are managed centrally by `gfx::Arena`. When one editor modifies a sheet, use `Arena::NotifySheetModified(sheet_index)` to propagate changes to all editors.

View File

@@ -0,0 +1,578 @@
# YAZE Dungeon Editor: Complete Guide
**Last Updated**: October 9, 2025
**Status**: PRODUCTION READY - Core features stable, tested, and functional
---
## Table of Contents
- [Overview](#overview)
- [Current Status](#current-status)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Core Features](#core-features)
- [Technical Details](#technical-details)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [ROM Internals](#rom-internals)
- [Reference](#reference)
---
## Overview
The Dungeon Editor uses a modern card-based architecture for editing dungeon rooms in The Legend of Zelda: A Link to the Past. The editor features lazy loading, per-room settings, and a component-based design for maximum flexibility.
### Key Capabilities
- **Visual room editing** with 512x512 canvas
- **Object placement** with pattern-based rendering
- **Live palette editing** with instant preview
- **Independent dockable UI cards**
- **Multi-room editing** support
- **Automatic graphics loading**
- **Per-room layer visibility** settings
- **Command-line quick testing** support
---
## Current Status
### ✅ Production Ready Features
- Core rendering pipeline (floor, walls, objects, sprites)
- Object drawing via ObjectDrawer with pattern-based rendering
- Live palette editing with HSV picker
- Per-room background buffers (no shared state corruption)
- Independent dockable card system
- Cross-editor navigation (overworld ↔ dungeon)
- Error recovery system
- Test suite (29/29 tests passing - 100%)
### 🔧 Recently Fixed Issues
1. **Object Visibility** ✅ FIXED
- **Problem**: Objects drawn to bitmaps but not visible on canvas
- **Root Cause**: Textures not updated after `RenderObjectsToBackground()`
- **Fix**: Added texture UPDATE commands after object rendering
2. **Property Change Re-rendering** ✅ FIXED
- **Problem**: Changing blockset/palette didn't trigger re-render
- **Fix**: Added change detection and automatic re-rendering
3. **One-Time Rendering** ✅ FIXED
- **Problem**: Objects only rendered once, never updated
- **Fix**: Removed restrictive rendering checks
4. **Per-Room Layer Settings** ✅ IMPLEMENTED
- Each room now has independent BG1/BG2 visibility settings
- Layer type controls (Normal, Translucent, Addition, Dark, Off)
5. **Canvas Context Menu** ✅ IMPLEMENTED
- Dungeon-specific options (Place Object, Delete Selected, Toggle Layers, Re-render)
- Dynamic menu based on current selection
---
## Architecture
### Component Hierarchy
```
DungeonEditorV2 (Coordinator)
├── Dungeon Controls (Collapsible panel)
│ └── Card visibility toggles
├── Independent Cards (all fully dockable)
│ ├── Rooms List Card (filterable, searchable)
│ ├── Room Matrix Card (16x19 grid, 296 rooms)
│ ├── Entrances List Card (entrance configuration)
│ ├── Room Graphics Card (blockset graphics display)
│ ├── Object Editor Card (unified object placement)
│ ├── Palette Editor Card (90-color palette editing)
│ └── Room Cards (dynamic, auto-dock together)
└── Per-Room Rendering
└── Room
├── bg1_buffer_ (BackgroundBuffer)
├── bg2_buffer_ (BackgroundBuffer)
└── DungeonCanvasViewer
```
### Card-Based Architecture Benefits
- ✅ Full freedom to drag, dock, resize
- ✅ No layout constraints or inheritance
- ✅ Can be arranged however user wants
- ✅ Session-aware card titles
- ✅ ImGui handles all docking logic
- ✅ Independent lifetime (close Dungeon Controls, rooms stay open)
---
## Quick Start
### Launch from Command Line
```bash
# Open specific room
./yaze --rom_file=zelda3.sfc --editor=Dungeon --cards="Room 0"
# Compare multiple rooms
./yaze --rom_file=zelda3.sfc --editor=Dungeon --cards="Room 0,Room 1,Room 105"
# Full workspace
./yaze --rom_file=zelda3.sfc --editor=Dungeon \
--cards="Rooms List,Room Matrix,Object Editor,Palette Editor"
# Debug mode with logging
./yaze --debug --log_file=debug.log --rom_file=zelda3.sfc --editor=Dungeon
```
### From GUI
1. Launch YAZE
2. Load ROM (File → Open ROM or drag & drop)
3. Open Dungeon Editor (Tools → Dungeon Editor)
4. Toggle cards via "Dungeon Controls" checkboxes
5. Click room in list/matrix to open
---
## Core Features
### 1. Rooms List Card
```
Features:
- Filter/search functionality (ICON_MD_SEARCH)
- Format: [HEX_ID] Room Name
- Click to open room card
- Double-click for instant focus
- Shows all 296 rooms (0x00-0x127)
```
### 2. Room Matrix Card (Visual Navigation)
```
Layout:
- 16 columns × 19 rows = 304 cells
- Displays all 296 rooms (0x00-0x127)
- 24px cells with 1px spacing (optimized)
- Window size: 440x520
Visual Features:
- Deterministic HSV colors (no loading needed)
- Light green outline: Currently selected room
- Green outline: Open rooms
- Gray outline: Inactive rooms
Performance:
- Before: 2-4 seconds (lazy loading 296 rooms)
- After: < 50ms (pure math, no I/O)
```
### 3. Entrances List Card
```
Configuration UI (ZScream Parity):
- Entrance ID, Room ID, Dungeon ID
- Blockset, Music, Floor
- Player Position (X, Y)
- Camera Trigger (X, Y)
- Scroll Position (X, Y)
- Exit value
- Camera Boundaries (quadrant & full room)
List Features:
- Format: [HEX_ID] Entrance Name -> Room Name
- Shows entrance-to-room relationship
- Click to select and open associated room
```
### 4. Object Editor Card (Unified)
```
Improved UX:
- Mode controls at top: None | Place | Select | Delete
- Current object info always visible
- 2 tabs:
- Browser: Object selection with previews
- Preview: Emulator rendering with controls
Object Browser:
- Categorized objects (Floor/Wall/Special)
- 32x32 preview icons
- Filter/search functionality
- Shows object ID and type
```
### 5. Palette Editor Card
```
Features:
- Palette selector dropdown (20 dungeon palettes)
- 90-color grid (15 per row)
- Visual selection with yellow border
- Tooltips: color index, SNES BGR555, RGB values
- HSV color wheel picker
- Live RGB display (0-255)
- SNES format display (15-bit BGR555)
- Reset button
Live Updates:
- Edit palette → all open rooms re-render automatically
- Callback system decouples palette editor from rooms
```
### 6. Room Cards (Auto-Loading)
```
Features:
- Auto-loads graphics when properties change
- Simple status indicator (✓ Loaded / ⏳ Not Loaded)
- Auto-saves with main Save command
- Per-room layer controls (BG1/BG2 visibility, BG2 layer type)
Docking Behavior:
- ImGuiWindowClass for automatic tab grouping
- New room cards auto-dock with existing rooms
- Can be undocked independently
- Maintains session state
```
### 7. Canvas Context Menu (NEW)
```
Dungeon-Specific Options:
- Place Object (Ctrl+P)
- Delete Selected (Del) - conditional on selection
- Toggle BG1 (1)
- Toggle BG2 (2)
- Re-render Room (Ctrl+R)
Integration:
- Dynamic menu based on current state
- Consistent with overworld editor UX
```
---
## Technical Details
### Rendering Pipeline
```
1. Room::CopyRoomGraphicsToBuffer()
→ Loads tile graphics into current_gfx16_ [128×N indexed pixels]
2. BackgroundBuffer::DrawFloor()
→ Fills tilemap buffer with floor tile IDs
3. BackgroundBuffer::DrawBackground()
→ Renders floor tiles to bitmap (512×512 indexed surface)
4. Room::SetPalette()
→ Apply 90-color dungeon palette to SDL surface
5. Room::RenderObjectsToBackground()
→ ObjectDrawer writes wall/object tiles to BG1/BG2 buffers
6. gfx::Arena::QueueTextureCommand(UPDATE, &bitmap)
→ CRITICAL: Update textures after object rendering
7. gfx::Arena::ProcessTextureQueue(renderer)
→ Process queued texture operations
8. DungeonCanvasViewer::DrawRoomBackgroundLayers()
→ Draw textures to canvas with ImGui::Image()
```
### Critical Fix: Texture Update After Object Rendering
**Problem**: Objects were drawn to bitmaps but textures were never updated.
**Solution** (in `room.cc`):
```cpp
void Room::RenderRoomGraphics() {
// 1. Draw floor and background
bg1_buffer_.DrawFloor(...);
bg1_buffer_.DrawBackground(...);
// 2. Apply palette and create initial textures
bg1_bmp.SetPalette(bg1_palette);
gfx::Arena::Get().QueueTextureCommand(CREATE, &bg1_bmp);
// 3. Render objects to bitmaps
RenderObjectsToBackground();
// 4. CRITICAL FIX: Update textures with new bitmap data
gfx::Arena::Get().QueueTextureCommand(UPDATE, &bg1_bmp);
gfx::Arena::Get().QueueTextureCommand(UPDATE, &bg2_bmp);
}
```
### SNES Graphics Format
**8-bit Indexed Color (3BPP for dungeons)**:
```cpp
// Each pixel is a palette index (0-7)
// RGB color comes from applying dungeon palette
bg1_bmp.SetPalette(dungeon_pal_group[palette_id]); // 90 colors
Renderer::Get().RenderBitmap(&bitmap); // indexed → RGB
```
**Color Format: 15-bit BGR555**
```
Bits: 0BBB BBGG GGGR RRRR
││││ ││││ ││││ ││││
│└──┴─┘└──┴─┘└──┴─┘
│ Blue Green Red
└─ Unused (always 0)
Each channel: 0-31 (5 bits)
Total colors: 32,768 (2^15)
```
**Palette Organization**:
- 20 total palettes (one per dungeon color scheme)
- 90 colors per palette (full SNES BG palette)
- ROM address: `kDungeonMainPalettes` (0xDD734)
### Critical Math Formulas
**Tile Position in Tilesheet (128px wide)**:
```cpp
int tile_x = (tile_id % 16) * 8;
int tile_y = (tile_id / 16) * 8;
int pixel_offset = (tile_y * 128) + tile_x;
```
**Tile Position in Canvas (512×512)**:
```cpp
int canvas_x = (tile_col * 8);
int canvas_y = (tile_row * 8);
int pixel_offset = (canvas_y * 512) + canvas_x;
// CRITICAL: For NxN tiles, advance by (tile_row * 8 * width)
int dest_offset = (yy * 8 * 512) + (xx * 8); // NOT just (yy * 512)!
```
**Palette Index Calculation**:
```cpp
// 3BPP: 8 colors per subpalette
int final_index = pixel_value + (palette_id * 8);
// NOT 4BPP (× 16)!
// int final_index = pixel_value + (palette_id << 4); // WRONG
```
### Per-Room Buffers
**Problem**: Multiple rooms shared `gfx::Arena::Get().bg1()` and corrupted each other.
**Solution**: Each `Room` has its own buffers:
```cpp
// In room.h
gfx::BackgroundBuffer bg1_buffer_;
gfx::BackgroundBuffer bg2_buffer_;
// In room.cc
bg1_buffer_.DrawFloor(...);
bg1_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
Renderer::Get().RenderBitmap(&bg1_buffer_.bitmap());
```
---
## Testing
### Test Suite Status
| Test Type | Total | Passing | Pass Rate |
| ----------------- | ----- | ------- | --------- |
| **Unit Tests** | 14 | 14 | 100% ✅ |
| **Integration** | 14 | 14 | 100% ✅ |
| **E2E Tests** | 1 | 1 | 100% ✅ |
| **TOTAL** | **29**| **29** | **100%** ✅ |
### Running Tests
```bash
# Build tests (mac-ai preset)
cmake --preset mac-ai -B build_ai
cmake --build build_ai --target yaze_test
# Run all dungeon tests
./build_ai/bin/yaze_test --gtest_filter="*Dungeon*"
# Run E2E tests with GUI (normal speed)
./build_ai/bin/yaze_test --ui --show-gui --normal --gtest_filter="*DungeonEditorSmokeTest*"
# Run E2E tests in slow-motion (cinematic mode)
./build_ai/bin/yaze_test --ui --show-gui --cinematic --gtest_filter="*DungeonEditorSmokeTest*"
# Run all tests with fast execution
./build_ai/bin/yaze_test --ui --fast
```
### Test Speed Modes (NEW)
```bash
--fast # Run tests as fast as possible (teleport mouse, skip delays)
--normal # Run tests at human watchable speed (for debugging)
--cinematic # Run tests in slow-motion with pauses (for demos/tutorials)
```
---
## Troubleshooting
### Common Issues & Fixes
#### Issue 1: Objects Not Visible
**Symptom**: Floor/walls render but objects invisible
**Fix**: ✅ RESOLVED - Texture update after object rendering now working
#### Issue 2: Wrong Colors
**Symptom**: Colors don't match expected palette
**Fix**: Use `SetPalette()` not `SetPaletteWithTransparent()` for dungeons
**Reason**:
```cpp
// WRONG (extracts only 8 colors):
bitmap.SetPaletteWithTransparent(palette);
// CORRECT (applies full 90-color palette):
bitmap.SetPalette(palette);
```
#### Issue 3: Bitmap Stretched/Corrupted
**Symptom**: Graphics only in top portion, repeated/stretched
**Fix**: Wrong offset in DrawBackground()
```cpp
// WRONG:
int offset = (yy * 512) + (xx * 8); // Only advances 512 per row
// CORRECT:
int offset = (yy * 8 * 512) + (xx * 8); // Advances 4096 per row
```
#### Issue 4: Room Properties Don't Update
**Symptom**: Changing blockset/palette has no effect
**Fix**: ✅ RESOLVED - Property change detection now working
---
## ROM Internals
### Object Encoding
Dungeon objects are stored in one of three formats:
#### Type 1: Standard Objects (ID 0x00-0xFF)
```
Format: xxxxxxss yyyyyyss iiiiiiii
Use: Common geometry like walls and floors
```
#### Type 2: Large Coordinate Objects (ID 0x100-0x1FF)
```
Format: 111111xx xxxxyyyy yyiiiiii
Use: More complex or interactive structures
```
#### Type 3: Special Objects (ID 0x200-0x27F)
```
Format: xxxxxxii yyyyyyii 11111iii
Use: Critical gameplay elements (chests, switches, bosses)
```
### Core Data Tables in ROM
- **`bank_01.asm`**:
- **`DrawObjects` (0x018000)**: Master tables mapping object ID → drawing routine
- **`LoadAndBuildRoom` (0x01873A)**: Primary routine that reads and draws a room
- **`rooms.asm`**:
- **`RoomData_ObjectDataPointers` (0x1F8000)**: Table of 3-byte pointers to object data for each of 296 rooms
### Key ROM Addresses
```cpp
constexpr int dungeons_palettes = 0xDD734;
constexpr int room_object_pointer = 0x874C; // Long pointer
constexpr int kRoomHeaderPointer = 0xB5DD; // LONG
constexpr int tile_address = 0x001B52;
constexpr int tile_address_floor = 0x001B5A;
constexpr int torch_data = 0x2736A;
constexpr int blocks_pointer1 = 0x15AFA;
constexpr int pit_pointer = 0x394AB;
constexpr int doorPointers = 0xF83C0;
```
---
## Reference
### File Organization
**Core Rendering**:
- `src/app/zelda3/dungeon/room.{h,cc}` - Room state, buffers
- `src/app/gfx/background_buffer.{h,cc}` - Tile → bitmap drawing
- `src/app/core/renderer.cc` - Bitmap → texture conversion
- `src/app/editor/dungeon/dungeon_canvas_viewer.cc` - Canvas display
**Object Drawing**:
- `src/app/zelda3/dungeon/object_drawer.{h,cc}` - Native C++ patterns
- `src/app/gui/widgets/dungeon_object_emulator_preview.{h,cc}` - Research tool
**Editor UI**:
- `src/app/editor/dungeon/dungeon_editor_v2.{h,cc}` - Main coordinator
- `src/app/gui/widgets/editor_card.{h,cc}` - Independent card system
- `src/app/editor/dungeon/dungeon_object_interaction.{h,cc}` - Object selection
**Palette System**:
- `src/app/gfx/snes_palette.{h,cc}` - Palette loading
- `src/app/gui/widgets/palette_editor_widget.{h,cc}` - Visual editor
- `src/app/gfx/bitmap.cc` - SetPalette() implementation
### Quick Reference: Key Functions
```cpp
// Load dungeon palette
auto& pal_group = rom->palette_group().dungeon_main;
auto palette = pal_group[palette_id]; // NOT .palette(id)!
// Apply palette to bitmap
bitmap.SetPalette(palette); // NOT SetPaletteWithTransparent()!
// Create texture from indexed bitmap
Renderer::Get().RenderBitmap(&bitmap); // NOT UpdateBitmap()!
// Tile sheet offset (128px wide)
int offset = (tile_id / 16) * 8 * 128 + (tile_id % 16) * 8;
// Canvas offset (512px wide)
int offset = (tile_row * 8 * 512) + (tile_col * 8);
// Palette offset (3BPP)
int offset = palette_id * 8; // NOT << 4 !
```
### Performance Metrics
**Matrix Loading**:
- Load time: < 50ms (pure calculation, no I/O)
- Memory allocations: ~20 per matrix draw (cached colors)
- Frame drops: None
**Room Loading**:
- Lazy loading: Rooms loaded on-demand
- Graphics caching: Reused across room switches
- Texture batching: Up to 8 textures processed per frame
---
## Summary
The Dungeon Editor is production-ready with all core features implemented and tested. Recent fixes ensure objects render correctly, property changes trigger re-renders, and the context menu provides dungeon-specific functionality. The card-based architecture provides maximum flexibility while maintaining stability.
### Critical Points
1. **Texture Update**: Always call UPDATE after modifying bitmap data
2. **Per-Room Buffers**: Each room has independent bg1/bg2 buffers
3. **Property Changes**: Automatically detected and trigger re-renders
4. **Palette Format**: Use SetPalette() for full 90-color dungeon palettes
5. **Context Menu**: Dungeon-specific options available via right-click
---
**For detailed debugging**: See `QUICK-DEBUG-REFERENCE.txt` for command-line shortcuts.

View File

@@ -168,3 +168,9 @@ Default palettes are applied during ROM loading based on sheet index:
- Sheets 0-112: Dungeon main palettes
- Sheets 113-127: Sprite palettes
- Sheets 128-222: HUD/menu palettes
### Naming Conventions
- Load: Reading data from ROM into memory
- Render: Processing graphics data into bitmaps/textures (CPU pixel operations)
- Draw: Displaying textures/shapes on canvas via ImGui (GPU rendering)
- Update: UI state changes, property updates, input handling

View File

@@ -1,585 +0,0 @@
# Yaze Dungeon Editor: Master Guide
**Last Updated**: October 4, 2025
This document provides a comprehensive, up-to-date overview of the Yaze Dungeon Editor, consolidating all recent design plans, testing results, and critical fixes. It serves as the single source of truth for the editor's architecture, data structures, and future development.
## 1. Current Status: Production Ready
After a significant refactoring and bug-fixing effort, the Dungeon Editor's core functionality is **complete and stable**.
- **Core Logic**: The most complex features, including the 3-type object encoding/decoding system and saving objects back to the ROM, are **fully implemented**.
- **Testing**: The test suite is now **100% stable**, with all 29 unit, integration, and E2E tests passing. Critical `SIGBUS` and `SIGSEGV` crashes have been resolved by replacing the unstable `MockRom` with a real ROM file for testing.
- **Rendering**: The rendering pipeline is verified, correct, and performant, properly using the graphics arena and a component-based architecture.
- **Coordinate System**: A critical object positioning bug has been fixed, ensuring all objects render in their correct locations.
### Known Issues & Next Steps
While the core is stable, several UI and performance items remain:
1. **UI Polish (High Priority)**:
* Implement human-readable labels for rooms and entrances in selection lists.
* Add tab management features (`+` to add, `x` to close) for a better multi-room workflow.
2. **Performance (Medium Priority)**:
* Address the slow initial load time (~2.6 seconds) by implementing lazy loading for rooms.
3. **Palette System (Medium Priority)**:
* Fix the handling of palette IDs greater than 19 to prevent fallbacks to palette 0.
## 2. Architecture: A Component-Based Design
The editor was refactored into a modern, component-based architecture, reducing the main editor's complexity by **79%**. The `DungeonEditorV2` class now acts as a thin coordinator, delegating all work to specialized components.
### Core Components
- **`DungeonEditorV2`**: The main orchestrator. Manages the 3-column layout and coordinates the other components.
- **`DungeonRoomLoader`**: Handles all data loading from the ROM, now optimized with parallel processing.
- **`DungeonRoomSelector`**: Manages the UI for selecting rooms and entrances.
- **`DungeonCanvasViewer`**: Responsible for rendering the room, objects, and sprites onto the main canvas.
- **`DungeonObjectSelector`**: Provides the UI for browsing and selecting objects, sprites, and other editable elements.
- **`DungeonObjectInteraction`**: Manages all mouse input, selection, and drag-and-drop on the canvas.
- **`ObjectRenderer`**: A high-performance system for rendering individual dungeon objects, featuring a graphics cache.
### Data Flow
1. **Load**: `DungeonEditorV2::Load()` calls `DungeonRoomLoader` to load all room data from the ROM.
2. **Update**: The editor's `Update()` method calls `Draw()` on each of the three main UI components (`RoomSelector`, `CanvasViewer`, `ObjectSelector`), which render their respective parts of the 3-column layout.
3. **Interaction**: `DungeonObjectInteraction` captures mouse events on the canvas and translates them into actions, such as selecting or moving an object.
4. **Save**: Changes are propagated back through the `DungeonEditorSystem` to be written to the ROM.
## 3. Key Recent Fixes
The editor's current stability is the result of two major fixes:
### Critical Fix 1: The Coordinate System
- **Problem**: Objects were rendering at twice their correct distance from the origin, often appearing outside the canvas entirely.
- **Root Cause**: The code incorrectly assumed dungeon tiles were 16x16 pixels, using `* 16` for coordinate conversions. SNES dungeon tiles are **8x8 pixels**.
- **The Fix**: All coordinate conversion functions in `dungeon_renderer.cc`, `dungeon_canvas_viewer.cc`, and `dungeon_object_interaction.cc` were corrected to use `* 8` and `/ 8`.
### Critical Fix 2: The Test Suite
- **Problem**: The integration test suite was unusable, crashing with `SIGBUS` and `SIGSEGV` errors.
- **Root Cause**: The `MockRom` implementation had severe memory management issues, causing crashes when test data was copied.
- **The Fix**: The `MockRom` was **completely abandoned**. All 28 integration and unit tests were refactored to use a real `zelda3.sfc` ROM via the `TestRomManager`. This provides more realistic testing and resolved all crashes.
## 4. ROM Internals & Data Structures
This information is critical for understanding the editor's core logic and has been cross-referenced with the `usdasm` disassembly.
### Object Encoding
Dungeon objects are stored in one of three formats. The encoding logic is correctly implemented in `src/app/zelda3/dungeon/room_object.cc`.
- **Type 1: Standard Objects (ID 0x00-0xFF)**
- **Format**: `xxxxxxss yyyyyyss iiiiiiii`
- **Use**: Common geometry like walls and floors.
- **Type 2: Large Coordinate Objects (ID 0x100-0x1FF)**
- **Format**: `111111xx xxxxyyyy yyiiiiii`
- **Use**: More complex or interactive structures.
- **Type 3: Special Objects (ID 0x200-0x27F)**
- **Format**: `xxxxxxii yyyyyyii 11111iii`
- **Use**: Critical gameplay elements like chests, switches, and bosses.
### Core Data Tables in ROM
- **`bank_01.asm`**:
- **`DrawObjects` (0x018000)**: Master tables mapping an object's ID to its drawing routine.
- **`LoadAndBuildRoom` (0x01873A)**: The primary routine that reads and draws a room.
- **`rooms.asm`**:
- **`RoomData_ObjectDataPointers` (0x1F8000)**: A table of 3-byte pointers to the object data for each of the 296 rooms.
## 5. Testing: 100% Pass Rate
The dungeon editor has comprehensive test coverage, ensuring its stability and correctness.
| Test Type | Total | Passing | Pass Rate |
| ----------------- | ----- | ------- | --------- |
| **Unit Tests** | 14 | 14 | 100% ✅ |
| **Integration** | 14 | 14 | 100% ✅ |
| **E2E Tests** | 1 | 1 | 100% ✅ |
| **TOTAL** | **29**| **29** | **100%** ✅ |
### How to Run Tests
1. **Build the Tests**:
```bash
cmake --preset macos-dev -B build_test
cmake --build build_test --target yaze_test
```
2. **Run All Dungeon Tests**:
```bash
./build_test/bin/yaze_test --gtest_filter="*Dungeon*"
```
3. **Run E2E Smoke Test (Requires GUI)**:
```bash
./build_test/bin/yaze_test --show-gui --gtest_filter="*DungeonEditorSmokeTest*"
```
## 6. Dungeon Object Reference Tables
The following tables were generated by parsing the `DrawObjects` tables in `bank_01.asm`.
### Type 1 Object Reference Table
| ID (Hex) | ID (Dec) | Description (from assembly) |
| :--- | :--- | :--- |
| 0x00 | 0 | Rightwards 2x2 |
| 0x01 | 1 | Rightwards 2x4 |
| 0x02 | 2 | Rightwards 2x4 |
| 0x03 | 3 | Rightwards 2x4 spaced 4 |
| 0x04 | 4 | Rightwards 2x4 spaced 4 |
| 0x05 | 5 | Rightwards 2x4 spaced 4 (Both BG) |
| 0x06 | 6 | Rightwards 2x4 spaced 4 (Both BG) |
| 0x07 | 7 | Rightwards 2x2 |
| 0x08 | 8 | Rightwards 2x2 |
| 0x09 | 9 | Diagonal Acute |
| 0x0A | 10 | Diagonal Grave |
| 0x0B | 11 | Diagonal Grave |
| 0x0C | 12 | Diagonal Acute |
| 0x0D | 13 | Diagonal Acute |
| 0x0E | 14 | Diagonal Grave |
| 0x0F | 15 | Diagonal Grave |
| 0x10 | 16 | Diagonal Acute |
| 0x11 | 17 | Diagonal Acute |
| 0x12 | 18 | Diagonal Grave |
| 0x13 | 19 | Diagonal Grave |
| 0x14 | 20 | Diagonal Acute |
| 0x15 | 21 | Diagonal Acute (Both BG) |
| 0x16 | 22 | Diagonal Grave (Both BG) |
| 0x17 | 23 | Diagonal Grave (Both BG) |
| 0x18 | 24 | Diagonal Acute (Both BG) |
| 0x19 | 25 | Diagonal Acute (Both BG) |
| 0x1A | 26 | Diagonal Grave (Both BG) |
| 0x1B | 27 | Diagonal Grave (Both BG) |
| 0x1C | 28 | Diagonal Acute (Both BG) |
| 0x1D | 29 | Diagonal Acute (Both BG) |
| 0x1E | 30 | Diagonal Grave (Both BG) |
| 0x1F | 31 | Diagonal Grave (Both BG) |
| 0x20 | 32 | Diagonal Acute (Both BG) |
| 0x21 | 33 | Rightwards 1x2 |
| 0x22 | 34 | Rightwards Has Edge 1x1 |
| 0x23 | 35 | Rightwards Has Edge 1x1 |
| 0x24 | 36 | Rightwards Has Edge 1x1 |
| 0x25 | 37 | Rightwards Has Edge 1x1 |
| 0x26 | 38 | Rightwards Has Edge 1x1 |
| 0x27 | 39 | Rightwards Has Edge 1x1 |
| 0x28 | 40 | Rightwards Has Edge 1x1 |
| 0x29 | 41 | Rightwards Has Edge 1x1 |
| 0x2A | 42 | Rightwards Has Edge 1x1 |
| 0x2B | 43 | Rightwards Has Edge 1x1 |
| 0x2C | 44 | Rightwards Has Edge 1x1 |
| 0x2D | 45 | Rightwards Has Edge 1x1 |
| 0x2E | 46 | Rightwards Has Edge 1x1 |
| 0x2F | 47 | Rightwards Top Corners 1x2 |
| 0x30 | 48 | Rightwards Bottom Corners 1x2 |
| 0x31 | 49 | Nothing |
| 0x32 | 50 | Nothing |
| 0x33 | 51 | Rightwards 4x4 |
| 0x34 | 52 | Rightwards 1x1 Solid |
| 0x35 | 53 | Door Switcherer |
| 0x36 | 54 | Rightwards Decor 4x4 spaced 2 |
| 0x37 | 55 | Rightwards Decor 4x4 spaced 2 |
| 0x38 | 56 | Rightwards Statue 2x3 spaced 2 |
| 0x39 | 57 | Rightwards Pillar 2x4 spaced 4 |
| 0x3A | 58 | Rightwards Decor 4x3 spaced 4 |
| 0x3B | 59 | Rightwards Decor 4x3 spaced 4 |
| 0x3C | 60 | Rightwards Doubled 2x2 spaced 2 |
| 0x3D | 61 | Rightwards Pillar 2x4 spaced 4 |
| 0x3E | 62 | Rightwards Decor 2x2 spaced 12 |
| 0x3F | 63 | Rightwards Has Edge 1x1 |
| 0x40 | 64 | Rightwards Has Edge 1x1 |
| 0x41 | 65 | Rightwards Has Edge 1x1 |
| 0x42 | 66 | Rightwards Has Edge 1x1 |
| 0x43 | 67 | Rightwards Has Edge 1x1 |
| 0x44 | 68 | Rightwards Has Edge 1x1 |
| 0x45 | 69 | Rightwards Has Edge 1x1 |
| 0x46 | 70 | Rightwards Has Edge 1x1 |
| 0x47 | 71 | Waterfall |
| 0x48 | 72 | Waterfall |
| 0x49 | 73 | Rightwards Floor Tile 4x2 |
| 0x4A | 74 | Rightwards Floor Tile 4x2 |
| 0x4B | 75 | Rightwards Decor 2x2 spaced 12 |
| 0x4C | 76 | Rightwards Bar 4x3 |
| 0x4D | 77 | Rightwards Shelf 4x4 |
| 0x4E | 78 | Rightwards Shelf 4x4 |
| 0x4F | 79 | Rightwards Shelf 4x4 |
| 0x50 | 80 | Rightwards Line 1x1 |
| 0x51 | 81 | Rightwards Cannon Hole 4x3 |
| 0x52 | 82 | Rightwards Cannon Hole 4x3 |
| 0x53 | 83 | Rightwards 2x2 |
| 0x54 | 84 | Nothing |
| 0x55 | 85 | Rightwards Decor 4x2 spaced 8 |
| 0x56 | 86 | Rightwards Decor 4x2 spaced 8 |
| 0x57 | 87 | Nothing |
| 0x58 | 88 | Nothing |
| 0x59 | 89 | Nothing |
| 0x5A | 90 | Nothing |
| 0x5B | 91 | Rightwards Cannon Hole 4x3 |
| 0x5C | 92 | Rightwards Cannon Hole 4x3 |
| 0x5D | 93 | Rightwards Big Rail 1x3 |
| 0x5E | 94 | Rightwards Block 2x2 spaced 2 |
| 0x5F | 95 | Rightwards Has Edge 1x1 |
| 0x60 | 96 | Downwards 2x2 |
| 0x61 | 97 | Downwards 4x2 |
| 0x62 | 98 | Downwards 4x2 |
| 0x63 | 99 | Downwards 4x2 (Both BG) |
| 0x64 | 100 | Downwards 4x2 (Both BG) |
| 0x65 | 101 | Downwards Decor 4x2 spaced 4 |
| 0x66 | 102 | Downwards Decor 4x2 spaced 4 |
| 0x67 | 103 | Downwards 2x2 |
| 0x68 | 104 | Downwards 2x2 |
| 0x69 | 105 | Downwards Has Edge 1x1 |
| 0x6A | 106 | Downwards Edge 1x1 |
| 0x6B | 107 | Downwards Edge 1x1 |
| 0x6C | 108 | Downwards Left Corners 2x1 |
| 0x6D | 109 | Downwards Right Corners 2x1 |
| 0x6E | 110 | Nothing |
| 0x6F | 111 | Nothing |
| 0x70 | 112 | Downwards Floor 4x4 |
| 0x71 | 113 | Downwards 1x1 Solid |
| 0x72 | 114 | Nothing |
| 0x73 | 115 | Downwards Decor 4x4 spaced 2 |
| 0x74 | 116 | Downwards Decor 4x4 spaced 2 |
| 0x75 | 117 | Downwards Pillar 2x4 spaced 2 |
| 0x76 | 118 | Downwards Decor 3x4 spaced 4 |
| 0x77 | 119 | Downwards Decor 3x4 spaced 4 |
| 0x78 | 120 | Downwards Decor 2x2 spaced 12 |
| 0x79 | 121 | Downwards Edge 1x1 |
| 0x7A | 122 | Downwards Edge 1x1 |
| 0x7B | 123 | Downwards Decor 2x2 spaced 12 |
| 0x7C | 124 | Downwards Line 1x1 |
| 0x7D | 125 | Downwards 2x2 |
| 0x7E | 126 | Nothing |
| 0x7F | 127 | Downwards Decor 2x4 spaced 8 |
| 0x80 | 128 | Downwards Decor 2x4 spaced 8 |
| 0x81 | 129 | Downwards Decor 3x4 spaced 2 |
| 0x82 | 130 | Downwards Decor 3x4 spaced 2 |
| 0x83 | 131 | Downwards Decor 3x4 spaced 2 |
| 0x84 | 132 | Downwards Decor 3x4 spaced 2 |
| 0x85 | 133 | Downwards Cannon Hole 3x4 |
| 0x86 | 134 | Downwards Cannon Hole 3x4 |
| 0x87 | 135 | Downwards Pillar 2x4 spaced 2 |
| 0x88 | 136 | Downwards Big Rail 3x1 |
| 0x89 | 137 | Downwards Block 2x2 spaced 2 |
| 0x8A | 138 | Downwards Has Edge 1x1 |
| 0x8B | 139 | Downwards Edge 1x1 |
| 0x8C | 140 | Downwards Edge 1x1 |
| 0x8D | 141 | Downwards Edge 1x1 |
| 0x8E | 142 | Downwards Edge 1x1 |
| 0x8F | 143 | Downwards Bar 2x5 |
| 0x90 | 144 | Downwards 4x2 |
| 0x91 | 145 | Downwards 4x2 |
| 0x92 | 146 | Downwards 2x2 |
| 0x93 | 147 | Downwards 2x2 |
| 0x94 | 148 | Downwards Floor 4x4 |
| 0x95 | 149 | Downwards Pots 2x2 |
| 0x96 | 150 | Downwards Hammer Pegs 2x2 |
| 0x97 | 151 | Nothing |
| 0x98 | 152 | Nothing |
| 0x99 | 153 | Nothing |
| 0x9A | 154 | Nothing |
| 0x9B | 155 | Nothing |
| 0x9C | 156 | Nothing |
| 0x9D | 157 | Nothing |
| 0x9E | 158 | Nothing |
| 0x9F | 159 | Nothing |
| 0xA0 | 160 | Diagonal Ceiling Top Left A |
| 0xA1 | 161 | Diagonal Ceiling Bottom Left A |
| 0xA2 | 162 | Diagonal Ceiling Top Right A |
| 0xA3 | 163 | Diagonal Ceiling Bottom Right A |
| 0xA4 | 164 | Big Hole 4x4 |
| 0xA5 | 165 | Diagonal Ceiling Top Left B |
| 0xA6 | 166 | Diagonal Ceiling Bottom Left B |
| 0xA7 | 167 | Diagonal Ceiling Top Right B |
| 0xA8 | 168 | Diagonal Ceiling Bottom Right B |
| 0xA9 | 169 | Diagonal Ceiling Top Left B |
| 0xAA | 170 | Diagonal Ceiling Bottom Left B |
| 0xAB | 171 | Diagonal Ceiling Top Right B |
| 0xAC | 172 | Diagonal Ceiling Bottom Right B |
| 0xAD | 173 | Nothing |
| 0xAE | 174 | Nothing |
| 0xAF | 175 | Nothing |
| 0xB0 | 176 | Rightwards Edge 1x1 |
| 0xB1 | 177 | Rightwards Edge 1x1 |
| 0xB2 | 178 | Rightwards 4x4 |
| 0xB3 | 179 | Rightwards Has Edge 1x1 |
| 0xB4 | 180 | Rightwards Has Edge 1x1 |
| 0xB5 | 181 | Weird 2x4 |
| 0xB6 | 182 | Rightwards 2x4 |
| 0xB7 | 183 | Rightwards 2x4 |
| 0xB8 | 184 | Rightwards 2x2 |
| 0xB9 | 185 | Rightwards 2x2 |
| 0xBA | 186 | Rightwards 4x4 |
| 0xBB | 187 | Rightwards Block 2x2 spaced 2 |
| 0xBC | 188 | Rightwards Pots 2x2 |
| 0xBD | 189 | Rightwards Hammer Pegs 2x2 |
| 0xBE | 190 | Nothing |
| 0xBF | 191 | Nothing |
| 0xC0 | 192 | 4x4 Blocks In 4x4 Super Square |
| 0xC1 | 193 | Closed Chest Platform |
| 0xC2 | 194 | 4x4 Blocks In 4x4 Super Square |
| 0xC3 | 195 | 3x3 Floor In 4x4 Super Square |
| 0xC4 | 196 | 4x4 Floor One In 4x4 Super Square |
| 0xC5 | 197 | 4x4 Floor In 4x4 Super Square |
| 0xC6 | 198 | 4x4 Floor In 4x4 Super Square |
| 0xC7 | 199 | 4x4 Floor In 4x4 Super Square |
| 0xC8 | 200 | 4x4 Floor In 4x4 Super Square |
| 0xC9 | 201 | 4x4 Floor In 4x4 Super Square |
| 0xCA | 202 | 4x4 Floor In 4x4 Super Square |
| 0xCB | 203 | Nothing |
| 0xCC | 204 | Nothing |
| 0xCD | 205 | Moving Wall West |
| 0xCE | 206 | Moving Wall East |
| 0xCF | 207 | Nothing |
| 0xD0 | 208 | Nothing |
| 0xD1 | 209 | 4x4 Floor In 4x4 Super Square |
| 0xD2 | 210 | 4x4 Floor In 4x4 Super Square |
| 0xD3 | 211 | Check If Wall Is Moved |
| 0xD4 | 212 | Check If Wall Is Moved |
| 0xD5 | 213 | Check If Wall Is Moved |
| 0xD6 | 214 | Check If Wall Is Moved |
| 0xD7 | 215 | 3x3 Floor In 4x4 Super Square |
| 0xD8 | 216 | Water Overlay A 8x8 |
| 0xD9 | 217 | 4x4 Floor In 4x4 Super Square |
| 0xDA | 218 | Water Overlay B 8x8 |
| 0xDB | 219 | 4x4 Floor Two In 4x4 Super Square |
| 0xDC | 220 | Open Chest Platform |
| 0xDD | 221 | Table Rock 4x4 |
| 0xDE | 222 | Spike 2x2 In 4x4 Super Square |
| 0xDF | 223 | 4x4 Floor In 4x4 Super Square |
| 0xE0 | 224 | 4x4 Floor In 4x4 Super Square |
| 0xE1 | 225 | 4x4 Floor In 4x4 Super Square |
| 0xE2 | 226 | 4x4 Floor In 4x4 Super Square |
| 0xE3 | 227 | 4x4 Floor In 4x4 Super Square |
| 0xE4 | 228 | 4x4 Floor In 4x4 Super Square |
| 0xE5 | 229 | 4x4 Floor In 4x4 Super Square |
| 0xE6 | 230 | 4x4 Floor In 4x4 Super Square |
| 0xE7 | 231 | 4x4 Floor In 4x4 Super Square |
| 0xE8 | 232 | 4x4 Floor In 4x4 Super Square |
| 0xE9 | 233 | Nothing |
| 0xEA | 234 | Nothing |
| 0xEB | 235 | Nothing |
| 0xEC | 236 | Nothing |
| 0xED | 237 | Nothing |
| 0xEE | 238 | Nothing |
| 0xEF | 239 | Nothing |
| 0xF0 | 240 | Nothing |
| 0xF1 | 241 | Nothing |
| 0xF2 | 242 | Nothing |
| 0xF3 | 243 | Nothing |
| 0xF4 | 244 | Nothing |
| 0xF5 | 245 | Nothing |
| 0xF6 | 246 | Nothing |
| 0xF7 | 247 | Nothing |
| 0xF8 | 248 | Nothing |
| 0xF9 | 249 | Nothing |
| 0xFA | 250 | Nothing |
| 0xFB | 251 | Nothing |
| 0xFC | 252 | Nothing |
| 0xFD | 253 | Nothing |
| 0xFE | 254 | Nothing |
| 0xFF | 255 | Nothing |
### Type 2 Object Reference Table
| ID (Hex) | ID (Dec) | Description (from assembly) |
| :--- | :--- | :--- |
| 0x100 | 256 | 4x4 |
| 0x101 | 257 | 4x4 |
| 0x102 | 258 | 4x4 |
| 0x103 | 259 | 4x4 |
| 0x104 | 260 | 4x4 |
| 0x105 | 261 | 4x4 |
| 0x106 | 262 | 4x4 |
| 0x107 | 263 | 4x4 |
| 0x108 | 264 | 4x4 Corner (Both BG) |
| 0x109 | 265 | 4x4 Corner (Both BG) |
| 0x10A | 266 | 4x4 Corner (Both BG) |
| 0x10B | 267 | 4x4 Corner (Both BG) |
| 0x10C | 268 | 4x4 Corner (Both BG) |
| 0x10D | 269 | 4x4 Corner (Both BG) |
| 0x10E | 270 | 4x4 Corner (Both BG) |
| 0x10F | 271 | 4x4 Corner (Both BG) |
| 0x110 | 272 | Weird Corner Bottom (Both BG) |
| 0x111 | 273 | Weird Corner Bottom (Both BG) |
| 0x112 | 274 | Weird Corner Bottom (Both BG) |
| 0x113 | 275 | Weird Corner Bottom (Both BG) |
| 0x114 | 276 | Weird Corner Top (Both BG) |
| 0x115 | 277 | Weird Corner Top (Both BG) |
| 0x116 | 278 | Weird Corner Top (Both BG) |
| 0x117 | 279 | Weird Corner Top (Both BG) |
| 0x118 | 280 | Rightwards 2x2 |
| 0x119 | 281 | Rightwards 2x2 |
| 0x11A | 282 | Rightwards 2x2 |
| 0x11B | 283 | Rightwards 2x2 |
| 0x11C | 284 | 4x4 |
| 0x11D | 285 | Single 2x3 Pillar |
| 0x11E | 286 | Single 2x2 |
| 0x11F | 287 | Enabled Star Switch |
| 0x120 | 288 | Lit Torch |
| 0x121 | 289 | Single 2x3 Pillar |
| 0x122 | 290 | Bed 4x5 |
| 0x123 | 291 | Table Rock 4x3 |
| 0x124 | 292 | 4x4 |
| 0x125 | 293 | 4x4 |
| 0x126 | 294 | Single 2x3 Pillar |
| 0x127 | 295 | Rightwards 2x2 |
| 0x128 | 296 | Bed 4x5 |
| 0x129 | 297 | 4x4 |
| 0x12A | 298 | Portrait Of Mario |
| 0x12B | 299 | Rightwards 2x2 |
| 0x12C | 300 | Draw Rightwards 3x6 |
| 0x12D | 301 | Inter-Room Fat Stairs Up |
| 0x12E | 302 | Inter-Room Fat Stairs Down A |
| 0x12F | 303 | Inter-Room Fat Stairs Down B |
| 0x130 | 304 | Auto Stairs North Multi Layer A |
| 0x131 | 305 | Auto Stairs North Multi Layer B |
| 0x132 | 306 | Auto Stairs North Merged Layer A |
| 0x133 | 307 | Auto Stairs North Merged Layer B |
| 0x134 | 308 | Rightwards 2x2 |
| 0x135 | 309 | Water Hop Stairs A |
| 0x136 | 310 | Water Hop Stairs B |
| 0x137 | 311 | Dam Flood Gate |
| 0x138 | 312 | Spiral Stairs Going Up Upper |
| 0x139 | 313 | Spiral Stairs Going Down Upper |
| 0x13A | 314 | Spiral Stairs Going Up Lower |
| 0x13B | 315 | Spiral Stairs Going Down Lower |
| 0x13C | 316 | Sanctuary Wall |
| 0x13D | 317 | Table Rock 4x3 |
| 0x13E | 318 | Utility 6x3 |
| 0x13F | 319 | Magic Bat Altar |
### Type 3 Object Reference Table
| ID (Hex) | ID (Dec) | Description (from assembly) |
| :--- | :--- | :--- |
| 0x200 | 512 | Empty Water Face |
| 0x201 | 513 | Spitting Water Face |
| 0x202 | 514 | Drenching Water Face |
| 0x203 | 515 | Somaria Line (increment count) |
| 0x204 | 516 | Somaria Line |
| 0x205 | 517 | Somaria Line |
| 0x206 | 518 | Somaria Line |
| 0x207 | 519 | Somaria Line |
| 0x208 | 520 | Somaria Line |
| 0x209 | 521 | Somaria Line |
| 0x20A | 522 | Somaria Line |
| 0x20B | 523 | Somaria Line |
| 0x20C | 524 | Somaria Line |
| 0x20D | 525 | Prison Cell |
| 0x20E | 526 | Somaria Line (increment count) |
| 0x20F | 527 | Somaria Line |
| 0x210 | 528 | Rightwards 2x2 |
| 0x211 | 529 | Rightwards 2x2 |
| 0x212 | 530 | Rupee Floor |
| 0x213 | 531 | Rightwards 2x2 |
| 0x214 | 532 | Table Rock 4x3 |
| 0x215 | 533 | Kholdstare Shell |
| 0x216 | 534 | Hammer Peg Single |
| 0x217 | 535 | Prison Cell |
| 0x218 | 536 | Big Key Lock |
| 0x219 | 537 | Chest |
| 0x21A | 538 | Open Chest |
| 0x21B | 539 | Auto Stairs South Multi Layer A |
| 0x21C | 540 | Auto Stairs South Multi Layer B |
| 0x21D | 541 | Auto Stairs South Multi Layer C |
| 0x21E | 542 | Straight Inter-room Stairs Going Up North Upper |
| 0x21F | 543 | Straight Inter-room Stairs Going Down North Upper |
| 0x220 | 544 | Straight Inter-room Stairs Going Up South Upper |
| 0x221 | 545 | Straight Inter-room Stairs Going Down South Upper |
| 0x222 | 546 | Rightwards 2x2 |
| 0x223 | 547 | Rightwards 2x2 |
| 0x224 | 548 | Rightwards 2x2 |
| 0x225 | 549 | Rightwards 2x2 |
| 0x226 | 550 | Straight Inter-room Stairs Going Up North Lower |
| 0x227 | 551 | Straight Inter-room Stairs Going Down North Lower |
| 0x228 | 552 | Straight Inter-room Stairs Going Up South Lower |
| 0x229 | 553 | Straight Inter-room Stairs Going Down South Lower |
| 0x22A | 554 | Lamp Cones |
| 0x22B | 555 | Weird Glove Required Pot |
| 0x22C | 556 | Big Gray Rock |
| 0x22D | 557 | Agahnims Altar |
| 0x22E | 558 | Agahnims Windows |
| 0x22F | 559 | Single Pot |
| 0x230 | 560 | Weird Ugly Pot |
| 0x231 | 561 | Big Chest |
| 0x232 | 562 | Open Big Chest |
| 0x233 | 563 | Auto Stairs South Merged Layer |
| 0x234 | 564 | Chest Platform Vertical Wall |
| 0x235 | 565 | Chest Platform Vertical Wall |
| 0x236 | 566 | Draw Rightwards 3x6 |
| 0x237 | 567 | Draw Rightwards 3x6 |
| 0x238 | 568 | Chest Platform Vertical Wall |
| 0x239 | 569 | Chest Platform Vertical Wall |
| 0x23A | 570 | Vertical Turtle Rock Pipe |
| 0x23B | 571 | Vertical Turtle Rock Pipe |
| 0x23C | 572 | Horizontal Turtle Rock Pipe |
| 0x23D | 573 | Horizontal Turtle Rock Pipe |
| 0x23E | 574 | Rightwards 2x2 |
| 0x23F | 575 | Rightwards 2x2 |
| 0x240 | 576 | Rightwards 2x2 |
| 0x241 | 577 | Rightwards 2x2 |
| 0x242 | 578 | Rightwards 2x2 |
| 0x243 | 579 | Rightwards 2x2 |
| 0x244 | 580 | Rightwards 2x2 |
| 0x245 | 581 | Rightwards 2x2 |
| 0x246 | 582 | Rightwards 2x2 |
| 0x247 | 583 | Bombable Floor |
| 0x248 | 584 | 4x4 |
| 0x249 | 585 | Rightwards 2x2 |
| 0x24A | 586 | Rightwards 2x2 |
| 0x24B | 587 | Big Wall Decor |
| 0x24C | 588 | Smithy Furnace |
| 0x24D | 589 | Utility 6x3 |
| 0x24E | 590 | Table Rock 4x3 |
| 0x24F | 591 | Rightwards 2x2 |
| 0x250 | 592 | Single 2x2 |
| 0x251 | 593 | Rightwards 2x2 |
| 0x252 | 594 | Rightwards 2x2 |
| 0x253 | 595 | Rightwards 2x2 |
| 0x254 | 596 | Fortune Teller Room |
| 0x255 | 597 | Utility 3x5 |
| 0x256 | 598 | Rightwards 2x2 |
| 0x257 | 599 | Rightwards 2x2 |
| 0x258 | 600 | Rightwards 2x2 |
| 0x259 | 601 | Rightwards 2x2 |
| 0x25A | 602 | Table Bowl |
| 0x25B | 603 | Utility 3x5 |
| 0x25C | 604 | Horizontal Turtle Rock Pipe |
| 0x25D | 605 | Utility 6x3 |
| 0x25E | 606 | Rightwards 2x2 |
| 0x25F | 607 | Rightwards 2x2 |
| 0x260 | 608 | Archery Game Target Door |
| 0x261 | 609 | Archery Game Target Door |
| 0x262 | 610 | Vitreous Goo Graphics |
| 0x263 | 611 | Rightwards 2x2 |
| 0x264 | 612 | Rightwards 2x2 |
| 0x265 | 613 | Rightwards 2x2 |
| 0x266 | 614 | 4x4 |
| 0x267 | 615 | Table Rock 4x3 |
| 0x268 | 616 | Table Rock 4x3 |
| 0x269 | 617 | Solid Wall Decor 3x4 |
| 0x26A | 618 | Solid Wall Decor 3x4 |
| 0x26B | 619 | 4x4 |
| 0x26C | 620 | Table Rock 4x3 |
| 0x26D | 621 | Table Rock 4x3 |
| 0x26E | 622 | Solid Wall Decor 3x4 |
| 0x26F | 623 | Solid Wall Decor 3x4 |
| 0x270 | 624 | Light Beam On Floor |
| 0x271 | 625 | Big Light Beam On Floor |
| 0x272 | 626 | Trinexx Shell |
| 0x273 | 627 | BG2 Mask Full |
| 0x274 | 628 | Floor Light |
| 0x275 | 629 | Rightwards 2x2 |
| 0x276 | 630 | Big Wall Decor |
| 0x277 | 631 | Big Wall Decor |
| 0x278 | 632 | Ganon Triforce Floor Decor |
| 0x279 | 633 | Table Rock 4x3 |
| 0x27A | 634 | 4x4 |
| 0x27B | 635 | Vitreous Goo Damage |
| 0x27C | 636 | Rightwards 2x2 |
| 0x27D | 637 | Rightwards 2x2 |
| 0x27E | 638 | Rightwards 2x2 |
| 0x27F | 639 | Nothing |

View File

@@ -135,6 +135,60 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) {
ImGui::EndGroup();
canvas_.DrawBackground();
// Add dungeon-specific context menu items
canvas_.ClearContextMenuItems();
if (rooms_ && rom_->is_loaded()) {
auto& room = (*rooms_)[room_id];
// Add object placement option
canvas_.AddContextMenuItem({
ICON_MD_ADD " Place Object",
[]() {
// TODO: Show object palette/selector
},
"Ctrl+P"
});
// Add object deletion for selected objects
canvas_.AddContextMenuItem({
ICON_MD_DELETE " Delete Selected",
[this]() {
object_interaction_.HandleDeleteSelected();
},
"Del"
});
// Add room property quick toggles
canvas_.AddContextMenuItem({
ICON_MD_LAYERS " Toggle BG1",
[this, room_id]() {
auto& settings = GetRoomLayerSettings(room_id);
settings.bg1_visible = !settings.bg1_visible;
},
"1"
});
canvas_.AddContextMenuItem({
ICON_MD_LAYERS " Toggle BG2",
[this, room_id]() {
auto& settings = GetRoomLayerSettings(room_id);
settings.bg2_visible = !settings.bg2_visible;
},
"2"
});
// Add re-render option
canvas_.AddContextMenuItem({
ICON_MD_REFRESH " Re-render Room",
[&room]() {
room.RenderRoomGraphics();
},
"Ctrl+R"
});
}
canvas_.DrawContextMenu();
if (rooms_ && rom_->is_loaded()) {

View File

@@ -22,8 +22,7 @@ void DungeonEditorV2::Initialize(gfx::IRenderer* renderer, Rom* rom) {
// Don't initialize emulator preview yet - ROM might not be loaded
// Will be initialized in Load() instead
// Setup docking class for room windows
room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
// Setup docking class for room windows (ImGui::GetID will be called in Update when ImGui is ready)
room_window_class_.DockingAllowUnclassed = true; // Room windows can dock with anything
room_window_class_.DockingAlwaysTabBar = true; // Always show tabs when multiple rooms
@@ -158,6 +157,11 @@ absl::Status DungeonEditorV2::Load() {
}
absl::Status DungeonEditorV2::Update() {
// Initialize docking class ID on first Update (when ImGui is ready)
if (room_window_class_.ClassId == 0) {
room_window_class_.ClassId = ImGui::GetID("DungeonRoomClass");
}
if (!is_loaded_) {
// CARD-BASED EDITOR: Create a minimal loading card
gui::EditorCard loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);

View File

@@ -1,784 +0,0 @@
#include <gtest/gtest.h>
#include <memory>
#include <chrono>
#include <vector>
#include <map>
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
#include "app/zelda3/dungeon/dungeon_object_editor.h"
#include "app/zelda3/dungeon/object_renderer.h"
#include "app/zelda3/dungeon/dungeon_editor_system.h"
#include "app/gfx/snes_palette.h"
namespace yaze {
namespace zelda3 {
class DungeonObjectRendererIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests on Linux for automated github builds
#if defined(__linux__)
GTEST_SKIP();
#endif
// Use the real ROM from build directory
rom_path_ = "build/bin/zelda3.sfc";
// Load ROM
rom_ = std::make_unique<Rom>();
ASSERT_TRUE(rom_->LoadFromFile(rom_path_).ok());
// Initialize dungeon editor system
dungeon_editor_system_ = std::make_unique<DungeonEditorSystem>(rom_.get());
ASSERT_TRUE(dungeon_editor_system_->Initialize().ok());
// Initialize object editor
object_editor_ = std::make_shared<DungeonObjectEditor>(rom_.get());
// Note: InitializeEditor() is private, so we skip this in integration tests
// Initialize object renderer
object_renderer_ = std::make_unique<ObjectRenderer>(rom_.get());
// Load test room data
ASSERT_TRUE(LoadTestRoomData().ok());
}
void TearDown() override {
object_renderer_.reset();
object_editor_.reset();
dungeon_editor_system_.reset();
rom_.reset();
}
absl::Status LoadTestRoomData() {
// Load representative rooms based on disassembly data
// Room 0x0000: Ganon's room (from disassembly)
// Room 0x0001: First dungeon room
// Room 0x0002: Sewer room (from disassembly)
// Room 0x0010: Another dungeon room (from disassembly)
// Room 0x0012: Sewer room (from disassembly)
// Room 0x0020: Agahnim's tower (from disassembly)
test_rooms_ = {0x0000, 0x0001, 0x0002, 0x0010, 0x0012, 0x0020, 0x0033, 0x005A};
for (int room_id : test_rooms_) {
auto room_result = zelda3::LoadRoomFromRom(rom_.get(), room_id);
rooms_[room_id] = room_result;
rooms_[room_id].LoadObjects();
// Log room data for debugging
if (!rooms_[room_id].GetTileObjects().empty()) {
std::cout << "Room 0x" << std::hex << room_id << std::dec
<< " loaded with " << rooms_[room_id].GetTileObjects().size()
<< " objects" << std::endl;
}
}
// Load palette data for testing based on vanilla values
auto palette_group = rom_->palette_group().dungeon_main;
test_palettes_ = {palette_group[0], palette_group[1], palette_group[2]};
return absl::OkStatus();
}
// Helper methods for creating test objects
RoomObject CreateTestObject(int object_id, int x, int y, int size = 0x12, int layer = 0) {
RoomObject obj(object_id, x, y, size, layer);
obj.set_rom(rom_.get());
obj.EnsureTilesLoaded();
return obj;
}
std::vector<RoomObject> CreateTestObjectSet(int room_id) {
std::vector<RoomObject> objects;
// Create test objects based on real object types from disassembly
// These correspond to actual object types found in the ROM
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall object
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor object
objects.push_back(CreateTestObject(0xF9, 15, 15, 0x12, 1)); // Small chest (from disassembly)
objects.push_back(CreateTestObject(0xFA, 20, 20, 0x12, 1)); // Big chest (from disassembly)
objects.push_back(CreateTestObject(0x13, 25, 25, 0x32, 2)); // Stairs
objects.push_back(CreateTestObject(0x17, 30, 30, 0x12, 0)); // Door
return objects;
}
// Create objects based on specific room types from disassembly
std::vector<RoomObject> CreateGanonRoomObjects() {
std::vector<RoomObject> objects;
// Ganon's room typically has specific objects
objects.push_back(CreateTestObject(0x10, 8, 8, 0x12, 0)); // Wall
objects.push_back(CreateTestObject(0x20, 12, 12, 0x22, 0)); // Floor
objects.push_back(CreateTestObject(0x30, 16, 16, 0x12, 1)); // Decoration
return objects;
}
std::vector<RoomObject> CreateSewerRoomObjects() {
std::vector<RoomObject> objects;
// Sewer rooms (like room 0x0002, 0x0012) have water and pipes
objects.push_back(CreateTestObject(0x20, 5, 5, 0x22, 0)); // Floor
objects.push_back(CreateTestObject(0x40, 10, 10, 0x12, 0)); // Water
objects.push_back(CreateTestObject(0x50, 15, 15, 0x32, 1)); // Pipe
return objects;
}
// Performance measurement helpers
struct PerformanceMetrics {
std::chrono::milliseconds render_time;
size_t objects_rendered;
size_t memory_used;
size_t cache_hits;
size_t cache_misses;
};
PerformanceMetrics MeasureRenderPerformance(const std::vector<RoomObject>& objects,
const gfx::SnesPalette& palette) {
auto start_time = std::chrono::high_resolution_clock::now();
auto stats_before = object_renderer_->GetPerformanceStats();
auto result = object_renderer_->RenderObjects(objects, palette);
auto end_time = std::chrono::high_resolution_clock::now();
auto stats_after = object_renderer_->GetPerformanceStats();
PerformanceMetrics metrics;
metrics.render_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time);
metrics.objects_rendered = objects.size();
metrics.cache_hits = stats_after.cache_hits - stats_before.cache_hits;
metrics.cache_misses = stats_after.cache_misses - stats_before.cache_misses;
metrics.memory_used = object_renderer_->GetMemoryUsage();
return metrics;
}
std::string rom_path_;
std::unique_ptr<Rom> rom_;
std::unique_ptr<DungeonEditorSystem> dungeon_editor_system_;
std::shared_ptr<DungeonObjectEditor> object_editor_;
std::unique_ptr<ObjectRenderer> object_renderer_;
// Test data
std::vector<int> test_rooms_;
std::map<int, Room> rooms_;
std::vector<gfx::SnesPalette> test_palettes_;
};
// Test basic object rendering functionality
TEST_F(DungeonObjectRendererIntegrationTest, BasicObjectRendering) {
auto test_objects = CreateTestObjectSet(0);
auto palette = test_palettes_[0];
auto result = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result.ok()) << "Failed to render objects: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test object rendering with different palettes
TEST_F(DungeonObjectRendererIntegrationTest, MultiPaletteRendering) {
auto test_objects = CreateTestObjectSet(0);
for (const auto& palette : test_palettes_) {
auto result = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result.ok()) << "Failed to render with palette: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
}
// Test object rendering with real room data
TEST_F(DungeonObjectRendererIntegrationTest, RealRoomObjectRendering) {
for (int room_id : test_rooms_) {
if (rooms_.find(room_id) == rooms_.end()) continue;
const auto& room = rooms_[room_id];
const auto& objects = room.GetTileObjects();
if (objects.empty()) continue;
// Test with first palette
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render room 0x" << std::hex << room_id
<< std::dec << " objects: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
// Log successful rendering
std::cout << "Successfully rendered room 0x" << std::hex << room_id << std::dec
<< " with " << objects.size() << " objects" << std::endl;
}
}
// Test specific rooms mentioned in disassembly
TEST_F(DungeonObjectRendererIntegrationTest, DisassemblyRoomValidation) {
// Test Ganon's room (0x0000) from disassembly
if (rooms_.find(0x0000) != rooms_.end()) {
const auto& ganon_room = rooms_[0x0000];
const auto& objects = ganon_room.GetTileObjects();
if (!objects.empty()) {
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render Ganon's room objects";
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Ganon's room (0x0000) rendered with " << objects.size()
<< " objects" << std::endl;
}
}
// Test sewer rooms (0x0002, 0x0012) from disassembly
for (int room_id : {0x0002, 0x0012}) {
if (rooms_.find(room_id) != rooms_.end()) {
const auto& sewer_room = rooms_[room_id];
const auto& objects = sewer_room.GetTileObjects();
if (!objects.empty()) {
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render sewer room 0x" << std::hex << room_id << std::dec;
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Sewer room 0x" << std::hex << room_id << std::dec
<< " rendered with " << objects.size() << " objects" << std::endl;
}
}
}
// Test Agahnim's tower room (0x0020) from disassembly
if (rooms_.find(0x0020) != rooms_.end()) {
const auto& agahnim_room = rooms_[0x0020];
const auto& objects = agahnim_room.GetTileObjects();
if (!objects.empty()) {
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render Agahnim's tower room objects";
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Agahnim's tower room (0x0020) rendered with " << objects.size()
<< " objects" << std::endl;
}
}
}
// Test object rendering performance
TEST_F(DungeonObjectRendererIntegrationTest, RenderingPerformance) {
auto test_objects = CreateTestObjectSet(0);
auto palette = test_palettes_[0];
// Measure performance for different object counts
std::vector<int> object_counts = {1, 5, 10, 20, 50};
for (int count : object_counts) {
std::vector<RoomObject> objects;
for (int i = 0; i < count; i++) {
objects.push_back(CreateTestObject(0x10 + (i % 10), i * 2, i * 2, 0x12, 0));
}
auto metrics = MeasureRenderPerformance(objects, palette);
// Performance should be reasonable (less than 500ms for 50 objects)
EXPECT_LT(metrics.render_time.count(), 500)
<< "Rendering " << count << " objects took too long: "
<< metrics.render_time.count() << "ms";
EXPECT_EQ(metrics.objects_rendered, count);
}
}
// Test object rendering cache effectiveness
TEST_F(DungeonObjectRendererIntegrationTest, CacheEffectiveness) {
auto test_objects = CreateTestObjectSet(0);
auto palette = test_palettes_[0];
// Reset performance stats
object_renderer_->ResetPerformanceStats();
// First render (should miss cache)
auto result1 = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result1.ok());
auto stats1 = object_renderer_->GetPerformanceStats();
EXPECT_GT(stats1.cache_misses, 0);
// Second render with same objects (should hit cache)
auto result2 = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result2.ok());
auto stats2 = object_renderer_->GetPerformanceStats();
// Cache hits should increase (or at least not decrease)
EXPECT_GE(stats2.cache_hits, stats1.cache_hits);
// Cache hit rate should be reasonable (lowered expectation since cache may not be fully functional yet)
EXPECT_GE(stats2.cache_hit_rate(), 0.0) << "Cache hit rate: "
<< stats2.cache_hit_rate();
}
// Test object rendering with different object types
TEST_F(DungeonObjectRendererIntegrationTest, DifferentObjectTypes) {
// Object types based on disassembly analysis
std::vector<int> object_types = {
0x10, // Wall objects
0x20, // Floor objects
0x30, // Decoration objects
0xF9, // Small chest (from disassembly)
0xFA, // Big chest (from disassembly)
0x13, // Stairs
0x17, // Door
0x18, // Door variant
0x40, // Water objects
0x50 // Pipe objects
};
auto palette = test_palettes_[0];
for (int object_type : object_types) {
auto object = CreateTestObject(object_type, 10, 10, 0x12, 0);
std::vector<RoomObject> objects = {object};
auto result = object_renderer_->RenderObjects(objects, palette);
// Some object types might not render (invalid IDs), that's okay
if (result.ok()) {
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Object type 0x" << std::hex << object_type << std::dec
<< " rendered successfully" << std::endl;
} else {
std::cout << "Object type 0x" << std::hex << object_type << std::dec
<< " failed to render: " << result.status().message() << std::endl;
}
}
}
// Test object types found in real ROM rooms
TEST_F(DungeonObjectRendererIntegrationTest, RealRoomObjectTypes) {
auto palette = test_palettes_[0];
std::set<int> found_object_types;
// Collect all object types from real rooms
for (const auto& [room_id, room] : rooms_) {
const auto& objects = room.GetTileObjects();
for (const auto& obj : objects) {
found_object_types.insert(obj.id_);
}
}
std::cout << "Found " << found_object_types.size()
<< " unique object types in real rooms:" << std::endl;
// Test rendering each unique object type
for (int object_type : found_object_types) {
auto object = CreateTestObject(object_type, 10, 10, 0x12, 0);
std::vector<RoomObject> objects = {object};
auto result = object_renderer_->RenderObjects(objects, palette);
if (result.ok()) {
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << " Object type 0x" << std::hex << object_type << std::dec
<< " - rendered successfully" << std::endl;
} else {
std::cout << " Object type 0x" << std::hex << object_type << std::dec
<< " - failed: " << result.status().message() << std::endl;
}
}
// We should find at least some object types
EXPECT_GT(found_object_types.size(), 0) << "No object types found in real rooms";
}
// Test object rendering with different sizes
TEST_F(DungeonObjectRendererIntegrationTest, DifferentObjectSizes) {
std::vector<int> object_sizes = {0x12, 0x22, 0x32, 0x42, 0x52};
auto palette = test_palettes_[0];
int object_type = 0x10; // Wall
for (int size : object_sizes) {
auto object = CreateTestObject(object_type, 10, 10, size, 0);
std::vector<RoomObject> objects = {object};
auto result = object_renderer_->RenderObjects(objects, palette);
ASSERT_TRUE(result.ok()) << "Failed to render object with size 0x"
<< std::hex << size << std::dec;
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
}
// Test object rendering with different layers
TEST_F(DungeonObjectRendererIntegrationTest, DifferentLayers) {
std::vector<int> layers = {0, 1, 2};
auto palette = test_palettes_[0];
int object_type = 0x10; // Wall
for (int layer : layers) {
auto object = CreateTestObject(object_type, 10, 10, 0x12, layer);
std::vector<RoomObject> objects = {object};
auto result = object_renderer_->RenderObjects(objects, palette);
ASSERT_TRUE(result.ok()) << "Failed to render object on layer " << layer;
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
}
// Test object rendering memory usage
TEST_F(DungeonObjectRendererIntegrationTest, MemoryUsage) {
auto test_objects = CreateTestObjectSet(0);
auto palette = test_palettes_[0];
size_t initial_memory = object_renderer_->GetMemoryUsage();
// Render objects multiple times
for (int i = 0; i < 10; i++) {
auto result = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result.ok());
}
size_t final_memory = object_renderer_->GetMemoryUsage();
// Memory usage should be reasonable (less than 100MB)
EXPECT_LT(final_memory, 100 * 1024 * 1024) << "Memory usage too high: "
<< final_memory / (1024 * 1024) << "MB";
// Memory usage shouldn't grow excessively
EXPECT_LT(final_memory - initial_memory, 50 * 1024 * 1024)
<< "Memory growth too high: "
<< (final_memory - initial_memory) / (1024 * 1024) << "MB";
}
// Test object rendering error handling
TEST_F(DungeonObjectRendererIntegrationTest, ErrorHandling) {
// Test with empty object list
std::vector<RoomObject> empty_objects;
auto palette = test_palettes_[0];
auto result = object_renderer_->RenderObjects(empty_objects, palette);
// Should either succeed with empty bitmap or fail gracefully
if (!result.ok()) {
EXPECT_TRUE(absl::IsInvalidArgument(result.status()) ||
absl::IsFailedPrecondition(result.status()));
}
// Test with invalid object (no ROM set)
RoomObject invalid_object(0x10, 5, 5, 0x12, 0);
// Don't set ROM - this should cause an error
std::vector<RoomObject> invalid_objects = {invalid_object};
result = object_renderer_->RenderObjects(invalid_objects, palette);
// May succeed or fail depending on implementation - just ensure it doesn't crash
// EXPECT_FALSE(result.ok());
}
// Test object rendering with large object sets
TEST_F(DungeonObjectRendererIntegrationTest, LargeObjectSetRendering) {
std::vector<RoomObject> large_object_set;
auto palette = test_palettes_[0];
// Create a large set of objects (100 objects)
for (int i = 0; i < 100; i++) {
int object_type = 0x10 + (i % 20); // Vary object types
int x = (i % 10) * 16; // Spread across 10x10 grid
int y = (i / 10) * 16;
int size = 0x12 + (i % 4) * 0x10; // Vary sizes
large_object_set.push_back(CreateTestObject(object_type, x, y, size, 0));
}
auto metrics = MeasureRenderPerformance(large_object_set, palette);
// Should complete in reasonable time (less than 500ms for 100 objects)
EXPECT_LT(metrics.render_time.count(), 500)
<< "Rendering 100 objects took too long: "
<< metrics.render_time.count() << "ms";
EXPECT_EQ(metrics.objects_rendered, 100);
}
// Test object rendering consistency
TEST_F(DungeonObjectRendererIntegrationTest, RenderingConsistency) {
auto test_objects = CreateTestObjectSet(0);
auto palette = test_palettes_[0];
// Render the same objects multiple times
std::vector<gfx::Bitmap> results;
for (int i = 0; i < 5; i++) {
auto result = object_renderer_->RenderObjects(test_objects, palette);
ASSERT_TRUE(result.ok()) << "Failed on iteration " << i;
results.push_back(std::move(result.value()));
}
// All results should have the same dimensions
for (size_t i = 1; i < results.size(); i++) {
EXPECT_EQ(results[0].width(), results[i].width());
EXPECT_EQ(results[0].height(), results[i].height());
}
}
// Test object rendering with dungeon editor integration
TEST_F(DungeonObjectRendererIntegrationTest, DungeonEditorIntegration) {
// Load a room into the object editor
ASSERT_TRUE(object_editor_->LoadRoom(0).ok());
// Disable collision checking for tests
auto config = object_editor_->GetConfig();
config.validate_objects = false;
object_editor_->SetConfig(config);
// Add some objects
ASSERT_TRUE(object_editor_->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor_->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Get the objects from the editor
const auto& objects = object_editor_->GetObjects();
ASSERT_EQ(objects.size(), 2);
// Render the objects
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render objects from editor: "
<< result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test object rendering with dungeon editor system integration
TEST_F(DungeonObjectRendererIntegrationTest, DungeonEditorSystemIntegration) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0).ok());
// Get object editor from system
auto system_object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(system_object_editor, nullptr);
// Disable collision checking for tests
auto config = system_object_editor->GetConfig();
config.validate_objects = false;
system_object_editor->SetConfig(config);
// Add objects through the system
ASSERT_TRUE(system_object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(system_object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Get objects and render them
const auto& objects = system_object_editor->GetObjects();
ASSERT_EQ(objects.size(), 2);
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render objects from system: "
<< result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test object rendering with undo/redo functionality
TEST_F(DungeonObjectRendererIntegrationTest, UndoRedoIntegration) {
// Load a room and add objects
ASSERT_TRUE(object_editor_->LoadRoom(0).ok());
// Disable collision checking for tests
auto config = object_editor_->GetConfig();
config.validate_objects = false;
object_editor_->SetConfig(config);
ASSERT_TRUE(object_editor_->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor_->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Render initial state
auto objects_before = object_editor_->GetObjects();
auto result_before = object_renderer_->RenderObjects(objects_before, test_palettes_[0]);
ASSERT_TRUE(result_before.ok());
// Undo one operation
ASSERT_TRUE(object_editor_->Undo().ok());
// Render after undo
auto objects_after = object_editor_->GetObjects();
auto result_after = object_renderer_->RenderObjects(objects_after, test_palettes_[0]);
ASSERT_TRUE(result_after.ok());
// Should have one fewer object
EXPECT_EQ(objects_after.size(), objects_before.size() - 1);
// Redo the operation
ASSERT_TRUE(object_editor_->Redo().ok());
// Render after redo
auto objects_redo = object_editor_->GetObjects();
auto result_redo = object_renderer_->RenderObjects(objects_redo, test_palettes_[0]);
ASSERT_TRUE(result_redo.ok());
// Should be back to original state
EXPECT_EQ(objects_redo.size(), objects_before.size());
}
// Test ROM integrity and validation
TEST_F(DungeonObjectRendererIntegrationTest, ROMIntegrityValidation) {
// Verify ROM is loaded correctly
EXPECT_TRUE(rom_->is_loaded());
EXPECT_GT(rom_->size(), 0);
// Test ROM header validation (if method exists)
// Note: ValidateHeader() may not be available in all ROM implementations
// EXPECT_TRUE(rom_->ValidateHeader().ok()) << "ROM header validation failed";
// Test that we can access room data pointers
// Based on disassembly, room data pointers start at 0x1F8000
constexpr uint32_t kRoomDataPointersStart = 0x1F8000;
constexpr int kMaxRooms = 512; // Reasonable upper bound
int valid_rooms = 0;
for (int room_id = 0; room_id < kMaxRooms; room_id++) {
uint32_t pointer_addr = kRoomDataPointersStart + (room_id * 3);
if (pointer_addr + 2 < rom_->size()) {
// Read the 3-byte pointer
auto pointer_result = rom_->ReadWord(pointer_addr);
if (pointer_result.ok()) {
uint32_t room_data_ptr = pointer_result.value();
// Check if pointer is reasonable (within ROM bounds)
if (room_data_ptr >= 0x80000 && room_data_ptr < rom_->size()) {
valid_rooms++;
}
}
}
}
// We should find many valid rooms (based on disassembly analysis)
EXPECT_GT(valid_rooms, 50) << "Found too few valid rooms: " << valid_rooms;
std::cout << "ROM integrity validation: " << valid_rooms << " valid rooms found" << std::endl;
}
// Test palette validation against vanilla values
TEST_F(DungeonObjectRendererIntegrationTest, PaletteValidation) {
// Load palette data and validate against expected vanilla values
auto palette_group = rom_->palette_group().dungeon_main;
EXPECT_GT(palette_group.size(), 0) << "No dungeon palettes found";
// Test that palettes have reasonable color counts
for (size_t i = 0; i < palette_group.size() && i < 10; i++) {
const auto& palette = palette_group[i];
EXPECT_GT(palette.size(), 0) << "Palette " << i << " is empty";
EXPECT_LE(palette.size(), 256) << "Palette " << i << " has too many colors";
// Test rendering with each palette
auto test_objects = CreateTestObjectSet(0);
auto result = object_renderer_->RenderObjects(test_objects, palette);
if (result.ok()) {
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Palette " << i << " rendered successfully with "
<< palette.size() << " colors" << std::endl;
}
}
}
// Test comprehensive room loading and validation
TEST_F(DungeonObjectRendererIntegrationTest, ComprehensiveRoomValidation) {
int total_objects = 0;
int rooms_with_objects = 0;
std::map<int, int> object_type_counts;
// Test loading a larger set of rooms
std::vector<int> extended_rooms = {
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0006, 0x0007, 0x0008, 0x0009,
0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x0010, 0x0011, 0x0012, 0x0013,
0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C,
0x001D, 0x001E, 0x001F, 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0026,
0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002E, 0x002F, 0x0030,
0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039,
0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x0040, 0x0041, 0x0042,
0x0043, 0x0044, 0x0045, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E,
0x004F, 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E
};
for (int room_id : extended_rooms) {
auto room_result = zelda3::LoadRoomFromRom(rom_.get(), room_id);
// Note: room_id_ is private, so we can't directly compare it
// We'll assume the room loaded successfully if we can get objects
room_result.LoadObjects();
const auto& objects = room_result.GetTileObjects();
if (!objects.empty()) {
rooms_with_objects++;
total_objects += objects.size();
// Count object types
for (const auto& obj : objects) {
object_type_counts[obj.id_]++;
}
// Test rendering this room
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
if (result.ok()) {
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
}
}
std::cout << "Comprehensive room validation results:" << std::endl;
std::cout << " Rooms with objects: " << rooms_with_objects << std::endl;
std::cout << " Total objects: " << total_objects << std::endl;
std::cout << " Unique object types: " << object_type_counts.size() << std::endl;
// Print most common object types
std::vector<std::pair<int, int>> sorted_types(object_type_counts.begin(), object_type_counts.end());
std::sort(sorted_types.begin(), sorted_types.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
std::cout << " Most common object types:" << std::endl;
for (size_t i = 0; i < std::min(size_t(10), sorted_types.size()); i++) {
std::cout << " 0x" << std::hex << sorted_types[i].first << std::dec
<< ": " << sorted_types[i].second << " instances" << std::endl;
}
// We should find a reasonable number of rooms and objects
EXPECT_GT(rooms_with_objects, 10) << "Too few rooms with objects found";
EXPECT_GT(total_objects, 50) << "Too few total objects found";
EXPECT_GT(object_type_counts.size(), 5) << "Too few unique object types found";
}
} // namespace zelda3
} // namespace yaze

View File

@@ -1,7 +1,10 @@
#include "app/zelda3/dungeon/object_renderer.h"
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
#include "app/zelda3/dungeon/object_drawer.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
#include "app/zelda3/dungeon/room_layout.h"
#include <gtest/gtest.h>
#include <memory>
@@ -10,6 +13,7 @@
#include "app/rom.h"
#include "app/gfx/snes_palette.h"
#include "app/gfx/background_buffer.h"
#include "testing.h"
#include "test_utils.h"
@@ -17,643 +21,192 @@ namespace yaze {
namespace test {
/**
* @brief Advanced tests for actual dungeon object rendering scenarios
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
*
* These tests focus on real-world dungeon editing scenarios including:
* - Complex room layouts with multiple object types
* - Object interaction and collision detection
* - Performance with realistic dungeon configurations
* - Edge cases in dungeon editing workflows
* These tests validate that ObjectDrawer correctly renders dungeon objects
* to BackgroundBuffers using pattern-based drawing routines.
*/
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
protected:
void SetUp() override {
BoundRomTest::SetUp();
// Setup palette data before scenarios require it
SetupTestPalettes();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
// Create renderer
renderer_ = std::make_unique<zelda3::ObjectRenderer>(rom());
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
// Setup realistic dungeon scenarios
SetupDungeonScenarios();
// Setup test palette
palette_group_ = CreateTestPaletteGroup();
}
void TearDown() override {
renderer_.reset();
bg2_.reset();
bg1_.reset();
drawer_.reset();
BoundRomTest::TearDown();
}
std::unique_ptr<zelda3::ObjectRenderer> renderer_;
gfx::PaletteGroup CreateTestPaletteGroup() {
gfx::PaletteGroup group;
gfx::SnesPalette palette;
// Create standard dungeon palette
for (int i = 0; i < 16; i++) {
int intensity = i * 16;
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
}
group.AddPalette(palette);
return group;
}
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
zelda3::RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom());
obj.EnsureTilesLoaded();
return obj;
}
struct DungeonScenario {
std::string name;
std::vector<zelda3::RoomObject> objects;
zelda3::RoomLayout layout;
gfx::SnesPalette palette;
int expected_width;
int expected_height;
};
std::vector<DungeonScenario> scenarios_;
std::vector<gfx::SnesPalette> test_palettes_;
private:
void SetupDungeonScenarios() {
// Scenario 1: Empty room with basic walls
CreateEmptyRoomScenario();
// Scenario 2: Room with multiple object types
CreateMultiObjectScenario();
// Scenario 3: Complex room with all subtypes
CreateComplexRoomScenario();
// Scenario 4: Large room with many objects
CreateLargeRoomScenario();
// Scenario 5: Boss room configuration
CreateBossRoomScenario();
// Scenario 6: Puzzle room with interactive elements
CreatePuzzleRoomScenario();
}
void SetupTestPalettes() {
// Create different palettes for different dungeon themes
CreateDungeonPalette(); // Standard dungeon
CreateIcePalacePalette(); // Ice Palace theme
CreateDesertPalacePalette(); // Desert Palace theme
CreateDarkPalacePalette(); // Palace of Darkness theme
CreateBossRoomPalette(); // Boss room theme
}
void CreateEmptyRoomScenario() {
DungeonScenario scenario;
scenario.name = "Empty Room";
// Create basic wall objects around the perimeter
for (int x = 0; x < 16; x++) {
// Top and bottom walls
scenario.objects.emplace_back(0x10, x, 0, 0x12, 0); // Top wall
scenario.objects.emplace_back(0x10, x, 10, 0x12, 0); // Bottom wall
}
for (int y = 1; y < 10; y++) {
// Left and right walls
scenario.objects.emplace_back(0x11, 0, y, 0x12, 0); // Left wall
scenario.objects.emplace_back(0x11, 15, y, 0x12, 0); // Right wall
}
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[0]; // Dungeon palette
scenario.expected_width = 256;
scenario.expected_height = 176;
scenarios_.push_back(scenario);
}
void CreateMultiObjectScenario() {
DungeonScenario scenario;
scenario.name = "Multi-Object Room";
// Walls
scenario.objects.emplace_back(0x10, 0, 0, 0x12, 0); // Wall
scenario.objects.emplace_back(0x10, 1, 0, 0x12, 0); // Wall
scenario.objects.emplace_back(0x10, 0, 1, 0x12, 0); // Wall
// Decorative objects
scenario.objects.emplace_back(0x20, 5, 5, 0x12, 0); // Statue
scenario.objects.emplace_back(0x21, 8, 7, 0x12, 0); // Pot
// Interactive objects
scenario.objects.emplace_back(0xF9, 10, 8, 0x12, 0); // Chest
scenario.objects.emplace_back(0x13, 3, 3, 0x12, 0); // Stairs
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[0];
scenario.expected_width = 256;
scenario.expected_height = 176;
scenarios_.push_back(scenario);
}
void CreateComplexRoomScenario() {
DungeonScenario scenario;
scenario.name = "Complex Room";
// Subtype 1 objects (basic)
for (int i = 0; i < 10; i++) {
scenario.objects.emplace_back(i, (i % 8) * 2, (i / 8) * 2, 0x12, 0);
}
// Subtype 2 objects (complex)
for (int i = 0; i < 5; i++) {
scenario.objects.emplace_back(0x100 + i, (i % 4) * 3, (i / 4) * 3, 0x12, 0);
}
// Subtype 3 objects (special)
for (int i = 0; i < 3; i++) {
scenario.objects.emplace_back(0x200 + i, (i % 3) * 4, (i / 3) * 4, 0x12, 0);
}
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[1]; // Ice Palace palette
scenario.expected_width = 256;
scenario.expected_height = 176;
scenarios_.push_back(scenario);
}
void CreateLargeRoomScenario() {
DungeonScenario scenario;
scenario.name = "Large Room";
// Create a room with many objects (stress test scenario)
for (int i = 0; i < 100; i++) {
int x = (i % 16) * 2;
int y = (i / 16) * 2;
int object_id = (i % 50) + 0x10; // Mix of different object types
scenario.objects.emplace_back(object_id, x, y, 0x12, i % 3);
}
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[2]; // Desert Palace palette
scenario.expected_width = 512;
scenario.expected_height = 256;
scenarios_.push_back(scenario);
}
void CreateBossRoomScenario() {
DungeonScenario scenario;
scenario.name = "Boss Room";
// Boss room typically has special objects
scenario.objects.emplace_back(0x30, 7, 4, 0x12, 0); // Boss platform
scenario.objects.emplace_back(0x31, 7, 5, 0x12, 0); // Boss platform
scenario.objects.emplace_back(0x32, 8, 4, 0x12, 0); // Boss platform
scenario.objects.emplace_back(0x33, 8, 5, 0x12, 0); // Boss platform
// Walls around the room
for (int x = 0; x < 16; x++) {
scenario.objects.emplace_back(0x10, x, 0, 0x12, 0);
scenario.objects.emplace_back(0x10, x, 10, 0x12, 0);
}
for (int y = 1; y < 10; y++) {
scenario.objects.emplace_back(0x11, 0, y, 0x12, 0);
scenario.objects.emplace_back(0x11, 15, y, 0x12, 0);
}
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[4]; // Boss room palette
scenario.expected_width = 256;
scenario.expected_height = 176;
scenarios_.push_back(scenario);
}
void CreatePuzzleRoomScenario() {
DungeonScenario scenario;
scenario.name = "Puzzle Room";
// Puzzle rooms have specific interactive elements
scenario.objects.emplace_back(0x40, 4, 4, 0x12, 0); // Switch
scenario.objects.emplace_back(0x41, 8, 6, 0x12, 0); // Block
scenario.objects.emplace_back(0x42, 6, 8, 0x12, 0); // Pressure plate
// Chests for puzzle rewards
scenario.objects.emplace_back(0xF9, 2, 2, 0x12, 0); // Small chest
scenario.objects.emplace_back(0xFA, 12, 2, 0x12, 0); // Large chest
// Decorative elements
scenario.objects.emplace_back(0x50, 1, 5, 0x12, 0); // Torch
scenario.objects.emplace_back(0x51, 14, 5, 0x12, 0); // Torch
// Set ROM references and load tiles
for (auto& obj : scenario.objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
scenario.palette = test_palettes_[3]; // Dark Palace palette
scenario.expected_width = 256;
scenario.expected_height = 176;
scenarios_.push_back(scenario);
}
void CreateDungeonPalette() {
gfx::SnesPalette palette;
// Standard dungeon colors (grays and browns)
palette.AddColor(gfx::SnesColor(0x00, 0x00, 0x00)); // Black
palette.AddColor(gfx::SnesColor(0x20, 0x20, 0x20)); // Dark gray
palette.AddColor(gfx::SnesColor(0x40, 0x40, 0x40)); // Medium gray
palette.AddColor(gfx::SnesColor(0x60, 0x60, 0x60)); // Light gray
palette.AddColor(gfx::SnesColor(0x80, 0x80, 0x80)); // Very light gray
palette.AddColor(gfx::SnesColor(0xA0, 0xA0, 0xA0)); // Almost white
palette.AddColor(gfx::SnesColor(0xC0, 0xC0, 0xC0)); // White
palette.AddColor(gfx::SnesColor(0x80, 0x40, 0x20)); // Brown
palette.AddColor(gfx::SnesColor(0xA0, 0x60, 0x40)); // Light brown
palette.AddColor(gfx::SnesColor(0x60, 0x80, 0x40)); // Green
palette.AddColor(gfx::SnesColor(0x40, 0x60, 0x80)); // Blue
palette.AddColor(gfx::SnesColor(0x80, 0x40, 0x80)); // Purple
palette.AddColor(gfx::SnesColor(0x80, 0x80, 0x40)); // Yellow
palette.AddColor(gfx::SnesColor(0x80, 0x40, 0x40)); // Red
palette.AddColor(gfx::SnesColor(0x40, 0x80, 0x80)); // Cyan
palette.AddColor(gfx::SnesColor(0xFF, 0xFF, 0xFF)); // Pure white
test_palettes_.push_back(palette);
}
void CreateIcePalacePalette() {
gfx::SnesPalette palette;
// Ice Palace colors (blues and whites)
palette.AddColor(gfx::SnesColor(0x00, 0x00, 0x00)); // Black
palette.AddColor(gfx::SnesColor(0x20, 0x40, 0x80)); // Dark blue
palette.AddColor(gfx::SnesColor(0x40, 0x60, 0xA0)); // Medium blue
palette.AddColor(gfx::SnesColor(0x60, 0x80, 0xC0)); // Light blue
palette.AddColor(gfx::SnesColor(0x80, 0xA0, 0xE0)); // Very light blue
palette.AddColor(gfx::SnesColor(0xA0, 0xC0, 0xFF)); // Pale blue
palette.AddColor(gfx::SnesColor(0xC0, 0xE0, 0xFF)); // Almost white
palette.AddColor(gfx::SnesColor(0xE0, 0xF0, 0xFF)); // White
palette.AddColor(gfx::SnesColor(0x40, 0x80, 0xC0)); // Ice blue
palette.AddColor(gfx::SnesColor(0x60, 0xA0, 0xE0)); // Light ice
palette.AddColor(gfx::SnesColor(0x80, 0xC0, 0xFF)); // Pale ice
palette.AddColor(gfx::SnesColor(0x20, 0x60, 0xA0)); // Deep ice
palette.AddColor(gfx::SnesColor(0x00, 0x40, 0x80)); // Dark ice
palette.AddColor(gfx::SnesColor(0x60, 0x80, 0xA0)); // Gray-blue
palette.AddColor(gfx::SnesColor(0x80, 0xA0, 0xC0)); // Light gray-blue
palette.AddColor(gfx::SnesColor(0xFF, 0xFF, 0xFF)); // Pure white
test_palettes_.push_back(palette);
}
void CreateDesertPalacePalette() {
gfx::SnesPalette palette;
// Desert Palace colors (yellows, oranges, and browns)
palette.AddColor(gfx::SnesColor(0x00, 0x00, 0x00)); // Black
palette.AddColor(gfx::SnesColor(0x40, 0x20, 0x00)); // Dark brown
palette.AddColor(gfx::SnesColor(0x60, 0x40, 0x20)); // Medium brown
palette.AddColor(gfx::SnesColor(0x80, 0x60, 0x40)); // Light brown
palette.AddColor(gfx::SnesColor(0xA0, 0x80, 0x60)); // Very light brown
palette.AddColor(gfx::SnesColor(0xC0, 0xA0, 0x80)); // Tan
palette.AddColor(gfx::SnesColor(0xE0, 0xC0, 0xA0)); // Light tan
palette.AddColor(gfx::SnesColor(0xFF, 0xE0, 0xC0)); // Cream
palette.AddColor(gfx::SnesColor(0x80, 0x40, 0x00)); // Orange
palette.AddColor(gfx::SnesColor(0xA0, 0x60, 0x20)); // Light orange
palette.AddColor(gfx::SnesColor(0xC0, 0x80, 0x40)); // Pale orange
palette.AddColor(gfx::SnesColor(0xE0, 0xA0, 0x60)); // Very pale orange
palette.AddColor(gfx::SnesColor(0x60, 0x60, 0x20)); // Olive
palette.AddColor(gfx::SnesColor(0x80, 0x80, 0x40)); // Light olive
palette.AddColor(gfx::SnesColor(0xA0, 0xA0, 0x60)); // Very light olive
palette.AddColor(gfx::SnesColor(0xFF, 0xFF, 0xFF)); // Pure white
test_palettes_.push_back(palette);
}
void CreateDarkPalacePalette() {
gfx::SnesPalette palette;
// Palace of Darkness colors (dark purples and grays)
palette.AddColor(gfx::SnesColor(0x00, 0x00, 0x00)); // Black
palette.AddColor(gfx::SnesColor(0x20, 0x00, 0x20)); // Dark purple
palette.AddColor(gfx::SnesColor(0x40, 0x20, 0x40)); // Medium purple
palette.AddColor(gfx::SnesColor(0x60, 0x40, 0x60)); // Light purple
palette.AddColor(gfx::SnesColor(0x80, 0x60, 0x80)); // Very light purple
palette.AddColor(gfx::SnesColor(0xA0, 0x80, 0xA0)); // Pale purple
palette.AddColor(gfx::SnesColor(0xC0, 0xA0, 0xC0)); // Almost white purple
palette.AddColor(gfx::SnesColor(0x10, 0x10, 0x10)); // Very dark gray
palette.AddColor(gfx::SnesColor(0x30, 0x30, 0x30)); // Dark gray
palette.AddColor(gfx::SnesColor(0x50, 0x50, 0x50)); // Medium gray
palette.AddColor(gfx::SnesColor(0x70, 0x70, 0x70)); // Light gray
palette.AddColor(gfx::SnesColor(0x90, 0x90, 0x90)); // Very light gray
palette.AddColor(gfx::SnesColor(0xB0, 0xB0, 0xB0)); // Almost white
palette.AddColor(gfx::SnesColor(0xD0, 0xD0, 0xD0)); // Off white
palette.AddColor(gfx::SnesColor(0xF0, 0xF0, 0xF0)); // Near white
palette.AddColor(gfx::SnesColor(0xFF, 0xFF, 0xFF)); // Pure white
test_palettes_.push_back(palette);
}
void CreateBossRoomPalette() {
gfx::SnesPalette palette;
// Boss room colors (dramatic reds, golds, and blacks)
palette.AddColor(gfx::SnesColor(0x00, 0x00, 0x00)); // Black
palette.AddColor(gfx::SnesColor(0x40, 0x00, 0x00)); // Dark red
palette.AddColor(gfx::SnesColor(0x60, 0x20, 0x00)); // Dark red-orange
palette.AddColor(gfx::SnesColor(0x80, 0x40, 0x00)); // Red-orange
palette.AddColor(gfx::SnesColor(0xA0, 0x60, 0x20)); // Orange
palette.AddColor(gfx::SnesColor(0xC0, 0x80, 0x40)); // Light orange
palette.AddColor(gfx::SnesColor(0xE0, 0xA0, 0x60)); // Very light orange
palette.AddColor(gfx::SnesColor(0x80, 0x60, 0x00)); // Dark gold
palette.AddColor(gfx::SnesColor(0xA0, 0x80, 0x20)); // Gold
palette.AddColor(gfx::SnesColor(0xC0, 0xA0, 0x40)); // Light gold
palette.AddColor(gfx::SnesColor(0xE0, 0xC0, 0x60)); // Very light gold
palette.AddColor(gfx::SnesColor(0x20, 0x20, 0x20)); // Dark gray
palette.AddColor(gfx::SnesColor(0x40, 0x40, 0x40)); // Medium gray
palette.AddColor(gfx::SnesColor(0x60, 0x60, 0x60)); // Light gray
palette.AddColor(gfx::SnesColor(0x80, 0x80, 0x80)); // Very light gray
palette.AddColor(gfx::SnesColor(0xFF, 0xFF, 0xFF)); // Pure white
test_palettes_.push_back(palette);
}
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
gfx::PaletteGroup palette_group_;
};
// Scenario-based rendering tests
TEST_F(DungeonObjectRenderingTests, EmptyRoomRendering) {
ASSERT_GE(scenarios_.size(), 1) << "Empty room scenario not available";
// Test basic object drawing
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
const auto& scenario = scenarios_[0];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
bg1_->ClearBuffer();
bg2_->ClearBuffer();
ASSERT_TRUE(result.ok()) << "Empty room rendering failed: " << result.status().message();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Empty room bitmap not active";
EXPECT_GE(bitmap.width(), scenario.expected_width) << "Empty room width too small";
EXPECT_GE(bitmap.height(), scenario.expected_height) << "Empty room height too small";
// Verify wall objects are rendered
EXPECT_GT(bitmap.size(), 0) << "Empty room bitmap has no content";
// Verify buffers have content
auto& bg1_bitmap = bg1_->bitmap();
EXPECT_TRUE(bg1_bitmap.is_active());
EXPECT_GT(bg1_bitmap.width(), 0);
}
TEST_F(DungeonObjectRenderingTests, MultiObjectRoomRendering) {
ASSERT_GE(scenarios_.size(), 2) << "Multi-object scenario not available";
// Test objects on different layers
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
const auto& scenario = scenarios_[1];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
bg1_->ClearBuffer();
bg2_->ClearBuffer();
ASSERT_TRUE(result.ok()) << "Multi-object room rendering failed: " << result.status().message();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok());
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Multi-object room bitmap not active";
EXPECT_GE(bitmap.width(), scenario.expected_width) << "Multi-object room width too small";
EXPECT_GE(bitmap.height(), scenario.expected_height) << "Multi-object room height too small";
// Verify different object types are rendered
EXPECT_GT(bitmap.size(), 0) << "Multi-object room bitmap has no content";
// Both buffers should be active
EXPECT_TRUE(bg1_->bitmap().is_active());
EXPECT_TRUE(bg2_->bitmap().is_active());
}
TEST_F(DungeonObjectRenderingTests, ComplexRoomRendering) {
ASSERT_GE(scenarios_.size(), 3) << "Complex room scenario not available";
// Test empty object list
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
std::vector<zelda3::RoomObject> objects; // Empty
const auto& scenario = scenarios_[2];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
bg1_->ClearBuffer();
bg2_->ClearBuffer();
ASSERT_TRUE(result.ok()) << "Complex room rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Complex room bitmap not active";
EXPECT_GT(bitmap.width(), 0) << "Complex room width not positive";
EXPECT_GT(bitmap.height(), 0) << "Complex room height not positive";
// Verify all subtypes are rendered correctly
EXPECT_GT(bitmap.size(), 0) << "Complex room bitmap has no content";
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Should succeed (drawing nothing is valid)
EXPECT_TRUE(status.ok());
}
TEST_F(DungeonObjectRenderingTests, LargeRoomRendering) {
ASSERT_GE(scenarios_.size(), 4) << "Large room scenario not available";
// Test large object set
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
std::vector<zelda3::RoomObject> objects;
const auto& scenario = scenarios_[3];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Large room rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Large room bitmap not active";
EXPECT_GT(bitmap.width(), 0) << "Large room width not positive";
EXPECT_GT(bitmap.height(), 0) << "Large room height not positive";
// Verify performance with many objects
auto stats = renderer_->GetPerformanceStats();
EXPECT_GT(stats.objects_rendered, 0) << "Large room objects not rendered";
EXPECT_GT(stats.tiles_rendered, 0) << "Large room tiles not rendered";
}
TEST_F(DungeonObjectRenderingTests, BossRoomRendering) {
ASSERT_GE(scenarios_.size(), 5) << "Boss room scenario not available";
const auto& scenario = scenarios_[4];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Boss room rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Boss room bitmap not active";
EXPECT_GT(bitmap.width(), 0) << "Boss room width not positive";
EXPECT_GT(bitmap.height(), 0) << "Boss room height not positive";
// Verify boss-specific objects are rendered
EXPECT_GT(bitmap.size(), 0) << "Boss room bitmap has no content";
}
TEST_F(DungeonObjectRenderingTests, PuzzleRoomRendering) {
ASSERT_GE(scenarios_.size(), 6) << "Puzzle room scenario not available";
const auto& scenario = scenarios_[5];
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Puzzle room rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Puzzle room bitmap not active";
EXPECT_GT(bitmap.width(), 0) << "Puzzle room width not positive";
EXPECT_GT(bitmap.height(), 0) << "Puzzle room height not positive";
// Verify puzzle elements are rendered
EXPECT_GT(bitmap.size(), 0) << "Puzzle room bitmap has no content";
}
// Palette-specific rendering tests
TEST_F(DungeonObjectRenderingTests, PaletteConsistency) {
ASSERT_GE(scenarios_.size(), 1) << "Test scenario not available";
const auto& scenario = scenarios_[0];
// Render with different palettes
for (size_t i = 0; i < test_palettes_.size(); i++) {
auto result = renderer_->RenderObjects(scenario.objects, test_palettes_[i]);
ASSERT_TRUE(result.ok()) << "Palette " << i << " rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Palette " << i << " bitmap not active";
EXPECT_GT(bitmap.size(), 0) << "Palette " << i << " bitmap has no content";
// Create 100 test objects
for (int i = 0; i < 100; i++) {
int x = (i % 10) * 5;
int y = (i / 10) * 5;
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
}
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto start = std::chrono::high_resolution_clock::now();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
auto end = std::chrono::high_resolution_clock::now();
ASSERT_TRUE(status.ok());
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Should complete in reasonable time
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
}
// Performance tests with realistic scenarios
TEST_F(DungeonObjectRenderingTests, ScenarioPerformanceBenchmark) {
const int iterations = 10;
// Test boundary conditions
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
std::vector<zelda3::RoomObject> objects;
for (const auto& scenario : scenarios_) {
auto start_time = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; i++) {
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Scenario " << scenario.name
<< " rendering failed: " << result.status().message();
}
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// Each scenario should render within reasonable time
EXPECT_LT(duration.count(), 5000) << "Scenario " << scenario.name
<< " performance below expectations: "
<< duration.count() << "ms";
}
// Objects at boundaries
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_TRUE(status.ok());
}
// Memory usage tests with realistic scenarios
TEST_F(DungeonObjectRenderingTests, ScenarioMemoryUsage) {
size_t initial_memory = renderer_->GetMemoryUsage();
// Test various object types
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
// Test common object types
std::vector<int> object_types = {
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
0x09, 0x0A, // Diagonal objects
0x10, 0x11, 0x12, // Standard objects
0x20, 0x21, // Decorative objects
0x34, // Solid block
};
// Render all scenarios multiple times
for (int round = 0; round < 3; round++) {
for (const auto& scenario : scenarios_) {
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Scenario memory test failed: " << result.status().message();
for (int obj_type : object_types) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Some object types might not be valid, that's okay
if (!status.ok()) {
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
size_t final_memory = renderer_->GetMemoryUsage();
// Memory usage should not grow excessively
EXPECT_LT(final_memory, initial_memory * 5) << "Memory leak detected in scenario tests: "
<< initial_memory << " -> " << final_memory;
// Clear cache and verify memory reduction
renderer_->ClearCache();
size_t memory_after_clear = renderer_->GetMemoryUsage();
EXPECT_LE(memory_after_clear, final_memory) << "Cache clear did not reduce memory usage";
}
// Object interaction tests
TEST_F(DungeonObjectRenderingTests, ObjectOverlapHandling) {
// Create objects that overlap
std::vector<zelda3::RoomObject> overlapping_objects;
// Test error handling
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
// Test with null ROM
zelda3::ObjectDrawer null_drawer(nullptr);
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5));
// Two objects at the same position
overlapping_objects.emplace_back(0x10, 5, 5, 0x12, 0);
overlapping_objects.emplace_back(0x20, 5, 5, 0x12, 1); // Different layer
bg1_->ClearBuffer();
bg2_->ClearBuffer();
// Objects that partially overlap
overlapping_objects.emplace_back(0x30, 3, 3, 0x12, 0);
overlapping_objects.emplace_back(0x31, 4, 4, 0x12, 0);
// Set ROM references and load tiles
for (auto& obj : overlapping_objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
auto result = renderer_->RenderObjects(overlapping_objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Overlapping objects rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Overlapping objects bitmap not active";
EXPECT_GT(bitmap.size(), 0) << "Overlapping objects bitmap has no content";
}
TEST_F(DungeonObjectRenderingTests, LayerRenderingOrder) {
// Create objects on different layers
std::vector<zelda3::RoomObject> layered_objects;
// Background layer (0)
layered_objects.emplace_back(0x10, 5, 5, 0x12, 0);
// Middle layer (1)
layered_objects.emplace_back(0x20, 5, 5, 0x12, 1);
// Foreground layer (2)
layered_objects.emplace_back(0x30, 5, 5, 0x12, 2);
// Set ROM references and load tiles
for (auto& obj : layered_objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
auto result = renderer_->RenderObjects(layered_objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Layered objects rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Layered objects bitmap not active";
EXPECT_GT(bitmap.size(), 0) << "Layered objects bitmap has no content";
}
// Cache efficiency with realistic scenarios
TEST_F(DungeonObjectRenderingTests, ScenarioCacheEfficiency) {
renderer_->ClearCache();
// Render scenarios multiple times to test cache
for (int round = 0; round < 5; round++) {
for (const auto& scenario : scenarios_) {
auto result = renderer_->RenderObjects(scenario.objects, scenario.palette);
ASSERT_TRUE(result.ok()) << "Cache efficiency test failed: " << result.status().message();
}
}
auto stats = renderer_->GetPerformanceStats();
// Cache hit rate should be high after multiple renders
EXPECT_GE(stats.cache_hits, 0) << "Cache hits unexpectedly negative";
EXPECT_GE(stats.cache_hit_rate(), 0.0) << "Cache hit rate negative: " << stats.cache_hit_rate();
}
// Edge cases in dungeon editing
TEST_F(DungeonObjectRenderingTests, BoundaryObjectPlacement) {
// Create objects at room boundaries
std::vector<zelda3::RoomObject> boundary_objects;
// Objects at exact boundaries
boundary_objects.emplace_back(0x10, 0, 0, 0x12, 0); // Top-left
boundary_objects.emplace_back(0x11, 15, 0, 0x12, 0); // Top-right
boundary_objects.emplace_back(0x12, 0, 10, 0x12, 0); // Bottom-left
boundary_objects.emplace_back(0x13, 15, 10, 0x12, 0); // Bottom-right
// Objects just outside boundaries (should be handled gracefully)
boundary_objects.emplace_back(0x14, -1, 5, 0x12, 0); // Left edge
boundary_objects.emplace_back(0x15, 16, 5, 0x12, 0); // Right edge
boundary_objects.emplace_back(0x16, 5, -1, 0x12, 0); // Top edge
boundary_objects.emplace_back(0x17, 5, 11, 0x12, 0); // Bottom edge
// Set ROM references and load tiles
for (auto& obj : boundary_objects) {
obj.set_rom(rom());
obj.EnsureTilesLoaded();
}
auto result = renderer_->RenderObjects(boundary_objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Boundary objects rendering failed: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_TRUE(bitmap.is_active()) << "Boundary objects bitmap not active";
EXPECT_GT(bitmap.size(), 0) << "Boundary objects bitmap has no content";
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,212 @@
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
#include "app/zelda3/dungeon/object_drawer.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <chrono>
#include "app/rom.h"
#include "app/gfx/snes_palette.h"
#include "app/gfx/background_buffer.h"
#include "testing.h"
#include "test_utils.h"
namespace yaze {
namespace test {
/**
* @brief Tests for ObjectDrawer with realistic dungeon scenarios
*
* These tests validate that ObjectDrawer correctly renders dungeon objects
* to BackgroundBuffers using pattern-based drawing routines.
*/
class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
protected:
void SetUp() override {
BoundRomTest::SetUp();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
// Setup test palette
palette_group_ = CreateTestPaletteGroup();
}
void TearDown() override {
bg2_.reset();
bg1_.reset();
drawer_.reset();
BoundRomTest::TearDown();
}
gfx::PaletteGroup CreateTestPaletteGroup() {
gfx::PaletteGroup group;
gfx::SnesPalette palette;
// Create standard dungeon palette
for (int i = 0; i < 16; i++) {
int intensity = i * 16;
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
}
group.AddPalette(palette);
return group;
}
zelda3::RoomObject CreateTestObject(int id, int x, int y, int size = 0x12, int layer = 0) {
zelda3::RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom());
obj.EnsureTilesLoaded();
return obj;
}
std::unique_ptr<zelda3::ObjectDrawer> drawer_;
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
gfx::PaletteGroup palette_group_;
};
// Test basic object drawing
TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // Wall
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 0)); // Floor
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
// Verify buffers have content
auto& bg1_bitmap = bg1_->bitmap();
EXPECT_TRUE(bg1_bitmap.is_active());
EXPECT_GT(bg1_bitmap.width(), 0);
}
// Test objects on different layers
TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5, 0x12, 0)); // BG1
objects.push_back(CreateTestObject(0x20, 10, 10, 0x22, 1)); // BG2
objects.push_back(CreateTestObject(0x30, 15, 15, 0x12, 2)); // BG3
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
ASSERT_TRUE(status.ok());
// Both buffers should be active
EXPECT_TRUE(bg1_->bitmap().is_active());
EXPECT_TRUE(bg2_->bitmap().is_active());
}
// Test empty object list
TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
std::vector<zelda3::RoomObject> objects; // Empty
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Should succeed (drawing nothing is valid)
EXPECT_TRUE(status.ok());
}
// Test large object set
TEST_F(DungeonObjectRenderingTests, LargeObjectSet) {
std::vector<zelda3::RoomObject> objects;
// Create 100 test objects
for (int i = 0; i < 100; i++) {
int x = (i % 10) * 5;
int y = (i / 10) * 5;
objects.push_back(CreateTestObject(0x10 + (i % 20), x, y, 0x12, i % 2));
}
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto start = std::chrono::high_resolution_clock::now();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
auto end = std::chrono::high_resolution_clock::now();
ASSERT_TRUE(status.ok());
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Should complete in reasonable time
EXPECT_LT(duration.count(), 1000) << "Rendered 100 objects in " << duration.count() << "ms";
}
// Test boundary conditions
TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
std::vector<zelda3::RoomObject> objects;
// Objects at boundaries
objects.push_back(CreateTestObject(0x10, 0, 0, 0x12, 0)); // Origin
objects.push_back(CreateTestObject(0x10, 63, 63, 0x12, 0)); // Max valid
objects.push_back(CreateTestObject(0x10, 32, 32, 0x12, 0)); // Center
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_TRUE(status.ok());
}
// Test various object types
TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
// Test common object types
std::vector<int> object_types = {
0x00, 0x01, 0x02, 0x03, // Floor/wall objects
0x09, 0x0A, // Diagonal objects
0x10, 0x11, 0x12, // Standard objects
0x20, 0x21, // Decorative objects
0x34, // Solid block
};
for (int obj_type : object_types) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(obj_type, 10, 10, 0x12, 0));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = drawer_->DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
// Some object types might not be valid, that's okay
if (!status.ok()) {
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
}
// Test error handling
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
// Test with null ROM
zelda3::ObjectDrawer null_drawer(nullptr);
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5));
bg1_->ClearBuffer();
bg2_->ClearBuffer();
auto status = null_drawer.DrawObjectList(objects, *bg1_, *bg2_, palette_group_);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
} // namespace test
} // namespace yaze

View File

@@ -1,484 +0,0 @@
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <map>
#include <chrono>
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
#include "app/zelda3/dungeon/dungeon_object_editor.h"
#include "app/zelda3/dungeon/object_renderer.h"
#include "app/zelda3/dungeon/dungeon_editor_system.h"
#include "app/gfx/snes_palette.h"
namespace yaze {
namespace zelda3 {
/**
* @brief Mock ROM class for testing without real ROM files
*
* This class provides a mock ROM implementation that can be used for testing
* the dungeon object rendering system without requiring actual ROM files.
*/
class MockRom : public Rom {
public:
MockRom() {
// Initialize mock ROM data
InitializeMockData();
}
~MockRom() = default;
// Override key methods for testing
absl::Status LoadFromFile(const std::string& filename) {
// Mock implementation - always succeeds
is_loaded_ = true;
return absl::OkStatus();
}
bool is_loaded() const { return is_loaded_; }
size_t size() const { return mock_data_.size(); }
uint8_t operator[](size_t index) const {
if (index < mock_data_.size()) {
return mock_data_[index];
}
return 0xFF; // Default value for out-of-bounds
}
absl::StatusOr<uint8_t> ReadByte(size_t address) const {
if (address < mock_data_.size()) {
return mock_data_[address];
}
return absl::OutOfRangeError("Address out of range");
}
absl::StatusOr<uint16_t> ReadWord(size_t address) const {
if (address + 1 < mock_data_.size()) {
return static_cast<uint16_t>(mock_data_[address]) |
(static_cast<uint16_t>(mock_data_[address + 1]) << 8);
}
return absl::OutOfRangeError("Address out of range");
}
absl::Status ValidateHeader() const {
// Mock validation - always succeeds
return absl::OkStatus();
}
// Mock palette data
struct MockPaletteGroup {
std::vector<gfx::SnesPalette> palettes;
};
MockPaletteGroup& palette_group() { return mock_palette_group_; }
const MockPaletteGroup& palette_group() const { return mock_palette_group_; }
private:
void InitializeMockData() {
// Create mock ROM data (2MB)
mock_data_.resize(2 * 1024 * 1024, 0xFF);
// Set up mock ROM header
mock_data_[0x7FC0] = 'Z'; // ROM name start
mock_data_[0x7FC1] = 'E';
mock_data_[0x7FC2] = 'L';
mock_data_[0x7FC3] = 'D';
mock_data_[0x7FC4] = 'A';
mock_data_[0x7FC5] = '3';
mock_data_[0x7FC6] = 0x00; // Version
mock_data_[0x7FC7] = 0x00;
mock_data_[0x7FD5] = 0x21; // ROM type
mock_data_[0x7FD6] = 0x20; // ROM size
mock_data_[0x7FD7] = 0x00; // SRAM size
mock_data_[0x7FD8] = 0x00; // Country
mock_data_[0x7FD9] = 0x00; // License
mock_data_[0x7FDA] = 0x00; // Version
mock_data_[0x7FDB] = 0x00;
// Set up mock room data pointers starting at 0x1F8000
constexpr uint32_t kRoomDataPointersStart = 0x1F8000;
constexpr uint32_t kRoomDataStart = 0x0A8000;
for (int i = 0; i < 512; i++) {
uint32_t pointer_addr = kRoomDataPointersStart + (i * 3);
uint32_t room_data_addr = kRoomDataStart + (i * 100); // Mock room data
if (pointer_addr + 2 < mock_data_.size()) {
mock_data_[pointer_addr] = room_data_addr & 0xFF;
mock_data_[pointer_addr + 1] = (room_data_addr >> 8) & 0xFF;
mock_data_[pointer_addr + 2] = (room_data_addr >> 16) & 0xFF;
}
}
// Initialize mock palette data
InitializeMockPalettes();
is_loaded_ = true;
}
void InitializeMockPalettes() {
// Create mock dungeon palettes
for (int i = 0; i < 8; i++) {
gfx::SnesPalette palette;
// Create a simple 16-color palette
for (int j = 0; j < 16; j++) {
int intensity = j * 16;
palette.AddColor(gfx::SnesColor(intensity, intensity, intensity));
}
mock_palette_group_.palettes.push_back(palette);
}
}
std::vector<uint8_t> mock_data_;
MockPaletteGroup mock_palette_group_;
bool is_loaded_ = false;
};
/**
* @brief Mock room data generator
*/
class MockRoomGenerator {
public:
static Room GenerateMockRoom(int room_id, Rom* rom) {
Room room(room_id, rom);
// Set basic room properties
room.SetPalette(room_id % 8);
room.SetBlockset(room_id % 16);
room.SetSpriteset(room_id % 8);
room.SetFloor1(0x00);
room.SetFloor2(0x00);
room.SetMessageId(0x0000);
// Generate mock objects based on room type
GenerateMockObjects(room, room_id);
return room;
}
private:
static void GenerateMockObjects(Room& room, int room_id) {
// Generate different object sets based on room ID
if (room_id == 0x0000) {
// Ganon's room - special objects
room.AddTileObject(RoomObject(0x10, 8, 8, 0x12, 0));
room.AddTileObject(RoomObject(0x20, 12, 12, 0x22, 0));
room.AddTileObject(RoomObject(0x30, 16, 16, 0x12, 1));
} else if (room_id == 0x0002 || room_id == 0x0012) {
// Sewer rooms - water and pipes
room.AddTileObject(RoomObject(0x20, 5, 5, 0x22, 0));
room.AddTileObject(RoomObject(0x40, 10, 10, 0x12, 0));
room.AddTileObject(RoomObject(0x50, 15, 15, 0x32, 1));
} else {
// Standard rooms - basic objects
room.AddTileObject(RoomObject(0x10, 5, 5, 0x12, 0));
room.AddTileObject(RoomObject(0x20, 10, 10, 0x22, 0));
if (room_id % 3 == 0) {
room.AddTileObject(RoomObject(0xF9, 15, 15, 0x12, 1)); // Chest
}
if (room_id % 5 == 0) {
room.AddTileObject(RoomObject(0x13, 20, 20, 0x32, 2)); // Stairs
}
}
}
};
class DungeonObjectRendererMockTest : public ::testing::Test {
protected:
void SetUp() override {
// Create mock ROM
mock_rom_ = std::make_unique<MockRom>();
// Initialize dungeon editor system with mock ROM
dungeon_editor_system_ = std::make_unique<DungeonEditorSystem>(mock_rom_.get());
ASSERT_TRUE(dungeon_editor_system_->Initialize().ok());
// Initialize object editor
object_editor_ = std::make_shared<DungeonObjectEditor>(mock_rom_.get());
// Note: InitializeEditor() is private, so we skip this in mock tests
// Initialize object renderer
object_renderer_ = std::make_unique<ObjectRenderer>(mock_rom_.get());
// Generate mock room data
ASSERT_TRUE(GenerateMockRoomData().ok());
}
void TearDown() override {
object_renderer_.reset();
object_editor_.reset();
dungeon_editor_system_.reset();
mock_rom_.reset();
}
absl::Status GenerateMockRoomData() {
// Generate mock rooms for testing
std::vector<int> test_rooms = {0x0000, 0x0001, 0x0002, 0x0010, 0x0012, 0x0020};
for (int room_id : test_rooms) {
auto mock_room = MockRoomGenerator::GenerateMockRoom(room_id, mock_rom_.get());
rooms_[room_id] = mock_room;
std::cout << "Generated mock room 0x" << std::hex << room_id << std::dec
<< " with " << mock_room.GetTileObjects().size() << " objects" << std::endl;
}
// Get mock palettes
auto palette_group = mock_rom_->palette_group().palettes;
test_palettes_ = {palette_group[0], palette_group[1], palette_group[2]};
return absl::OkStatus();
}
// Helper methods
RoomObject CreateMockObject(int object_id, int x, int y, int size = 0x12, int layer = 0) {
RoomObject obj(object_id, x, y, size, layer);
obj.set_rom(mock_rom_.get());
obj.EnsureTilesLoaded();
return obj;
}
std::vector<RoomObject> CreateMockObjectSet() {
std::vector<RoomObject> objects;
objects.push_back(CreateMockObject(0x10, 5, 5, 0x12, 0)); // Wall
objects.push_back(CreateMockObject(0x20, 10, 10, 0x22, 0)); // Floor
objects.push_back(CreateMockObject(0xF9, 15, 15, 0x12, 1)); // Chest
return objects;
}
std::unique_ptr<MockRom> mock_rom_;
std::unique_ptr<DungeonEditorSystem> dungeon_editor_system_;
std::shared_ptr<DungeonObjectEditor> object_editor_;
std::unique_ptr<ObjectRenderer> object_renderer_;
std::map<int, Room> rooms_;
std::vector<gfx::SnesPalette> test_palettes_;
};
// Test basic mock ROM functionality
TEST_F(DungeonObjectRendererMockTest, MockROMBasicFunctionality) {
EXPECT_TRUE(mock_rom_->is_loaded());
EXPECT_GT(mock_rom_->size(), 0);
// Test ROM header validation
auto header_result = mock_rom_->ValidateHeader();
EXPECT_TRUE(header_result.ok());
// Test reading ROM data
auto byte_result = mock_rom_->ReadByte(0x7FC0);
EXPECT_TRUE(byte_result.ok());
EXPECT_EQ(byte_result.value(), 'Z');
auto word_result = mock_rom_->ReadWord(0x1F8000);
EXPECT_TRUE(word_result.ok());
EXPECT_GT(word_result.value(), 0);
}
// Test mock room generation
TEST_F(DungeonObjectRendererMockTest, MockRoomGeneration) {
EXPECT_GT(rooms_.size(), 0);
for (const auto& [room_id, room] : rooms_) {
// Note: room_id_ is private, so we can't directly access it in tests
EXPECT_GT(room.GetTileObjects().size(), 0);
std::cout << "Mock room 0x" << std::hex << room_id << std::dec
<< " has " << room.GetTileObjects().size() << " objects" << std::endl;
}
}
// Test object rendering with mock data
TEST_F(DungeonObjectRendererMockTest, MockObjectRendering) {
auto mock_objects = CreateMockObjectSet();
auto palette = test_palettes_[0];
auto result = object_renderer_->RenderObjects(mock_objects, palette);
ASSERT_TRUE(result.ok()) << "Failed to render mock objects: " << result.status().message();
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test mock room object rendering
TEST_F(DungeonObjectRendererMockTest, MockRoomObjectRendering) {
for (const auto& [room_id, room] : rooms_) {
const auto& objects = room.GetTileObjects();
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render mock room 0x" << std::hex << room_id << std::dec;
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Successfully rendered mock room 0x" << std::hex << room_id << std::dec
<< " with " << objects.size() << " objects" << std::endl;
}
}
// Test mock object editor functionality
TEST_F(DungeonObjectRendererMockTest, MockObjectEditorFunctionality) {
// Load a mock room
ASSERT_TRUE(object_editor_->LoadRoom(0x0000).ok());
// Add objects
ASSERT_TRUE(object_editor_->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor_->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Get objects and render them
const auto& objects = object_editor_->GetObjects();
EXPECT_GT(objects.size(), 0);
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render objects from mock editor";
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test mock object editor undo/redo
TEST_F(DungeonObjectRendererMockTest, MockObjectEditorUndoRedo) {
// Load a mock room and add objects
ASSERT_TRUE(object_editor_->LoadRoom(0x0000).ok());
ASSERT_TRUE(object_editor_->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor_->InsertObject(10, 10, 0x20, 0x22, 1).ok());
auto objects_before = object_editor_->GetObjects();
// Undo one operation
ASSERT_TRUE(object_editor_->Undo().ok());
auto objects_after = object_editor_->GetObjects();
EXPECT_EQ(objects_after.size(), objects_before.size() - 1);
// Redo the operation
ASSERT_TRUE(object_editor_->Redo().ok());
auto objects_redo = object_editor_->GetObjects();
EXPECT_EQ(objects_redo.size(), objects_before.size());
}
// Test mock dungeon editor system integration
TEST_F(DungeonObjectRendererMockTest, MockDungeonEditorSystemIntegration) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Get object editor from system
auto system_object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(system_object_editor, nullptr);
// Add objects through the system
ASSERT_TRUE(system_object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(system_object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Get objects and render them
const auto& objects = system_object_editor->GetObjects();
ASSERT_GT(objects.size(), 0);
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
ASSERT_TRUE(result.ok()) << "Failed to render objects from mock system";
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
// Test mock performance
TEST_F(DungeonObjectRendererMockTest, MockPerformanceTest) {
auto mock_objects = CreateMockObjectSet();
auto palette = test_palettes_[0];
auto start_time = std::chrono::high_resolution_clock::now();
// Render objects multiple times
for (int i = 0; i < 100; i++) {
auto result = object_renderer_->RenderObjects(mock_objects, palette);
ASSERT_TRUE(result.ok());
}
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// Should complete in reasonable time (less than 1000ms for 100 renders)
EXPECT_LT(duration.count(), 1000) << "Mock rendering too slow: " << duration.count() << "ms";
std::cout << "Mock performance test: 100 renders took " << duration.count() << "ms" << std::endl;
}
// Test mock error handling
TEST_F(DungeonObjectRendererMockTest, MockErrorHandling) {
// Test with empty object list
std::vector<RoomObject> empty_objects;
auto result = object_renderer_->RenderObjects(empty_objects, test_palettes_[0]);
// Should either succeed with empty bitmap or fail gracefully
if (!result.ok()) {
EXPECT_TRUE(absl::IsInvalidArgument(result.status()) ||
absl::IsFailedPrecondition(result.status()));
}
// Test with invalid object (no ROM set)
RoomObject invalid_object(0x10, 5, 5, 0x12, 0);
// Don't set ROM - this should cause an error
std::vector<RoomObject> invalid_objects = {invalid_object};
result = object_renderer_->RenderObjects(invalid_objects, test_palettes_[0]);
// May succeed or fail depending on implementation - just ensure it doesn't crash
// EXPECT_FALSE(result.ok());
}
// Test mock object type validation
TEST_F(DungeonObjectRendererMockTest, MockObjectTypeValidation) {
std::vector<int> object_types = {0x10, 0x20, 0x30, 0xF9, 0x13, 0x17};
for (int object_type : object_types) {
auto object = CreateMockObject(object_type, 10, 10, 0x12, 0);
std::vector<RoomObject> objects = {object};
auto result = object_renderer_->RenderObjects(objects, test_palettes_[0]);
if (result.ok()) {
auto bitmap = std::move(result.value());
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
std::cout << "Mock object type 0x" << std::hex << object_type << std::dec
<< " rendered successfully" << std::endl;
} else {
std::cout << "Mock object type 0x" << std::hex << object_type << std::dec
<< " failed to render: " << result.status().message() << std::endl;
}
}
}
// Test mock cache functionality
TEST_F(DungeonObjectRendererMockTest, MockCacheFunctionality) {
auto mock_objects = CreateMockObjectSet();
auto palette = test_palettes_[0];
// Reset performance stats
object_renderer_->ResetPerformanceStats();
// First render (should miss cache)
auto result1 = object_renderer_->RenderObjects(mock_objects, palette);
ASSERT_TRUE(result1.ok());
auto stats1 = object_renderer_->GetPerformanceStats();
// Second render with same objects (should hit cache)
auto result2 = object_renderer_->RenderObjects(mock_objects, palette);
ASSERT_TRUE(result2.ok());
auto stats2 = object_renderer_->GetPerformanceStats();
EXPECT_GE(stats2.cache_hits, stats1.cache_hits);
std::cout << "Mock cache test: " << stats2.cache_hits << " hits, "
<< stats2.cache_misses << " misses" << std::endl;
}
} // namespace zelda3
} // namespace yaze

View File

@@ -1,11 +1,12 @@
#include "test_dungeon_objects.h"
#include "mocks/mock_rom.h"
#include "app/zelda3/dungeon/object_parser.h"
#include "app/zelda3/dungeon/object_renderer.h"
#include "app/zelda3/dungeon/object_drawer.h"
#include "app/zelda3/dungeon/room_object.h"
#include "app/zelda3/dungeon/room_layout.h"
#include "app/gfx/snes_color.h"
#include "app/gfx/snes_palette.h"
#include "app/gfx/background_buffer.h"
#include "testing.h"
#include <vector>
@@ -116,8 +117,8 @@ TEST_F(TestDungeonObjects, ObjectParserBasicTest) {
EXPECT_FALSE(result->empty());
}
TEST_F(TestDungeonObjects, ObjectRendererBasicTest) {
zelda3::ObjectRenderer renderer(test_rom_.get());
TEST_F(TestDungeonObjects, ObjectDrawerBasicTest) {
zelda3::ObjectDrawer drawer(test_rom_.get());
// Create test object
auto room_object = zelda3::RoomObject(kTestObjectId, 0, 0, 0x12, 0);
@@ -129,11 +130,16 @@ TEST_F(TestDungeonObjects, ObjectRendererBasicTest) {
for (int i = 0; i < 16; i++) {
palette.AddColor(gfx::SnesColor(i * 16, i * 16, i * 16));
}
gfx::PaletteGroup palette_group;
palette_group.AddPalette(palette);
auto result = renderer.RenderObject(room_object, palette);
ASSERT_TRUE(result.ok());
EXPECT_GT(result->width(), 0);
EXPECT_GT(result->height(), 0);
// Create background buffers
gfx::BackgroundBuffer bg1(512, 512);
gfx::BackgroundBuffer bg2(512, 512);
auto status = drawer.DrawObject(room_object, bg1, bg2, palette_group);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
EXPECT_GT(bg1.bitmap().width(), 0);
}
TEST_F(TestDungeonObjects, RoomObjectTileLoadingTest) {
@@ -182,8 +188,8 @@ TEST_F(TestDungeonObjects, RoomObjectTileAccessTest) {
EXPECT_FALSE(bad_tile_result.ok());
}
TEST_F(TestDungeonObjects, ObjectRendererGraphicsSheetTest) {
zelda3::ObjectRenderer renderer(test_rom_.get());
TEST_F(TestDungeonObjects, ObjectDrawerGraphicsSheetTest) {
zelda3::ObjectDrawer drawer(test_rom_.get());
// Create test object
auto room_object = zelda3::RoomObject(kTestObjectId, 0, 0, 0x12, 0);
@@ -195,16 +201,21 @@ TEST_F(TestDungeonObjects, ObjectRendererGraphicsSheetTest) {
for (int i = 0; i < 16; i++) {
palette.AddColor(gfx::SnesColor(i * 16, i * 16, i * 16));
}
gfx::PaletteGroup palette_group;
palette_group.AddPalette(palette);
// Test rendering with graphics sheet lookup
auto result = renderer.RenderObject(room_object, palette);
ASSERT_TRUE(result.ok());
// Create background buffers
gfx::BackgroundBuffer bg1(512, 512);
gfx::BackgroundBuffer bg2(512, 512);
auto bitmap = std::move(result.value());
// Test drawing with graphics sheet lookup
auto status = drawer.DrawObject(room_object, bg1, bg2, palette_group);
ASSERT_TRUE(status.ok()) << "Drawing failed: " << status.message();
auto& bitmap = bg1.bitmap();
EXPECT_TRUE(bitmap.is_active());
EXPECT_NE(bitmap.surface(), nullptr);
EXPECT_GT(bitmap.width(), 0);
EXPECT_GT(bitmap.height(), 0);
}
TEST_F(TestDungeonObjects, BitmapCopySemanticsTest) {