Files
yaze/test/zelda3/dungeon_object_renderer_mock_test.cc
2025-09-27 00:25:45 -04:00

485 lines
16 KiB
C++

#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