Files
yaze/test/integration/zelda3/dungeon_object_rom_validation_test.cc

514 lines
18 KiB
C++

// 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