Files
yaze/test/e2e/rom_dependent/rom_version_test.cc

374 lines
12 KiB
C++

#include <gtest/gtest.h>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include "e2e/rom_dependent/editor_save_test_base.h"
#include "rom/rom.h"
#include "rom/snes.h"
#include "testing.h"
#include "zelda3/game_data.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/overworld/overworld_version_helper.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for Multi-ROM Version Validation
*
* Validates save/load operations work correctly across different ROM versions:
* - Japanese (JP) ROM
* - US (USA) ROM
* - European (EU) ROM
*
* Tests version-specific constants and data layouts.
*/
class RomVersionTest : public MultiVersionEditorSaveTest {
protected:
static int FindPrimaryMapId(const zelda3::Overworld& overworld) {
for (int i = 0; i < static_cast<int>(overworld.overworld_maps().size());
i++) {
if (overworld.overworld_map(i)->parent() == i) {
return i;
}
}
return 0;
}
// Version-specific address constants
struct VersionAddresses {
uint32_t overworld_gfx_ptr1;
uint32_t overworld_gfx_ptr2;
uint32_t overworld_gfx_ptr3;
uint32_t map32_tile_tl;
uint32_t compressed_map_ptr;
};
VersionAddresses GetVersionAddresses(zelda3_version version) {
VersionAddresses addrs;
switch (version) {
case JP:
addrs.overworld_gfx_ptr1 = 0x0885A3;
addrs.overworld_gfx_ptr2 = 0x089AB1;
addrs.overworld_gfx_ptr3 = 0x08B2C1;
addrs.map32_tile_tl = 0x18F8A0;
addrs.compressed_map_ptr = 0x02FFC1;
break;
case US:
addrs.overworld_gfx_ptr1 = 0x0886E3;
addrs.overworld_gfx_ptr2 = 0x089BF1;
addrs.overworld_gfx_ptr3 = 0x08B401;
addrs.map32_tile_tl = 0x18F8A0;
addrs.compressed_map_ptr = 0x02FFE1;
break;
case RANDO:
default:
// Default to US addresses
addrs.overworld_gfx_ptr1 = 0x0886E3;
addrs.overworld_gfx_ptr2 = 0x089BF1;
addrs.overworld_gfx_ptr3 = 0x08B401;
addrs.map32_tile_tl = 0x18F8A0;
addrs.compressed_map_ptr = 0x02FFE1;
break;
}
return addrs;
}
};
// Test 1: Detect ROM version correctly
TEST_F(RomVersionTest, DetectVersion_Default) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
// Should detect a valid version
EXPECT_TRUE(version_info.version == JP ||
version_info.version == US ||
version_info.version == RANDO)
<< "Should detect a valid ROM version";
// Log detected version for debugging
std::string version_name;
switch (version_info.version) {
case JP: version_name = "JP"; break;
case US: version_name = "US"; break;
case RANDO: version_name = "RANDO"; break;
default: version_name = "Unknown"; break;
}
std::cout << "Detected ROM version: " << version_name << std::endl;
std::cout << "ZSCustomOverworld: " << (version_info.zscustom_version == 0xFF ?
"vanilla" : std::to_string(version_info.zscustom_version)) << std::endl;
}
// Test 2: US ROM save/load cycle
TEST_F(RomVersionTest, UsRom_SaveLoadCycle) {
if (!HasUsRom()) {
// Try to use the default test ROM
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
if (version_info.version != US) {
GTEST_SKIP() << "US ROM not available";
}
}
std::string rom_path = HasUsRom() ? us_rom_path_ : test_rom_path_;
// Copy ROM for testing
std::string test_path = "test_us_rom.sfc";
std::filesystem::copy_file(rom_path, test_path,
std::filesystem::copy_options::overwrite_existing);
// Load ROM
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_path, rom));
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()));
// Verify basic data loads correctly
EXPECT_TRUE(overworld.is_loaded());
EXPECT_EQ(overworld.overworld_maps().size(), 160);
// Modify something
auto* map0 = overworld.mutable_overworld_map(0);
uint8_t original_gfx = map0->area_graphics();
map0->set_area_graphics((original_gfx + 1) % 256);
// Save
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom.get(), test_path));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_path, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
EXPECT_EQ(reloaded_ow.overworld_map(0)->area_graphics(),
(original_gfx + 1) % 256)
<< "US ROM modification should persist";
// Cleanup
std::filesystem::remove(test_path);
}
// Test 3: Version constants validation
TEST_F(RomVersionTest, VersionConstants_Validation) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
auto addrs = GetVersionAddresses(version_info.version);
// Verify pointers are within ROM bounds
EXPECT_LT(addrs.overworld_gfx_ptr1, rom->size())
<< "GFX pointer 1 should be within ROM";
EXPECT_LT(addrs.overworld_gfx_ptr2, rom->size())
<< "GFX pointer 2 should be within ROM";
EXPECT_LT(addrs.overworld_gfx_ptr3, rom->size())
<< "GFX pointer 3 should be within ROM";
EXPECT_LT(addrs.map32_tile_tl, rom->size())
<< "Map32 tile pointer should be within ROM";
// Read some data at these addresses to verify they're valid
auto gfx1 = rom->ReadByte(addrs.overworld_gfx_ptr1);
auto gfx2 = rom->ReadByte(addrs.overworld_gfx_ptr2);
auto gfx3 = rom->ReadByte(addrs.overworld_gfx_ptr3);
EXPECT_TRUE(gfx1.ok()) << "Should be able to read at GFX pointer 1";
EXPECT_TRUE(gfx2.ok()) << "Should be able to read at GFX pointer 2";
EXPECT_TRUE(gfx3.ok()) << "Should be able to read at GFX pointer 3";
}
// Test 4: Game data loads correctly for detected version
TEST_F(RomVersionTest, GameData_LoadsCorrectly) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
zelda3::GameData game_data;
ASSERT_OK(zelda3::LoadGameData(*rom, game_data));
// Verify palettes loaded
EXPECT_GT(game_data.palette_groups.overworld_main.size(), 0)
<< "Overworld main palettes should load";
EXPECT_GT(game_data.palette_groups.dungeon_main.size(), 0)
<< "Dungeon main palettes should load";
// Verify version was detected
EXPECT_TRUE(game_data.version == JP ||
game_data.version == US ||
game_data.version == RANDO);
}
// Test 5: Vanilla ROM identification
TEST_F(RomVersionTest, VanillaRom_Identification) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
// Check if ROM is vanilla (no ZSCustomOverworld)
bool is_vanilla = (version_info.zscustom_version == 0xFF ||
version_info.zscustom_version == 0x00);
if (is_vanilla) {
EXPECT_FALSE(version_info.is_expanded_tile16)
<< "Vanilla ROM should not have expanded tile16";
EXPECT_FALSE(version_info.is_expanded_tile32)
<< "Vanilla ROM should not have expanded tile32";
}
}
// Test 6: ROM header validation
TEST_F(RomVersionTest, RomHeader_Validation) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
// Check ROM title in header (at offset 0x7FC0)
std::string title;
for (int i = 0; i < 21; ++i) {
auto byte = rom->ReadByte(0x7FC0 + i);
if (byte.ok() && *byte >= 0x20 && *byte < 0x7F) {
title += static_cast<char>(*byte);
}
}
EXPECT_FALSE(title.empty()) << "ROM should have a valid title";
std::cout << "ROM Title: " << title << std::endl;
// Check ROM makeup byte (at 0x7FD5)
auto makeup = rom->ReadByte(0x7FD5);
ASSERT_TRUE(makeup.ok());
EXPECT_EQ(*makeup & 0x01, 0) << "Should be LoROM mapping";
// Check ROM type byte (at 0x7FD6)
auto rom_type = rom->ReadByte(0x7FD6);
ASSERT_TRUE(rom_type.ok());
// Type 0x02 = ROM + SRAM, common for ALTTP
// Check ROM size byte (at 0x7FD7)
auto rom_size = rom->ReadByte(0x7FD7);
ASSERT_TRUE(rom_size.ok());
// Size = 2^(rom_size + 10) bytes
// 0x0A = 1MB, 0x0B = 2MB
EXPECT_GE(*rom_size, 0x0A) << "ROM should be at least 1MB";
}
// Test 7: Cross-version data compatibility
TEST_F(RomVersionTest, CrossVersion_DataCompatibility) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()));
// These structures should be consistent across versions
EXPECT_EQ(overworld.overworld_maps().size(), 160)
<< "All versions should have 160 overworld maps";
EXPECT_EQ(overworld.entrances().size(), 129)
<< "All versions should have 129 entrances";
EXPECT_EQ(overworld.exits()->size(), 0x4F)
<< "All versions should have 0x4F exits";
EXPECT_EQ(overworld.holes().size(), 0x13)
<< "All versions should have 0x13 holes";
}
// Test 8: ROM checksum after modification
TEST_F(RomVersionTest, Checksum_AfterModification) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
// Read original checksum (at 0x7FDC-0x7FDF)
auto checksum_comp = rom->ReadWord(0x7FDC); // Checksum complement
auto checksum = rom->ReadWord(0x7FDE); // Checksum
ASSERT_TRUE(checksum_comp.ok());
ASSERT_TRUE(checksum.ok());
// Verify checksum and complement are inverses
EXPECT_EQ((*checksum_comp ^ *checksum) & 0xFFFF, 0xFFFF)
<< "Checksum and complement should be inverses";
// Modify ROM
ASSERT_OK(rom->WriteByte(0x1000, 0xAB));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify checksum was updated
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto new_checksum_comp = reloaded->ReadWord(0x7FDC);
auto new_checksum = reloaded->ReadWord(0x7FDE);
ASSERT_TRUE(new_checksum_comp.ok());
ASSERT_TRUE(new_checksum.ok());
// Checksum should still be valid (complement relationship)
// Note: yaze may or may not update checksums on save
if ((*new_checksum_comp ^ *new_checksum) == 0xFFFF) {
// Checksum was updated correctly
SUCCEED();
} else {
// Checksum wasn't updated - this might be intentional
std::cout << "Note: ROM checksum not updated after modification" << std::endl;
}
}
// Test 9: Multiple save/load cycles stability
TEST_F(RomVersionTest, MultipleCycles_Stability) {
const int num_cycles = 5;
std::unique_ptr<Rom> rom;
for (int cycle = 0; cycle < num_cycles; ++cycle) {
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom))
<< "Failed on load cycle " << cycle;
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()))
<< "Failed to load overworld on cycle " << cycle;
// Verify consistent data
EXPECT_EQ(overworld.overworld_maps().size(), 160)
<< "Map count mismatch on cycle " << cycle;
// Make a modification
const int map_id = FindPrimaryMapId(overworld);
auto* map = overworld.mutable_overworld_map(map_id);
uint8_t new_value = static_cast<uint8_t>(cycle);
map->set_area_graphics(new_value);
ASSERT_OK(overworld.SaveMapProperties())
<< "Failed to save on cycle " << cycle;
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_))
<< "Failed to save ROM on cycle " << cycle;
}
// Final verification
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
zelda3::Overworld final_ow(rom.get());
ASSERT_OK(final_ow.Load(rom.get()));
// Verify last modification persisted
const int map_id = FindPrimaryMapId(final_ow);
EXPECT_EQ(final_ow.overworld_map(map_id)->area_graphics(),
static_cast<uint8_t>(num_cycles - 1));
}
} // namespace test
} // namespace yaze