374 lines
12 KiB
C++
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
|