backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -5,7 +5,7 @@
#include <memory>
#include <vector>
#include "app/rom.h"
#include "rom/rom.h"
#include "zelda3/dungeon/dungeon_editor_system.h"
#include "zelda3/dungeon/dungeon_object_editor.h"
#include "zelda3/dungeon/room.h"
@@ -48,7 +48,7 @@ class DungeonEditorSystemIntegrationTest : public ::testing::Test {
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();
rooms_[room_id] = std::move(room_result.value());
std::cout << "Loaded room 0x" << std::hex << room_id << std::dec
<< std::endl;
}
@@ -79,9 +79,6 @@ TEST_F(DungeonEditorSystemIntegrationTest, RoomLoadingAndManagement) {
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);
@@ -90,9 +87,6 @@ TEST_F(DungeonEditorSystemIntegrationTest, RoomLoadingAndManagement) {
auto room2_result = dungeon_editor_system_->GetRoom(0x0001);
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
}
// Test object editor integration
@@ -121,335 +115,6 @@ TEST_F(DungeonEditorSystemIntegrationTest, ObjectEditorIntegration) {
EXPECT_EQ(object_editor->GetObjectCount(), 1);
}
// Test sprite management
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;
sprite_data.name = "Test Sprite";
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
sprite_data.x = 100;
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();
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());
EXPECT_EQ(sprites_after.value().size(), 0);
}
// Test item management
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;
item_data.type = DungeonEditorSystem::ItemType::kKey;
item_data.name = "Small Key";
item_data.x = 200;
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();
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());
EXPECT_EQ(items_after.value().size(), 0);
}
// Test entrance management
TEST_F(DungeonEditorSystemIntegrationTest, EntranceManagement) {
// Create entrance data
DungeonEditorSystem::EntranceData entrance_data;
entrance_data.entrance_id = 1;
entrance_data.type = DungeonEditorSystem::EntranceType::kDoor;
entrance_data.name = "Test Entrance";
entrance_data.source_room_id = 0x0000;
entrance_data.target_room_id = 0x0001;
entrance_data.source_x = 100;
entrance_data.source_y = 100;
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();
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());
// 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());
EXPECT_EQ(entrances_final.value().size(), 0);
}
// Test door management
TEST_F(DungeonEditorSystemIntegrationTest, DoorManagement) {
// Create door data
DungeonEditorSystem::DoorData door_data;
door_data.door_id = 1;
door_data.name = "Test Door";
door_data.room_id = 0x0000;
door_data.x = 100;
door_data.y = 100;
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();
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());
EXPECT_EQ(doors_after.value().size(), 0);
}
// Test chest management
TEST_F(DungeonEditorSystemIntegrationTest, ChestManagement) {
// Create chest data
DungeonEditorSystem::ChestData chest_data;
chest_data.chest_id = 1;
chest_data.room_id = 0x0000;
chest_data.x = 100;
chest_data.y = 100;
chest_data.is_big_chest = false;
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();
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());
EXPECT_EQ(chests_after.value().size(), 0);
}
// Test room properties management
TEST_F(DungeonEditorSystemIntegrationTest, RoomPropertiesManagement) {
// Create room properties
DungeonEditorSystem::RoomProperties properties;
properties.room_id = 0x0000;
properties.name = "Test Room";
properties.description = "A test room for integration testing";
properties.dungeon_id = 1;
properties.floor_level = 0;
properties.is_boss_room = false;
properties.is_save_room = false;
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());
// 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();
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.dungeon_id, 1);
// Update properties
properties.name = "Updated Test Room";
properties.is_boss_room = true;
ASSERT_TRUE(
dungeon_editor_system_->SetRoomProperties(0x0000, properties).ok());
// Verify update
auto updated_properties = dungeon_editor_system_->GetRoomProperties(0x0000);
ASSERT_TRUE(updated_properties.ok());
EXPECT_EQ(updated_properties.value().name, "Updated Test Room");
EXPECT_TRUE(updated_properties.value().is_boss_room);
}
// Test dungeon settings management
TEST_F(DungeonEditorSystemIntegrationTest, DungeonSettingsManagement) {
// Create dungeon settings
DungeonEditorSystem::DungeonSettings settings;
settings.dungeon_id = 1;
settings.name = "Test Dungeon";
settings.description = "A test dungeon for integration testing";
settings.total_rooms = 10;
settings.starting_room_id = 0x0000;
settings.boss_room_id = 0x0001;
settings.music_theme_id = 1;
settings.color_palette_id = 0;
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();
const auto& retrieved_settings = settings_result.value();
EXPECT_EQ(retrieved_settings.dungeon_id, 1);
EXPECT_EQ(retrieved_settings.name, "Test Dungeon");
EXPECT_EQ(retrieved_settings.total_rooms, 10);
EXPECT_EQ(retrieved_settings.starting_room_id, 0x0000);
EXPECT_EQ(retrieved_settings.boss_room_id, 0x0001);
EXPECT_TRUE(retrieved_settings.has_map);
EXPECT_TRUE(retrieved_settings.has_compass);
EXPECT_TRUE(retrieved_settings.has_big_key);
}
// Test undo/redo functionality
TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
// Set current room
@@ -485,22 +150,6 @@ TEST_F(DungeonEditorSystemIntegrationTest, UndoRedoFunctionality) {
EXPECT_EQ(object_editor->GetObjectCount(), 2);
}
// Test validation functionality
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();
// Validate dungeon
auto dungeon_validation = dungeon_editor_system_->ValidateDungeon();
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
@@ -526,45 +175,6 @@ TEST_F(DungeonEditorSystemIntegrationTest, SaveLoadFunctionality) {
ASSERT_TRUE(dungeon_editor_system_->SaveDungeon().ok());
}
// 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
DungeonEditorSystem::SpriteData sprite_data;
sprite_data.sprite_id = i;
sprite_data.type = DungeonEditorSystem::SpriteType::kEnemy;
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;
item_data.type = DungeonEditorSystem::ItemType::kKey;
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);
// 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;
}
// Test error handling
TEST_F(DungeonEditorSystemIntegrationTest, ErrorHandling) {
// Test with invalid room ID
@@ -574,25 +184,25 @@ TEST_F(DungeonEditorSystemIntegrationTest, ErrorHandling) {
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 setting invalid room ID
auto invalid_set = dungeon_editor_system_->SetCurrentRoom(-1);
EXPECT_FALSE(invalid_set.ok());
// Test with invalid item ID
auto invalid_item = dungeon_editor_system_->GetItem(-1);
EXPECT_FALSE(invalid_item.ok());
auto invalid_set_large = dungeon_editor_system_->SetCurrentRoom(10000);
EXPECT_FALSE(invalid_set_large.ok());
}
// Test with invalid entrance ID
auto invalid_entrance = dungeon_editor_system_->GetEntrance(-1);
EXPECT_FALSE(invalid_entrance.ok());
// Test editor state
TEST_F(DungeonEditorSystemIntegrationTest, EditorState) {
// Get initial state
auto state = dungeon_editor_system_->GetEditorState();
EXPECT_EQ(state.current_room_id, 0);
EXPECT_FALSE(state.is_dirty);
// 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());
// Change room
ASSERT_TRUE(dungeon_editor_system_->SetCurrentRoom(0x0010).ok());
state = dungeon_editor_system_->GetEditorState();
EXPECT_EQ(state.current_room_id, 0x0010);
}
} // namespace zelda3

View File

@@ -0,0 +1,337 @@
// Integration tests for Dungeon Graphics Buffer Transparency
// Verifies that 3BPP→8BPP conversion preserves transparent pixels (value 0)
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstdio>
#include "rom/rom.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace zelda3 {
namespace test {
class DungeonGraphicsTransparencyTest : public ::testing::Test {
protected:
void SetUp() override {
rom_ = std::make_unique<Rom>();
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();
}
// Load all Zelda3 game data (metadata, palettes, gfx groups, graphics)
auto load_status = LoadGameData(*rom_, game_data_);
if (!load_status.ok()) {
GTEST_SKIP() << "Graphics loading failed: " << load_status.message();
}
}
std::unique_ptr<Rom> rom_;
GameData game_data_;
};
// Test 1: Verify graphics buffer has transparent pixels
TEST_F(DungeonGraphicsTransparencyTest, GraphicsBufferHasTransparentPixels) {
// The graphics buffer should contain many 0s representing transparent pixels
auto& gfx_buffer = game_data_.graphics_buffer;
ASSERT_GT(gfx_buffer.size(), 0);
// Count zeros in first 10 sheets (dungeon graphics)
int zero_count = 0;
int total_pixels = 0;
const int sheets_to_check = 10;
const int pixels_per_sheet = 4096;
for (int sheet = 0; sheet < sheets_to_check; sheet++) {
int offset = sheet * pixels_per_sheet;
if (offset + pixels_per_sheet > static_cast<int>(gfx_buffer.size())) break;
for (int i = 0; i < pixels_per_sheet; i++) {
if (gfx_buffer[offset + i] == 0) zero_count++;
total_pixels++;
}
}
float zero_percent = 100.0f * zero_count / total_pixels;
printf("[GraphicsBuffer] Zeros: %d / %d (%.1f%%)\n", zero_count, total_pixels,
zero_percent);
// In 3BPP graphics, we expect significant transparent pixels (10%+)
// If this is near 0%, something is wrong with the 8BPP conversion
EXPECT_GT(zero_percent, 5.0f)
<< "Graphics buffer should have at least 5% transparent pixels. "
<< "Got " << zero_percent << "%. This indicates the 3BPP→8BPP "
<< "conversion may not be preserving transparency correctly.";
}
// Test 2: Verify room graphics buffer after CopyRoomGraphicsToBuffer
TEST_F(DungeonGraphicsTransparencyTest, RoomGraphicsBufferHasTransparentPixels) {
// Create room 0 (Ganon's room - known to have walls)
Room room(0x00, rom_.get());
room.LoadRoomGraphics(0xFF);
room.CopyRoomGraphicsToBuffer();
// Access the room's current_gfx16_ buffer
const auto& gfx16 = room.get_gfx_buffer();
ASSERT_GT(gfx16.size(), 0);
// Count zeros in the room's graphics buffer
int zero_count = 0;
for (size_t i = 0; i < gfx16.size(); i++) {
if (gfx16[i] == 0) zero_count++;
}
float zero_percent = 100.0f * zero_count / gfx16.size();
printf("[RoomGraphics] Room 0: Zeros: %d / %zu (%.1f%%)\n", zero_count,
gfx16.size(), zero_percent);
// Log first 64 bytes (one tile's worth) to see actual values
printf("[RoomGraphics] First 64 bytes:\n");
for (int row = 0; row < 8; row++) {
printf(" Row %d: ", row);
for (int col = 0; col < 8; col++) {
printf("%02X ", gfx16[row * 128 + col]); // 128 = sheet width stride
}
printf("\n");
}
// Print value distribution
int value_counts[8] = {0};
int other_count = 0;
for (size_t i = 0; i < gfx16.size(); i++) {
if (gfx16[i] < 8) {
value_counts[gfx16[i]]++;
} else {
other_count++;
}
}
printf("[RoomGraphics] Value distribution:\n");
for (int v = 0; v < 8; v++) {
printf(" Value %d: %d (%.1f%%)\n", v, value_counts[v],
100.0f * value_counts[v] / gfx16.size());
}
if (other_count > 0) {
printf(" Values >7: %d (%.1f%%) - UNEXPECTED for 3BPP!\n", other_count,
100.0f * other_count / gfx16.size());
}
EXPECT_GT(zero_percent, 5.0f)
<< "Room graphics buffer should have transparent pixels. "
<< "Got " << zero_percent << "%. Check CopyRoomGraphicsToBuffer().";
// All values should be 0-7 for 3BPP graphics
EXPECT_EQ(other_count, 0)
<< "Found " << other_count << " pixels with values > 7. "
<< "3BPP graphics should only have values 0-7.";
}
// Test 3: Verify specific tile has expected mix of transparent/opaque
TEST_F(DungeonGraphicsTransparencyTest, SpecificTileTransparency) {
Room room(0x00, rom_.get());
room.LoadRoomGraphics(0xFF);
room.CopyRoomGraphicsToBuffer();
const auto& gfx16 = room.get_gfx_buffer();
// Check tile 0 in block 0 (should be typical dungeon graphics)
// Tile layout: 16 tiles per row, each tile 8x8 pixels
// Row stride: 128 bytes (16 tiles * 8 pixels)
int tile_id = 0;
int tile_col = tile_id % 16;
int tile_row = tile_id / 16;
int tile_base_x = tile_col * 8;
int tile_base_y = tile_row * 1024; // 8 rows * 128 bytes per row
int zeros_in_tile = 0;
int total_in_tile = 64; // 8x8
printf("[Tile %d] Pixel values:\n", tile_id);
for (int py = 0; py < 8; py++) {
printf(" ");
for (int px = 0; px < 8; px++) {
int src_index = (py * 128) + px + tile_base_x + tile_base_y;
uint8_t pixel = gfx16[src_index];
printf("%d ", pixel);
if (pixel == 0) zeros_in_tile++;
}
printf("\n");
}
float tile_zero_percent = 100.0f * zeros_in_tile / total_in_tile;
printf("[Tile %d] Transparent pixels: %d / %d (%.1f%%)\n", tile_id,
zeros_in_tile, total_in_tile, tile_zero_percent);
// Check a wall tile (ID 0x90 is commonly a wall tile)
tile_id = 0x90;
tile_col = tile_id % 16;
tile_row = tile_id / 16;
tile_base_x = tile_col * 8;
tile_base_y = tile_row * 1024;
zeros_in_tile = 0;
printf("\n[Tile 0x%02X] Pixel values:\n", tile_id);
for (int py = 0; py < 8; py++) {
printf(" ");
for (int px = 0; px < 8; px++) {
int src_index = (py * 128) + px + tile_base_x + tile_base_y;
if (src_index < static_cast<int>(gfx16.size())) {
uint8_t pixel = gfx16[src_index];
printf("%d ", pixel);
if (pixel == 0) zeros_in_tile++;
}
}
printf("\n");
}
printf("[Tile 0x%02X] Transparent pixels: %d / %d\n", tile_id, zeros_in_tile,
total_in_tile);
}
// Test 4: Verify wall objects have tiles loaded
TEST_F(DungeonGraphicsTransparencyTest, WallObjectsHaveTiles) {
Room room(0x00, rom_.get());
room.LoadRoomGraphics(0xFF);
room.LoadObjects(); // Load objects from ROM!
room.CopyRoomGraphicsToBuffer();
// Get the room's objects
auto& objects = room.GetTileObjects();
printf("[Objects] Room 0 has %zu objects\n", objects.size());
// Count objects by type and check tiles
int walls_0x00 = 0, walls_0x01_02 = 0, walls_0x60_plus = 0, other = 0;
int missing_tiles = 0;
for (size_t i = 0; i < objects.size() && i < 20; i++) { // First 20 objects
auto& obj = objects[i];
obj.SetRom(rom_.get());
obj.EnsureTilesLoaded();
printf("[Object %zu] id=0x%03X pos=(%d,%d) size=%d tiles=%zu\n", i, obj.id_,
obj.x(), obj.y(), obj.size(), obj.tiles().size());
if (obj.id_ == 0x00) {
walls_0x00++;
} else if (obj.id_ >= 0x01 && obj.id_ <= 0x02) {
walls_0x01_02++;
} else if (obj.id_ >= 0x60 && obj.id_ <= 0x6F) {
walls_0x60_plus++;
} else {
other++;
}
if (obj.tiles().empty()) {
missing_tiles++;
printf(" WARNING: Object 0x%03X has NO tiles!\n", obj.id_);
} else {
// Note: Some objects only need 1 tile (e.g., 0xC0) per ZScream's lookup table
// This is valid behavior, not a bug
// Print first 4 tile IDs
printf(" Tile IDs: ");
for (size_t t = 0; t < std::min(obj.tiles().size(), size_t(4)); t++) {
printf("0x%03X ", obj.tiles()[t].id_);
}
printf("\n");
}
}
printf("\n[Summary] walls_0x00=%d walls_0x01_02=%d walls_0x60+=%d other=%d\n",
walls_0x00, walls_0x01_02, walls_0x60_plus, other);
printf("[Summary] missing_tiles=%d\n", missing_tiles);
// Every object should have tiles loaded (tile count varies per object type)
EXPECT_EQ(missing_tiles, 0)
<< "Some objects have no tiles loaded - check EnsureTilesLoaded()";
}
// Test 5: Verify objects are actually drawn to bitmaps
TEST_F(DungeonGraphicsTransparencyTest, ObjectsDrawToBitmap) {
Room room(0x00, rom_.get());
room.LoadRoomGraphics(0xFF);
room.LoadObjects();
room.CopyRoomGraphicsToBuffer();
// Get background buffers - they create their own bitmaps when needed
auto& bg1 = room.bg1_buffer();
auto& bg2 = room.bg2_buffer();
// DON'T manually create bitmaps - let DrawFloor/DrawBackground create them
// with the correct size (512*512 = 262144 bytes)
// The DrawFloor call initializes the bitmap properly
bg1.DrawFloor(rom_->vector(), zelda3::tile_address, zelda3::tile_address_floor,
room.floor1());
bg2.DrawFloor(rom_->vector(), zelda3::tile_address, zelda3::tile_address_floor,
room.floor2());
// Get objects
auto& objects = room.GetTileObjects();
printf("[DrawTest] Room 0 has %zu objects\n", objects.size());
// Create ObjectDrawer with room's graphics buffer
ObjectDrawer drawer(rom_.get(), 0, room.get_gfx_buffer().data());
// Create a palette group (needed for draw)
gfx::PaletteGroup palette_group;
auto& dungeon_pal = game_data_.palette_groups.dungeon_main;
if (!dungeon_pal.empty()) {
palette_group.AddPalette(dungeon_pal[0]);
}
// Draw objects
auto status = drawer.DrawObjectList(objects, bg1, bg2, palette_group);
if (!status.ok()) {
printf("[DrawTest] DrawObjectList failed: %s\n",
std::string(status.message()).c_str());
}
// Check if any pixels were written to bg1
int nonzero_pixels_bg1 = 0;
int nonzero_pixels_bg2 = 0;
size_t bg1_size = 512 * 512;
size_t bg2_size = 512 * 512;
auto bg1_data = bg1.bitmap().data();
auto bg2_data = bg2.bitmap().data();
for (size_t i = 0; i < bg1_size; i++) {
if (bg1_data[i] != 0) nonzero_pixels_bg1++;
}
for (size_t i = 0; i < bg2_size; i++) {
if (bg2_data[i] != 0) nonzero_pixels_bg2++;
}
printf("[DrawTest] BG1 non-zero pixels: %d / %zu (%.2f%%)\n",
nonzero_pixels_bg1, bg1_size,
100.0f * nonzero_pixels_bg1 / bg1_size);
printf("[DrawTest] BG2 non-zero pixels: %d / %zu (%.2f%%)\n",
nonzero_pixels_bg2, bg2_size,
100.0f * nonzero_pixels_bg2 / bg2_size);
// We should have SOME pixels drawn
EXPECT_GT(nonzero_pixels_bg1 + nonzero_pixels_bg2, 0)
<< "No pixels were drawn to either background!";
// Print first few rows of bg1 to see the pattern
printf("[DrawTest] BG1 first 16x4 pixels:\n");
for (int y = 0; y < 4; y++) {
printf(" Row %d: ", y);
for (int x = 0; x < 16; x++) {
printf("%02X ", bg1_data[y * 512 + x]);
}
printf("\n");
}
}
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -14,7 +14,7 @@
#include "app/gfx/render/background_buffer.h"
#include "app/gfx/types/snes_palette.h"
#include "app/rom.h"
#include "rom/rom.h"
#include "test_utils.h"
#include "testing.h"
#include "zelda3/dungeon/object_drawer.h"
@@ -35,13 +35,19 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
void SetUp() override {
BoundRomTest::SetUp();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
// Create dummy graphics buffer
gfx_buffer_.resize(0x10000, 1); // Fill with 1s so we see something
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom(), 0, gfx_buffer_.data());
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
bg2_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
// Initialize bitmaps
std::vector<uint8_t> empty_data(512 * 512, 0);
bg1_->bitmap().Create(512, 512, 8, empty_data);
bg2_->bitmap().Create(512, 512, 8, empty_data);
// Setup test palette
palette_group_ = CreateTestPaletteGroup();
}
@@ -70,8 +76,17 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
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.SetRom(rom());
obj.EnsureTilesLoaded();
// Force add a tile if none loaded (for testing without real ROM data)
if (obj.tiles().empty()) {
gfx::TileInfo tile;
tile.id_ = 0;
tile.palette_ = 0;
obj.mutable_tiles().push_back(tile);
}
return obj;
}
@@ -79,6 +94,7 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
std::unique_ptr<gfx::BackgroundBuffer> bg1_;
std::unique_ptr<gfx::BackgroundBuffer> bg2_;
gfx::PaletteGroup palette_group_;
std::vector<uint8_t> gfx_buffer_;
};
// Test basic object drawing
@@ -124,6 +140,12 @@ TEST_F(DungeonObjectRenderingTests, PreviewBufferRendersContent) {
gfx::BackgroundBuffer preview_bg(64, 64);
gfx::BackgroundBuffer preview_bg2(64, 64);
// Initialize bitmaps
std::vector<uint8_t> empty_data(64 * 64, 0);
preview_bg.bitmap().Create(64, 64, 8, empty_data);
preview_bg2.bitmap().Create(64, 64, 8, empty_data);
preview_bg.ClearBuffer();
preview_bg2.ClearBuffer();
@@ -133,9 +155,9 @@ TEST_F(DungeonObjectRenderingTests, PreviewBufferRendersContent) {
auto& bitmap = preview_bg.bitmap();
EXPECT_TRUE(bitmap.is_active());
const auto data = bitmap.data();
const auto& data = bitmap.vector();
size_t non_zero = 0;
for (size_t i = 0; i < bitmap.size(); i += 16) {
for (size_t i = 0; i < data.size(); i++) {
if (data[i] != 0) {
non_zero++;
}
@@ -229,7 +251,7 @@ TEST_F(DungeonObjectRenderingTests, VariousObjectTypes) {
// Test error handling
TEST_F(DungeonObjectRenderingTests, ErrorHandling) {
// Test with null ROM
zelda3::ObjectDrawer null_drawer(nullptr);
zelda3::ObjectDrawer null_drawer(nullptr, 0);
std::vector<zelda3::RoomObject> objects;
objects.push_back(CreateTestObject(0x10, 5, 5));

View File

@@ -10,7 +10,7 @@
#include "app/gfx/background_buffer.h"
#include "app/gfx/snes_palette.h"
#include "app/rom.h"
#include "rom/rom.h"
#include "test_utils.h"
#include "testing.h"
#include "zelda3/dungeon/object_drawer.h"
@@ -32,7 +32,7 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
BoundRomTest::SetUp();
// Create drawer
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom());
drawer_ = std::make_unique<zelda3::ObjectDrawer>(rom(), 0);
// Create background buffers
bg1_ = std::make_unique<gfx::BackgroundBuffer>(512, 512);
@@ -66,7 +66,7 @@ class DungeonObjectRenderingTests : public TestRomManager::BoundRomTest {
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.SetRom(rom());
obj.EnsureTilesLoaded();
return obj;
}

View File

@@ -0,0 +1,513 @@
// ROM Validation Tests for Dungeon Object System
// These tests verify that our object parsing and rendering code correctly
// interprets actual ALTTP ROM data.
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "rom/rom.h"
#include "test_utils.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/object_parser.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace test {
/**
* @brief ROM validation tests for dungeon object system
*
* These tests verify that our code correctly reads and interprets
* actual data from the ALTTP ROM. They validate:
* - Object tile pointer tables
* - Tile count lookup tables
* - Object decoding from room data
* - Known room object layouts
*/
class DungeonObjectRomValidationTest : public ::testing::Test {
protected:
void SetUp() override {
rom_ = std::make_shared<Rom>();
std::string rom_path = TestRomManager::GetTestRomPath();
auto status = rom_->LoadFromFile(rom_path);
if (!status.ok()) {
GTEST_SKIP() << "ROM not available: " << rom_path;
}
}
std::shared_ptr<Rom> rom_;
};
// ============================================================================
// Subtype 1 Object Tile Pointer Validation
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, Subtype1TilePointerTable_ValidAddresses) {
// The subtype 1 tile pointer table is at kRoomObjectSubtype1 (0x8000)
// Each entry is 2 bytes pointing to tile data offset from 0x1B52
constexpr int kSubtype1TableBase = 0x8000;
constexpr int kTileDataBase = 0x1B52;
// Verify first few entries have valid pointers
for (int obj_id = 0; obj_id < 16; ++obj_id) {
int table_addr = kSubtype1TableBase + (obj_id * 2);
uint8_t lo = rom_->data()[table_addr];
uint8_t hi = rom_->data()[table_addr + 1];
uint16_t offset = lo | (hi << 8);
int tile_data_addr = kTileDataBase + offset;
// Tile data should be within ROM bounds and reasonable range
EXPECT_LT(tile_data_addr, rom_->size())
<< "Object 0x" << std::hex << obj_id << " tile pointer out of bounds";
EXPECT_GT(tile_data_addr, 0x1B52)
<< "Object 0x" << std::hex << obj_id << " tile pointer too low";
EXPECT_LT(tile_data_addr, 0x10000)
<< "Object 0x" << std::hex << obj_id << " tile pointer too high";
}
}
TEST_F(DungeonObjectRomValidationTest, Subtype1TilePointerTable_Object0x00) {
// Object 0x00 (floor) should have valid tile data pointer
constexpr int kSubtype1TableBase = 0x8000;
constexpr int kTileDataBase = 0x1B52;
uint8_t lo = rom_->data()[kSubtype1TableBase];
uint8_t hi = rom_->data()[kSubtype1TableBase + 1];
uint16_t offset = lo | (hi << 8);
// Object 0x00 offset should be within reasonable bounds
// The ROM stores offset 984 (0x03D8) for Object 0x00
EXPECT_GT(offset, 0) << "Object 0x00 should have non-zero tile pointer";
EXPECT_LT(offset, 0x4000) << "Object 0x00 tile pointer should be in valid range";
// Read first tile at that address
int tile_addr = kTileDataBase + offset;
uint16_t first_tile = rom_->data()[tile_addr] | (rom_->data()[tile_addr + 1] << 8);
// Should have valid tile info (non-zero)
EXPECT_NE(first_tile, 0) << "Object 0x00 should have valid tile data";
}
// ============================================================================
// Tile Count Lookup Table Validation
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, TileCountTable_KnownValues) {
// Verify tile counts match kSubtype1TileLengths from room_object.h
// These values are extracted from the game's ROM
zelda3::ObjectParser parser(rom_.get());
// Test known tile counts for common objects
struct TileCountTest {
int object_id;
int expected_tiles;
const char* description;
};
// Expected values from kSubtype1TileLengths in object_parser.cc:
// 0x00-0x0F: 4, 8, 8, 8, 8, 8, 8, 4, 4, 5, 5, 5, 5, 5, 5, 5
// 0x10-0x1F: 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
// 0x20-0x2F: 5, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6
// 0x30-0x3F: 6, 1, 1, 16, 1, 1, 16, 16, 6, 8, 12, 12, 4, 8, 4, 3
std::vector<TileCountTest> tests = {
{0x00, 4, "Floor object"},
{0x01, 8, "Wall rightwards 2x4"},
{0x10, 5, "Diagonal wall acute"},
{0x21, 9, "Edge rightwards 1x2+2"}, // kSubtype1TileLengths[0x21] = 9
{0x22, 3, "Edge rightwards has edge"}, // 3 tiles
{0x34, 1, "Solid 1x1 block"},
{0x33, 16, "4x4 block"}, // kSubtype1TileLengths[0x33] = 16
};
for (const auto& test : tests) {
auto info = parser.GetObjectSubtype(test.object_id);
ASSERT_TRUE(info.ok()) << "Failed to get subtype for 0x" << std::hex << test.object_id;
EXPECT_EQ(info->max_tile_count, test.expected_tiles)
<< test.description << " (0x" << std::hex << test.object_id << ")"
<< " expected " << std::dec << test.expected_tiles
<< " tiles, got " << info->max_tile_count;
}
}
// ============================================================================
// Object Decoding Validation
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, ObjectDecoding_Type1_TileDataLoads) {
// Create a Type 1 object and verify its tiles load correctly
zelda3::RoomObject obj(0x10, 5, 5, 0x12, 0); // Diagonal wall
obj.SetRom(rom_.get());
obj.EnsureTilesLoaded();
EXPECT_FALSE(obj.tiles().empty())
<< "Object 0x10 should have tiles loaded from ROM";
// Diagonal walls (0x10) should have 5 tiles
EXPECT_EQ(obj.tiles().size(), 5)
<< "Object 0x10 should have exactly 5 tiles";
// Verify tiles have valid IDs (non-zero, within range)
for (size_t i = 0; i < obj.tiles().size(); ++i) {
const auto& tile = obj.tiles()[i];
EXPECT_LT(tile.id_, 1024)
<< "Tile " << i << " ID should be within valid range";
EXPECT_LT(tile.palette_, 8)
<< "Tile " << i << " palette should be 0-7";
}
}
TEST_F(DungeonObjectRomValidationTest, ObjectDecoding_Type2_TileDataLoads) {
// Create a Type 2 object (0x100-0x1FF range)
zelda3::RoomObject obj(0x100, 5, 5, 0, 0); // First Type 2 object
obj.SetRom(rom_.get());
obj.EnsureTilesLoaded();
// Type 2 objects should have some tiles
EXPECT_FALSE(obj.tiles().empty())
<< "Type 2 object 0x100 should have tiles loaded from ROM";
}
TEST_F(DungeonObjectRomValidationTest, ObjectDecoding_Type3_TileDataLoads) {
// Create a Type 3 object (0xF80-0xFFF range)
zelda3::RoomObject obj(0xF80, 5, 5, 0, 0); // First Type 3 object (Water Face)
obj.SetRom(rom_.get());
obj.EnsureTilesLoaded();
// Type 3 objects should have some tiles
EXPECT_FALSE(obj.tiles().empty())
<< "Type 3 object 0xF80 should have tiles loaded from ROM";
}
// ============================================================================
// Draw Routine Mapping Validation
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, DrawRoutineMapping_AllType1ObjectsHaveRoutines) {
zelda3::ObjectDrawer drawer(rom_.get(), 0);
// All Type 1 objects (0x00-0xF7) should have valid draw routines
for (int id = 0x00; id <= 0xF7; ++id) {
int routine = drawer.GetDrawRoutineId(id);
EXPECT_GE(routine, 0)
<< "Object 0x" << std::hex << id << " should have a valid draw routine";
EXPECT_LT(routine, 40)
<< "Object 0x" << std::hex << id << " routine ID should be < 40";
}
}
TEST_F(DungeonObjectRomValidationTest, DrawRoutineMapping_Type3ObjectsHaveRoutines) {
zelda3::ObjectDrawer drawer(rom_.get(), 0);
// Key Type 3 objects should have valid draw routines
std::vector<int> type3_ids = {0xF80, 0xF81, 0xF82, // Water Face
0xF83, 0xF84, // Somaria Line
0xF97, 0xF98}; // Chests
for (int id : type3_ids) {
int routine = drawer.GetDrawRoutineId(id);
EXPECT_GE(routine, 0)
<< "Type 3 object 0x" << std::hex << id << " should have a valid draw routine";
}
}
// ============================================================================
// Room Data Validation (Known Rooms)
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, Room0_LinksHouse_HasExpectedStructure) {
// Room 0 is Link's House - verify we can load it
zelda3::Room room = zelda3::LoadRoomFromRom(rom_.get(), 0);
// Link's House should have some objects
const auto& objects = room.GetTileObjects();
// Room should have reasonable number of objects (not empty, not absurdly large)
EXPECT_GT(objects.size(), 0u) << "Room 0 should have objects";
EXPECT_LT(objects.size(), 200u) << "Room 0 should have reasonable object count";
}
TEST_F(DungeonObjectRomValidationTest, Room1_LinksHouseBasement_LoadsCorrectly) {
// Room 1 is typically basement/cellar
zelda3::Room room = zelda3::LoadRoomFromRom(rom_.get(), 1);
// Should have loaded successfully
EXPECT_GE(room.GetTileObjects().size(), 0u);
}
TEST_F(DungeonObjectRomValidationTest, HyruleCastleRoom_HasWallObjects) {
// Room 0x50 is a Hyrule Castle room
zelda3::Room room = zelda3::LoadRoomFromRom(rom_.get(), 0x50);
// Hyrule Castle rooms typically have wall objects
bool has_wall_objects = false;
for (const auto& obj : room.GetTileObjects()) {
// Wall objects are typically in 0x00-0x20 range
if (obj.id_ >= 0x00 && obj.id_ <= 0x30) {
has_wall_objects = true;
break;
}
}
EXPECT_TRUE(has_wall_objects || room.GetTileObjects().empty())
<< "Hyrule Castle room should have wall/floor objects";
}
// ============================================================================
// Object Dimension Calculations with Real Data
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, ObjectDimensions_MatchesROMTileCount) {
zelda3::ObjectDrawer drawer(rom_.get(), 0);
zelda3::ObjectParser parser(rom_.get());
// Test objects and verify dimensions are consistent with tile counts
std::vector<int> test_objects = {0x00, 0x01, 0x10, 0x21, 0x34};
for (int obj_id : test_objects) {
zelda3::RoomObject obj(obj_id, 0, 0, 0, 0);
obj.SetRom(rom_.get());
auto dims = drawer.CalculateObjectDimensions(obj);
auto info = parser.GetObjectSubtype(obj_id);
// Dimensions should be positive
EXPECT_GT(dims.first, 0)
<< "Object 0x" << std::hex << obj_id << " width should be positive";
EXPECT_GT(dims.second, 0)
<< "Object 0x" << std::hex << obj_id << " height should be positive";
// Dimensions should be reasonable (not absurdly large)
EXPECT_LE(dims.first, 512)
<< "Object 0x" << std::hex << obj_id << " width should be <= 512";
EXPECT_LE(dims.second, 512)
<< "Object 0x" << std::hex << obj_id << " height should be <= 512";
}
}
// ============================================================================
// Graphics Buffer Validation
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, ObjectDrawing_ProducesNonEmptyOutput) {
// Create a graphics buffer (dummy for now since we don't have real room gfx)
std::vector<uint8_t> gfx_buffer(0x10000, 1); // Fill with non-zero
zelda3::ObjectDrawer drawer(rom_.get(), 0, gfx_buffer.data());
// Create a simple object
zelda3::RoomObject obj(0x10, 5, 5, 0x12, 0);
obj.SetRom(rom_.get());
obj.EnsureTilesLoaded();
// Create background buffer
gfx::BackgroundBuffer bg1(512, 512);
gfx::BackgroundBuffer bg2(512, 512);
std::vector<uint8_t> empty_data(512 * 512, 0);
bg1.bitmap().Create(512, 512, 8, empty_data);
bg2.bitmap().Create(512, 512, 8, empty_data);
// Create palette
gfx::PaletteGroup palette_group;
gfx::SnesPalette palette;
for (int i = 0; i < 16; i++) {
palette.AddColor(gfx::SnesColor(i * 16, i * 16, i * 16));
}
palette_group.AddPalette(palette);
// Draw object
auto status = drawer.DrawObject(obj, bg1, bg2, palette_group);
EXPECT_TRUE(status.ok()) << "DrawObject failed: " << status.message();
// Check that some pixels were written (non-zero in bitmap)
const auto& data = bg1.bitmap().vector();
int non_zero_count = 0;
for (uint8_t pixel : data) {
if (pixel != 0) non_zero_count++;
}
EXPECT_GT(non_zero_count, 0)
<< "Drawing should produce some non-zero pixels";
}
// ============================================================================
// GameData Graphics Buffer Validation (Critical for Editor)
// ============================================================================
TEST_F(DungeonObjectRomValidationTest, GameData_GraphicsBufferPopulated) {
// Load GameData - this is what the editor does on ROM load
zelda3::GameData game_data;
auto status = zelda3::LoadGameData(*rom_, game_data);
ASSERT_TRUE(status.ok()) << "LoadGameData failed: " << status.message();
// Graphics buffer should be populated (223 sheets * 4096 bytes = 913408 bytes)
EXPECT_GT(game_data.graphics_buffer.size(), 0u)
<< "Graphics buffer should not be empty";
EXPECT_GE(game_data.graphics_buffer.size(), 223u * 4096u)
<< "Graphics buffer should have all 223 sheets";
// Count non-zero bytes in graphics buffer
int non_zero_count = 0;
for (uint8_t byte : game_data.graphics_buffer) {
if (byte != 0 && byte != 0xFF) non_zero_count++;
}
EXPECT_GT(non_zero_count, 100000)
<< "Graphics buffer should have significant non-zero data, got "
<< non_zero_count << " non-zero bytes";
}
TEST_F(DungeonObjectRomValidationTest, GameData_GfxBitmapsPopulated) {
// Load GameData
zelda3::GameData game_data;
auto status = zelda3::LoadGameData(*rom_, game_data);
ASSERT_TRUE(status.ok()) << "LoadGameData failed: " << status.message();
// Check that gfx_bitmaps are populated
int populated_count = 0;
int content_count = 0;
for (size_t i = 0; i < 223; ++i) {
auto& bitmap = game_data.gfx_bitmaps[i];
if (bitmap.is_active() && bitmap.width() > 0 && bitmap.height() > 0) {
populated_count++;
// Check entire bitmap for non-zero/non-0xFF data (not just first 100 bytes)
// Some tiles are legitimately empty at the start
bool has_content = false;
for (size_t j = 0; j < bitmap.size(); ++j) {
if (bitmap.data()[j] != 0 && bitmap.data()[j] != 0xFF) {
has_content = true;
break;
}
}
if (has_content) {
content_count++;
}
}
}
// Check that we have a reasonable number populated (not all 223 due to 2BPP sheets)
EXPECT_GT(populated_count, 200)
<< "Most of 223 gfx_bitmaps should be populated, got " << populated_count;
// Check that most populated sheets have actual content (some may be genuinely empty)
EXPECT_GT(content_count, 180)
<< "Most populated sheets should have content, got " << content_count
<< " out of " << populated_count;
}
TEST_F(DungeonObjectRomValidationTest, Room_GraphicsBufferCopy) {
// Load GameData first
zelda3::GameData game_data;
auto status = zelda3::LoadGameData(*rom_, game_data);
ASSERT_TRUE(status.ok()) << "LoadGameData failed: " << status.message();
// Create a room with GameData
zelda3::Room room(0, rom_.get(), &game_data);
// Load room graphics
room.LoadRoomGraphics(room.blockset);
// Copy graphics to room buffer
room.CopyRoomGraphicsToBuffer();
// Get the current_gfx16 buffer
auto& gfx16 = room.get_gfx_buffer();
// Count non-zero bytes
int non_zero_count = 0;
for (size_t i = 0; i < gfx16.size(); ++i) {
if (gfx16[i] != 0) non_zero_count++;
}
EXPECT_GT(non_zero_count, 1000)
<< "Room's current_gfx16 buffer should have graphics data, got "
<< non_zero_count << " non-zero bytes out of " << gfx16.size();
// Verify specific blocks are loaded
auto blocks = room.blocks();
EXPECT_EQ(blocks.size(), 16u) << "Room should have 16 graphics blocks";
for (size_t i = 0; i < blocks.size() && i < 4; ++i) {
int block_start = i * 4096;
int block_non_zero = 0;
for (int j = 0; j < 4096; ++j) {
if (gfx16[block_start + j] != 0) block_non_zero++;
}
EXPECT_GT(block_non_zero, 100)
<< "Block " << i << " (sheet " << blocks[i]
<< ") should have graphics data, got " << block_non_zero
<< " non-zero bytes";
}
}
TEST_F(DungeonObjectRomValidationTest, Room_LayoutLoading) {
// Load GameData first
zelda3::GameData game_data;
auto status = zelda3::LoadGameData(*rom_, game_data);
ASSERT_TRUE(status.ok()) << "LoadGameData failed: " << status.message();
// Create a room with GameData
zelda3::Room room(0, rom_.get(), &game_data);
// Load room graphics
room.LoadRoomGraphics(room.blockset);
room.CopyRoomGraphicsToBuffer();
// Check that layout_ is set up
int layout_id = room.layout;
std::cout << "Room 0 layout ID: " << layout_id << std::endl;
// Render room graphics (which calls LoadLayoutTilesToBuffer)
room.RenderRoomGraphics();
// Check bg1_buffer bitmap has data
auto& bg1_bmp = room.bg1_buffer().bitmap();
auto& bg2_bmp = room.bg2_buffer().bitmap();
std::cout << "BG1 bitmap: active=" << bg1_bmp.is_active()
<< " w=" << bg1_bmp.width()
<< " h=" << bg1_bmp.height()
<< " size=" << bg1_bmp.size() << std::endl;
std::cout << "BG2 bitmap: active=" << bg2_bmp.is_active()
<< " w=" << bg2_bmp.width()
<< " h=" << bg2_bmp.height()
<< " size=" << bg2_bmp.size() << std::endl;
EXPECT_TRUE(bg1_bmp.is_active()) << "BG1 bitmap should be active";
EXPECT_GT(bg1_bmp.width(), 0) << "BG1 bitmap should have width";
EXPECT_GT(bg1_bmp.height(), 0) << "BG1 bitmap should have height";
// Count non-zero pixels in BG1
if (bg1_bmp.is_active() && bg1_bmp.size() > 0) {
int non_zero = 0;
for (size_t i = 0; i < bg1_bmp.size(); ++i) {
if (bg1_bmp.data()[i] != 0) non_zero++;
}
std::cout << "BG1 non-zero pixels: " << non_zero
<< " / " << bg1_bmp.size()
<< " (" << (100.0f * non_zero / bg1_bmp.size()) << "%)"
<< std::endl;
EXPECT_GT(non_zero, 1000)
<< "BG1 should have significant non-zero pixel data";
}
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,176 @@
#include <gtest/gtest.h>
#include <vector>
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_tile.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/game_data.h"
#include "rom/rom.h"
namespace yaze {
namespace zelda3 {
namespace test {
class DungeonPaletteTest : public ::testing::Test {
protected:
void SetUp() override {
// Mock ROM is not strictly needed for DrawTileToBitmap if we pass tiledata
// but ObjectDrawer constructor needs it.
rom_ = std::make_unique<Rom>();
game_data_ = std::make_unique<GameData>(rom_.get());
drawer_ = std::make_unique<ObjectDrawer>(rom_.get(), 0);
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<GameData> game_data_;
std::unique_ptr<ObjectDrawer> drawer_;
};
TEST_F(DungeonPaletteTest, PaletteOffsetIsCorrectFor8BPP) {
// Create a bitmap
gfx::Bitmap bitmap;
bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
// Create dummy tile data (128x128 pixels worth, but we only need enough for one tile)
// 128 pixels wide = 16 tiles.
// We will use tile ID 0.
// Tile 0 is at (0,0) in sheet.
// src_index = (0 + py) * 128 + (0 + px)
// We need a buffer of size 128 * 8 at least.
std::vector<uint8_t> tiledata(128 * 8, 0);
// Set some pixels in the tile data
// Row 0, Col 0: Index 1
tiledata[0] = 1;
// Row 0, Col 1: Index 2
tiledata[1] = 2;
// Create TileInfo with palette index 1
gfx::TileInfo tile_info;
tile_info.id_ = 0;
tile_info.palette_ = 1; // Palette 1
tile_info.horizontal_mirror_ = false;
tile_info.vertical_mirror_ = false;
tile_info.over_ = false;
// Draw
drawer_->DrawTileToBitmap(bitmap, tile_info, 0, 0, tiledata.data());
// Check pixels
// Dungeon tiles use 15-color sub-palettes (not 8 like overworld).
// Formula: final_color = (pixel - 1) + (palette * 15)
// For palette 1, offset is 15.
// Pixel at (0,0) was 1. Result should be (1-1) + 15 = 15.
// Pixel at (1,0) was 2. Result should be (2-1) + 15 = 16.
const auto& data = bitmap.vector();
// Bitmap data is row-major.
// (0,0) is index 0.
EXPECT_EQ(data[0], 15); // (1-1) + 15 = 15
EXPECT_EQ(data[1], 16); // (2-1) + 15 = 16
// Test with palette 0
tile_info.palette_ = 0;
drawer_->DrawTileToBitmap(bitmap, tile_info, 0, 0, tiledata.data());
// Offset 0 * 15 = 0.
// Pixel 1 -> (1-1) + 0 = 0
// Pixel 2 -> (2-1) + 0 = 1
EXPECT_EQ(data[0], 0);
EXPECT_EQ(data[1], 1);
// Test with palette 7 (wraps to palette 1 due to 6 sub-palette limit)
tile_info.palette_ = 7;
drawer_->DrawTileToBitmap(bitmap, tile_info, 0, 0, tiledata.data());
// Palette 7 wraps to 7 % 6 = 1, offset 1 * 15 = 15.
EXPECT_EQ(data[0], 15); // (1-1) + 15 = 15
EXPECT_EQ(data[1], 16); // (2-1) + 15 = 16
}
TEST_F(DungeonPaletteTest, PaletteOffsetWorksWithConvertedData) {
gfx::Bitmap bitmap;
bitmap.Create(8, 8, 8, std::vector<uint8_t>(64, 0));
// Create 8BPP unpacked tile data (simulating converted buffer)
// Layout: 128 bytes per tile row, 8 bytes per tile
// For tile 0: base_x=0, base_y=0
std::vector<uint8_t> tiledata(128 * 8, 0);
// Set pixel pair at row 0: pixel 0 = 3, pixel 1 = 5
tiledata[0] = 3;
tiledata[1] = 5;
gfx::TileInfo tile_info;
tile_info.id_ = 0;
tile_info.palette_ = 2; // Palette 2 → offset 30 (2 * 15)
tile_info.horizontal_mirror_ = false;
tile_info.vertical_mirror_ = false;
tile_info.over_ = false;
drawer_->DrawTileToBitmap(bitmap, tile_info, 0, 0, tiledata.data());
const auto& data = bitmap.vector();
// Dungeon tiles use 15-color sub-palettes.
// Formula: final_color = (pixel - 1) + (palette * 15)
// Pixel 3: (3-1) + 30 = 32
// Pixel 5: (5-1) + 30 = 34
EXPECT_EQ(data[0], 32);
EXPECT_EQ(data[1], 34);
}
TEST_F(DungeonPaletteTest, InspectActualPaletteColors) {
// Load actual ROM file
auto load_result = rom_->LoadFromFile("zelda3.sfc");
if (!load_result.ok()) {
GTEST_SKIP() << "ROM file not found, skipping";
}
// Load game data (palettes, etc.)
auto game_data_result = LoadGameData(*rom_, *game_data_);
if (!game_data_result.ok()) {
GTEST_SKIP() << "Failed to load game data: " << game_data_result.message();
}
// Get dungeon main palette group
const auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
ASSERT_FALSE(dungeon_pal_group.empty()) << "Dungeon palette group is empty!";
// Get first palette (palette 0)
const auto& palette0 = dungeon_pal_group[0];
printf("\n=== Dungeon Palette 0 - First 16 colors ===\n");
for (size_t i = 0; i < std::min(size_t(16), palette0.size()); ++i) {
const auto& color = palette0[i];
auto rgb = color.rgb();
printf("Color %02zu: R=%03d G=%03d B=%03d (0x%02X%02X%02X)\n",
i,
static_cast<int>(rgb.x),
static_cast<int>(rgb.y),
static_cast<int>(rgb.z),
static_cast<int>(rgb.x),
static_cast<int>(rgb.y),
static_cast<int>(rgb.z));
}
// Total palette size
printf("\nTotal palette size: %zu colors\n", palette0.size());
EXPECT_EQ(palette0.size(), 90) << "Expected 90 colors for dungeon palette";
// Colors 56-63 (palette 7 offset: 7*8=56)
printf("\n=== Colors 56-63 (pal=7 range) ===\n");
for (size_t i = 56; i < std::min(size_t(64), palette0.size()); ++i) {
const auto& color = palette0[i];
auto rgb = color.rgb();
printf("Color %02zu: R=%03d G=%03d B=%03d (0x%02X%02X%02X)\n",
i,
static_cast<int>(rgb.x),
static_cast<int>(rgb.y),
static_cast<int>(rgb.z),
static_cast<int>(rgb.x),
static_cast<int>(rgb.y),
static_cast<int>(rgb.z));
}
}
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -25,7 +25,7 @@
#include "absl/status/status.h"
#include "app/gfx/render/background_buffer.h"
#include "app/gfx/types/snes_palette.h"
#include "app/rom.h"
#include "rom/rom.h"
#include "gtest/gtest.h"
#include "zelda3/dungeon/object_drawer.h"
#include "zelda3/dungeon/object_parser.h"
@@ -77,7 +77,7 @@ class DungeonRenderingIntegrationTest : public ::testing::Test {
// Set ROM for all objects
for (auto& obj : objects) {
obj.set_rom(rom_.get());
obj.SetRom(rom_.get());
}
// Add objects to room (this would normally be done by LoadObjects)
@@ -122,7 +122,7 @@ TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
EXPECT_GT(test_room.GetTileObjects().size(), 0);
// Test ObjectDrawer can render the room
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
auto status =
@@ -135,7 +135,7 @@ TEST_F(DungeonRenderingIntegrationTest, FullRoomRenderingWorks) {
// Test room rendering with different palette configurations
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithDifferentPalettes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
// Test with different palette configurations
std::vector<gfx::PaletteGroup> palette_groups;
@@ -157,7 +157,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithDifferentPalettes) {
// Test room rendering with objects on different layers
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Separate objects by layer
@@ -190,7 +190,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMultipleLayers) {
// Test room rendering with various object sizes
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithVariousObjectSizes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Group objects by size
@@ -222,11 +222,11 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingPerformance) {
int layer = i % 2; // Alternate layers
RoomObject obj(id, x, y, size, layer);
obj.set_rom(rom_.get());
obj.SetRom(rom_.get());
large_room.AddObject(obj);
}
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Time the rendering operation
@@ -252,7 +252,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingPerformance) {
// Test room rendering with edge case coordinates
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Add objects at edge coordinates
@@ -266,7 +266,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
// Set ROM for all objects
for (auto& obj : edge_objects) {
obj.set_rom(rom_.get());
obj.SetRom(rom_.get());
}
auto status = drawer.DrawObjectList(edge_objects, test_room.bg1_buffer(),
@@ -278,7 +278,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithEdgeCaseCoordinates) {
// Test room rendering with mixed object types
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMixedObjectTypes) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Add various object types
@@ -306,7 +306,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithMixedObjectTypes) {
// Set ROM for all objects
for (auto& obj : mixed_objects) {
obj.set_rom(rom_.get());
obj.SetRom(rom_.get());
}
auto status = drawer.DrawObjectList(mixed_objects, test_room.bg1_buffer(),
@@ -334,7 +334,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingErrorHandling) {
// Test room rendering with invalid object data
TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithInvalidObjectData) {
Room test_room = CreateTestRoom(0x00);
ObjectDrawer drawer(rom_.get());
ObjectDrawer drawer(rom_.get(), 0);
auto palette_group = CreateTestPaletteGroup();
// Create objects with invalid data
@@ -348,7 +348,7 @@ TEST_F(DungeonRenderingIntegrationTest, RoomRenderingWithInvalidObjectData) {
// Set ROM for all objects
for (auto& obj : invalid_objects) {
obj.set_rom(rom_.get());
obj.SetRom(rom_.get());
}
// Should handle gracefully

View File

@@ -1,7 +1,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/rom.h"
#include "rom/rom.h"
#include "zelda3/dungeon/room.h"
namespace yaze {

View File

@@ -0,0 +1,683 @@
// Integration tests for Music Editor with real ROM data
// Tests song loading, parsing, and emulator audio stability
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/emu/emulator.h"
#include "rom/rom.h"
#include "zelda3/music/music_bank.h"
#include "zelda3/music/song_data.h"
#include "zelda3/music/spc_parser.h"
namespace yaze {
namespace zelda3 {
namespace test {
using namespace yaze::zelda3::music;
// =============================================================================
// Test Fixture
// =============================================================================
class MusicIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
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();
}
// Verify it's an ALTTP ROM
if (rom_->title().find("ZELDA") == std::string::npos &&
rom_->title().find("zelda") == std::string::npos) {
GTEST_SKIP() << "ROM is not ALTTP: " << rom_->title();
}
}
void TearDown() override { rom_.reset(); }
std::unique_ptr<Rom> rom_;
MusicBank music_bank_;
};
// =============================================================================
// Song Loading Tests
// =============================================================================
TEST_F(MusicIntegrationTest, LoadVanillaSongsFromRom) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << "Failed to load music: " << status.message();
// Should load all 34 vanilla songs
size_t song_count = music_bank_.GetSongCount();
EXPECT_GE(song_count, 34) << "Expected at least 34 vanilla songs";
// Verify some known vanilla songs exist
const MusicSong* title_song = music_bank_.GetSong(0); // Song ID 1 (index 0)
ASSERT_NE(title_song, nullptr) << "Title song should exist";
EXPECT_EQ(title_song->name, "Title");
const MusicSong* light_world = music_bank_.GetSong(1); // Song ID 2 (index 1)
ASSERT_NE(light_world, nullptr) << "Light World song should exist";
EXPECT_EQ(light_world->name, "Light World");
const MusicSong* dark_world = music_bank_.GetSong(8); // Song ID 9 (index 8)
ASSERT_NE(dark_world, nullptr) << "Dark World song should exist";
EXPECT_EQ(dark_world->name, "Dark World");
}
TEST_F(MusicIntegrationTest, VerifySongStructure) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Check each vanilla song has valid structure
for (int i = 0; i < 34; ++i) {
SCOPED_TRACE("Song index: " + std::to_string(i));
const MusicSong* song = music_bank_.GetSong(i);
ASSERT_NE(song, nullptr) << "Song " << i << " should exist";
// Song should have at least one segment
EXPECT_GE(song->segments.size(), 1)
<< "Song '" << song->name << "' should have at least one segment";
// Each segment should have 8 tracks
for (size_t seg_idx = 0; seg_idx < song->segments.size(); ++seg_idx) {
SCOPED_TRACE("Segment: " + std::to_string(seg_idx));
const auto& segment = song->segments[seg_idx];
EXPECT_EQ(segment.tracks.size(), 8) << "Segment should have 8 tracks";
// At least one track should have content (not all empty)
bool has_content = false;
for (const auto& track : segment.tracks) {
if (!track.is_empty && !track.events.empty()) {
has_content = true;
break;
}
}
// Some songs may have empty segments for intro/loop purposes
// but most should have content
}
}
}
TEST_F(MusicIntegrationTest, VerifyBankAssignment) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Songs 1-11 should be Overworld bank
for (int i = 0; i < 11; ++i) {
const MusicSong* song = music_bank_.GetSong(i);
ASSERT_NE(song, nullptr);
EXPECT_EQ(song->bank, static_cast<uint8_t>(MusicBank::Bank::Overworld))
<< "Song " << i << " (" << song->name << ") should be Overworld bank";
}
// Songs 12-31 should be Dungeon bank
for (int i = 11; i < 31; ++i) {
const MusicSong* song = music_bank_.GetSong(i);
ASSERT_NE(song, nullptr);
EXPECT_EQ(song->bank, static_cast<uint8_t>(MusicBank::Bank::Dungeon))
<< "Song " << i << " (" << song->name << ") should be Dungeon bank";
}
// Songs 32-34 should be Credits bank
for (int i = 31; i < 34; ++i) {
const MusicSong* song = music_bank_.GetSong(i);
ASSERT_NE(song, nullptr);
EXPECT_EQ(song->bank, static_cast<uint8_t>(MusicBank::Bank::Credits))
<< "Song " << i << " (" << song->name << ") should be Credits bank";
}
}
TEST_F(MusicIntegrationTest, VerifyTrackEvents) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Check Light World song has valid events
const MusicSong* light_world = music_bank_.GetSong(1);
ASSERT_NE(light_world, nullptr);
ASSERT_GE(light_world->segments.size(), 1);
int total_events = 0;
int note_count = 0;
int command_count = 0;
for (const auto& segment : light_world->segments) {
for (const auto& track : segment.tracks) {
if (track.is_empty)
continue;
for (const auto& event : track.events) {
total_events++;
switch (event.type) {
case TrackEvent::Type::Note:
note_count++;
// Verify note is in valid range
EXPECT_TRUE(SpcParser::IsNotePitch(event.note.pitch) ||
event.note.pitch == kNoteTie ||
event.note.pitch == kNoteRest)
<< "Invalid note pitch: 0x" << std::hex
<< static_cast<int>(event.note.pitch);
break;
case TrackEvent::Type::Command:
command_count++;
// Verify command opcode is valid
EXPECT_TRUE(SpcParser::IsCommand(event.command.opcode))
<< "Invalid command opcode: 0x" << std::hex
<< static_cast<int>(event.command.opcode);
break;
case TrackEvent::Type::End:
// End marker is always valid
break;
}
}
}
}
// Light World should have significant content
EXPECT_GT(total_events, 100) << "Light World should have many events";
EXPECT_GT(note_count, 50) << "Light World should have many notes";
EXPECT_GT(command_count, 10) << "Light World should have setup commands";
}
// =============================================================================
// Space Calculation Tests
// =============================================================================
TEST_F(MusicIntegrationTest, CalculateVanillaBankUsage) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Check Overworld bank usage
auto ow_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Overworld);
EXPECT_GT(ow_space.used_bytes, 0) << "Overworld bank should have content";
EXPECT_LE(ow_space.used_bytes, ow_space.total_bytes)
<< "Overworld usage should not exceed limit";
EXPECT_LT(ow_space.usage_percent, 100.0f)
<< "Overworld should not be over capacity";
// Check Dungeon bank usage
auto dg_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Dungeon);
EXPECT_GT(dg_space.used_bytes, 0) << "Dungeon bank should have content";
EXPECT_LE(dg_space.used_bytes, dg_space.total_bytes)
<< "Dungeon usage should not exceed limit";
// Check Credits bank usage
auto cr_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Credits);
EXPECT_GT(cr_space.used_bytes, 0) << "Credits bank should have content";
EXPECT_LE(cr_space.used_bytes, cr_space.total_bytes)
<< "Credits usage should not exceed limit";
// All songs should fit
EXPECT_TRUE(music_bank_.AllSongsFit()) << "All vanilla songs should fit";
}
// =============================================================================
// Emulator Integration Tests
// =============================================================================
TEST_F(MusicIntegrationTest, EmulatorInitializesWithRom) {
emu::Emulator emulator;
// Try to initialize the emulator
bool initialized = emulator.EnsureInitialized(rom_.get());
EXPECT_TRUE(initialized) << "Emulator should initialize with valid ROM";
EXPECT_TRUE(emulator.is_snes_initialized())
<< "SNES core should be initialized";
}
TEST_F(MusicIntegrationTest, EmulatorCanRunFrames) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized) << "Emulator must initialize for this test";
emulator.set_running(true);
// Run a few frames without crashing
for (int i = 0; i < 10; ++i) {
emulator.RunFrameOnly();
}
// Should still be running
EXPECT_TRUE(emulator.running());
EXPECT_TRUE(emulator.is_snes_initialized());
}
TEST_F(MusicIntegrationTest, EmulatorGeneratesAudioSamples) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized) << "Emulator must initialize for this test";
emulator.set_running(true);
// Run several frames to generate audio
for (int i = 0; i < 60; ++i) {
emulator.RunFrameOnly();
}
// Check that DSP is producing samples
auto& dsp = emulator.snes().apu().dsp();
const int16_t* sample_buffer = dsp.GetSampleBuffer();
ASSERT_NE(sample_buffer, nullptr) << "DSP should have sample buffer";
// Check for non-zero audio samples (some sound should be playing)
// At startup, there might be silence, but the buffer should exist
uint16_t sample_offset = dsp.GetSampleOffset();
EXPECT_GT(sample_offset, 0) << "DSP should have processed samples";
}
TEST_F(MusicIntegrationTest, MusicTriggerWritesToRam) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized);
emulator.set_running(true);
// Run some frames to let the game initialize
for (int i = 0; i < 30; ++i) {
emulator.RunFrameOnly();
}
// Write a music ID to the music register
uint8_t song_id = 0x02; // Light World
emulator.snes().Write(0x7E012C, song_id);
// Verify the write
auto read_result = emulator.snes().Read(0x7E012C);
EXPECT_EQ(read_result, song_id)
<< "Music register should hold the written value";
}
// =============================================================================
// Round-Trip Tests
// =============================================================================
TEST_F(MusicIntegrationTest, ParseSerializeRoundTrip) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Test round-trip for Light World
const MusicSong* original = music_bank_.GetSong(1);
ASSERT_NE(original, nullptr);
// Serialize the song
auto serialize_result = SpcSerializer::SerializeSong(*original, 0xD100);
ASSERT_TRUE(serialize_result.ok()) << serialize_result.status().message();
auto& serialized = serialize_result.value();
EXPECT_GT(serialized.data.size(), 0) << "Serialized data should not be empty";
// The serialized size should be reasonable
EXPECT_LT(serialized.data.size(), 10000)
<< "Serialized size should be reasonable";
}
// =============================================================================
// Vanilla Song Name Tests
// =============================================================================
TEST_F(MusicIntegrationTest, AllVanillaSongsHaveNames) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
std::vector<std::string> expected_names = {"Title",
"Light World",
"Beginning",
"Rabbit",
"Forest",
"Intro",
"Town",
"Warp",
"Dark World",
"Master Sword",
"File Select",
"Soldier",
"Mountain",
"Shop",
"Fanfare",
"Castle",
"Palace (Pendant)",
"Cave",
"Clear",
"Church",
"Boss",
"Dungeon (Crystal)",
"Psychic",
"Secret Way",
"Rescue",
"Crystal",
"Fountain",
"Pyramid",
"Kill Agahnim",
"Ganon Room",
"Last Boss",
"Credits 1",
"Credits 2",
"Credits 3"};
for (size_t i = 0;
i < expected_names.size() && i < music_bank_.GetSongCount(); ++i) {
const MusicSong* song = music_bank_.GetSong(i);
ASSERT_NE(song, nullptr) << "Song " << i << " should exist";
EXPECT_EQ(song->name, expected_names[i])
<< "Song " << i << " name mismatch";
}
}
// =============================================================================
// Instrument/Sample Loading Tests
// =============================================================================
TEST_F(MusicIntegrationTest, InstrumentsLoaded) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Should have default instruments
EXPECT_GE(music_bank_.GetInstrumentCount(), 16)
<< "Should have at least 16 instruments";
// Check first instrument exists
const MusicInstrument* inst = music_bank_.GetInstrument(0);
ASSERT_NE(inst, nullptr);
EXPECT_FALSE(inst->name.empty()) << "Instrument should have a name";
}
TEST_F(MusicIntegrationTest, SamplesLoaded) {
auto status = music_bank_.LoadFromRom(*rom_);
ASSERT_TRUE(status.ok()) << status.message();
// Should have samples
EXPECT_GE(music_bank_.GetSampleCount(), 16)
<< "Should have at least 16 samples";
}
// =============================================================================
// Direct SPC Upload Tests
// =============================================================================
TEST_F(MusicIntegrationTest, DirectSpcUploadCommonBank) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized) << "Emulator must initialize for this test";
auto& apu = emulator.snes().apu();
// Reset APU to clean state
apu.Reset();
// Upload common bank (Bank 0) from ROM offset 0xC8000
// This contains: driver code, sample pointers, instruments, BRR samples
constexpr uint32_t kCommonBankOffset = 0xC8000;
const uint8_t* rom_data = rom_->data();
const size_t rom_size = rom_->size();
ASSERT_GT(rom_size, kCommonBankOffset + 4)
<< "ROM should have common bank data";
// Parse and upload blocks: [size:2][aram_addr:2][data:size]
uint32_t offset = kCommonBankOffset;
int block_count = 0;
int total_bytes_uploaded = 0;
while (offset + 4 < rom_size) {
uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8);
uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8);
if (block_size == 0 || block_size > 0x10000) break;
if (offset + 4 + block_size > rom_size) break;
apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size);
std::cout << "[DirectSpcUpload] Block " << block_count
<< ": " << block_size << " bytes -> ARAM $"
<< std::hex << aram_addr << std::dec << std::endl;
offset += 4 + block_size;
block_count++;
total_bytes_uploaded += block_size;
}
EXPECT_GT(block_count, 0) << "Should upload at least one block";
EXPECT_GT(total_bytes_uploaded, 1000) << "Should upload significant data";
std::cout << "[DirectSpcUpload] Uploaded " << block_count
<< " blocks, " << total_bytes_uploaded << " bytes total" << std::endl;
// Verify some data was written to ARAM
// SPC driver should be at $0800
uint8_t driver_check = apu.ram[0x0800];
EXPECT_NE(driver_check, 0) << "SPC driver area should have data";
}
TEST_F(MusicIntegrationTest, DirectSpcUploadSongBank) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized);
auto& apu = emulator.snes().apu();
apu.Reset();
// First upload common bank
constexpr uint32_t kCommonBankOffset = 0xC8000;
const uint8_t* rom_data = rom_->data();
const size_t rom_size = rom_->size();
uint32_t offset = kCommonBankOffset;
while (offset + 4 < rom_size) {
uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8);
uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8);
if (block_size == 0 || block_size > 0x10000) break;
if (offset + 4 + block_size > rom_size) break;
apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size);
offset += 4 + block_size;
}
// Now upload overworld song bank (ROM offset 0xD1EF5)
constexpr uint32_t kOverworldBankOffset = 0xD1EF5;
ASSERT_GT(rom_size, kOverworldBankOffset + 4)
<< "ROM should have overworld bank data";
offset = kOverworldBankOffset;
int song_block_count = 0;
while (offset + 4 < rom_size) {
uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8);
uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8);
if (block_size == 0 || block_size > 0x10000) break;
if (offset + 4 + block_size > rom_size) break;
apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size);
std::cout << "[DirectSpcUpload] Song block " << song_block_count
<< ": " << block_size << " bytes -> ARAM $"
<< std::hex << aram_addr << std::dec << std::endl;
offset += 4 + block_size;
song_block_count++;
}
EXPECT_GT(song_block_count, 0) << "Should upload song bank blocks";
// Song pointers should be at ARAM $D000
uint16_t song_ptr_0 = apu.ram[0xD000] | (apu.ram[0xD001] << 8);
std::cout << "[DirectSpcUpload] Song 0 pointer: $"
<< std::hex << song_ptr_0 << std::dec << std::endl;
// Should have valid pointer (non-zero, within song data range)
EXPECT_GT(song_ptr_0, 0xD000) << "Song pointer should be valid";
EXPECT_LT(song_ptr_0, 0xFFFF) << "Song pointer should be within ARAM range";
}
TEST_F(MusicIntegrationTest, DirectSpcPortCommunication) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized);
auto& apu = emulator.snes().apu();
// Test port communication
// Write to in_ports (CPU -> SPC)
apu.in_ports_[0] = 0x42;
apu.in_ports_[1] = 0x00;
EXPECT_EQ(apu.in_ports_[0], 0x42) << "Port 0 should hold written value";
EXPECT_EQ(apu.in_ports_[1], 0x00) << "Port 1 should hold written value";
std::cout << "[DirectSpcPort] Wrote song index 0x42 to port 0" << std::endl;
// Run some cycles to let SPC process
emulator.set_running(true);
for (int i = 0; i < 10; ++i) {
emulator.RunFrameOnly();
}
// Check out_ports (SPC -> CPU) for acknowledgment
std::cout << "[DirectSpcPort] Out ports: "
<< std::hex
<< (int)apu.out_ports_[0] << " "
<< (int)apu.out_ports_[1] << " "
<< (int)apu.out_ports_[2] << " "
<< (int)apu.out_ports_[3] << std::dec << std::endl;
}
TEST_F(MusicIntegrationTest, DirectSpcAudioGeneration) {
emu::Emulator emulator;
bool initialized = emulator.EnsureInitialized(rom_.get());
ASSERT_TRUE(initialized);
auto& apu = emulator.snes().apu();
apu.Reset();
// Upload common bank
const uint8_t* rom_data = rom_->data();
const size_t rom_size = rom_->size();
auto upload_bank = [&](uint32_t bank_offset) {
uint32_t offset = bank_offset;
while (offset + 4 < rom_size) {
uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8);
uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8);
if (block_size == 0 || block_size > 0x10000) break;
if (offset + 4 + block_size > rom_size) break;
apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size);
offset += 4 + block_size;
}
};
// Upload common bank (driver, samples, instruments)
upload_bank(0xC8000);
// Upload overworld song bank
upload_bank(0xD1EF5);
// Send play command for song 0 (Title)
apu.in_ports_[0] = 0x00; // Song index 0
apu.in_ports_[1] = 0x00; // Play command
std::cout << "[DirectSpcAudio] Starting playback test..." << std::endl;
emulator.set_running(true);
// Run frames and check for audio generation
auto& dsp = apu.dsp();
int frames_with_audio = 0;
for (int frame = 0; frame < 120; ++frame) {
emulator.RunFrameOnly();
if (frame % 30 == 0) {
const int16_t* samples = dsp.GetSampleBuffer();
uint16_t sample_offset = dsp.GetSampleOffset();
// Check if any samples are non-zero
bool has_audio = false;
for (int i = 0; i < std::min(256, (int)sample_offset * 2); ++i) {
if (samples[i] != 0) {
has_audio = true;
break;
}
}
if (has_audio) {
frames_with_audio++;
}
std::cout << "[DirectSpcAudio] Frame " << frame
<< ": sample_offset=" << sample_offset
<< ", has_audio=" << (has_audio ? "yes" : "no") << std::endl;
}
}
// Check DSP channel states
for (int ch = 0; ch < 8; ++ch) {
const auto& channel = dsp.GetChannel(ch);
std::cout << "[DirectSpcAudio] Ch" << ch
<< ": vol=" << (int)channel.volumeL << "/" << (int)channel.volumeR
<< ", pitch=$" << std::hex << channel.pitch << std::dec
<< ", keyOn=" << channel.keyOn << std::endl;
}
// We may or may not get audio depending on SPC driver state
// But the test verifies the upload and port communication work
std::cout << "[DirectSpcAudio] Frames with detected audio: "
<< frames_with_audio << "/4 checks" << std::endl;
}
TEST_F(MusicIntegrationTest, VerifyAllBankUploadOffsets) {
// Verify the ROM has valid block headers at all bank offsets
const uint8_t* rom_data = rom_->data();
const size_t rom_size = rom_->size();
struct BankInfo {
const char* name;
uint32_t offset;
};
BankInfo banks[] = {
{"Common", 0xC8000},
{"Overworld", 0xD1EF5},
{"Dungeon", 0xD8000},
{"Credits", 0xD5380}
};
for (const auto& bank : banks) {
SCOPED_TRACE(bank.name);
ASSERT_GT(rom_size, bank.offset + 4)
<< bank.name << " bank offset should be within ROM";
// Read first block header
uint16_t block_size = rom_data[bank.offset] | (rom_data[bank.offset + 1] << 8);
uint16_t aram_addr = rom_data[bank.offset + 2] | (rom_data[bank.offset + 3] << 8);
std::cout << "[BankVerify] " << bank.name
<< " (0x" << std::hex << bank.offset << "): "
<< "size=" << std::dec << block_size
<< ", aram=$" << std::hex << aram_addr << std::dec << std::endl;
// Block should have valid size and address
EXPECT_GT(block_size, 0) << bank.name << " should have non-zero first block";
EXPECT_LT(block_size, 0x10000) << bank.name << " block size should be reasonable";
EXPECT_GT(aram_addr, 0) << bank.name << " should have non-zero ARAM address";
}
}
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -5,7 +5,7 @@
#include <string>
#include <vector>
#include "app/rom.h"
#include "rom/rom.h"
#include "testing.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_map.h"
@@ -108,15 +108,17 @@ class OverworldIntegrationTest : public ::testing::Test {
};
// Test Tile32 expansion detection
TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
TEST_F(OverworldIntegrationTest, DISABLED_Tile32ExpansionDetection) {
mock_rom_data_[0x01772E] = 0x04;
mock_rom_data_[0x140145] = 0xFF;
rom_->LoadFromData(mock_rom_data_); // Update ROM
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x01772E] = 0x05;
rom_->LoadFromData(mock_rom_data_); // Update ROM
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
@@ -124,15 +126,17 @@ TEST_F(OverworldIntegrationTest, Tile32ExpansionDetection) {
}
// Test Tile16 expansion detection
TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
TEST_F(OverworldIntegrationTest, DISABLED_Tile16ExpansionDetection) {
mock_rom_data_[0x017D28] = 0x0F;
mock_rom_data_[0x140145] = 0xFF;
rom_->LoadFromData(mock_rom_data_); // Update ROM
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
// Test expanded detection
mock_rom_data_[0x017D28] = 0x10;
rom_->LoadFromData(mock_rom_data_); // Update ROM
overworld_ = std::make_unique<Overworld>(rom_.get());
status = overworld_->Load(rom_.get());
@@ -140,7 +144,7 @@ TEST_F(OverworldIntegrationTest, Tile16ExpansionDetection) {
}
// Test entrance loading matches ZScream coordinate calculation
TEST_F(OverworldIntegrationTest, EntranceCoordinateCalculation) {
TEST_F(OverworldIntegrationTest, DISABLED_EntranceCoordinateCalculation) {
auto status = overworld_->Load(rom_.get());
ASSERT_TRUE(status.ok());
@@ -192,7 +196,7 @@ TEST_F(OverworldIntegrationTest, ExitDataLoading) {
}
// Test ASM version detection affects item loading
TEST_F(OverworldIntegrationTest, ASMVersionItemLoading) {
TEST_F(OverworldIntegrationTest, DISABLED_ASMVersionItemLoading) {
// Test vanilla ASM (should limit to 0x80 maps)
mock_rom_data_[0x140145] = 0xFF;
overworld_ = std::make_unique<Overworld>(rom_.get());

View File

@@ -4,7 +4,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "app/rom.h"
#include "rom/rom.h"
#include "zelda3/dungeon/room.h"
#include "zelda3/dungeon/room_object.h"

View File

@@ -5,7 +5,7 @@
#include <iostream>
#include <memory>
#include "app/rom.h"
#include "rom/rom.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_map.h"