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:
@@ -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.
|
||||
578
docs/DUNGEON_EDITOR_GUIDE.md
Normal file
578
docs/DUNGEON_EDITOR_GUIDE.md
Normal 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
212
test/integration/zelda3/dungeon_object_rendering_tests_new.cc
Normal file
212
test/integration/zelda3/dungeon_object_rendering_tests_new.cc
Normal 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
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user