backend-infra-engineer: Release v0.3.3 snapshot

This commit is contained in:
scawful
2025-11-21 21:35:50 -05:00
parent 3d71417f62
commit 476dd1cd1c
818 changed files with 65706 additions and 35514 deletions

View File

@@ -1,13 +1,14 @@
#include <gtest/gtest.h>
#include <chrono>
#include <map>
#include <memory>
#include <vector>
#include <map>
#include <chrono>
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/dungeon_editor_system.h"
#include "zelda3/dungeon/dungeon_object_editor.h"
#include "zelda3/dungeon/room.h"
namespace yaze {
namespace zelda3 {
@@ -19,18 +20,18 @@ class DungeonEditorSystemIntegrationTest : public ::testing::Test {
#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());
// Load test room data
ASSERT_TRUE(LoadTestRoomData().ok());
}
@@ -43,22 +44,23 @@ class DungeonEditorSystemIntegrationTest : public ::testing::Test {
absl::Status LoadTestRoomData() {
// Load representative rooms for testing
test_rooms_ = {0x0000, 0x0001, 0x0002, 0x0010, 0x0012, 0x0020};
for (int room_id : test_rooms_) {
auto room_result = dungeon_editor_system_->GetRoom(room_id);
if (room_result.ok()) {
rooms_[room_id] = room_result.value();
std::cout << "Loaded room 0x" << std::hex << room_id << std::dec << std::endl;
std::cout << "Loaded room 0x" << std::hex << room_id << std::dec
<< std::endl;
}
}
return absl::OkStatus();
}
std::string rom_path_;
std::unique_ptr<Rom> rom_;
std::unique_ptr<DungeonEditorSystem> dungeon_editor_system_;
std::vector<int> test_rooms_;
std::map<int, Room> rooms_;
};
@@ -74,19 +76,21 @@ TEST_F(DungeonEditorSystemIntegrationTest, BasicInitialization) {
TEST_F(DungeonEditorSystemIntegrationTest, RoomLoadingAndManagement) {
// Test loading a specific room
auto room_result = dungeon_editor_system_->GetRoom(0x0000);
ASSERT_TRUE(room_result.ok()) << "Failed to load room 0x0000: " << room_result.status().message();
ASSERT_TRUE(room_result.ok())
<< "Failed to load room 0x0000: " << room_result.status().message();
const auto& room = room_result.value();
// Note: room_id_ is private, so we can't directly access it in tests
// Test setting current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
EXPECT_EQ(dungeon_editor_system_->GetCurrentRoom(), 0x0000);
// Test loading another room
auto room2_result = dungeon_editor_system_->GetRoom(0x0001);
ASSERT_TRUE(room2_result.ok()) << "Failed to load room 0x0001: " << room2_result.status().message();
ASSERT_TRUE(room2_result.ok())
<< "Failed to load room 0x0001: " << room2_result.status().message();
const auto& room2 = room2_result.value();
// Note: room_id_ is private, so we can't directly access it in tests
}
@@ -96,22 +100,22 @@ TEST_F(DungeonEditorSystemIntegrationTest, ObjectEditorIntegration) {
// Get object editor from system
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Test object insertion
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Verify objects were added
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test object selection
ASSERT_TRUE(object_editor->SelectObject(5 * 16, 5 * 16).ok());
auto selection = object_editor->GetSelection();
EXPECT_EQ(selection.selected_objects.size(), 1);
// Test object deletion
ASSERT_TRUE(object_editor->DeleteSelectedObjects().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 1);
@@ -121,7 +125,7 @@ TEST_F(DungeonEditorSystemIntegrationTest, ObjectEditorIntegration) {
TEST_F(DungeonEditorSystemIntegrationTest, SpriteManagement) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Create sprite data
DungeonEditorSystem::SpriteData sprite_data;
sprite_data.sprite_id = 1;
@@ -131,31 +135,32 @@ TEST_F(DungeonEditorSystemIntegrationTest, SpriteManagement) {
sprite_data.y = 100;
sprite_data.layer = 0;
sprite_data.is_active = true;
// Add sprite
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
// Get sprites for room
auto sprites_result = dungeon_editor_system_->GetSpritesByRoom(0x0000);
ASSERT_TRUE(sprites_result.ok()) << "Failed to get sprites: " << sprites_result.status().message();
ASSERT_TRUE(sprites_result.ok())
<< "Failed to get sprites: " << sprites_result.status().message();
const auto& sprites = sprites_result.value();
EXPECT_EQ(sprites.size(), 1);
EXPECT_EQ(sprites[0].sprite_id, 1);
EXPECT_EQ(sprites[0].name, "Test Sprite");
// Update sprite
sprite_data.x = 150;
ASSERT_TRUE(dungeon_editor_system_->UpdateSprite(1, sprite_data).ok());
// Get updated sprite
auto sprite_result = dungeon_editor_system_->GetSprite(1);
ASSERT_TRUE(sprite_result.ok());
EXPECT_EQ(sprite_result.value().x, 150);
// Remove sprite
ASSERT_TRUE(dungeon_editor_system_->RemoveSprite(1).ok());
// Verify sprite was removed
auto sprites_after = dungeon_editor_system_->GetSpritesByRoom(0x0000);
ASSERT_TRUE(sprites_after.ok());
@@ -166,7 +171,7 @@ TEST_F(DungeonEditorSystemIntegrationTest, SpriteManagement) {
TEST_F(DungeonEditorSystemIntegrationTest, ItemManagement) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Create item data
DungeonEditorSystem::ItemData item_data;
item_data.item_id = 1;
@@ -176,31 +181,32 @@ TEST_F(DungeonEditorSystemIntegrationTest, ItemManagement) {
item_data.y = 200;
item_data.room_id = 0x0000;
item_data.is_hidden = false;
// Add item
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
// Get items for room
auto items_result = dungeon_editor_system_->GetItemsByRoom(0x0000);
ASSERT_TRUE(items_result.ok()) << "Failed to get items: " << items_result.status().message();
ASSERT_TRUE(items_result.ok())
<< "Failed to get items: " << items_result.status().message();
const auto& items = items_result.value();
EXPECT_EQ(items.size(), 1);
EXPECT_EQ(items[0].item_id, 1);
EXPECT_EQ(items[0].name, "Small Key");
// Update item
item_data.is_hidden = true;
ASSERT_TRUE(dungeon_editor_system_->UpdateItem(1, item_data).ok());
// Get updated item
auto item_result = dungeon_editor_system_->GetItem(1);
ASSERT_TRUE(item_result.ok());
EXPECT_TRUE(item_result.value().is_hidden);
// Remove item
ASSERT_TRUE(dungeon_editor_system_->RemoveItem(1).ok());
// Verify item was removed
auto items_after = dungeon_editor_system_->GetItemsByRoom(0x0000);
ASSERT_TRUE(items_after.ok());
@@ -221,32 +227,35 @@ TEST_F(DungeonEditorSystemIntegrationTest, EntranceManagement) {
entrance_data.target_x = 200;
entrance_data.target_y = 200;
entrance_data.is_bidirectional = true;
// Add entrance
ASSERT_TRUE(dungeon_editor_system_->AddEntrance(entrance_data).ok());
// Get entrances for room
auto entrances_result = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_result.ok()) << "Failed to get entrances: " << entrances_result.status().message();
ASSERT_TRUE(entrances_result.ok())
<< "Failed to get entrances: " << entrances_result.status().message();
const auto& entrances = entrances_result.value();
EXPECT_EQ(entrances.size(), 1);
EXPECT_EQ(entrances[0].name, "Test Entrance");
// Store the entrance ID for later removal
int entrance_id = entrances[0].entrance_id;
// Test room connection
ASSERT_TRUE(dungeon_editor_system_->ConnectRooms(0x0000, 0x0001, 150, 150, 250, 250).ok());
ASSERT_TRUE(
dungeon_editor_system_->ConnectRooms(0x0000, 0x0001, 150, 150, 250, 250)
.ok());
// Get updated entrances
auto entrances_after = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_after.ok());
EXPECT_GE(entrances_after.value().size(), 1);
// Remove entrance using the correct ID
ASSERT_TRUE(dungeon_editor_system_->RemoveEntrance(entrance_id).ok());
// Verify entrance was removed
auto entrances_final = dungeon_editor_system_->GetEntrancesByRoom(0x0000);
ASSERT_TRUE(entrances_final.ok());
@@ -262,47 +271,48 @@ TEST_F(DungeonEditorSystemIntegrationTest, DoorManagement) {
door_data.room_id = 0x0000;
door_data.x = 100;
door_data.y = 100;
door_data.direction = 0; // up
door_data.direction = 0; // up
door_data.target_room_id = 0x0001;
door_data.target_x = 200;
door_data.target_y = 200;
door_data.requires_key = false;
door_data.key_type = 0;
door_data.is_locked = false;
// Add door
ASSERT_TRUE(dungeon_editor_system_->AddDoor(door_data).ok());
// Get doors for room
auto doors_result = dungeon_editor_system_->GetDoorsByRoom(0x0000);
ASSERT_TRUE(doors_result.ok()) << "Failed to get doors: " << doors_result.status().message();
ASSERT_TRUE(doors_result.ok())
<< "Failed to get doors: " << doors_result.status().message();
const auto& doors = doors_result.value();
EXPECT_EQ(doors.size(), 1);
EXPECT_EQ(doors[0].door_id, 1);
EXPECT_EQ(doors[0].name, "Test Door");
// Update door
door_data.is_locked = true;
ASSERT_TRUE(dungeon_editor_system_->UpdateDoor(1, door_data).ok());
// Get updated door
auto door_result = dungeon_editor_system_->GetDoor(1);
ASSERT_TRUE(door_result.ok());
EXPECT_TRUE(door_result.value().is_locked);
// Set door key requirement
ASSERT_TRUE(dungeon_editor_system_->SetDoorKeyRequirement(1, true, 1).ok());
// Get door with key requirement
auto door_with_key = dungeon_editor_system_->GetDoor(1);
ASSERT_TRUE(door_with_key.ok());
EXPECT_TRUE(door_with_key.value().requires_key);
EXPECT_EQ(door_with_key.value().key_type, 1);
// Remove door
ASSERT_TRUE(dungeon_editor_system_->RemoveDoor(1).ok());
// Verify door was removed
auto doors_after = dungeon_editor_system_->GetDoorsByRoom(0x0000);
ASSERT_TRUE(doors_after.ok());
@@ -321,39 +331,40 @@ TEST_F(DungeonEditorSystemIntegrationTest, ChestManagement) {
chest_data.item_id = 10;
chest_data.item_quantity = 1;
chest_data.is_opened = false;
// Add chest
ASSERT_TRUE(dungeon_editor_system_->AddChest(chest_data).ok());
// Get chests for room
auto chests_result = dungeon_editor_system_->GetChestsByRoom(0x0000);
ASSERT_TRUE(chests_result.ok()) << "Failed to get chests: " << chests_result.status().message();
ASSERT_TRUE(chests_result.ok())
<< "Failed to get chests: " << chests_result.status().message();
const auto& chests = chests_result.value();
EXPECT_EQ(chests.size(), 1);
EXPECT_EQ(chests[0].chest_id, 1);
EXPECT_EQ(chests[0].item_id, 10);
// Update chest item
ASSERT_TRUE(dungeon_editor_system_->SetChestItem(1, 20, 5).ok());
// Get updated chest
auto chest_result = dungeon_editor_system_->GetChest(1);
ASSERT_TRUE(chest_result.ok());
EXPECT_EQ(chest_result.value().item_id, 20);
EXPECT_EQ(chest_result.value().item_quantity, 5);
// Set chest as opened
ASSERT_TRUE(dungeon_editor_system_->SetChestOpened(1, true).ok());
// Get opened chest
auto opened_chest = dungeon_editor_system_->GetChest(1);
ASSERT_TRUE(opened_chest.ok());
EXPECT_TRUE(opened_chest.value().is_opened);
// Remove chest
ASSERT_TRUE(dungeon_editor_system_->RemoveChest(1).ok());
// Verify chest was removed
auto chests_after = dungeon_editor_system_->GetChestsByRoom(0x0000);
ASSERT_TRUE(chests_after.ok());
@@ -374,25 +385,29 @@ TEST_F(DungeonEditorSystemIntegrationTest, RoomPropertiesManagement) {
properties.is_shop_room = false;
properties.music_id = 1;
properties.ambient_sound_id = 0;
// Set room properties
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
ASSERT_TRUE(
dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
// Get room properties
auto properties_result = dungeon_editor_system_->GetRoomProperties(0x0000);
ASSERT_TRUE(properties_result.ok()) << "Failed to get room properties: " << properties_result.status().message();
ASSERT_TRUE(properties_result.ok()) << "Failed to get room properties: "
<< properties_result.status().message();
const auto& retrieved_properties = properties_result.value();
EXPECT_EQ(retrieved_properties.room_id, 0x0000);
EXPECT_EQ(retrieved_properties.name, "Test Room");
EXPECT_EQ(retrieved_properties.description, "A test room for integration testing");
EXPECT_EQ(retrieved_properties.description,
"A test room for integration testing");
EXPECT_EQ(retrieved_properties.dungeon_id, 1);
// Update properties
properties.name = "Updated Test Room";
properties.is_boss_room = true;
ASSERT_TRUE(dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
ASSERT_TRUE(
dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
// Verify update
auto updated_properties = dungeon_editor_system_->GetRoomProperties(0x0000);
ASSERT_TRUE(updated_properties.ok());
@@ -415,14 +430,15 @@ TEST_F(DungeonEditorSystemIntegrationTest, DungeonSettingsManagement) {
settings.has_map = true;
settings.has_compass = true;
settings.has_big_key = true;
// Set dungeon settings
ASSERT_TRUE(dungeon_editor_system_->SetDungeonSettings(settings).ok());
// Get dungeon settings
auto settings_result = dungeon_editor_system_->GetDungeonSettings();
ASSERT_TRUE(settings_result.ok()) << "Failed to get dungeon settings: " << settings_result.status().message();
ASSERT_TRUE(settings_result.ok()) << "Failed to get dungeon settings: "
<< settings_result.status().message();
const auto& retrieved_settings = settings_result.value();
EXPECT_EQ(retrieved_settings.dungeon_id, 1);
EXPECT_EQ(retrieved_settings.name, "Test Dungeon");
@@ -438,31 +454,31 @@ TEST_F(DungeonEditorSystemIntegrationTest, DungeonSettingsManagement) {
TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Get object editor
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
// 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());
// Verify objects were added
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test undo
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 1);
// Test redo
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 2);
// Test multiple undos
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
ASSERT_TRUE(dungeon_editor_system_->Undo().ok());
EXPECT_EQ(object_editor->GetObjectCount(), 0);
// Test multiple redos
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
ASSERT_TRUE(dungeon_editor_system_->Redo().ok());
@@ -473,37 +489,39 @@ TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
TEST_F(DungeonEditorSystemIntegrationTest, ValidationFunctionality) {
// Set current room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
// Validate room
auto room_validation = dungeon_editor_system_->ValidateRoom(0x0000);
ASSERT_TRUE(room_validation.ok()) << "Room validation failed: " << room_validation.message();
ASSERT_TRUE(room_validation.ok())
<< "Room validation failed: " << room_validation.message();
// Validate dungeon
auto dungeon_validation = dungeon_editor_system_->ValidateDungeon();
ASSERT_TRUE(dungeon_validation.ok()) << "Dungeon validation failed: " << dungeon_validation.message();
ASSERT_TRUE(dungeon_validation.ok())
<< "Dungeon validation failed: " << dungeon_validation.message();
}
// Test save/load functionality
TEST_F(DungeonEditorSystemIntegrationTest, SaveLoadFunctionality) {
// Set current room and add some objects
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0000).ok());
auto object_editor = dungeon_editor_system_->GetObjectEditor();
ASSERT_NE(object_editor, nullptr);
ASSERT_TRUE(object_editor->InsertObject(5, 5, 0x10, 0x12, 0).ok());
ASSERT_TRUE(object_editor->InsertObject(10, 10, 0x20, 0x22, 1).ok());
// Save room
ASSERT_TRUE(dungeon_editor_system_->SaveRoom(0x0000).ok());
// Reload room
ASSERT_TRUE(dungeon_editor_system_->ReloadRoom(0x0000).ok());
// Verify objects are still there
auto reloaded_objects = object_editor->GetObjects();
EXPECT_EQ(reloaded_objects.size(), 2);
// Save entire dungeon
ASSERT_TRUE(dungeon_editor_system_->SaveDungeon().ok());
}
@@ -511,7 +529,7 @@ TEST_F(DungeonEditorSystemIntegrationTest, SaveLoadFunctionality) {
// Test performance with multiple operations
TEST_F(DungeonEditorSystemIntegrationTest, PerformanceTest) {
auto start_time = std::chrono::high_resolution_clock::now();
// Perform many operations
for (int i = 0; i < 100; i++) {
// Add sprite
@@ -521,9 +539,9 @@ TEST_F(DungeonEditorSystemIntegrationTest, PerformanceTest) {
sprite_data.x = i * 10;
sprite_data.y = i * 10;
sprite_data.layer = 0;
ASSERT_TRUE(dungeon_editor_system_->AddSprite(sprite_data).ok());
// Add item
DungeonEditorSystem::ItemData item_data;
item_data.item_id = i;
@@ -531,17 +549,20 @@ TEST_F(DungeonEditorSystemIntegrationTest, PerformanceTest) {
item_data.x = i * 15;
item_data.y = i * 15;
item_data.room_id = 0x0000;
ASSERT_TRUE(dungeon_editor_system_->AddItem(item_data).ok());
}
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time);
// Should complete in reasonable time (less than 5 seconds for 200 operations)
EXPECT_LT(duration.count(), 5000) << "Performance test too slow: " << duration.count() << "ms";
std::cout << "Performance test: 200 operations took " << duration.count() << "ms" << std::endl;
EXPECT_LT(duration.count(), 5000)
<< "Performance test too slow: " << duration.count() << "ms";
std::cout << "Performance test: 200 operations took " << duration.count()
<< "ms" << std::endl;
}
// Test error handling
@@ -549,26 +570,26 @@ TEST_F(DungeonEditorSystemIntegrationTest, ErrorHandling) {
// Test with invalid room ID
auto invalid_room = dungeon_editor_system_->GetRoom(-1);
EXPECT_FALSE(invalid_room.ok());
auto invalid_room_large = dungeon_editor_system_->GetRoom(10000);
EXPECT_FALSE(invalid_room_large.ok());
// Test with invalid sprite ID
auto invalid_sprite = dungeon_editor_system_->GetSprite(-1);
EXPECT_FALSE(invalid_sprite.ok());
// Test with invalid item ID
auto invalid_item = dungeon_editor_system_->GetItem(-1);
EXPECT_FALSE(invalid_item.ok());
// Test with invalid entrance ID
auto invalid_entrance = dungeon_editor_system_->GetEntrance(-1);
EXPECT_FALSE(invalid_entrance.ok());
// Test with invalid door ID
auto invalid_door = dungeon_editor_system_->GetDoor(-1);
EXPECT_FALSE(invalid_door.ok());
// Test with invalid chest ID
auto invalid_chest = dungeon_editor_system_->GetChest(-1);
EXPECT_FALSE(invalid_chest.ok());

View File

@@ -1,32 +1,32 @@
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production
// system) instead of the obsolete ObjectRenderer
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <vector>
#include "app/gfx/render/background_buffer.h"
#include "app/gfx/types/snes_palette.h"
#include "app/rom.h"
#include "test_utils.h"
#include "testing.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <chrono>
#include "app/rom.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gfx/render/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.
*/
@@ -56,18 +56,19 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
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 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();
@@ -83,15 +84,15 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
// 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(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());
@@ -101,28 +102,54 @@ TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
// 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
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 that a compact buffer (preview-sized) receives pixels
TEST_F(DungeonObjectRenderingTests, PreviewBufferRendersContent) {
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 1, 1, 0x12, 0)); // Small wall
gfx::BackgroundBuffer preview_bg(64, 64);
gfx::BackgroundBuffer preview_bg2(64, 64);
preview_bg.ClearBuffer();
preview_bg2.ClearBuffer();
auto status =
drawer_->DrawObjectList(objects, preview_bg, preview_bg2, palette_group_);
ASSERT_TRUE(status.ok());
auto& bitmap = preview_bg.bitmap();
EXPECT_TRUE(bitmap.is_active());
const auto& data = bitmap.data();
size_t non_zero = 0;
for (size_t i = 0; i < data.size(); i += 16) {
if (data[i] != 0) {
non_zero++;
}
}
EXPECT_GT(non_zero, 0u);
}
// 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());
@@ -131,40 +158,42 @@ TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
// 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);
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";
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
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());
}
@@ -173,24 +202,25 @@ TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
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
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_);
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
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
@@ -202,15 +232,15 @@ TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
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_);
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,28 +1,28 @@
// Integration tests for dungeon object rendering using ObjectDrawer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production system)
// instead of the obsolete ObjectRenderer
// Updated for DungeonEditorV2 architecture - uses ObjectDrawer (production
// system) instead of the obsolete ObjectRenderer
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <vector>
#include "app/gfx/background_buffer.h"
#include "app/gfx/snes_palette.h"
#include "app/rom.h"
#include "test_utils.h"
#include "testing.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/room.h"
#include "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.
*/
@@ -52,18 +52,19 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
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 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();
@@ -79,15 +80,15 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
// 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(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());
@@ -97,16 +98,16 @@ TEST_F(DungeonObjectRenderingTests, BasicObjectDrawing) {
// 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
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());
@@ -115,10 +116,10 @@ TEST_F(DungeonObjectRenderingTests, MultiLayerRendering) {
// 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());
@@ -127,40 +128,42 @@ TEST_F(DungeonObjectRenderingTests, EmptyObjectList) {
// 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);
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";
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
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());
}
@@ -169,24 +172,25 @@ TEST_F(DungeonObjectRenderingTests, BoundaryObjects) {
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
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_);
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
std::cout << "Object type 0x" << std::hex << obj_type << std::dec
<< " not renderable: " << status.message() << std::endl;
}
}
@@ -198,15 +202,15 @@ TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
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_);
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,9 +1,8 @@
#include "gtest/gtest.h"
#include "absl/status/status.h"
#include "app/gfx/background_buffer.h"
#include "app/gfx/snes_palette.h"
#include "app/rom.h"
#include "gtest/gtest.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/object_parser.h"
#include "zelda3/dungeon/room.h"
@@ -18,70 +17,68 @@ class DungeonRenderingIntegrationTest : public ::testing::Test {
// Create a mock ROM for testing
rom_ = std::make_unique<Rom>();
// Initialize with minimal ROM data for testing
std::vector<uint8_t> mock_rom_data(1024 * 1024, 0); // 1MB mock ROM
std::vector<uint8_t> mock_rom_data(1024 * 1024, 0); // 1MB mock ROM
rom_->LoadFromData(mock_rom_data);
// Create test rooms
room_0x00_ = CreateTestRoom(0x00); // Link's House
room_0x01_ = CreateTestRoom(0x01); // Another test room
room_0x00_ = CreateTestRoom(0x00); // Link's House
room_0x01_ = CreateTestRoom(0x01); // Another test room
}
void TearDown() override {
rom_.reset();
}
void TearDown() override { rom_.reset(); }
std::unique_ptr<Rom> rom_;
// Create a test room with various objects
Room CreateTestRoom(int room_id) {
Room room(room_id, rom_.get());
// Add some test objects to the room
std::vector<RoomObject> objects;
// Add floor objects (object 0x00)
objects.emplace_back(0x00, 5, 5, 3, 0); // Horizontal floor
objects.emplace_back(0x00, 10, 10, 5, 0); // Another floor section
objects.emplace_back(0x00, 5, 5, 3, 0); // Horizontal floor
objects.emplace_back(0x00, 10, 10, 5, 0); // Another floor section
// Add wall objects (object 0x01)
objects.emplace_back(0x01, 15, 15, 2, 0); // Vertical wall
objects.emplace_back(0x01, 20, 20, 4, 1); // Horizontal wall on BG2
objects.emplace_back(0x01, 15, 15, 2, 0); // Vertical wall
objects.emplace_back(0x01, 20, 20, 4, 1); // Horizontal wall on BG2
// Add diagonal stairs (object 0x09)
objects.emplace_back(0x09, 25, 25, 6, 0); // Diagonal stairs
objects.emplace_back(0x09, 25, 25, 6, 0); // Diagonal stairs
// Add solid blocks (object 0x34)
objects.emplace_back(0x34, 30, 30, 1, 0); // Solid block
objects.emplace_back(0x34, 35, 35, 2, 1); // Another solid block on BG2
objects.emplace_back(0x34, 30, 30, 1, 0); // Solid block
objects.emplace_back(0x34, 35, 35, 2, 1); // Another solid block on BG2
// Set ROM for all objects
for (auto& obj : objects) {
obj.set_rom(rom_.get());
}
// Add objects to room (this would normally be done by LoadObjects)
for (const auto& obj : objects) {
room.AddObject(obj);
}
return room;
}
// Create a test palette
gfx::SnesPalette CreateTestPalette() {
gfx::SnesPalette palette;
// Add some test colors
palette.AddColor(gfx::SnesColor(0, 0, 0)); // Transparent
palette.AddColor(gfx::SnesColor(255, 0, 0)); // Red
palette.AddColor(gfx::SnesColor(0, 255, 0)); // Green
palette.AddColor(gfx::SnesColor(0, 0, 255)); // Blue
palette.AddColor(gfx::SnesColor(255, 255, 0)); // Yellow
palette.AddColor(gfx::SnesColor(255, 0, 255)); // Magenta
palette.AddColor(gfx::SnesColor(0, 255, 255)); // Cyan
palette.AddColor(gfx::SnesColor(255, 255, 255)); // White
palette.AddColor(gfx::SnesColor(0, 0, 0)); // Transparent
palette.AddColor(gfx::SnesColor(255, 0, 0)); // Red
palette.AddColor(gfx::SnesColor(0, 255, 0)); // Green
palette.AddColor(gfx::SnesColor(0, 0, 255)); // Blue
palette.AddColor(gfx::SnesColor(255, 255, 0)); // Yellow
palette.AddColor(gfx::SnesColor(255, 0, 255)); // Magenta
palette.AddColor(gfx::SnesColor(0, 255, 255)); // Cyan
palette.AddColor(gfx::SnesColor(255, 255, 255)); // White
return palette;
}
gfx::PaletteGroup CreateTestPaletteGroup() {
gfx::PaletteGroup group;
group.AddPalette(CreateTestPalette());
@@ -96,19 +93,18 @@ class DungeonRenderingIntegrationTest : public ::testing::Test {
// Test full room rendering with ObjectDrawer
TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
Room test_room = CreateTestRoom(0x00);
// Test that room has objects
EXPECT_GT(test_room.GetTileObjects().size(), 0);
// Test ObjectDrawer can render the room
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status =
drawer.DrawObjectList(test_room.GetTileObjects(), test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
@@ -116,21 +112,20 @@ TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithDifferentPalettes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
// Test with different palette configurations
std::vector<gfx::PaletteGroup> palette_groups;
// Create multiple palette groups
for (int i = 0; i < 3; ++i) {
palette_groups.push_back(CreateTestPaletteGroup());
}
for (const auto& palette_group : palette_groups) {
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
@@ -140,11 +135,11 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Separate objects by layer
std::vector<RoomObject> bg1_objects;
std::vector<RoomObject> bg2_objects;
for (const auto& obj : test_room.GetTileObjects()) {
if (obj.GetLayerValue() == 0) {
bg1_objects.push_back(obj);
@@ -152,22 +147,18 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
bg2_objects.push_back(obj);
}
}
// Render BG1 objects
if (!bg1_objects.empty()) {
auto status = drawer.DrawObjectList(bg1_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(bg1_objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Render BG2 objects
if (!bg2_objects.empty()) {
auto status = drawer.DrawObjectList(bg2_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(bg2_objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
@@ -177,20 +168,18 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithVariousObjectSizes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Group objects by size
std::map<int, std::vector<RoomObject>> objects_by_size;
for (const auto& obj : test_room.GetTileObjects()) {
objects_by_size[obj.size_].push_back(obj);
}
// Render objects of each size
for (const auto& [size, objects] : objects_by_size) {
auto status = drawer.DrawObjectList(objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
}
@@ -199,41 +188,41 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithVariousObjectSizes) {
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingPerformance) {
// Create a room with many objects
Room large_room(0x00, rom_.get());
// Add many test objects
for (int i = 0; i < 200; ++i) {
int id = i % 65; // Cycle through object IDs 0-64
int x = (i * 2) % 60; // Spread across buffer
int id = i % 65; // Cycle through object IDs 0-64
int x = (i * 2) % 60; // Spread across buffer
int y = (i * 3) % 60;
int size = (i % 8) + 1; // Size 1-8
int layer = i % 2; // Alternate layers
int size = (i % 8) + 1; // Size 1-8
int layer = i % 2; // Alternate layers
RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom_.get());
large_room.AddObject(obj);
}
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Time the rendering operation
auto start_time = std::chrono::high_resolution_clock::now();
auto status = drawer.DrawObjectList(large_room.GetTileObjects(),
large_room.bg1_buffer(),
large_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(large_room.GetTileObjects(),
large_room.bg1_buffer(),
large_room.bg2_buffer(), palette_group);
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
// Should complete in reasonable time (less than 2 seconds for 200 objects)
EXPECT_LT(duration.count(), 2000);
std::cout << "Rendered room with 200 objects in " << duration.count() << "ms" << std::endl;
std::cout << "Rendered room with 200 objects in " << duration.count() << "ms"
<< std::endl;
}
// Test room rendering with edge case coordinates
@@ -241,26 +230,24 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Add objects at edge coordinates
std::vector<RoomObject> edge_objects;
edge_objects.emplace_back(0x34, 0, 0, 1, 0); // Origin
edge_objects.emplace_back(0x34, 63, 63, 1, 0); // Near buffer edge
edge_objects.emplace_back(0x34, 32, 32, 1, 0); // Center
edge_objects.emplace_back(0x34, 1, 1, 1, 0); // Near origin
edge_objects.emplace_back(0x34, 62, 62, 1, 0); // Near edge
edge_objects.emplace_back(0x34, 0, 0, 1, 0); // Origin
edge_objects.emplace_back(0x34, 63, 63, 1, 0); // Near buffer edge
edge_objects.emplace_back(0x34, 32, 32, 1, 0); // Center
edge_objects.emplace_back(0x34, 1, 1, 1, 0); // Near origin
edge_objects.emplace_back(0x34, 62, 62, 1, 0); // Near edge
// Set ROM for all objects
for (auto& obj : edge_objects) {
obj.set_rom(rom_.get());
}
auto status = drawer.DrawObjectList(edge_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(edge_objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
@@ -269,56 +256,53 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMixedObjectTypes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Add various object types
std::vector<RoomObject> mixed_objects;
// Floor objects
mixed_objects.emplace_back(0x00, 5, 5, 3, 0);
mixed_objects.emplace_back(0x01, 10, 10, 2, 0);
// Wall objects
mixed_objects.emplace_back(0x02, 15, 15, 4, 0);
mixed_objects.emplace_back(0x03, 20, 20, 1, 1);
// Diagonal objects
mixed_objects.emplace_back(0x09, 25, 25, 5, 0);
mixed_objects.emplace_back(0x0A, 30, 30, 3, 0);
// Solid objects
mixed_objects.emplace_back(0x34, 35, 35, 1, 0);
mixed_objects.emplace_back(0x33, 40, 40, 2, 1);
// Decorative objects
mixed_objects.emplace_back(0x36, 45, 45, 3, 0);
mixed_objects.emplace_back(0x38, 50, 50, 1, 0);
// Set ROM for all objects
for (auto& obj : mixed_objects) {
obj.set_rom(rom_.get());
}
auto status = drawer.DrawObjectList(mixed_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(mixed_objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}
// Test room rendering error handling
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingErrorHandling) {
Room test_room = CreateTestRoom(0x00);
// Test with null ROM
ObjectDrawer null_drawer(nullptr);
auto palette_group = CreateTestPaletteGroup();
auto status = null_drawer.DrawObjectList(test_room.GetTileObjects(),
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = null_drawer.DrawObjectList(
test_room.GetTileObjects(), test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kFailedPrecondition);
}
@@ -328,26 +312,25 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithInvalidObjectData) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
auto palette_group = CreateTestPaletteGroup();
// Create objects with invalid data
std::vector<RoomObject> invalid_objects;
invalid_objects.emplace_back(0x999, 5, 5, 1, 0); // Invalid object ID
invalid_objects.emplace_back(0x00, -1, -1, 1, 0); // Negative coordinates
invalid_objects.emplace_back(0x00, 100, 100, 1, 0); // Out of bounds coordinates
invalid_objects.emplace_back(0x00, 100, 100, 1,
0); // Out of bounds coordinates
invalid_objects.emplace_back(0x00, 5, 5, 255, 0); // Maximum size
// Set ROM for all objects
for (auto& obj : invalid_objects) {
obj.set_rom(rom_.get());
}
// Should handle gracefully
auto status = drawer.DrawObjectList(invalid_objects,
test_room.bg1_buffer(),
test_room.bg2_buffer(),
palette_group);
auto status = drawer.DrawObjectList(invalid_objects, test_room.bg1_buffer(),
test_room.bg2_buffer(), palette_group);
// Should succeed or fail gracefully
EXPECT_TRUE(status.ok() || status.code() == absl::StatusCode::kOk);
}

View File

@@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <filesystem>
#include "app/editor/message/message_data.h"
@@ -211,23 +212,24 @@ TEST_F(MessageRomTest, BuildDictionaryEntries_CorrectSize) {
TEST_F(MessageRomTest, ParseMessageData_CommandWithArgument_NoExtraCharacters) {
// This test specifically checks for the bug where command arguments
// were being incorrectly parsed as characters (e.g., capital 'A' after [W])
// The bug was caused by using a range-based for loop while also tracking position
// The bug was caused by using a range-based for loop while also tracking
// position
// Message: [W:01]ABC
// Bytes: 0x6B (W command), 0x01 (argument), 0x00 (A), 0x01 (B), 0x02 (C)
std::vector<uint8_t> data = {0x6B, 0x01, 0x00, 0x01, 0x02};
editor::MessageData message;
message.ID = 0;
message.Address = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
// Should be "[W:01]ABC" NOT "[W:01]BABC" or "[W:01]AABC"
EXPECT_EQ(parsed[0], "[W:01]ABC");
// The 'B' should not appear twice or be skipped
EXPECT_EQ(parsed[0].find("BABC"), std::string::npos);
EXPECT_EQ(parsed[0].find("AABC"), std::string::npos);
@@ -237,20 +239,20 @@ TEST_F(MessageRomTest, ParseMessageData_MultipleCommandsWithArguments) {
// Test multiple commands with arguments in sequence
// [W:01][C:02]AB
std::vector<uint8_t> data = {
0x6B, 0x01, // [W:01] - Window border command with arg
0x77, 0x02, // [C:02] - Color command with arg
0x00, 0x01 // AB - Regular characters
0x6B, 0x01, // [W:01] - Window border command with arg
0x77, 0x02, // [C:02] - Color command with arg
0x00, 0x01 // AB - Regular characters
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[W:01][C:02]AB");
// Make sure argument bytes (0x01, 0x02) weren't parsed as characters
EXPECT_EQ(parsed[0].find("BAB"), std::string::npos);
EXPECT_EQ(parsed[0].find("CAB"), std::string::npos);
@@ -260,17 +262,17 @@ TEST_F(MessageRomTest, ParseMessageData_CommandWithoutArgument) {
// Test command without argument followed by text
// [K]ABC - Wait for key command (no arg) followed by ABC
std::vector<uint8_t> data = {
0x7E, // [K] - Wait for key (no argument)
0x00, 0x01, 0x02 // ABC
0x7E, // [K] - Wait for key (no argument)
0x00, 0x01, 0x02 // ABC
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[K]ABC");
}
@@ -278,21 +280,21 @@ TEST_F(MessageRomTest, ParseMessageData_MixedCommands) {
// Test mix of commands with and without arguments
// [W:01]A[K]B[C:02]C
std::vector<uint8_t> data = {
0x6B, 0x01, // [W:01] - with arg
0x00, // A
0x7E, // [K] - no arg
0x01, // B
0x77, 0x02, // [C:02] - with arg
0x02 // C
0x6B, 0x01, // [W:01] - with arg
0x00, // A
0x7E, // [K] - no arg
0x01, // B
0x77, 0x02, // [C:02] - with arg
0x02 // C
};
editor::MessageData message;
message.ID = 0;
message.Data = data;
std::vector<editor::MessageData> message_data_vector = {message};
auto parsed = editor::ParseMessageData(message_data_vector, dictionary_);
EXPECT_EQ(parsed[0], "[W:01]A[K]B[C:02]C");
}

View File

@@ -1,21 +1,23 @@
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include "app/rom.h"
#include "testing.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_map.h"
#include "testing.h"
namespace yaze {
namespace zelda3 {
/**
* @brief Comprehensive overworld integration test that validates YAZE C++
* implementation against ZScream C# logic and existing test infrastructure
*
* @brief Comprehensive overworld integration test that validates YAZE C++
* implementation against ZScream C# logic and existing test
* infrastructure
*
* This test suite:
* 1. Validates overworld loading logic matches ZScream behavior
* 2. Tests integration with ZSCustomOverworld versions (vanilla, v2, v3)
@@ -28,15 +30,15 @@ class OverworldIntegrationTest : public ::testing::Test {
#if defined(__linux__)
GTEST_SKIP();
#endif
// Check if we should use real ROM or mock data
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
const char* skip_rom_tests = getenv("YAZE_SKIP_ROM_TESTS");
if (skip_rom_tests) {
GTEST_SKIP() << "ROM tests disabled";
}
if (rom_path_env && std::filesystem::exists(rom_path_env)) {
// Use real ROM for testing
rom_ = std::make_unique<Rom>();
@@ -47,7 +49,7 @@ class OverworldIntegrationTest : public ::testing::Test {
return;
}
}
// Fall back to mock data
use_real_rom_ = false;
rom_ = std::make_unique<Rom>();
@@ -63,36 +65,37 @@ class OverworldIntegrationTest : public ::testing::Test {
void SetupMockRomData() {
mock_rom_data_.resize(0x200000, 0x00);
// Basic ROM structure
mock_rom_data_[0x140145] = 0xFF; // Vanilla ASM
mock_rom_data_[0x140145] = 0xFF; // Vanilla ASM
// Tile16 expansion flag
mock_rom_data_[0x017D28] = 0x0F; // Vanilla
// Tile32 expansion flag
mock_rom_data_[0x01772E] = 0x04; // Vanilla
mock_rom_data_[0x017D28] = 0x0F; // Vanilla
// Tile32 expansion flag
mock_rom_data_[0x01772E] = 0x04; // Vanilla
// Basic map data
for (int i = 0; i < 160; i++) {
mock_rom_data_[0x012844 + i] = 0x00; // Small areas
mock_rom_data_[0x012844 + i] = 0x00; // Small areas
}
// Setup entrance data (matches ZScream Constants.OWEntranceMap/Pos/EntranceId)
// Setup entrance data (matches ZScream
// Constants.OWEntranceMap/Pos/EntranceId)
for (int i = 0; i < 129; i++) {
mock_rom_data_[0x0DB96F + (i * 2)] = i & 0xFF; // Map ID
mock_rom_data_[0x0DB96F + (i * 2)] = i & 0xFF; // Map ID
mock_rom_data_[0x0DB96F + (i * 2) + 1] = (i >> 8) & 0xFF;
mock_rom_data_[0x0DBA71 + (i * 2)] = (i * 16) & 0xFF; // Map Position
mock_rom_data_[0x0DBA71 + (i * 2)] = (i * 16) & 0xFF; // Map Position
mock_rom_data_[0x0DBA71 + (i * 2) + 1] = ((i * 16) >> 8) & 0xFF;
mock_rom_data_[0x0DBB73 + i] = i & 0xFF; // Entrance ID
mock_rom_data_[0x0DBB73 + i] = i & 0xFF; // Entrance ID
}
// Setup exit data (matches ZScream Constants.OWExit*)
for (int i = 0; i < 0x4F; i++) {
mock_rom_data_[0x015D8A + (i * 2)] = i & 0xFF; // Room ID
mock_rom_data_[0x015D8A + (i * 2)] = i & 0xFF; // Room ID
mock_rom_data_[0x015D8A + (i * 2) + 1] = (i >> 8) & 0xFF;
mock_rom_data_[0x015E28 + i] = i & 0xFF; // Map ID
mock_rom_data_[0x015E77 + (i * 2)] = i & 0xFF; // VRAM
mock_rom_data_[0x015E28 + i] = i & 0xFF; // Map ID
mock_rom_data_[0x015E77 + (i * 2)] = i & 0xFF; // VRAM
mock_rom_data_[0x015E77 + (i * 2) + 1] = (i >> 8) & 0xFF;
// Add other exit fields...
}
@@ -108,30 +111,30 @@ class OverworldIntegrationTest : public ::testing::Test {
TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
mock_rom_data_[0x01772E] = 0x04;
mock_rom_data_[0x140145] = 0xFF;
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x01772E] = 0x05;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
}
// Test Tile16 expansion detection
// Test Tile16 expansion detection
TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
mock_rom_data_[0x017D28] = 0x0F;
mock_rom_data_[0x140145] = 0xFF;
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x017D28] = 0x10;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
}
@@ -140,29 +143,30 @@ TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& entrances = overworld_->entrances();
EXPECT_EQ(entrances.size(), 129);
// Verify coordinate calculation matches ZScream logic:
// int p = mapPos >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
const auto& entrance = entrances[i];
uint16_t map_pos = i * 16; // Our test data
uint16_t map_id = i; // Our test data
uint16_t map_pos = i * 16; // Our test data
uint16_t map_id = i; // Our test data
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_x =
(x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(entrance.x_, expected_x);
EXPECT_EQ(entrance.y_, expected_y);
EXPECT_EQ(entrance.entrance_id_, i);
@@ -174,10 +178,10 @@ TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
TEST_F(OverworldIntegrationTest, ExitDataLoading) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& exits = overworld_->exits();
EXPECT_EQ(exits->size(), 0x4F);
// Verify exit data matches our test data
for (int i = 0; i < std::min(5, static_cast<int>(exits->size())); i++) {
const auto& exit = exits->at(i);
@@ -192,19 +196,19 @@ TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
// Test vanilla ASM (should limit to 0x80 maps)
mock_rom_data_[0x140145] = 0xFF;
overworld_ = std::make_unique<Overworld>(rom_.get());
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& items = overworld_->all_items();
// Test v3+ ASM (should support all 0xA0 maps)
mock_rom_data_[0x140145] = 0x03;
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& items_v3 = overworld_->all_items();
// v3 should have more comprehensive support
EXPECT_GE(items_v3.size(), items.size());
@@ -214,10 +218,10 @@ TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
TEST_F(OverworldIntegrationTest, MapSizeAssignment) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
// Verify all maps are initialized
for (const auto& map : maps) {
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
@@ -230,16 +234,16 @@ TEST_F(OverworldIntegrationTest, ZSCustomOverworldVersionIntegration) {
if (!use_real_rom_) {
GTEST_SKIP() << "Real ROM required for ZSCustomOverworld version testing";
}
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Check ASM version detection
auto version_byte = rom_->ReadByte(0x140145);
ASSERT_TRUE(version_byte.ok());
uint8_t asm_version = *version_byte;
if (asm_version == 0xFF) {
// Vanilla ROM
EXPECT_FALSE(overworld_->expanded_tile16());
@@ -250,50 +254,53 @@ TEST_F(OverworldIntegrationTest, ZSCustomOverworldVersionIntegration) {
EXPECT_TRUE(overworld_->expanded_tile16());
EXPECT_TRUE(overworld_->expanded_tile32());
}
// Verify version-specific features are properly detected
if (asm_version >= 0x03) {
// v3 features should be available
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160); // All 160 maps supported in v3
EXPECT_EQ(maps.size(), 160); // All 160 maps supported in v3
}
}
// Test compatibility with RomDependentTestSuite infrastructure
TEST_F(OverworldIntegrationTest, RomDependentTestSuiteCompatibility) {
if (!use_real_rom_) {
GTEST_SKIP() << "Real ROM required for RomDependentTestSuite compatibility testing";
GTEST_SKIP()
<< "Real ROM required for RomDependentTestSuite compatibility testing";
}
// Test that our overworld loading works with the same patterns as RomDependentTestSuite
// Test that our overworld loading works with the same patterns as
// RomDependentTestSuite
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Verify ROM-dependent features work correctly
EXPECT_TRUE(overworld_->is_loaded());
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
// Test that we can access the same data structures as RomDependentTestSuite
for (int i = 0; i < std::min(10, static_cast<int>(maps.size())); i++) {
const auto& map = maps[i];
// Verify map properties are accessible
EXPECT_GE(map.area_graphics(), 0);
EXPECT_GE(map.main_palette(), 0);
EXPECT_GE(map.area_size(), AreaSizeEnum::SmallArea);
EXPECT_LE(map.area_size(), AreaSizeEnum::TallArea);
}
// Test that sprite data is accessible (matches RomDependentTestSuite expectations)
// Test that sprite data is accessible (matches RomDependentTestSuite
// expectations)
const auto& sprites = overworld_->sprites(0);
EXPECT_EQ(sprites.size(), 3); // Three game states
EXPECT_EQ(sprites.size(), 3); // Three game states
// Test that item data is accessible
const auto& items = overworld_->all_items();
EXPECT_GE(items.size(), 0);
// Test that entrance/exit data is accessible
const auto& entrances = overworld_->entrances();
const auto& exits = overworld_->exits();
@@ -305,17 +312,17 @@ TEST_F(OverworldIntegrationTest, RomDependentTestSuiteCompatibility) {
TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Verify all major data structures are properly loaded
EXPECT_GT(overworld_->tiles16().size(), 0);
EXPECT_GT(overworld_->tiles32_unique().size(), 0);
// Verify map organization matches ZScream expectations
const auto& map_tiles = overworld_->map_tiles();
EXPECT_EQ(map_tiles.light_world.size(), 512);
EXPECT_EQ(map_tiles.dark_world.size(), 512);
EXPECT_EQ(map_tiles.special_world.size(), 512);
// Verify each world has proper 512x512 tile data
for (const auto& row : map_tiles.light_world) {
EXPECT_EQ(row.size(), 512);
@@ -326,16 +333,16 @@ TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
for (const auto& row : map_tiles.special_world) {
EXPECT_EQ(row.size(), 512);
}
// Verify overworld maps are properly initialized
const auto& maps = overworld_->overworld_maps();
EXPECT_EQ(maps.size(), 160);
for (const auto& map : maps) {
// TODO: Find a way to compare
// EXPECT_TRUE(map.bitmap_data() != nullptr);
}
// Verify tile types are loaded
const auto& tile_types = overworld_->all_tiles_types();
EXPECT_EQ(tile_types.size(), 0x200);
@@ -345,62 +352,64 @@ TEST_F(OverworldIntegrationTest, ComprehensiveDataIntegrity) {
TEST_F(OverworldIntegrationTest, ZScreamCoordinateCompatibility) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
const auto& entrances = overworld_->entrances();
EXPECT_EQ(entrances.size(), 129);
// Test coordinate calculation matches ZScream logic exactly
for (int i = 0; i < std::min(10, static_cast<int>(entrances.size())); i++) {
const auto& entrance = entrances[i];
// ZScream coordinate calculation:
// int p = mapPos >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) *
// 512); int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
uint16_t map_pos = entrance.map_pos_;
uint16_t map_id = entrance.map_id_;
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_x =
(x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(entrance.x_, expected_x);
EXPECT_EQ(entrance.y_, expected_y);
}
// Test hole coordinate calculation with 0x400 offset
const auto& holes = overworld_->holes();
EXPECT_EQ(holes.size(), 0x13);
for (int i = 0; i < std::min(5, static_cast<int>(holes.size())); i++) {
const auto& hole = holes[i];
// ZScream hole coordinate calculation:
// int p = (mapPos + 0x400) >> 1;
// int x = p % 64;
// int y = p >> 6;
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) * 512);
// int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
// int real_x = (x * 16) + (((mapId % 64) - (((mapId % 64) / 8) * 8)) *
// 512); int real_y = (y * 16) + (((mapId % 64) / 8) * 512);
uint16_t map_pos = hole.map_pos_;
uint16_t map_id = hole.map_id_;
int position = map_pos >> 1;
int x_coord = position % 64;
int y_coord = position >> 6;
int expected_x = (x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_x =
(x_coord * 16) + (((map_id % 64) - (((map_id % 64) / 8) * 8)) * 512);
int expected_y = (y_coord * 16) + (((map_id % 64) / 8) * 512);
EXPECT_EQ(hole.x_, expected_x);
EXPECT_EQ(hole.y_, expected_y);
EXPECT_TRUE(hole.is_hole_);
}
}
} // namespace zelda3
} // namespace yaze
} // namespace zelda3
} // namespace yaze

View File

@@ -1,8 +1,8 @@
// Integration tests for Room object load/save cycle with real ROM data
// Phase 1, Task 2.1: Full round-trip verification
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/rom.h"
#include "zelda3/dungeon/room.h"
@@ -23,22 +23,22 @@ class RoomIntegrationTest : public ::testing::Test {
void SetUp() override {
// Load the ROM file
rom_ = std::make_unique<Rom>();
// Check if ROM file exists
const char* rom_path = std::getenv("YAZE_TEST_ROM_PATH");
if (!rom_path) {
rom_path = "zelda3.sfc";
}
auto status = rom_->LoadFromFile(rom_path);
if (!status.ok()) {
GTEST_SKIP() << "ROM file not available: " << status.message();
}
// Create backup of ROM data for restoration after tests
original_rom_data_ = rom_->vector();
}
void TearDown() override {
// Restore original ROM data
if (rom_ && !original_rom_data_.empty()) {
@@ -47,7 +47,7 @@ class RoomIntegrationTest : public ::testing::Test {
}
}
}
std::unique_ptr<Rom> rom_;
std::vector<uint8_t> original_rom_data_;
};
@@ -59,34 +59,34 @@ class RoomIntegrationTest : public ::testing::Test {
TEST_F(RoomIntegrationTest, BasicLoadSaveRoundTrip) {
// Load room 0 (Hyrule Castle Entrance)
Room room1(0x00, rom_.get());
// Get original object count
size_t original_count = room1.GetTileObjects().size();
ASSERT_GT(original_count, 0) << "Room should have objects";
// Store original objects
auto original_objects = room1.GetTileObjects();
// Save the room (should write same data back)
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Load the room again
Room room2(0x00, rom_.get());
// Verify object count matches
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
// Verify each object matches
auto reloaded_objects = room2.GetTileObjects();
ASSERT_EQ(reloaded_objects.size(), original_objects.size());
for (size_t i = 0; i < original_objects.size(); i++) {
SCOPED_TRACE("Object " + std::to_string(i));
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_) << "ID mismatch";
EXPECT_EQ(reload.x(), orig.x()) << "X position mismatch";
EXPECT_EQ(reload.y(), orig.y()) << "Y position mismatch";
@@ -102,33 +102,34 @@ TEST_F(RoomIntegrationTest, BasicLoadSaveRoundTrip) {
TEST_F(RoomIntegrationTest, MultiRoomLoadSaveRoundTrip) {
// Test several different rooms to ensure broad coverage
std::vector<int> test_rooms = {0x00, 0x01, 0x02, 0x10, 0x20};
for (int room_id : test_rooms) {
SCOPED_TRACE("Room " + std::to_string(room_id));
// Load room
Room room1(room_id, rom_.get());
auto original_objects = room1.GetTileObjects();
if (original_objects.empty()) {
continue; // Skip empty rooms
continue; // Skip empty rooms
}
// Save objects
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Reload and verify
Room room2(room_id, rom_.get());
auto reloaded_objects = room2.GetTileObjects();
EXPECT_EQ(reloaded_objects.size(), original_objects.size());
// Verify objects match
for (size_t i = 0; i < std::min(original_objects.size(), reloaded_objects.size()); i++) {
for (size_t i = 0;
i < std::min(original_objects.size(), reloaded_objects.size()); i++) {
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_);
EXPECT_EQ(reload.x(), orig.x());
EXPECT_EQ(reload.y(), orig.y());
@@ -145,36 +146,48 @@ TEST_F(RoomIntegrationTest, MultiRoomLoadSaveRoundTrip) {
TEST_F(RoomIntegrationTest, LayerPreservation) {
// Load a room known to have multiple layers
Room room(0x01, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count objects per layer
int layer0_count = 0, layer1_count = 0, layer2_count = 0;
for (const auto& obj : objects) {
switch (obj.GetLayerValue()) {
case 0: layer0_count++; break;
case 1: layer1_count++; break;
case 2: layer2_count++; break;
case 0:
layer0_count++;
break;
case 1:
layer1_count++;
break;
case 2:
layer2_count++;
break;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x01, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify layer counts match
int reload_layer0 = 0, reload_layer1 = 0, reload_layer2 = 0;
for (const auto& obj : reloaded) {
switch (obj.GetLayerValue()) {
case 0: reload_layer0++; break;
case 1: reload_layer1++; break;
case 2: reload_layer2++; break;
case 0:
reload_layer0++;
break;
case 1:
reload_layer1++;
break;
case 2:
reload_layer2++;
break;
}
}
EXPECT_EQ(reload_layer0, layer0_count);
EXPECT_EQ(reload_layer1, layer1_count);
EXPECT_EQ(reload_layer2, layer2_count);
@@ -186,15 +199,15 @@ TEST_F(RoomIntegrationTest, LayerPreservation) {
TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count object types
int type1_count = 0; // ID < 0x100
int type2_count = 0; // ID 0x100-0x13F
int type3_count = 0; // ID >= 0xF00
for (const auto& obj : objects) {
if (obj.id_ >= 0xF00) {
type3_count++;
@@ -204,13 +217,13 @@ TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
type1_count++;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x00, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify type distribution matches
int reload_type1 = 0, reload_type2 = 0, reload_type3 = 0;
for (const auto& obj : reloaded) {
@@ -222,7 +235,7 @@ TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
reload_type1++;
}
}
EXPECT_EQ(reload_type1, type1_count);
EXPECT_EQ(reload_type2, type2_count);
EXPECT_EQ(reload_type3, type3_count);
@@ -235,54 +248,56 @@ TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
TEST_F(RoomIntegrationTest, BinaryDataExactMatch) {
// This test verifies that saving doesn't change ROM data
// when no modifications are made
Room room(0x02, rom_.get());
// Get the ROM location where objects are stored
auto rom_data = rom_->vector();
int object_pointer = (rom_data[0x874C + 2] << 16) +
(rom_data[0x874C + 1] << 8) +
(rom_data[0x874C]);
(rom_data[0x874C + 1] << 8) + (rom_data[0x874C]);
object_pointer = SnesToPc(object_pointer);
int room_address = object_pointer + (0x02 * 3);
int tile_address = (rom_data[room_address + 2] << 16) +
(rom_data[room_address + 1] << 8) +
rom_data[room_address];
(rom_data[room_address + 1] << 8) + rom_data[room_address];
int objects_location = SnesToPc(tile_address);
// Read original bytes (up to 500 bytes should cover most rooms)
std::vector<uint8_t> original_bytes;
for (int i = 0; i < 500 && objects_location + i < (int)rom_data.size(); i++) {
original_bytes.push_back(rom_data[objects_location + i]);
// Stop at final terminator
if (i > 0 && original_bytes[i] == 0xFF && original_bytes[i-1] == 0xFF) {
if (i > 0 && original_bytes[i] == 0xFF && original_bytes[i - 1] == 0xFF) {
// Check if this is the final terminator (3rd layer end)
bool might_be_final = true;
for (int j = i - 10; j < i - 1; j += 2) {
if (j >= 0 && original_bytes[j] == 0xFF && original_bytes[j+1] == 0xFF) {
if (j >= 0 && original_bytes[j] == 0xFF &&
original_bytes[j + 1] == 0xFF) {
// Found another FF FF marker, keep going
break;
}
}
if (might_be_final) break;
if (might_be_final)
break;
}
}
// Save objects (should write identical data)
ASSERT_TRUE(room.SaveObjects().ok());
// Read bytes after save
rom_data = rom_->vector();
std::vector<uint8_t> saved_bytes;
for (size_t i = 0; i < original_bytes.size() && objects_location + i < rom_data.size(); i++) {
for (size_t i = 0;
i < original_bytes.size() && objects_location + i < rom_data.size();
i++) {
saved_bytes.push_back(rom_data[objects_location + i]);
}
// Verify binary match
ASSERT_EQ(saved_bytes.size(), original_bytes.size());
for (size_t i = 0; i < original_bytes.size(); i++) {
EXPECT_EQ(saved_bytes[i], original_bytes[i])
EXPECT_EQ(saved_bytes[i], original_bytes[i])
<< "Byte mismatch at offset " << i;
}
}
@@ -294,24 +309,27 @@ TEST_F(RoomIntegrationTest, BinaryDataExactMatch) {
TEST_F(RoomIntegrationTest, KnownRoomData) {
// Room 0x00 (Hyrule Castle Entrance) - verify known objects exist
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0) << "Room 0x00 should have objects";
// Verify we can find common object types
bool found_type1 = false;
bool found_layer0 = false;
bool found_layer1 = false;
for (const auto& obj : objects) {
if (obj.id_ < 0x100) found_type1 = true;
if (obj.GetLayerValue() == 0) found_layer0 = true;
if (obj.GetLayerValue() == 1) found_layer1 = true;
if (obj.id_ < 0x100)
found_type1 = true;
if (obj.GetLayerValue() == 0)
found_layer0 = true;
if (obj.GetLayerValue() == 1)
found_layer1 = true;
}
EXPECT_TRUE(found_type1) << "Should have Type 1 objects";
EXPECT_TRUE(found_layer0) << "Should have Layer 0 objects";
// Verify coordinates are in valid range (0-63)
for (const auto& obj : objects) {
EXPECT_GE(obj.x(), 0);
@@ -324,4 +342,3 @@ TEST_F(RoomIntegrationTest, KnownRoomData) {
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -1,8 +1,9 @@
#include <gtest/gtest.h>
#include <memory>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include "app/rom.h"
#include "zelda3/overworld/overworld.h"
@@ -12,23 +13,25 @@ namespace yaze {
namespace zelda3 {
class SpritePositionTest : public ::testing::Test {
protected:
protected:
void SetUp() override {
// Try to load a vanilla ROM for testing
rom_ = std::make_unique<Rom>();
std::string rom_path = "bin/zelda3.sfc";
// Check if ROM exists in build directory
std::ifstream rom_file(rom_path);
if (rom_file.good()) {
ASSERT_TRUE(rom_->LoadFromFile(rom_path).ok()) << "Failed to load ROM from " << rom_path;
ASSERT_TRUE(rom_->LoadFromFile(rom_path).ok())
<< "Failed to load ROM from " << rom_path;
} else {
// Skip test if ROM not found
GTEST_SKIP() << "ROM file not found at " << rom_path;
}
overworld_ = std::make_unique<Overworld>(rom_.get());
ASSERT_TRUE(overworld_->Load(rom_.get()).ok()) << "Failed to load overworld";
ASSERT_TRUE(overworld_->Load(rom_.get()).ok())
<< "Failed to load overworld";
}
void TearDown() override {
@@ -47,24 +50,28 @@ TEST_F(SpritePositionTest, SpriteCoordinateSystem) {
const auto& sprites = overworld_->sprites(game_state);
std::cout << "\n=== Game State " << game_state << " ===" << std::endl;
std::cout << "Total sprites: " << sprites.size() << std::endl;
int sprite_count = 0;
for (const auto& sprite : sprites) {
if (!sprite.deleted() && sprite_count < 10) { // Show first 10 sprites
std::cout << "Sprite " << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(sprite.id()) << " (" << const_cast<Sprite&>(sprite).name() << ")" << std::endl;
std::cout << " Map ID: 0x" << std::hex << std::setw(2) << std::setfill('0')
<< sprite.map_id() << std::endl;
std::cout << " X: " << std::dec << sprite.x() << " (0x" << std::hex << sprite.x() << ")" << std::endl;
std::cout << " Y: " << std::dec << sprite.y() << " (0x" << std::hex << sprite.y() << ")" << std::endl;
if (!sprite.deleted() && sprite_count < 10) { // Show first 10 sprites
std::cout << "Sprite " << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(sprite.id()) << " ("
<< const_cast<Sprite&>(sprite).name() << ")" << std::endl;
std::cout << " Map ID: 0x" << std::hex << std::setw(2)
<< std::setfill('0') << sprite.map_id() << std::endl;
std::cout << " X: " << std::dec << sprite.x() << " (0x" << std::hex
<< sprite.x() << ")" << std::endl;
std::cout << " Y: " << std::dec << sprite.y() << " (0x" << std::hex
<< sprite.y() << ")" << std::endl;
std::cout << " map_x: " << std::dec << sprite.map_x() << std::endl;
std::cout << " map_y: " << std::dec << sprite.map_y() << std::endl;
// Calculate expected world ranges
int world_start = game_state * 0x40;
int world_end = world_start + 0x40;
std::cout << " World range: 0x" << std::hex << world_start << " - 0x" << world_end << std::endl;
std::cout << " World range: 0x" << std::hex << world_start << " - 0x"
<< world_end << std::endl;
sprite_count++;
}
}
@@ -76,30 +83,32 @@ TEST_F(SpritePositionTest, SpriteFilteringLogic) {
// Test the filtering logic used in DrawOverworldSprites
for (int current_world = 0; current_world < 3; current_world++) {
const auto& sprites = overworld_->sprites(current_world);
std::cout << "\n=== Testing World " << current_world << " Filtering ===" << std::endl;
std::cout << "\n=== Testing World " << current_world
<< " Filtering ===" << std::endl;
int visible_sprites = 0;
int total_sprites = 0;
for (const auto& sprite : sprites) {
if (!sprite.deleted()) {
total_sprites++;
// This is the filtering logic from DrawOverworldSprites
bool should_show = (sprite.map_id() < 0x40 + (current_world * 0x40) &&
sprite.map_id() >= (current_world * 0x40));
sprite.map_id() >= (current_world * 0x40));
if (should_show) {
visible_sprites++;
std::cout << " Visible: Sprite 0x" << std::hex << static_cast<int>(sprite.id())
<< " on map 0x" << sprite.map_id() << " at ("
<< std::dec << sprite.x() << ", " << sprite.y() << ")" << std::endl;
std::cout << " Visible: Sprite 0x" << std::hex
<< static_cast<int>(sprite.id()) << " on map 0x"
<< sprite.map_id() << " at (" << std::dec << sprite.x()
<< ", " << sprite.y() << ")" << std::endl;
}
}
}
std::cout << "World " << current_world << ": " << visible_sprites << "/"
std::cout << "World " << current_world << ": " << visible_sprites << "/"
<< total_sprites << " sprites visible" << std::endl;
}
}
@@ -109,45 +118,53 @@ TEST_F(SpritePositionTest, MapCoordinateCalculations) {
// Test how map coordinates should be calculated
for (int current_world = 0; current_world < 3; current_world++) {
const auto& sprites = overworld_->sprites(current_world);
std::cout << "\n=== World " << current_world << " Coordinate Analysis ===" << std::endl;
std::cout << "\n=== World " << current_world
<< " Coordinate Analysis ===" << std::endl;
for (const auto& sprite : sprites) {
if (!sprite.deleted() &&
if (!sprite.deleted() &&
sprite.map_id() < 0x40 + (current_world * 0x40) &&
sprite.map_id() >= (current_world * 0x40)) {
// Calculate map position
int sprite_map_id = sprite.map_id();
int local_map_index = sprite_map_id - (current_world * 0x40);
int map_col = local_map_index % 8;
int map_row = local_map_index / 8;
int map_canvas_x = map_col * 512; // kOverworldMapSize
int map_canvas_x = map_col * 512; // kOverworldMapSize
int map_canvas_y = map_row * 512;
std::cout << "Sprite 0x" << std::hex << static_cast<int>(sprite.id())
std::cout << "Sprite 0x" << std::hex << static_cast<int>(sprite.id())
<< " on map 0x" << sprite_map_id << std::endl;
std::cout << " Local map index: " << std::dec << local_map_index << std::endl;
std::cout << " Map position: (" << map_col << ", " << map_row << ")" << std::endl;
std::cout << " Map canvas pos: (" << map_canvas_x << ", " << map_canvas_y << ")" << std::endl;
std::cout << " Sprite global pos: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
std::cout << " Sprite local pos: (" << sprite.map_x() << ", " << sprite.map_y() << ")" << std::endl;
std::cout << " Local map index: " << std::dec << local_map_index
<< std::endl;
std::cout << " Map position: (" << map_col << ", " << map_row << ")"
<< std::endl;
std::cout << " Map canvas pos: (" << map_canvas_x << ", "
<< map_canvas_y << ")" << std::endl;
std::cout << " Sprite global pos: (" << sprite.x() << ", "
<< sprite.y() << ")" << std::endl;
std::cout << " Sprite local pos: (" << sprite.map_x() << ", "
<< sprite.map_y() << ")" << std::endl;
// Verify the calculation
int expected_global_x = map_canvas_x + sprite.map_x();
int expected_global_y = map_canvas_y + sprite.map_y();
std::cout << " Expected global: (" << expected_global_x << ", " << expected_global_y << ")" << std::endl;
std::cout << " Actual global: (" << sprite.x() << ", " << sprite.y() << ")" << std::endl;
if (expected_global_x == sprite.x() && expected_global_y == sprite.y()) {
std::cout << " Expected global: (" << expected_global_x << ", "
<< expected_global_y << ")" << std::endl;
std::cout << " Actual global: (" << sprite.x() << ", " << sprite.y()
<< ")" << std::endl;
if (expected_global_x == sprite.x() &&
expected_global_y == sprite.y()) {
std::cout << " ✓ Coordinates match!" << std::endl;
} else {
std::cout << " ✗ Coordinate mismatch!" << std::endl;
}
break; // Only test first sprite for brevity
break; // Only test first sprite for brevity
}
}
}