refactor: Replace deprecated dungeon editor guide with updated documentation

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

View File

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

View File

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

View File

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

View File

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

View File

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