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

@@ -0,0 +1,501 @@
#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/screen/dungeon_map.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for Cross-Editor Data Integrity
*
* Validates that editing with multiple editors simultaneously
* doesn't cause data corruption:
* 1. Overworld + Tile16 combined edits
* 2. Dungeon + Palette combined edits
* 3. Full editor workflow: Load -> Edit multiple editors -> Save -> Reload
* 4. Concurrent modification detection
*/
class CrossEditorIntegrityTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(test_rom_path_);
if (!load_result.ok()) {
GTEST_SKIP() << "Failed to load test ROM: " << load_result.message();
}
// Load game data
game_data_ = std::make_unique<zelda3::GameData>();
auto gd_result = zelda3::LoadGameData(*rom_, *game_data_);
if (!gd_result.ok()) {
GTEST_SKIP() << "Failed to load game data: " << gd_result.message();
}
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<zelda3::GameData> game_data_;
};
// Test 1: Overworld + Tile16 combined edits
TEST_F(CrossEditorIntegrityTest, Overworld_Plus_Tile16) {
// Load overworld
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
// --- Overworld Edit ---
auto* map0 = overworld.mutable_overworld_map(0);
uint8_t original_map_gfx = map0->area_graphics();
map0->set_area_graphics((original_map_gfx + 1) % 256);
// --- Tile16 Edit ---
auto* tiles16_ptr = overworld.mutable_tiles16();
if (tiles16_ptr == nullptr || tiles16_ptr->empty()) {
GTEST_SKIP() << "No tile16 data to edit";
}
gfx::Tile16 original_tile0 = (*tiles16_ptr)[0];
(*tiles16_ptr)[0].tile0_.id_ = (original_tile0.tile0_.id_ + 1) % 0x200;
// --- Save Both ---
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(overworld.SaveMap16Tiles());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// --- Reload and Verify Both Edits ---
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
// Verify overworld edit
EXPECT_EQ(reloaded_ow.overworld_map(0)->area_graphics(),
(original_map_gfx + 1) % 256)
<< "Overworld map edit should persist";
// Verify tile16 edit
const auto reloaded_tiles16 = reloaded_ow.tiles16();
ASSERT_FALSE(reloaded_tiles16.empty());
EXPECT_EQ(reloaded_tiles16[0].tile0_.id_,
(original_tile0.tile0_.id_ + 1) % 0x200)
<< "Tile16 edit should persist";
}
// Test 2: Overworld + Palette combined edits
TEST_F(CrossEditorIntegrityTest, Overworld_Plus_Palette) {
// Load overworld
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
// --- Overworld Edit ---
auto* map5 = overworld.mutable_overworld_map(5);
uint8_t original_palette_id = map5->main_palette();
map5->set_main_palette((original_palette_id + 1) % 8);
// --- Palette Edit ---
const uint32_t palette_offset = 0xDE6C8; // Overworld main palette
auto original_color = rom_->ReadWord(palette_offset);
ASSERT_TRUE(original_color.ok());
uint16_t new_color = (*original_color + 0x0421) & 0x7FFF;
ASSERT_OK(rom_->WriteWord(palette_offset, new_color));
// --- Save Both ---
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// --- Reload and Verify Both Edits ---
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
// Verify overworld edit
EXPECT_EQ(reloaded_ow.overworld_map(5)->main_palette(),
(original_palette_id + 1) % 8)
<< "Overworld palette ID edit should persist";
// Verify palette color edit
auto reloaded_color = reloaded->ReadWord(palette_offset);
ASSERT_TRUE(reloaded_color.ok());
EXPECT_EQ(*reloaded_color, new_color)
<< "Palette color edit should persist";
}
// Test 3: Dungeon + Palette combined edits
TEST_F(CrossEditorIntegrityTest, Dungeon_Plus_Palette) {
// --- Dungeon Edit ---
const int room_id = 0;
uint32_t room_header_addr = 0xF8000 + (room_id * 14);
auto original_header = rom_->ReadByte(room_header_addr);
ASSERT_TRUE(original_header.ok());
uint8_t modified_header = (*original_header + 0x10) & 0xFF;
ASSERT_OK(rom_->WriteByte(room_header_addr, modified_header));
// --- Palette Edit ---
const uint32_t dungeon_palette_offset = 0xDD734;
auto original_color = rom_->ReadWord(dungeon_palette_offset);
ASSERT_TRUE(original_color.ok());
uint16_t new_color = (*original_color + 0x0842) & 0x7FFF;
ASSERT_OK(rom_->WriteWord(dungeon_palette_offset, new_color));
// --- Save ---
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// --- Reload and Verify Both Edits ---
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
// Verify dungeon edit
auto reloaded_header = reloaded->ReadByte(room_header_addr);
ASSERT_TRUE(reloaded_header.ok());
EXPECT_EQ(*reloaded_header, modified_header)
<< "Dungeon header edit should persist";
// Verify palette edit
auto reloaded_color = reloaded->ReadWord(dungeon_palette_offset);
ASSERT_TRUE(reloaded_color.ok());
EXPECT_EQ(*reloaded_color, new_color)
<< "Dungeon palette color edit should persist";
}
// Test 4: Dungeon Map + Overworld combined edits
TEST_F(CrossEditorIntegrityTest, DungeonMap_Plus_Overworld) {
// --- Load Dungeon Maps ---
zelda3::DungeonMapLabels labels;
auto maps_result = zelda3::LoadDungeonMaps(*rom_, labels);
if (!maps_result.ok()) {
GTEST_SKIP() << "Failed to load dungeon maps";
}
auto dungeon_maps = std::move(*maps_result);
// --- Dungeon Map Edit ---
if (dungeon_maps.empty()) {
GTEST_SKIP() << "No dungeon maps available";
}
uint8_t original_dm_room = dungeon_maps[0].floor_rooms[0][0];
dungeon_maps[0].floor_rooms[0][0] = (original_dm_room + 5) % 0xFF;
// --- Overworld Edit ---
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
auto* map10 = overworld.mutable_overworld_map(10);
uint8_t original_ow_gfx = map10->area_graphics();
map10->set_area_graphics((original_ow_gfx + 3) % 256);
// --- Save Both ---
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps));
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// --- Reload and Verify Both Edits ---
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
// Verify dungeon map edit
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
EXPECT_EQ((*reloaded_maps)[0].floor_rooms[0][0],
(original_dm_room + 5) % 0xFF)
<< "Dungeon map edit should persist";
// Verify overworld edit
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
EXPECT_EQ(reloaded_ow.overworld_map(10)->area_graphics(),
(original_ow_gfx + 3) % 256)
<< "Overworld edit should persist";
}
// Test 5: Full editor workflow - all editors
TEST_F(CrossEditorIntegrityTest, FullWorkflow_AllEditors) {
// --- Load All Data ---
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
zelda3::DungeonMapLabels labels;
auto dungeon_maps_result = zelda3::LoadDungeonMaps(*rom_, labels);
if (!dungeon_maps_result.ok()) {
GTEST_SKIP() << "Failed to load dungeon maps";
}
auto dungeon_maps = std::move(*dungeon_maps_result);
// --- Record Original Values ---
uint8_t orig_ow_gfx = overworld.overworld_map(0)->area_graphics();
auto* tiles16_ptr = overworld.mutable_tiles16();
uint16_t orig_tile16_id = (tiles16_ptr && !tiles16_ptr->empty())
? (*tiles16_ptr)[0].tile0_.id_ : 0;
uint8_t orig_dm_room = dungeon_maps.empty() ? 0 :
dungeon_maps[0].floor_rooms[0][0];
const uint32_t palette_offset = 0xDE6C8;
auto orig_palette = rom_->ReadWord(palette_offset);
ASSERT_TRUE(orig_palette.ok());
const uint32_t room_header = 0xF8000;
auto orig_room_header = rom_->ReadByte(room_header);
ASSERT_TRUE(orig_room_header.ok());
// --- Make All Edits ---
// Overworld map
overworld.mutable_overworld_map(0)->set_area_graphics((orig_ow_gfx + 1) % 256);
// Tile16
if (tiles16_ptr && !tiles16_ptr->empty()) {
(*tiles16_ptr)[0].tile0_.id_ = (orig_tile16_id + 1) % 0x200;
}
// Dungeon map
if (!dungeon_maps.empty()) {
dungeon_maps[0].floor_rooms[0][0] = (orig_dm_room + 1) % 0xFF;
}
// Palette
uint16_t new_palette = (*orig_palette + 0x0421) & 0x7FFF;
ASSERT_OK(rom_->WriteWord(palette_offset, new_palette));
// Room header
uint8_t new_room_header = (*orig_room_header + 0x10) & 0xFF;
ASSERT_OK(rom_->WriteByte(room_header, new_room_header));
// --- Save All ---
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(overworld.SaveMap16Tiles());
if (!dungeon_maps.empty()) {
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps));
}
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// --- Reload and Verify All ---
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
// Verify overworld
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
EXPECT_EQ(reloaded_ow.overworld_map(0)->area_graphics(),
(orig_ow_gfx + 1) % 256)
<< "Full workflow: Overworld edit should persist";
// Verify tile16
const auto reloaded_tiles16 = reloaded_ow.tiles16();
if (!reloaded_tiles16.empty()) {
EXPECT_EQ(reloaded_tiles16[0].tile0_.id_, (orig_tile16_id + 1) % 0x200)
<< "Full workflow: Tile16 edit should persist";
}
// Verify dungeon map
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_dm = zelda3::LoadDungeonMaps(*reloaded, reloaded_labels);
if (reloaded_dm.ok() && !(*reloaded_dm).empty()) {
EXPECT_EQ((*reloaded_dm)[0].floor_rooms[0][0], (orig_dm_room + 1) % 0xFF)
<< "Full workflow: Dungeon map edit should persist";
}
// Verify palette
auto reloaded_palette = reloaded->ReadWord(palette_offset);
ASSERT_TRUE(reloaded_palette.ok());
EXPECT_EQ(*reloaded_palette, new_palette)
<< "Full workflow: Palette edit should persist";
// Verify room header
auto reloaded_room = reloaded->ReadByte(room_header);
ASSERT_TRUE(reloaded_room.ok());
EXPECT_EQ(*reloaded_room, new_room_header)
<< "Full workflow: Room header edit should persist";
}
// Test 6: Multiple maps with no cross-corruption
TEST_F(CrossEditorIntegrityTest, MultipleMaps_NoCrossCorruption) {
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
// Record data for maps we won't modify
std::vector<uint8_t> untouched_gfx;
const std::vector<int> untouched_maps = {5, 10, 15, 20, 25};
for (int map_id : untouched_maps) {
untouched_gfx.push_back(overworld.overworld_map(map_id)->area_graphics());
}
// Modify only maps 0-4
for (int i = 0; i < 5; ++i) {
auto* map = overworld.mutable_overworld_map(i);
map->set_area_graphics((map->area_graphics() + i + 1) % 256);
}
// Save
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
// Verify untouched maps weren't corrupted
for (size_t i = 0; i < untouched_maps.size(); ++i) {
int map_id = untouched_maps[i];
EXPECT_EQ(reloaded_ow.overworld_map(map_id)->area_graphics(), untouched_gfx[i])
<< "Map " << map_id << " should not be corrupted";
}
}
// Test 7: Large scale combined edits
TEST_F(CrossEditorIntegrityTest, LargeScale_CombinedEdits) {
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
// Edit many overworld maps
const int num_map_edits = 50;
std::map<int, uint8_t> expected_gfx;
for (int i = 0; i < num_map_edits; ++i) {
auto* map = overworld.mutable_overworld_map(i);
expected_gfx[i] = (map->area_graphics() + i) % 256;
map->set_area_graphics(expected_gfx[i]);
}
// Edit many palette colors
const uint32_t palette_base = 0xDE6C8;
const int num_palette_edits = 32;
std::map<uint32_t, uint16_t> expected_colors;
for (int i = 0; i < num_palette_edits; ++i) {
uint32_t offset = palette_base + (i * 2);
auto orig = rom_->ReadWord(offset);
if (orig.ok()) {
expected_colors[offset] = (*orig + i) & 0x7FFF;
ASSERT_OK(rom_->WriteWord(offset, expected_colors[offset]));
}
}
// Save
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
// Verify map edits
int map_verified = 0;
for (const auto& [map_id, gfx] : expected_gfx) {
if (reloaded_ow.overworld_map(map_id)->area_graphics() == gfx) {
map_verified++;
}
}
EXPECT_EQ(map_verified, num_map_edits)
<< "All map edits should persist";
// Verify palette edits
int palette_verified = 0;
for (const auto& [offset, color] : expected_colors) {
auto reloaded_color = reloaded->ReadWord(offset);
if (reloaded_color.ok() && *reloaded_color == color) {
palette_verified++;
}
}
EXPECT_EQ(palette_verified, static_cast<int>(expected_colors.size()))
<< "All palette edits should persist";
}
// Test 8: Sequential save operations
TEST_F(CrossEditorIntegrityTest, SequentialSaveOperations) {
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
// First save cycle - overworld only
auto* map0 = overworld.mutable_overworld_map(0);
uint8_t gfx_v1 = (map0->area_graphics() + 1) % 256;
map0->set_area_graphics(gfx_v1);
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Second save cycle - add palette edit
const uint32_t palette_offset = 0xDE6C8;
auto color1 = rom_->ReadWord(palette_offset);
ASSERT_TRUE(color1.ok());
uint16_t new_color = (*color1 + 0x0421) & 0x7FFF;
ASSERT_OK(rom_->WriteWord(palette_offset, new_color));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Third save cycle - modify overworld again
uint8_t gfx_v2 = (gfx_v1 + 1) % 256;
map0->set_area_graphics(gfx_v2);
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Verify final state
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
// Should have the LATEST values
EXPECT_EQ(reloaded_ow.overworld_map(0)->area_graphics(), gfx_v2)
<< "Latest overworld edit should persist";
auto final_color = reloaded->ReadWord(palette_offset);
ASSERT_TRUE(final_color.ok());
EXPECT_EQ(*final_color, new_color)
<< "Palette edit should persist through subsequent saves";
}
// Test 9: Interleaved load/edit/save across regions
TEST_F(CrossEditorIntegrityTest, InterleavedOperations) {
// Take snapshots of different ROM regions
auto overworld_region = TakeSnapshot(*rom_, 0x7C9C, 128); // Map properties
auto dungeon_region = TakeSnapshot(*rom_, 0xF8000, 1400); // Room headers
auto palette_region = TakeSnapshot(*rom_, 0xDE6C8, 512); // Palettes
// Modify only overworld region
zelda3::Overworld overworld(rom_.get());
ASSERT_OK(overworld.Load(rom_.get()));
overworld.mutable_overworld_map(0)->set_area_graphics(
(overworld.overworld_map(0)->area_graphics() + 1) % 256);
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
// Verify untouched regions are preserved
// Note: Dungeon and palette regions should be unchanged
EXPECT_TRUE(VerifyNoCorruption(*reloaded, dungeon_region, "Dungeon Headers"));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, palette_region, "Palettes"));
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,368 @@
#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/dungeon/room.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for DungeonEditor Save Operations
*
* Validates the complete dungeon editing workflow:
* 1. Load ROM and room data
* 2. Modify room objects and sprites
* 3. Save changes to ROM
* 4. Reload ROM and verify edits persisted
* 5. Verify no data corruption occurred
*/
class DungeonEditorSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(test_rom_path_);
if (!load_result.ok()) {
GTEST_SKIP() << "Failed to load test ROM: " << load_result.message();
}
// Load game data
game_data_ = std::make_unique<zelda3::GameData>();
auto gd_result = zelda3::LoadGameData(*rom_, *game_data_);
if (!gd_result.ok()) {
GTEST_SKIP() << "Failed to load game data: " << gd_result.message();
}
}
// Room header location helper
uint32_t GetRoomHeaderAddress(int room_id) {
// Room headers start at 0xF8000 in vanilla ROM
// Each room header is 14 bytes
return 0xF8000 + (room_id * 14);
}
uint32_t GetRoomObjectPointerAddress(int room_id) {
// Object pointers at $1E8000 + room_id * 3
return 0x1E8000 + (room_id * 3);
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<zelda3::GameData> game_data_;
};
// Test 1: Room header save/reload persistence
TEST_F(DungeonEditorSaveTest, RoomHeader_SaveAndReload) {
const int test_room_id = 0; // Ganon's Room
// Get room header address
uint32_t header_addr = GetRoomHeaderAddress(test_room_id);
// Read original header byte
auto original_byte = rom_->ReadByte(header_addr + 1);
if (!original_byte.ok()) {
GTEST_SKIP() << "Failed to read room header";
}
// Modify the header byte
uint8_t modified_byte = (*original_byte + 0x10) & 0xFF;
ASSERT_OK(rom_->WriteByte(header_addr + 1, modified_byte));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_byte = reloaded_rom->ReadByte(header_addr + 1);
ASSERT_TRUE(reloaded_byte.ok());
EXPECT_EQ(*reloaded_byte, modified_byte)
<< "Room header modification should persist";
}
// Test 2: Room object data save/reload
TEST_F(DungeonEditorSaveTest, RoomObjects_SaveAndReload) {
const int test_room_id = 1;
// Get room object pointer
uint32_t obj_ptr_addr = GetRoomObjectPointerAddress(test_room_id);
auto obj_ptr_low = rom_->ReadWord(obj_ptr_addr);
auto obj_ptr_high = rom_->ReadByte(obj_ptr_addr + 2);
if (!obj_ptr_low.ok() || !obj_ptr_high.ok()) {
GTEST_SKIP() << "Failed to read object pointer";
}
uint32_t obj_data_addr = (*obj_ptr_low) | ((*obj_ptr_high) << 16);
obj_data_addr = SnesToPc(obj_data_addr);
// Record original first few bytes of object data
std::vector<uint8_t> original_data(8);
for (int i = 0; i < 8; ++i) {
auto byte = rom_->ReadByte(obj_data_addr + i);
original_data[i] = byte.ok() ? *byte : 0;
}
// Modify object data (change first object's position/type)
// Object format: 2 bytes position, 1 byte object type
if (original_data[0] != 0xFF && original_data[1] != 0xFF) {
uint8_t modified_pos = (original_data[0] + 0x10) & 0xFF;
ASSERT_OK(rom_->WriteByte(obj_data_addr, modified_pos));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_byte = reloaded_rom->ReadByte(obj_data_addr);
ASSERT_TRUE(reloaded_byte.ok());
EXPECT_EQ(*reloaded_byte, modified_pos)
<< "Room object modification should persist";
// Verify other bytes weren't corrupted
for (int i = 1; i < 8; ++i) {
auto byte = reloaded_rom->ReadByte(obj_data_addr + i);
ASSERT_TRUE(byte.ok());
EXPECT_EQ(*byte, original_data[i])
<< "Adjacent object data should not be corrupted at offset " << i;
}
}
}
// Test 3: Sprite data save/reload
TEST_F(DungeonEditorSaveTest, SpriteData_SaveAndReload) {
const int test_room_id = 2;
// Sprite pointers are at different location
// $09D62E + room_id * 2 for the pointer table
uint32_t sprite_ptr_table = 0x09D62E;
auto sprite_ptr = rom_->ReadWord(sprite_ptr_table + (test_room_id * 2));
if (!sprite_ptr.ok()) {
GTEST_SKIP() << "Failed to read sprite pointer";
}
uint32_t sprite_data_addr = SnesToPc(0x090000 | *sprite_ptr);
// Record original sprite data
std::vector<uint8_t> original_sprite_data(6);
bool has_sprites = true;
for (int i = 0; i < 6; ++i) {
auto byte = rom_->ReadByte(sprite_data_addr + i);
if (!byte.ok()) {
has_sprites = false;
break;
}
original_sprite_data[i] = *byte;
}
if (!has_sprites || original_sprite_data[0] == 0xFF) {
GTEST_SKIP() << "Room has no sprites to modify";
}
// Modify sprite Y position (first byte of sprite entry)
uint8_t modified_y = (original_sprite_data[0] + 0x08) & 0xFF;
ASSERT_OK(rom_->WriteByte(sprite_data_addr, modified_y));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_y = reloaded_rom->ReadByte(sprite_data_addr);
ASSERT_TRUE(reloaded_y.ok());
EXPECT_EQ(*reloaded_y, modified_y)
<< "Sprite position modification should persist";
}
// Test 4: Multiple room edits without cross-corruption
TEST_F(DungeonEditorSaveTest, MultipleRooms_NoCrossCorruption) {
const std::vector<int> test_rooms = {0, 10, 50, 100};
std::map<int, uint8_t> original_first_bytes;
std::map<int, uint8_t> modified_first_bytes;
// Record original data for each room's header
for (int room_id : test_rooms) {
uint32_t header_addr = GetRoomHeaderAddress(room_id);
auto first_byte = rom_->ReadByte(header_addr);
if (!first_byte.ok()) continue;
original_first_bytes[room_id] = *first_byte;
// Create unique modification for each room
modified_first_bytes[room_id] = (*first_byte + room_id) & 0xFF;
}
// Apply all modifications
for (const auto& [room_id, new_value] : modified_first_bytes) {
uint32_t header_addr = GetRoomHeaderAddress(room_id);
ASSERT_OK(rom_->WriteByte(header_addr, new_value));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify all changes persisted without cross-corruption
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (const auto& [room_id, expected_value] : modified_first_bytes) {
uint32_t header_addr = GetRoomHeaderAddress(room_id);
auto reloaded_byte = reloaded_rom->ReadByte(header_addr);
ASSERT_TRUE(reloaded_byte.ok());
EXPECT_EQ(*reloaded_byte, expected_value)
<< "Room " << room_id << " modification should persist";
}
}
// Test 5: Room floor/layer data persistence
TEST_F(DungeonEditorSaveTest, FloorLayerData_Persistence) {
const int test_room_id = 5;
// Floor data is part of the room header
uint32_t header_addr = GetRoomHeaderAddress(test_room_id);
// Byte 0 contains floor information
auto floor_byte = rom_->ReadByte(header_addr);
if (!floor_byte.ok()) {
GTEST_SKIP() << "Failed to read floor data";
}
uint8_t original_floor = *floor_byte;
uint8_t modified_floor = (original_floor ^ 0x07) & 0xFF; // Toggle lower bits
ASSERT_OK(rom_->WriteByte(header_addr, modified_floor));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_floor = reloaded_rom->ReadByte(header_addr);
ASSERT_TRUE(reloaded_floor.ok());
EXPECT_EQ(*reloaded_floor, modified_floor)
<< "Floor/layer data should persist";
}
// Test 6: Room via Room class
TEST_F(DungeonEditorSaveTest, RoomClass_LoadAndModify) {
const int test_room_id = 3;
// Create Room using constructor with room_id
zelda3::Room room(test_room_id, rom_.get(), game_data_.get());
// Get current palette value
uint8_t original_palette = room.palette;
// Get the header address for direct verification
uint32_t header_addr = GetRoomHeaderAddress(test_room_id);
auto original_header = rom_->ReadByte(header_addr);
if (!original_header.ok()) {
GTEST_SKIP() << "Failed to read room header";
}
// Modify header directly
uint8_t modified_header = (*original_header + 0x10) & 0xFF;
ASSERT_OK(rom_->WriteByte(header_addr, modified_header));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_header = reloaded_rom->ReadByte(header_addr);
ASSERT_TRUE(reloaded_header.ok());
EXPECT_EQ(*reloaded_header, modified_header)
<< "Room header modification should persist";
}
// Test 7: Large batch room modifications
TEST_F(DungeonEditorSaveTest, LargeBatch_RoomModifications) {
const int batch_size = 50;
std::map<int, uint8_t> original_headers;
std::map<int, uint8_t> modified_headers;
// Prepare batch modifications
for (int i = 0; i < batch_size; ++i) {
int room_id = i * 2; // Every other room
uint32_t header_addr = GetRoomHeaderAddress(room_id);
auto header_byte = rom_->ReadByte(header_addr);
if (!header_byte.ok()) continue;
original_headers[room_id] = *header_byte;
modified_headers[room_id] = (*header_byte + i) & 0xFF;
}
// Apply all modifications
for (const auto& [room_id, new_value] : modified_headers) {
uint32_t header_addr = GetRoomHeaderAddress(room_id);
ASSERT_OK(rom_->WriteByte(header_addr, new_value));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
int verified_count = 0;
for (const auto& [room_id, expected_value] : modified_headers) {
uint32_t header_addr = GetRoomHeaderAddress(room_id);
auto reloaded_byte = reloaded_rom->ReadByte(header_addr);
if (reloaded_byte.ok() && *reloaded_byte == expected_value) {
verified_count++;
}
}
EXPECT_EQ(verified_count, static_cast<int>(modified_headers.size()))
<< "All batch room modifications should persist";
}
// Test 8: Room palette data persistence
TEST_F(DungeonEditorSaveTest, PaletteData_Persistence) {
const int test_room_id = 10;
// Palette info is in the room header
uint32_t header_addr = GetRoomHeaderAddress(test_room_id);
// Read palette byte (offset varies by header layout)
auto palette_byte = rom_->ReadByte(header_addr + 2);
if (!palette_byte.ok()) {
GTEST_SKIP() << "Failed to read palette data";
}
uint8_t original_palette = *palette_byte;
uint8_t modified_palette = (original_palette + 1) & 0x07; // Cycle palette
ASSERT_OK(rom_->WriteByte(header_addr + 2, modified_palette));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_palette = reloaded_rom->ReadByte(header_addr + 2);
ASSERT_TRUE(reloaded_palette.ok());
EXPECT_EQ(*reloaded_palette, modified_palette)
<< "Palette data should persist";
}
} // namespace test
} // namespace yaze

View File

@@ -6,9 +6,10 @@
#include <string>
#include <vector>
#include "app/rom.h"
#include "app/transaction.h"
#include "rom/rom.h"
#include "rom/transaction.h"
#include "testing.h"
#include "util/macro.h"
namespace yaze {
namespace test {

View File

@@ -0,0 +1,468 @@
#ifndef YAZE_TEST_E2E_ROM_DEPENDENT_EDITOR_SAVE_TEST_BASE_H
#define YAZE_TEST_E2E_ROM_DEPENDENT_EDITOR_SAVE_TEST_BASE_H
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "rom/rom.h"
#include "rom/snes.h"
#include "testing.h"
#include "util/macro.h"
#include "zelda.h"
namespace yaze {
namespace test {
/**
* @brief ROM version information for testing
*/
struct RomVersionInfo {
std::string path;
zelda3_version version;
bool is_expanded_tile16;
bool is_expanded_tile32;
uint8_t zscustom_version; // 0xFF = vanilla, 0x02 = v2, 0x03+ = v3+
};
/**
* @brief Environment variable names for ROM paths
*/
struct RomEnvVars {
static constexpr const char* kDefaultRomPath = "YAZE_TEST_ROM_PATH";
static constexpr const char* kSkipRomTests = "YAZE_SKIP_ROM_TESTS";
static constexpr const char* kJpRomPath = "YAZE_TEST_ROM_JP_PATH";
static constexpr const char* kUsRomPath = "YAZE_TEST_ROM_US_PATH";
static constexpr const char* kEuRomPath = "YAZE_TEST_ROM_EU_PATH";
static constexpr const char* kExpandedRomPath = "YAZE_TEST_ROM_EXPANDED_PATH";
};
/**
* @brief Default ROM paths relative to workspace (roms/ directory)
*/
struct DefaultRomPaths {
static constexpr const char* kVanilla = "roms/alttp_vanilla.sfc";
static constexpr const char* kUsRom = "roms/Legend of Zelda, The - A Link to the Past (USA).sfc";
static constexpr const char* kExpanded = "roms/oos168.sfc";
static constexpr const char* kFallback = "zelda3.sfc";
};
/**
* @brief Base test fixture for E2E editor save tests
*
* Provides common functionality for:
* - ROM loading/saving with automatic cleanup
* - ROM version detection
* - Golden data comparison
* - Backup/restore ROM state
*/
class EditorSaveTestBase : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests if ROM tests are disabled
if (getenv(RomEnvVars::kSkipRomTests)) {
GTEST_SKIP() << "ROM tests disabled via YAZE_SKIP_ROM_TESTS";
}
// Determine ROM path
const char* rom_path_env = getenv(RomEnvVars::kDefaultRomPath);
if (rom_path_env && std::filesystem::exists(rom_path_env)) {
vanilla_rom_path_ = rom_path_env;
} else if (std::filesystem::exists(DefaultRomPaths::kVanilla)) {
vanilla_rom_path_ = DefaultRomPaths::kVanilla;
} else if (std::filesystem::exists(DefaultRomPaths::kUsRom)) {
vanilla_rom_path_ = DefaultRomPaths::kUsRom;
} else if (std::filesystem::exists(DefaultRomPaths::kFallback)) {
vanilla_rom_path_ = DefaultRomPaths::kFallback;
} else {
GTEST_SKIP() << "No test ROM found. Set YAZE_TEST_ROM_PATH or place ROM in roms/";
}
// Create test file paths with unique names per test
test_id_ = ::testing::UnitTest::GetInstance()->current_test_info()->name();
test_rom_path_ = "test_" + test_id_ + ".sfc";
backup_rom_path_ = "backup_" + test_id_ + ".sfc";
// Copy vanilla ROM for testing
std::error_code ec;
std::filesystem::copy_file(
vanilla_rom_path_, test_rom_path_,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
GTEST_SKIP() << "Failed to copy test ROM: " << ec.message();
}
// Create backup
std::filesystem::copy_file(
vanilla_rom_path_, backup_rom_path_,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
GTEST_SKIP() << "Failed to create backup ROM: " << ec.message();
}
}
void TearDown() override {
// Clean up test files
CleanupTestFiles();
}
void CleanupTestFiles() {
std::vector<std::string> files_to_remove = {
test_rom_path_,
backup_rom_path_,
};
for (const auto& file : files_to_remove) {
if (std::filesystem::exists(file)) {
std::filesystem::remove(file);
}
}
// Also clean up any .bak files created by ROM saving
if (std::filesystem::exists(test_rom_path_ + ".bak")) {
std::filesystem::remove(test_rom_path_ + ".bak");
}
}
// ===========================================================================
// ROM Loading Helpers
// ===========================================================================
/**
* @brief Load a ROM and verify basic integrity
*/
absl::Status LoadAndVerifyRom(const std::string& path,
std::unique_ptr<Rom>& rom) {
rom = std::make_unique<Rom>();
RETURN_IF_ERROR(rom->LoadFromFile(path));
// Basic integrity checks
if (rom->size() < 0x100000) { // At least 1MB
return absl::FailedPreconditionError("ROM too small");
}
if (rom->data() == nullptr) {
return absl::FailedPreconditionError("ROM data is null");
}
return absl::OkStatus();
}
/**
* @brief Save ROM to disk
*/
absl::Status SaveRomToFile(Rom* rom, const std::string& path) {
if (!rom) {
return absl::InvalidArgumentError("ROM is null");
}
Rom::SaveSettings settings;
settings.filename = path;
settings.backup = false; // We handle backups ourselves
settings.save_new = true;
return rom->SaveToFile(settings);
}
// ===========================================================================
// ROM Version Detection
// ===========================================================================
/**
* @brief Detect ROM version information
*/
RomVersionInfo DetectRomVersion(Rom& rom) {
RomVersionInfo info;
info.path = rom.filename();
// Detect ZSCustomOverworld version
auto version_byte = rom.ReadByte(0x140145);
info.zscustom_version = version_byte.ok() ? *version_byte : 0xFF;
// Detect expanded tile16
auto tile16_check = rom.ReadByte(0x02FD28);
info.is_expanded_tile16 = tile16_check.ok() && *tile16_check != 0x0F;
// Detect expanded tile32
auto tile32_check = rom.ReadByte(0x01772E);
info.is_expanded_tile32 = tile32_check.ok() && *tile32_check != 0x04;
// Determine zelda3 version based on header
info.version = zelda3_detect_version(rom.data(), rom.size());
return info;
}
/**
* @brief Check if ROM has ZSCustomOverworld ASM applied
*/
bool IsExpandedRom(Rom& rom) {
auto version_byte = rom.ReadByte(0x140145);
if (!version_byte.ok()) return false;
return *version_byte != 0xFF && *version_byte != 0x00;
}
// ===========================================================================
// Data Comparison Helpers
// ===========================================================================
/**
* @brief Compare two ROM regions for equality
*/
bool CompareRomRegions(Rom& rom1, Rom& rom2, uint32_t offset, uint32_t size) {
if (offset + size > rom1.size() || offset + size > rom2.size()) {
return false;
}
for (uint32_t i = 0; i < size; ++i) {
auto byte1 = rom1.ReadByte(offset + i);
auto byte2 = rom2.ReadByte(offset + i);
if (!byte1.ok() || !byte2.ok() || *byte1 != *byte2) {
return false;
}
}
return true;
}
/**
* @brief Read a byte from ROM with default value on error
*/
uint8_t ReadByteOrDefault(Rom& rom, uint32_t offset, uint8_t default_value = 0) {
auto result = rom.ReadByte(offset);
return result.ok() ? *result : default_value;
}
/**
* @brief Read a word from ROM with default value on error
*/
uint16_t ReadWordOrDefault(Rom& rom, uint32_t offset, uint16_t default_value = 0) {
auto result = rom.ReadWord(offset);
return result.ok() ? *result : default_value;
}
/**
* @brief Verify ROM byte matches expected value
*/
::testing::AssertionResult VerifyRomByte(Rom& rom, uint32_t offset,
uint8_t expected,
const std::string& description = "") {
auto result = rom.ReadByte(offset);
if (!result.ok()) {
return ::testing::AssertionFailure()
<< "Failed to read byte at 0x" << std::hex << offset
<< (description.empty() ? "" : " (" + description + ")");
}
if (*result != expected) {
return ::testing::AssertionFailure()
<< "Byte mismatch at 0x" << std::hex << offset
<< ": expected 0x" << static_cast<int>(expected)
<< ", got 0x" << static_cast<int>(*result)
<< (description.empty() ? "" : " (" + description + ")");
}
return ::testing::AssertionSuccess();
}
/**
* @brief Verify ROM word matches expected value
*/
::testing::AssertionResult VerifyRomWord(Rom& rom, uint32_t offset,
uint16_t expected,
const std::string& description = "") {
auto result = rom.ReadWord(offset);
if (!result.ok()) {
return ::testing::AssertionFailure()
<< "Failed to read word at 0x" << std::hex << offset
<< (description.empty() ? "" : " (" + description + ")");
}
if (*result != expected) {
return ::testing::AssertionFailure()
<< "Word mismatch at 0x" << std::hex << offset
<< ": expected 0x" << expected
<< ", got 0x" << *result
<< (description.empty() ? "" : " (" + description + ")");
}
return ::testing::AssertionSuccess();
}
// ===========================================================================
// Corruption Detection Helpers
// ===========================================================================
/**
* @brief Record ROM state for later comparison (records specific regions)
*/
struct RomSnapshot {
std::vector<uint8_t> data;
uint32_t offset;
uint32_t size;
};
RomSnapshot TakeSnapshot(Rom& rom, uint32_t offset, uint32_t size) {
RomSnapshot snapshot;
snapshot.offset = offset;
snapshot.size = std::min(size, static_cast<uint32_t>(rom.size() - offset));
snapshot.data.resize(snapshot.size);
for (uint32_t i = 0; i < snapshot.size; ++i) {
auto byte = rom.ReadByte(offset + i);
snapshot.data[i] = byte.ok() ? *byte : 0;
}
return snapshot;
}
/**
* @brief Verify ROM region matches snapshot (no corruption)
*/
::testing::AssertionResult VerifyNoCorruption(Rom& rom,
const RomSnapshot& snapshot,
const std::string& region_name = "") {
for (uint32_t i = 0; i < snapshot.size; ++i) {
auto byte = rom.ReadByte(snapshot.offset + i);
if (!byte.ok()) {
return ::testing::AssertionFailure()
<< "Failed to read byte at offset 0x" << std::hex
<< (snapshot.offset + i);
}
if (*byte != snapshot.data[i]) {
return ::testing::AssertionFailure()
<< "Corruption detected in " << (region_name.empty() ? "ROM region" : region_name)
<< " at offset 0x" << std::hex << (snapshot.offset + i)
<< ": expected 0x" << static_cast<int>(snapshot.data[i])
<< ", got 0x" << static_cast<int>(*byte);
}
}
return ::testing::AssertionSuccess();
}
// ===========================================================================
// Test Utility Methods
// ===========================================================================
/**
* @brief Get path to expanded ROM for v3 feature tests
*/
std::string GetExpandedRomPath() {
const char* expanded_path = getenv(RomEnvVars::kExpandedRomPath);
if (expanded_path && std::filesystem::exists(expanded_path)) {
return expanded_path;
}
if (std::filesystem::exists(DefaultRomPaths::kExpanded)) {
return DefaultRomPaths::kExpanded;
}
return ""; // Not available
}
/**
* @brief Skip test if expanded ROM is required but not available
*/
void RequireExpandedRom() {
std::string path = GetExpandedRomPath();
if (path.empty()) {
GTEST_SKIP() << "Expanded ROM not available for v3 feature tests";
}
}
// ===========================================================================
// Member Variables
// ===========================================================================
std::string vanilla_rom_path_;
std::string test_rom_path_;
std::string backup_rom_path_;
std::string test_id_;
};
/**
* @brief Extended test fixture for multi-ROM version testing
*/
class MultiVersionEditorSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Check for additional ROM versions
const char* jp_path = getenv(RomEnvVars::kJpRomPath);
const char* us_path = getenv(RomEnvVars::kUsRomPath);
const char* eu_path = getenv(RomEnvVars::kEuRomPath);
if (jp_path && std::filesystem::exists(jp_path)) {
jp_rom_path_ = jp_path;
}
if (us_path && std::filesystem::exists(us_path)) {
us_rom_path_ = us_path;
} else if (std::filesystem::exists(DefaultRomPaths::kUsRom)) {
us_rom_path_ = DefaultRomPaths::kUsRom;
}
if (eu_path && std::filesystem::exists(eu_path)) {
eu_rom_path_ = eu_path;
}
}
bool HasJpRom() const { return !jp_rom_path_.empty(); }
bool HasUsRom() const { return !us_rom_path_.empty(); }
bool HasEuRom() const { return !eu_rom_path_.empty(); }
std::string jp_rom_path_;
std::string us_rom_path_;
std::string eu_rom_path_;
};
/**
* @brief Test fixture specifically for ZSCustomOverworld v3 expanded ROMs
*/
class ExpandedRomSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
// Skip if ROM tests disabled
if (getenv(RomEnvVars::kSkipRomTests)) {
GTEST_SKIP() << "ROM tests disabled via YAZE_SKIP_ROM_TESTS";
}
// Get expanded ROM path
const char* expanded_path = getenv(RomEnvVars::kExpandedRomPath);
if (expanded_path && std::filesystem::exists(expanded_path)) {
expanded_rom_path_ = expanded_path;
} else if (std::filesystem::exists(DefaultRomPaths::kExpanded)) {
expanded_rom_path_ = DefaultRomPaths::kExpanded;
} else {
GTEST_SKIP() << "Expanded ROM not available. Set YAZE_TEST_ROM_EXPANDED_PATH";
}
// Use vanilla for baseline comparison
if (std::filesystem::exists(DefaultRomPaths::kVanilla)) {
vanilla_rom_path_ = DefaultRomPaths::kVanilla;
} else {
vanilla_rom_path_ = "";
}
// Create test file paths
test_id_ = ::testing::UnitTest::GetInstance()->current_test_info()->name();
test_rom_path_ = "test_expanded_" + test_id_ + ".sfc";
backup_rom_path_ = "backup_expanded_" + test_id_ + ".sfc";
// Copy expanded ROM for testing
std::error_code ec;
std::filesystem::copy_file(
expanded_rom_path_, test_rom_path_,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
GTEST_SKIP() << "Failed to copy expanded ROM: " << ec.message();
}
std::filesystem::copy_file(
expanded_rom_path_, backup_rom_path_,
std::filesystem::copy_options::overwrite_existing, ec);
}
std::string expanded_rom_path_;
};
} // namespace test
} // namespace yaze
#endif // YAZE_TEST_E2E_ROM_DEPENDENT_EDITOR_SAVE_TEST_BASE_H

View File

@@ -0,0 +1,412 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "app/gfx/resource/arena.h"
#include "app/gfx/util/compression.h"
#include "rom/rom.h"
#include "zelda3/game_data.h"
#include "zelda3/game_data.h"
#include "testing.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for Graphics Editor Save Operations
*
* Validates the complete graphics editing workflow:
* 1. Load ROM and graphics sheets
* 2. Modify pixel data in sheets
* 3. Save changes (8BPP→SNES indexed + LC-LZ2 compression)
* 4. Reload ROM and verify edits persisted
* 5. Verify no data corruption occurred
*/
class GraphicsEditorSaveTest : public ::testing::Test {
protected:
void SetUp() override {
// Skip tests if ROM is not available
if (getenv("YAZE_SKIP_ROM_TESTS")) {
GTEST_SKIP() << "ROM tests disabled";
}
// Get ROM path from environment or use default (vanilla.sfc to avoid edited ROMs)
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
vanilla_rom_path_ = rom_path_env ? rom_path_env : "vanilla.sfc";
if (!std::filesystem::exists(vanilla_rom_path_)) {
GTEST_SKIP() << "Test ROM not found: " << vanilla_rom_path_;
}
// Create test ROM copies
test_rom_path_ = "test_graphics_edit.sfc";
backup_rom_path_ = "test_graphics_backup.sfc";
// Copy vanilla ROM for testing
std::filesystem::copy_file(
vanilla_rom_path_, test_rom_path_,
std::filesystem::copy_options::overwrite_existing);
std::filesystem::copy_file(
vanilla_rom_path_, backup_rom_path_,
std::filesystem::copy_options::overwrite_existing);
}
void TearDown() override {
// Clean up test files
if (std::filesystem::exists(test_rom_path_)) {
std::filesystem::remove(test_rom_path_);
}
if (std::filesystem::exists(backup_rom_path_)) {
std::filesystem::remove(backup_rom_path_);
}
}
// Helper to load ROM and verify basic integrity
static absl::Status LoadAndVerifyROM(const std::string& path,
std::unique_ptr<Rom>& rom) {
rom = std::make_unique<Rom>();
RETURN_IF_ERROR(rom->LoadFromFile(path));
// Basic ROM integrity checks
EXPECT_EQ(rom->size(), 0x200000) << "ROM size should be 2MB";
EXPECT_NE(rom->data(), nullptr) << "ROM data should not be null";
return absl::OkStatus();
}
// Helper to load graphics sheets from ROM into Arena
static absl::Status LoadGraphicsFromRom(Rom& rom) {
zelda3::GameData game_data;
RETURN_IF_ERROR(zelda3::LoadGameData(rom, game_data));
// Copy loaded sheets to Arena
auto& arena_sheets = gfx::Arena::Get().gfx_sheets();
for (size_t i = 0; i < zelda3::kNumGfxSheets; i++) {
arena_sheets[i] = std::move(game_data.gfx_bitmaps[i]);
}
return absl::OkStatus();
}
// Helper to save modified sheets to ROM (mirrors GraphicsEditor::Save())
static absl::Status SaveSheetToRom(Rom& rom, uint16_t sheet_id) {
if (sheet_id >= zelda3::kNumGfxSheets) {
return absl::InvalidArgumentError("Sheet ID out of range");
}
auto& sheets = gfx::Arena::Get().gfx_sheets();
auto& sheet = sheets[sheet_id];
if (!sheet.is_active()) {
return absl::FailedPreconditionError("Sheet not active");
}
// Determine BPP and compression based on sheet range
int bpp = 3; // Default 3BPP
bool compressed = true;
// Sheets 113-114, 218+ are 2BPP
if (sheet_id == 113 || sheet_id == 114 || sheet_id >= 218) {
bpp = 2;
}
// Sheets 115-126 are uncompressed
if (sheet_id >= 115 && sheet_id <= 126) {
compressed = false;
}
// Convert 8BPP bitmap data to SNES indexed format
auto indexed_data = gfx::Bpp8SnesToIndexed(sheet.vector(), bpp);
std::vector<uint8_t> final_data;
if (compressed) {
// Compress using Hyrule Magic LC-LZ2
int compressed_size = 0;
auto compressed_data = gfx::HyruleMagicCompress(
indexed_data.data(), static_cast<int>(indexed_data.size()),
&compressed_size, 1);
final_data.assign(compressed_data.begin(),
compressed_data.begin() + compressed_size);
} else {
final_data = std::move(indexed_data);
}
// Calculate ROM offset for this sheet
// Use JP version constants as default for vanilla ROM
const auto& vc = zelda3::kVersionConstantsMap.at(zelda3_version::JP);
uint32_t offset = zelda3::GetGraphicsAddress(
rom.data(), static_cast<uint8_t>(sheet_id),
vc.kOverworldGfxPtr1, vc.kOverworldGfxPtr2,
vc.kOverworldGfxPtr3, rom.size());
// Write data to ROM buffer
for (size_t i = 0; i < final_data.size(); i++) {
RETURN_IF_ERROR(rom.WriteByte(offset + i, final_data[i]));
}
return absl::OkStatus();
}
std::string vanilla_rom_path_;
std::string test_rom_path_;
std::string backup_rom_path_;
};
// Test 1: Single sheet edit, save, and reload verification
TEST_F(GraphicsEditorSaveTest, SingleSheetEdit_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
// Load graphics into Arena
ASSERT_OK(LoadGraphicsFromRom(*rom));
// Get initial pixel value from sheet 0
auto& sheets = gfx::Arena::Get().gfx_sheets();
auto& sheet = sheets[0];
ASSERT_TRUE(sheet.is_active()) << "Sheet 0 should be active after loading";
// Record original pixel value at (0,0)
uint8_t original_pixel = sheet.GetPixel(0, 0);
// Modify pixel (cycle to next value, wrapping at 16 for 4-bit indexed)
uint8_t new_pixel = (original_pixel + 1) % 16;
sheet.WriteToPixel(0, 0, new_pixel);
// Verify modification took effect in memory
EXPECT_EQ(sheet.GetPixel(0, 0), new_pixel)
<< "Pixel modification should be reflected immediately";
// Save modified sheet to ROM
ASSERT_OK(SaveSheetToRom(*rom, 0));
// Save ROM to file
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
// --- Reload and verify ---
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
// Load graphics from reloaded ROM
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
// Verify pixel change persisted
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[0];
EXPECT_EQ(reloaded_sheet.GetPixel(0, 0), new_pixel)
<< "Pixel modification should persist after save/reload";
}
// Test 2: Multiple sheet edits save atomically
TEST_F(GraphicsEditorSaveTest, MultipleSheetEdit_Atomicity) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
ASSERT_OK(LoadGraphicsFromRom(*rom));
auto& sheets = gfx::Arena::Get().gfx_sheets();
// Modify sheets 0, 50, and 100 with distinct values
const std::vector<uint16_t> test_sheets = {0, 50, 100};
const std::vector<uint8_t> test_values = {5, 10, 15};
for (size_t i = 0; i < test_sheets.size(); i++) {
auto& sheet = sheets[test_sheets[i]];
if (sheet.is_active()) {
sheet.WriteToPixel(0, 0, test_values[i]);
}
}
// Save all modified sheets
for (uint16_t sheet_id : test_sheets) {
if (sheets[sheet_id].is_active()) {
ASSERT_OK(SaveSheetToRom(*rom, sheet_id));
}
}
// Save ROM
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
// Reload and verify ALL changes persisted
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
for (size_t i = 0; i < test_sheets.size(); i++) {
if (reloaded_sheets[test_sheets[i]].is_active()) {
EXPECT_EQ(reloaded_sheets[test_sheets[i]].GetPixel(0, 0), test_values[i])
<< "Sheet " << test_sheets[i] << " modification should persist";
}
}
}
// Test 3: Compression integrity for LC-LZ2 compressed sheets
TEST_F(GraphicsEditorSaveTest, CompressionIntegrity_LZ2Sheets) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
ASSERT_OK(LoadGraphicsFromRom(*rom));
// Sheet 50 should be compressed (in range 0-112)
const uint16_t test_sheet = 50;
auto& sheets = gfx::Arena::Get().gfx_sheets();
auto& sheet = sheets[test_sheet];
if (!sheet.is_active()) {
GTEST_SKIP() << "Sheet 50 not active in this ROM";
}
// Record original data for a small region
std::vector<uint8_t> original_region;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
original_region.push_back(sheet.GetPixel(x, y));
}
}
// Modify a single pixel
uint8_t original_pixel = sheet.GetPixel(4, 4);
uint8_t new_pixel = (original_pixel + 7) % 16;
sheet.WriteToPixel(4, 4, new_pixel);
// Save and reload
ASSERT_OK(SaveSheetToRom(*rom, test_sheet));
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
// Verify the modified pixel
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[test_sheet];
EXPECT_EQ(reloaded_sheet.GetPixel(4, 4), new_pixel)
<< "Modified pixel should persist through compression round-trip";
// Verify surrounding pixels weren't corrupted
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
if (x == 4 && y == 4) continue; // Skip modified pixel
int idx = y * 8 + x;
EXPECT_EQ(reloaded_sheet.GetPixel(x, y), original_region[idx])
<< "Pixel at (" << x << "," << y << ") should not be corrupted";
}
}
}
// Test 4: Uncompressed sheets (115-126) save correctly
TEST_F(GraphicsEditorSaveTest, UncompressedSheets_SaveCorrectly) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
ASSERT_OK(LoadGraphicsFromRom(*rom));
// Sheet 115 is uncompressed
const uint16_t test_sheet = 115;
auto& sheets = gfx::Arena::Get().gfx_sheets();
auto& sheet = sheets[test_sheet];
if (!sheet.is_active()) {
GTEST_SKIP() << "Sheet 115 not active in this ROM";
}
// Modify pixel
uint8_t original_pixel = sheet.GetPixel(0, 0);
uint8_t new_pixel = (original_pixel + 3) % 16;
sheet.WriteToPixel(0, 0, new_pixel);
// Save and reload
ASSERT_OK(SaveSheetToRom(*rom, test_sheet));
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
auto& reloaded_sheet = gfx::Arena::Get().gfx_sheets()[test_sheet];
EXPECT_EQ(reloaded_sheet.GetPixel(0, 0), new_pixel)
<< "Uncompressed sheet modification should persist";
}
// Test 5: Save without corrupting adjacent sheet data
TEST_F(GraphicsEditorSaveTest, SaveWithoutCorruption_AdjacentData) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
ASSERT_OK(LoadGraphicsFromRom(*rom));
auto& sheets = gfx::Arena::Get().gfx_sheets();
// Record pixel values from adjacent sheets (49 and 51)
uint8_t sheet49_pixel = 0;
uint8_t sheet51_pixel = 0;
if (sheets[49].is_active()) {
sheet49_pixel = sheets[49].GetPixel(0, 0);
}
if (sheets[51].is_active()) {
sheet51_pixel = sheets[51].GetPixel(0, 0);
}
// Modify only sheet 50
if (sheets[50].is_active()) {
sheets[50].WriteToPixel(0, 0, 8);
ASSERT_OK(SaveSheetToRom(*rom, 50));
}
// Save and reload
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
// Verify adjacent sheets weren't corrupted
if (reloaded_sheets[49].is_active()) {
EXPECT_EQ(reloaded_sheets[49].GetPixel(0, 0), sheet49_pixel)
<< "Sheet 49 should not be corrupted by saving sheet 50";
}
if (reloaded_sheets[51].is_active()) {
EXPECT_EQ(reloaded_sheets[51].GetPixel(0, 0), sheet51_pixel)
<< "Sheet 51 should not be corrupted by saving sheet 50";
}
}
// Test 6: Round-trip with no modifications preserves data
TEST_F(GraphicsEditorSaveTest, RoundTrip_NoModifications) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyROM(vanilla_rom_path_, rom));
ASSERT_OK(LoadGraphicsFromRom(*rom));
// Record sample pixel values from multiple sheets
auto& sheets = gfx::Arena::Get().gfx_sheets();
std::map<uint16_t, uint8_t> original_pixels;
for (uint16_t id : {0, 25, 50, 75, 100, 150, 200}) {
if (sheets[id].is_active()) {
original_pixels[id] = sheets[id].GetPixel(0, 0);
}
}
// Save ROM without modifications
ASSERT_OK(rom->SaveToFile(Rom::SaveSettings{.filename = test_rom_path_}));
// Reload
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyROM(test_rom_path_, reloaded_rom));
ASSERT_OK(LoadGraphicsFromRom(*reloaded_rom));
auto& reloaded_sheets = gfx::Arena::Get().gfx_sheets();
// Verify all recorded pixels match
for (const auto& [id, pixel] : original_pixels) {
if (reloaded_sheets[id].is_active()) {
EXPECT_EQ(reloaded_sheets[id].GetPixel(0, 0), pixel)
<< "Sheet " << id << " should preserve data through round-trip";
}
}
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,439 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include "e2e/rom_dependent/editor_save_test_base.h"
#include "app/gfx/types/snes_color.h"
#include "app/gfx/types/snes_palette.h"
#include "app/gfx/util/palette_manager.h"
#include "rom/rom.h"
#include "testing.h"
#include "zelda3/game_data.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for PaletteEditor Save Operations
*
* Validates the complete palette editing workflow:
* 1. Load ROM and palette data
* 2. Modify colors in various palette groups
* 3. Save changes to ROM
* 4. Reload ROM and verify edits persisted
* 5. Verify SNES color format round-trip accuracy
*/
class PaletteEditorSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(test_rom_path_);
if (!load_result.ok()) {
GTEST_SKIP() << "Failed to load test ROM: " << load_result.message();
}
// Load game data (which includes all palettes)
game_data_ = std::make_unique<zelda3::GameData>();
auto gd_result = zelda3::LoadGameData(*rom_, *game_data_);
if (!gd_result.ok()) {
GTEST_SKIP() << "Failed to load game data: " << gd_result.message();
}
// Initialize PaletteManager with game data
gfx::PaletteManager::Get().Initialize(game_data_.get());
}
void TearDown() override {
// Reset PaletteManager state
gfx::PaletteManager::Get().DiscardAllChanges();
EditorSaveTestBase::TearDown();
}
// Helper to read a SNES color directly from ROM
absl::StatusOr<gfx::SnesColor> ReadColorFromRom(Rom& rom, uint32_t offset) {
auto word = rom.ReadWord(offset);
if (!word.ok()) {
return word.status();
}
return gfx::SnesColor(*word);
}
// Helper to write a SNES color to ROM
absl::Status WriteColorToRom(Rom& rom, uint32_t offset, const gfx::SnesColor& color) {
return rom.WriteWord(offset, color.snes());
}
// Known palette addresses in vanilla ROM (version-specific)
static constexpr uint32_t kOverworldPaletteMain = 0xDE6C8;
static constexpr uint32_t kDungeonPaletteMain = 0xDD734;
static constexpr uint32_t kSpritePaletteGlobal = 0xDD218;
static constexpr uint32_t kHudPalette = 0xDD660;
std::unique_ptr<Rom> rom_;
std::unique_ptr<zelda3::GameData> game_data_;
};
// Test 1: Single color modification persistence
TEST_F(PaletteEditorSaveTest, SingleColor_SaveAndReload) {
// Read original color from overworld palette
auto original_color = ReadColorFromRom(*rom_, kOverworldPaletteMain);
if (!original_color.ok()) {
GTEST_SKIP() << "Failed to read original color";
}
// Create a modified color (shift hue)
uint16_t original_snes = original_color->snes();
uint16_t modified_snes = ((original_snes + 0x0421) & 0x7FFF); // Add some color
gfx::SnesColor modified_color(modified_snes);
// Write modified color to ROM
ASSERT_OK(WriteColorToRom(*rom_, kOverworldPaletteMain, modified_color));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
auto reloaded_color = ReadColorFromRom(*reloaded_rom, kOverworldPaletteMain);
ASSERT_TRUE(reloaded_color.ok());
EXPECT_EQ(reloaded_color->snes(), modified_snes)
<< "Color modification should persist after save/reload";
}
// Test 2: Multiple palette group modifications
TEST_F(PaletteEditorSaveTest, MultiplePaletteGroups_SaveAndReload) {
// Test modifying colors in different palette groups
const std::vector<std::pair<uint32_t, std::string>> palette_offsets = {
{kOverworldPaletteMain, "Overworld Main"},
{kDungeonPaletteMain, "Dungeon Main"},
{kSpritePaletteGlobal, "Sprite Global"},
};
std::map<uint32_t, uint16_t> original_colors;
std::map<uint32_t, uint16_t> modified_colors;
// Record originals and prepare modifications
for (const auto& [offset, name] : palette_offsets) {
auto color = ReadColorFromRom(*rom_, offset);
if (!color.ok()) continue;
original_colors[offset] = color->snes();
// Create unique modification for each palette
modified_colors[offset] = (color->snes() ^ 0x1234) & 0x7FFF;
}
// Apply all modifications
for (const auto& [offset, new_color] : modified_colors) {
ASSERT_OK(WriteColorToRom(*rom_, offset, gfx::SnesColor(new_color)));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify all changes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (const auto& [offset, expected_color] : modified_colors) {
auto reloaded = ReadColorFromRom(*reloaded_rom, offset);
ASSERT_TRUE(reloaded.ok());
EXPECT_EQ(reloaded->snes(), expected_color)
<< "Palette at 0x" << std::hex << offset << " should persist";
}
}
// Test 3: SNES color format round-trip accuracy
TEST_F(PaletteEditorSaveTest, SnesColorFormat_RoundTrip) {
// Test that SNES color format conversions are accurate
const std::vector<uint16_t> test_colors = {
0x0000, // Black
0x7FFF, // White
0x001F, // Red (max)
0x03E0, // Green (max)
0x7C00, // Blue (max)
0x0421, // Dark gray
0x294A, // Medium gray
0x5294, // Light gray
0x1234, // Random color
0x5678, // Another random
};
for (uint16_t test_snes : test_colors) {
// Convert to SnesColor and back
gfx::SnesColor color(test_snes);
// Get RGB representation
auto rgb = color.rgb();
// Create new color from RGB
gfx::SnesColor reconstructed(rgb);
// Verify SNES value matches (may have small rounding differences)
uint16_t reconstructed_snes = reconstructed.snes();
// Allow for minor quantization differences (SNES uses 5-bit color)
int diff_r = std::abs((test_snes & 0x1F) - (reconstructed_snes & 0x1F));
int diff_g = std::abs(((test_snes >> 5) & 0x1F) -
((reconstructed_snes >> 5) & 0x1F));
int diff_b = std::abs(((test_snes >> 10) & 0x1F) -
((reconstructed_snes >> 10) & 0x1F));
EXPECT_LE(diff_r, 1) << "Red channel should be accurate for 0x"
<< std::hex << test_snes;
EXPECT_LE(diff_g, 1) << "Green channel should be accurate for 0x"
<< std::hex << test_snes;
EXPECT_LE(diff_b, 1) << "Blue channel should be accurate for 0x"
<< std::hex << test_snes;
}
}
// Test 4: Full palette (16 colors) save/reload
TEST_F(PaletteEditorSaveTest, FullPalette_SaveAndReload) {
// Save/reload a complete 16-color palette
const uint32_t palette_base = kOverworldPaletteMain;
std::vector<uint16_t> original_palette(16);
std::vector<uint16_t> modified_palette(16);
// Read original palette
for (int i = 0; i < 16; ++i) {
auto color = ReadColorFromRom(*rom_, palette_base + (i * 2));
original_palette[i] = color.ok() ? color->snes() : 0;
// Create gradient modification
modified_palette[i] = (i * 0x0842) & 0x7FFF;
}
// Write modified palette
for (int i = 0; i < 16; ++i) {
ASSERT_OK(WriteColorToRom(*rom_, palette_base + (i * 2),
gfx::SnesColor(modified_palette[i])));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (int i = 0; i < 16; ++i) {
auto reloaded = ReadColorFromRom(*reloaded_rom, palette_base + (i * 2));
ASSERT_TRUE(reloaded.ok());
EXPECT_EQ(reloaded->snes(), modified_palette[i])
<< "Palette entry " << i << " should persist";
}
}
// Test 5: Adjacent palette data not corrupted
TEST_F(PaletteEditorSaveTest, NoAdjacentCorruption) {
// Modify middle palette and verify adjacent palettes aren't corrupted
const uint32_t target_offset = kOverworldPaletteMain + 32; // 16th color
const uint32_t prev_offset = kOverworldPaletteMain + 30; // 15th color
const uint32_t next_offset = kOverworldPaletteMain + 34; // 17th color
// Record adjacent colors
auto prev_color = ReadColorFromRom(*rom_, prev_offset);
auto next_color = ReadColorFromRom(*rom_, next_offset);
if (!prev_color.ok() || !next_color.ok()) {
GTEST_SKIP() << "Failed to read adjacent colors";
}
// Modify target color
gfx::SnesColor target_modified(0x5555);
ASSERT_OK(WriteColorToRom(*rom_, target_offset, target_modified));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
// Verify target was modified
auto target_reloaded = ReadColorFromRom(*reloaded_rom, target_offset);
ASSERT_TRUE(target_reloaded.ok());
EXPECT_EQ(target_reloaded->snes(), 0x5555);
// Verify adjacent colors not corrupted
auto prev_reloaded = ReadColorFromRom(*reloaded_rom, prev_offset);
auto next_reloaded = ReadColorFromRom(*reloaded_rom, next_offset);
ASSERT_TRUE(prev_reloaded.ok());
ASSERT_TRUE(next_reloaded.ok());
EXPECT_EQ(prev_reloaded->snes(), prev_color->snes())
<< "Previous color should not be corrupted";
EXPECT_EQ(next_reloaded->snes(), next_color->snes())
<< "Next color should not be corrupted";
}
// Test 6: PaletteManager integration
TEST_F(PaletteEditorSaveTest, PaletteManager_SaveAllToRom) {
// Get a palette group through PaletteManager
auto& pm = gfx::PaletteManager::Get();
if (!pm.IsInitialized()) {
GTEST_SKIP() << "PaletteManager not initialized";
}
// Try to modify a color through PaletteManager
// Access overworld main palettes through game_data
auto* ow_main = game_data_->palette_groups.overworld_main.mutable_palette(0);
if (!ow_main || ow_main->size() == 0) {
GTEST_SKIP() << "No overworld main palette available";
}
// Record original color
gfx::SnesColor original_color = (*ow_main)[0];
// Modify the color
uint16_t new_snes_value = (original_color.snes() + 0x0842) & 0x7FFF;
(*ow_main)[0] = gfx::SnesColor(new_snes_value);
(*ow_main)[0].set_modified(true);
// Save through PaletteManager
auto save_result = pm.SaveAllToRom();
if (!save_result.ok()) {
// If save isn't implemented, skip gracefully
GTEST_SKIP() << "PaletteManager SaveAllToRom not implemented: "
<< save_result.message();
}
// Save ROM to disk
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
std::unique_ptr<zelda3::GameData> reloaded_gd = std::make_unique<zelda3::GameData>();
ASSERT_OK(zelda3::LoadGameData(*reloaded_rom, *reloaded_gd));
auto* reloaded_palette = reloaded_gd->palette_groups.overworld_main.mutable_palette(0);
if (reloaded_palette && reloaded_palette->size() > 0) {
EXPECT_EQ((*reloaded_palette)[0].snes(), new_snes_value)
<< "Color should persist through PaletteManager save";
}
}
// Test 7: HUD palette modifications
TEST_F(PaletteEditorSaveTest, HudPalette_Persistence) {
// HUD palette should persist correctly
std::vector<uint16_t> original_hud(16);
std::vector<uint16_t> modified_hud(16);
// Read and modify HUD palette
for (int i = 0; i < 16; ++i) {
auto color = ReadColorFromRom(*rom_, kHudPalette + (i * 2));
original_hud[i] = color.ok() ? color->snes() : 0;
// Invert colors for testing
modified_hud[i] = (original_hud[i] ^ 0x7FFF) & 0x7FFF;
}
// Apply modifications
for (int i = 0; i < 16; ++i) {
ASSERT_OK(WriteColorToRom(*rom_, kHudPalette + (i * 2),
gfx::SnesColor(modified_hud[i])));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (int i = 0; i < 16; ++i) {
auto reloaded = ReadColorFromRom(*reloaded_rom, kHudPalette + (i * 2));
ASSERT_TRUE(reloaded.ok());
EXPECT_EQ(reloaded->snes(), modified_hud[i])
<< "HUD palette entry " << i << " should persist";
}
}
// Test 8: Large batch palette modifications
TEST_F(PaletteEditorSaveTest, LargeBatch_PaletteModifications) {
// Test modifying many palette entries at once
const int batch_size = 256; // 256 colors = 16 full palettes
const uint32_t base_offset = kOverworldPaletteMain;
std::vector<uint16_t> original_colors(batch_size);
std::vector<uint16_t> modified_colors(batch_size);
// Read and prepare modifications
for (int i = 0; i < batch_size; ++i) {
auto color = ReadColorFromRom(*rom_, base_offset + (i * 2));
original_colors[i] = color.ok() ? color->snes() : 0;
// Create rainbow pattern
modified_colors[i] = ((i * 0x0102) & 0x7FFF);
}
// Apply all modifications
for (int i = 0; i < batch_size; ++i) {
ASSERT_OK(WriteColorToRom(*rom_, base_offset + (i * 2),
gfx::SnesColor(modified_colors[i])));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify all changes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
int verified_count = 0;
for (int i = 0; i < batch_size; ++i) {
auto reloaded = ReadColorFromRom(*reloaded_rom, base_offset + (i * 2));
if (reloaded.ok() && reloaded->snes() == modified_colors[i]) {
verified_count++;
}
}
EXPECT_EQ(verified_count, batch_size)
<< "All batch palette modifications should persist";
}
// Test 9: Round-trip without modification preserves data
TEST_F(PaletteEditorSaveTest, RoundTrip_NoModification) {
// Record sample palette colors
const std::vector<uint32_t> sample_offsets = {
kOverworldPaletteMain,
kOverworldPaletteMain + 16,
kDungeonPaletteMain,
kSpritePaletteGlobal,
kHudPalette,
};
std::map<uint32_t, uint16_t> original_colors;
for (uint32_t offset : sample_offsets) {
auto color = ReadColorFromRom(*rom_, offset);
if (color.ok()) {
original_colors[offset] = color->snes();
}
}
// Save ROM without modifications
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (const auto& [offset, original_value] : original_colors) {
auto reloaded = ReadColorFromRom(*reloaded_rom, offset);
ASSERT_TRUE(reloaded.ok());
EXPECT_EQ(reloaded->snes(), original_value)
<< "Color at 0x" << std::hex << offset << " should be preserved";
}
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,361 @@
#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:
// 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
auto* map = overworld.mutable_overworld_map(cycle % 160);
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
EXPECT_EQ(final_ow.overworld_map((num_cycles - 1) % 160)->area_graphics(),
static_cast<uint8_t>(num_cycles - 1));
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,397 @@
#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/screen/dungeon_map.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for ScreenEditor (DungeonMap) Save Operations
*
* Validates the complete dungeon map editing workflow:
* 1. Load ROM and dungeon map data
* 2. Modify floor/room assignments
* 3. Save changes to ROM
* 4. Reload ROM and verify edits persisted
* 5. Verify no data corruption occurred
*/
class ScreenEditorSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(test_rom_path_);
if (!load_result.ok()) {
GTEST_SKIP() << "Failed to load test ROM: " << load_result.message();
}
// Load game data
game_data_ = std::make_unique<zelda3::GameData>();
auto gd_result = zelda3::LoadGameData(*rom_, *game_data_);
if (!gd_result.ok()) {
GTEST_SKIP() << "Failed to load game data: " << gd_result.message();
}
// Load dungeon maps
auto maps_result = zelda3::LoadDungeonMaps(*rom_, dungeon_map_labels_);
if (!maps_result.ok()) {
GTEST_SKIP() << "Failed to load dungeon maps: "
<< maps_result.status().message();
}
dungeon_maps_ = std::move(*maps_result);
}
// Helper to read dungeon map room data directly from ROM
uint8_t ReadDungeonMapRoom(Rom& rom, int dungeon_id, int floor, int room) {
int ptr = zelda3::kDungeonMapRoomsPtr + (dungeon_id * 2);
int pc_ptr = SnesToPc(ptr);
auto byte = rom.ReadByte(pc_ptr + room + (floor * zelda3::kNumRooms));
return byte.ok() ? *byte : 0;
}
// Helper to write dungeon map room data to ROM
absl::Status WriteDungeonMapRoom(Rom& rom, int dungeon_id, int floor,
int room, uint8_t value) {
int ptr = zelda3::kDungeonMapRoomsPtr + (dungeon_id * 2);
int pc_ptr = SnesToPc(ptr);
return rom.WriteByte(pc_ptr + room + (floor * zelda3::kNumRooms), value);
}
// Helper to read dungeon map GFX data from ROM
uint8_t ReadDungeonMapGfx(Rom& rom, int dungeon_id, int floor, int room) {
int ptr = zelda3::kDungeonMapGfxPtr + (dungeon_id * 2);
int pc_ptr = SnesToPc(ptr);
// Note: GFX pointer increments differently (see SaveDungeonMaps)
auto byte = rom.ReadByte(pc_ptr + room + (floor * zelda3::kNumRooms));
return byte.ok() ? *byte : 0;
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<zelda3::GameData> game_data_;
std::vector<zelda3::DungeonMap> dungeon_maps_;
zelda3::DungeonMapLabels dungeon_map_labels_;
};
// Test 1: Single dungeon map floor room modification
TEST_F(ScreenEditorSaveTest, SingleFloorRoom_SaveAndReload) {
if (dungeon_maps_.empty()) {
GTEST_SKIP() << "No dungeon maps loaded";
}
// Test with first dungeon (Hyrule Castle)
const int dungeon_id = 0;
const int floor = 0;
const int room = 0;
// Record original value
uint8_t original_room = dungeon_maps_[dungeon_id].floor_rooms[floor][room];
// Modify the room assignment
uint8_t new_room = (original_room + 1) % 0xFF;
dungeon_maps_[dungeon_id].floor_rooms[floor][room] = new_room;
// Save via SaveDungeonMaps
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
// Save ROM to disk
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
EXPECT_EQ((*reloaded_maps)[dungeon_id].floor_rooms[floor][room], new_room)
<< "Dungeon map room modification should persist";
}
// Test 2: Multiple dungeon modifications
TEST_F(ScreenEditorSaveTest, MultipleDungeons_SaveAndReload) {
if (dungeon_maps_.size() < 3) {
GTEST_SKIP() << "Not enough dungeons for multi-dungeon test";
}
// Modify rooms in dungeons 0, 1, and 2
const std::vector<int> test_dungeons = {0, 1, 2};
std::map<int, uint8_t> original_rooms;
std::map<int, uint8_t> modified_rooms;
for (int d : test_dungeons) {
if (dungeon_maps_[d].nbr_of_floor > 0) {
original_rooms[d] = dungeon_maps_[d].floor_rooms[0][0];
modified_rooms[d] = (original_rooms[d] + d + 1) % 0xFF;
dungeon_maps_[d].floor_rooms[0][0] = modified_rooms[d];
}
}
// Save all modifications
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify all changes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
for (int d : test_dungeons) {
if (modified_rooms.count(d) > 0) {
EXPECT_EQ((*reloaded_maps)[d].floor_rooms[0][0], modified_rooms[d])
<< "Dungeon " << d << " modification should persist";
}
}
}
// Test 3: Floor and basement data persistence
TEST_F(ScreenEditorSaveTest, FloorBasement_Persistence) {
// Test dungeon with multiple floors and basements
int target_dungeon = -1;
for (size_t d = 0; d < dungeon_maps_.size(); ++d) {
if (dungeon_maps_[d].nbr_of_floor >= 2 ||
dungeon_maps_[d].nbr_of_basement >= 1) {
target_dungeon = static_cast<int>(d);
break;
}
}
if (target_dungeon < 0) {
GTEST_SKIP() << "No dungeon with multiple floors/basements found";
}
auto& dm = dungeon_maps_[target_dungeon];
const int total_levels = dm.nbr_of_floor + dm.nbr_of_basement;
// Modify a room on each level
std::vector<uint8_t> original_rooms(total_levels);
std::vector<uint8_t> modified_rooms(total_levels);
for (int level = 0; level < total_levels; ++level) {
original_rooms[level] = dm.floor_rooms[level][0];
modified_rooms[level] = (original_rooms[level] + level + 5) % 0xFF;
dm.floor_rooms[level][0] = modified_rooms[level];
}
// Save and reload
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
for (int level = 0; level < total_levels; ++level) {
EXPECT_EQ((*reloaded_maps)[target_dungeon].floor_rooms[level][0],
modified_rooms[level])
<< "Level " << level << " modification should persist";
}
}
// Test 4: GFX data persistence
TEST_F(ScreenEditorSaveTest, GfxData_Persistence) {
if (dungeon_maps_.empty()) {
GTEST_SKIP() << "No dungeon maps loaded";
}
const int dungeon_id = 0;
const int floor = 0;
const int room = 0;
// Record and modify GFX data
uint8_t original_gfx = dungeon_maps_[dungeon_id].floor_gfx[floor][room];
uint8_t modified_gfx = (original_gfx + 0x10) & 0xFF;
dungeon_maps_[dungeon_id].floor_gfx[floor][room] = modified_gfx;
// Save and reload
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
EXPECT_EQ((*reloaded_maps)[dungeon_id].floor_gfx[floor][room], modified_gfx)
<< "GFX modification should persist";
}
// Test 5: No cross-dungeon corruption
TEST_F(ScreenEditorSaveTest, NoCrossDungeonCorruption) {
if (dungeon_maps_.size() < 3) {
GTEST_SKIP() << "Not enough dungeons for corruption test";
}
// Record data from dungeons 0 and 2
uint8_t dungeon0_room = dungeon_maps_[0].floor_rooms[0][0];
uint8_t dungeon2_room = dungeon_maps_[2].floor_rooms[0][0];
// Modify only dungeon 1
uint8_t original_d1 = dungeon_maps_[1].floor_rooms[0][0];
dungeon_maps_[1].floor_rooms[0][0] = (original_d1 + 0x55) % 0xFF;
// Save
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
// Verify dungeon 1 was modified
EXPECT_EQ((*reloaded_maps)[1].floor_rooms[0][0], (original_d1 + 0x55) % 0xFF);
// Verify dungeons 0 and 2 were NOT corrupted
EXPECT_EQ((*reloaded_maps)[0].floor_rooms[0][0], dungeon0_room)
<< "Dungeon 0 should not be corrupted";
EXPECT_EQ((*reloaded_maps)[2].floor_rooms[0][0], dungeon2_room)
<< "Dungeon 2 should not be corrupted";
}
// Test 6: All rooms on a floor
TEST_F(ScreenEditorSaveTest, AllRoomsOnFloor_Persistence) {
if (dungeon_maps_.empty()) {
GTEST_SKIP() << "No dungeon maps loaded";
}
const int dungeon_id = 0;
const int floor = 0;
// Modify all rooms on the floor
std::vector<uint8_t> original_rooms(zelda3::kNumRooms);
std::vector<uint8_t> modified_rooms(zelda3::kNumRooms);
for (int r = 0; r < zelda3::kNumRooms; ++r) {
original_rooms[r] = dungeon_maps_[dungeon_id].floor_rooms[floor][r];
modified_rooms[r] = (r * 7) % 0xFF;
dungeon_maps_[dungeon_id].floor_rooms[floor][r] = modified_rooms[r];
}
// Save and reload
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
for (int r = 0; r < zelda3::kNumRooms; ++r) {
EXPECT_EQ((*reloaded_maps)[dungeon_id].floor_rooms[floor][r], modified_rooms[r])
<< "Room " << r << " modification should persist";
}
}
// Test 7: Round-trip without modification
TEST_F(ScreenEditorSaveTest, RoundTrip_NoModification) {
// Record original state of first few dungeons
std::vector<std::vector<std::vector<uint8_t>>> original_data;
const int test_dungeons = std::min(3, static_cast<int>(dungeon_maps_.size()));
for (int d = 0; d < test_dungeons; ++d) {
std::vector<std::vector<uint8_t>> dungeon_data;
const int levels = dungeon_maps_[d].nbr_of_floor +
dungeon_maps_[d].nbr_of_basement;
for (int l = 0; l < levels; ++l) {
std::vector<uint8_t> floor_data(zelda3::kNumRooms);
for (int r = 0; r < zelda3::kNumRooms; ++r) {
floor_data[r] = dungeon_maps_[d].floor_rooms[l][r];
}
dungeon_data.push_back(floor_data);
}
original_data.push_back(dungeon_data);
}
// Save without modification
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
for (int d = 0; d < test_dungeons; ++d) {
const int levels = dungeon_maps_[d].nbr_of_floor +
dungeon_maps_[d].nbr_of_basement;
for (int l = 0; l < levels && l < static_cast<int>(original_data[d].size()); ++l) {
for (int r = 0; r < zelda3::kNumRooms; ++r) {
EXPECT_EQ((*reloaded_maps)[d].floor_rooms[l][r], original_data[d][l][r])
<< "Dungeon " << d << " level " << l << " room " << r
<< " should be preserved";
}
}
}
}
// Test 8: Large batch dungeon modifications
TEST_F(ScreenEditorSaveTest, LargeBatch_DungeonModifications) {
// Modify all dungeons, all floors, first room
std::map<std::pair<int, int>, uint8_t> modifications;
for (size_t d = 0; d < dungeon_maps_.size(); ++d) {
const int levels = dungeon_maps_[d].nbr_of_floor +
dungeon_maps_[d].nbr_of_basement;
for (int l = 0; l < levels; ++l) {
uint8_t original = dungeon_maps_[d].floor_rooms[l][0];
uint8_t modified = (original + d + l) % 0xFF;
dungeon_maps_[d].floor_rooms[l][0] = modified;
modifications[{static_cast<int>(d), l}] = modified;
}
}
// Save
ASSERT_OK(zelda3::SaveDungeonMaps(*rom_, dungeon_maps_));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::DungeonMapLabels reloaded_labels;
auto reloaded_maps = zelda3::LoadDungeonMaps(*reloaded_rom, reloaded_labels);
ASSERT_TRUE(reloaded_maps.ok());
int verified_count = 0;
for (const auto& [key, expected] : modifications) {
auto [d, l] = key;
if ((*reloaded_maps)[d].floor_rooms[l][0] == expected) {
verified_count++;
}
}
EXPECT_EQ(verified_count, static_cast<int>(modifications.size()))
<< "All batch dungeon map modifications should persist";
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,338 @@
#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"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for Tile16Editor Save Operations
*
* Validates the complete tile16 editing workflow:
* 1. Load ROM and tile16 data
* 2. Modify tile16 compositions
* 3. Save changes to ROM
* 4. Reload ROM and verify edits persisted
* 5. Verify no data corruption occurred
*/
class Tile16EditorSaveTest : public EditorSaveTestBase {
protected:
void SetUp() override {
EditorSaveTestBase::SetUp();
// Load the test ROM
rom_ = std::make_unique<Rom>();
auto load_result = rom_->LoadFromFile(test_rom_path_);
if (!load_result.ok()) {
GTEST_SKIP() << "Failed to load test ROM: " << load_result.message();
}
// Load overworld data (which includes tile16 data)
overworld_ = std::make_unique<zelda3::Overworld>(rom_.get());
auto ow_load = overworld_->Load(rom_.get());
if (!ow_load.ok()) {
GTEST_SKIP() << "Failed to load overworld: " << ow_load.message();
}
}
// Helper to read tile16 data from ROM (4 tile8 entries = 8 bytes per tile16)
std::vector<uint16_t> ReadTile16FromRom(Rom& rom, int tile16_id) {
std::vector<uint16_t> tiles(4);
int addr = zelda3::kMap16Tiles + (tile16_id * 8);
for (int i = 0; i < 4; ++i) {
auto word = rom.ReadWord(addr + (i * 2));
tiles[i] = word.ok() ? *word : 0;
}
return tiles;
}
// Helper to write tile16 data to ROM
absl::Status WriteTile16ToRom(Rom& rom, int tile16_id,
const std::vector<uint16_t>& tiles) {
if (tiles.size() != 4) {
return absl::InvalidArgumentError("Tile16 requires exactly 4 tile8 entries");
}
int addr = zelda3::kMap16Tiles + (tile16_id * 8);
for (int i = 0; i < 4; ++i) {
RETURN_IF_ERROR(rom.WriteWord(addr + (i * 2), tiles[i]));
}
return absl::OkStatus();
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<zelda3::Overworld> overworld_;
};
// Test 1: Single tile16 edit, save, and reload verification
TEST_F(Tile16EditorSaveTest, SingleTile16Edit_SaveAndReload) {
// Record original tile16 data for tile 0
const int test_tile_id = 0;
std::vector<uint16_t> original_tiles = ReadTile16FromRom(*rom_, test_tile_id);
// Modify the tile16 (change first tile8 entry)
std::vector<uint16_t> modified_tiles = original_tiles;
modified_tiles[0] = (original_tiles[0] + 1) % 0x400; // Cycle tile index
// Write modification to ROM
ASSERT_OK(WriteTile16ToRom(*rom_, test_tile_id, modified_tiles));
// Save ROM to disk
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload ROM
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
// Verify modification persisted
std::vector<uint16_t> reloaded_tiles = ReadTile16FromRom(*reloaded_rom, test_tile_id);
EXPECT_EQ(reloaded_tiles[0], modified_tiles[0])
<< "Tile16 modification should persist after save/reload";
EXPECT_EQ(reloaded_tiles[1], modified_tiles[1]);
EXPECT_EQ(reloaded_tiles[2], modified_tiles[2]);
EXPECT_EQ(reloaded_tiles[3], modified_tiles[3]);
}
// Test 2: Multiple tile16 edits save atomically
TEST_F(Tile16EditorSaveTest, MultipleTile16Edits_Atomicity) {
// Test editing multiple tile16 entries
const std::vector<int> test_tile_ids = {10, 50, 100, 200};
std::map<int, std::vector<uint16_t>> original_data;
std::map<int, std::vector<uint16_t>> modified_data;
// Record original data and prepare modifications
for (int tile_id : test_tile_ids) {
original_data[tile_id] = ReadTile16FromRom(*rom_, tile_id);
modified_data[tile_id] = original_data[tile_id];
// Modify each tile differently
modified_data[tile_id][0] = (original_data[tile_id][0] + tile_id) % 0x400;
}
// Apply all modifications
for (int tile_id : test_tile_ids) {
ASSERT_OK(WriteTile16ToRom(*rom_, tile_id, modified_data[tile_id]));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify ALL changes persisted
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (int tile_id : test_tile_ids) {
std::vector<uint16_t> reloaded_tiles = ReadTile16FromRom(*reloaded_rom, tile_id);
EXPECT_EQ(reloaded_tiles[0], modified_data[tile_id][0])
<< "Tile16 " << tile_id << " modification should persist";
}
}
// Test 3: Verify adjacent tile16 entries are not corrupted
TEST_F(Tile16EditorSaveTest, NoAdjacentCorruption) {
// Test that modifying tile16 #50 doesn't corrupt #49 or #51
const int target_tile = 50;
const int prev_tile = 49;
const int next_tile = 51;
// Record adjacent tile data
std::vector<uint16_t> prev_original = ReadTile16FromRom(*rom_, prev_tile);
std::vector<uint16_t> next_original = ReadTile16FromRom(*rom_, next_tile);
// Modify target tile
std::vector<uint16_t> target_tiles = ReadTile16FromRom(*rom_, target_tile);
target_tiles[0] = 0x1234; // Arbitrary modification
target_tiles[1] = 0x5678;
ASSERT_OK(WriteTile16ToRom(*rom_, target_tile, target_tiles));
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
// Verify adjacent tiles were NOT corrupted
std::vector<uint16_t> prev_reloaded = ReadTile16FromRom(*reloaded_rom, prev_tile);
std::vector<uint16_t> next_reloaded = ReadTile16FromRom(*reloaded_rom, next_tile);
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(prev_reloaded[i], prev_original[i])
<< "Tile16 " << prev_tile << " should not be corrupted";
EXPECT_EQ(next_reloaded[i], next_original[i])
<< "Tile16 " << next_tile << " should not be corrupted";
}
// Verify target tile has the modification
std::vector<uint16_t> target_reloaded = ReadTile16FromRom(*reloaded_rom, target_tile);
EXPECT_EQ(target_reloaded[0], 0x1234);
EXPECT_EQ(target_reloaded[1], 0x5678);
}
// Test 4: Round-trip without modification preserves data
TEST_F(Tile16EditorSaveTest, RoundTrip_NoModification) {
// Record sample tile16 data
const std::vector<int> sample_tiles = {0, 25, 50, 75, 100, 150, 200, 250};
std::map<int, std::vector<uint16_t>> original_data;
for (int tile_id : sample_tiles) {
original_data[tile_id] = ReadTile16FromRom(*rom_, tile_id);
}
// Save ROM without any modifications
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify data is preserved
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
for (int tile_id : sample_tiles) {
std::vector<uint16_t> reloaded = ReadTile16FromRom(*reloaded_rom, tile_id);
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(reloaded[i], original_data[tile_id][i])
<< "Tile16 " << tile_id << " entry " << i << " should be preserved";
}
}
}
// Test 5: Tile16 flip attributes persistence
TEST_F(Tile16EditorSaveTest, FlipAttributes_Persistence) {
const int test_tile_id = 100;
std::vector<uint16_t> tiles = ReadTile16FromRom(*rom_, test_tile_id);
// Modify with flip flags (bits 14-15 in SNES tile format)
// Bit 14 = horizontal flip, Bit 15 = vertical flip
tiles[0] = (tiles[0] & 0x03FF) | 0x4000; // Set H-flip
tiles[1] = (tiles[1] & 0x03FF) | 0x8000; // Set V-flip
tiles[2] = (tiles[2] & 0x03FF) | 0xC000; // Set both flips
tiles[3] = (tiles[3] & 0x03FF); // No flips
ASSERT_OK(WriteTile16ToRom(*rom_, test_tile_id, tiles));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify flip attributes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
std::vector<uint16_t> reloaded = ReadTile16FromRom(*reloaded_rom, test_tile_id);
EXPECT_EQ(reloaded[0] & 0xC000, 0x4000) << "H-flip should persist";
EXPECT_EQ(reloaded[1] & 0xC000, 0x8000) << "V-flip should persist";
EXPECT_EQ(reloaded[2] & 0xC000, 0xC000) << "Both flips should persist";
EXPECT_EQ(reloaded[3] & 0xC000, 0x0000) << "No flips should persist";
}
// Test 6: Palette attribute persistence
TEST_F(Tile16EditorSaveTest, PaletteAttributes_Persistence) {
const int test_tile_id = 150;
std::vector<uint16_t> tiles = ReadTile16FromRom(*rom_, test_tile_id);
// Modify palette bits (bits 10-12 in SNES tile format)
tiles[0] = (tiles[0] & 0xE3FF) | (0 << 10); // Palette 0
tiles[1] = (tiles[1] & 0xE3FF) | (3 << 10); // Palette 3
tiles[2] = (tiles[2] & 0xE3FF) | (5 << 10); // Palette 5
tiles[3] = (tiles[3] & 0xE3FF) | (7 << 10); // Palette 7
ASSERT_OK(WriteTile16ToRom(*rom_, test_tile_id, tiles));
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify palette attributes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
std::vector<uint16_t> reloaded = ReadTile16FromRom(*reloaded_rom, test_tile_id);
EXPECT_EQ((reloaded[0] >> 10) & 0x07, 0) << "Palette 0 should persist";
EXPECT_EQ((reloaded[1] >> 10) & 0x07, 3) << "Palette 3 should persist";
EXPECT_EQ((reloaded[2] >> 10) & 0x07, 5) << "Palette 5 should persist";
EXPECT_EQ((reloaded[3] >> 10) & 0x07, 7) << "Palette 7 should persist";
}
// Test 7: Large batch tile16 modifications
TEST_F(Tile16EditorSaveTest, LargeBatch_Modifications) {
// Test modifying a large number of tile16 entries
const int batch_size = 100;
std::map<int, std::vector<uint16_t>> original_data;
std::map<int, std::vector<uint16_t>> modified_data;
// Prepare batch modifications
for (int i = 0; i < batch_size; ++i) {
int tile_id = i * 3; // Spread across tile16 space
original_data[tile_id] = ReadTile16FromRom(*rom_, tile_id);
modified_data[tile_id] = original_data[tile_id];
// Apply unique modification pattern
modified_data[tile_id][0] = static_cast<uint16_t>((i * 7) % 0x400);
}
// Apply all modifications
for (const auto& [tile_id, tiles] : modified_data) {
ASSERT_OK(WriteTile16ToRom(*rom_, tile_id, tiles));
}
// Save ROM
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify all changes
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
int verified_count = 0;
for (const auto& [tile_id, expected_tiles] : modified_data) {
std::vector<uint16_t> reloaded = ReadTile16FromRom(*reloaded_rom, tile_id);
if (reloaded[0] == expected_tiles[0]) {
verified_count++;
}
}
EXPECT_EQ(verified_count, batch_size)
<< "All batch modifications should persist";
}
// Test 8: Overworld integration - SaveMap16Tiles via Overworld class
TEST_F(Tile16EditorSaveTest, OverworldIntegration_SaveMap16Tiles) {
// Modify tiles16_ directly through Overworld class
auto* tiles16_ptr = overworld_->mutable_tiles16();
if (tiles16_ptr == nullptr || tiles16_ptr->empty()) {
GTEST_SKIP() << "No tile16 data loaded";
}
// Record original first tile
auto original_tile0 = (*tiles16_ptr)[0];
// Modify the first tile16
gfx::Tile16 modified_tile = original_tile0;
modified_tile.tile0_.id_ = (original_tile0.tile0_.id_ + 1) % 0x200;
(*tiles16_ptr)[0] = modified_tile;
// Save via Overworld's SaveMap16Tiles
ASSERT_OK(overworld_->SaveMap16Tiles());
// Save ROM to disk
ASSERT_OK(SaveRomToFile(rom_.get(), test_rom_path_));
// Reload and verify through Overworld class
std::unique_ptr<Rom> reloaded_rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded_rom));
zelda3::Overworld reloaded_ow(reloaded_rom.get());
ASSERT_OK(reloaded_ow.Load(reloaded_rom.get()));
const auto reloaded_tiles16 = reloaded_ow.tiles16();
ASSERT_FALSE(reloaded_tiles16.empty());
EXPECT_EQ(reloaded_tiles16[0].tile0_.id_, modified_tile.tile0_.id_)
<< "Tile16 modification via Overworld should persist";
}
} // namespace test
} // namespace yaze

View File

@@ -0,0 +1,439 @@
#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 "testing.h"
#include "zelda3/game_data.h"
#include "zelda3/overworld/overworld.h"
namespace yaze {
namespace test {
/**
* @brief E2E Test Suite for ZSCustomOverworld v3 Expanded Save Operations
*
* Validates save operations for expanded ROM features:
* 1. Expanded tile16/tile32 save operations
* 2. v3 feature flag persistence
* 3. Area-specific BG colors
* 4. Custom tile GFX groups
* 5. Large map expanded transitions
*/
class ZSCustomOverworldSaveTest : public ExpandedRomSaveTest {
protected:
// v3 feature flag addresses
static constexpr uint32_t kVersionFlag = 0x140145;
static constexpr uint32_t kMainPalettesFlag = 0x140146;
static constexpr uint32_t kAreaBgFlag = 0x140147;
static constexpr uint32_t kSubscreenOverlayFlag = 0x140148;
static constexpr uint32_t kAnimatedGfxFlag = 0x140149;
static constexpr uint32_t kCustomTilesFlag = 0x14014A;
static constexpr uint32_t kMosaicFlag = 0x14014B;
// Expanded data addresses
static constexpr uint32_t kExpandedBgColors = 0x140000;
static constexpr uint32_t kExpandedMainPalettes = 0x140160;
static constexpr uint32_t kExpandedAnimatedGfx = 0x1402A0;
static constexpr uint32_t kExpandedSubscreenOverlays = 0x140340;
static constexpr uint32_t kExpandedCustomTiles = 0x140480;
static constexpr uint32_t kExpandedAreaSizes = 0x140140;
// Tile16/32 expansion check addresses
static constexpr uint32_t kTile16ExpansionCheck = 0x02FD28;
static constexpr uint32_t kTile32ExpansionCheck = 0x01772E;
};
// Test 1: v3 version flag persistence
TEST_F(ZSCustomOverworldSaveTest, VersionFlag_Persistence) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
// Verify ROM is v3
auto version = rom->ReadByte(kVersionFlag);
ASSERT_TRUE(version.ok());
if (*version == 0xFF || *version == 0x00) {
GTEST_SKIP() << "Test ROM is vanilla, not v3";
}
if (*version < 0x03) {
GTEST_SKIP() << "Test ROM is v" << static_cast<int>(*version) << ", not v3";
}
// Modify version (shouldn't normally do this, but testing persistence)
uint8_t original_version = *version;
// Save without modification
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_version = reloaded->ReadByte(kVersionFlag);
ASSERT_TRUE(reloaded_version.ok());
EXPECT_EQ(*reloaded_version, original_version)
<< "v3 version flag should persist";
}
// Test 2: v3 feature flags persistence
TEST_F(ZSCustomOverworldSaveTest, FeatureFlags_Persistence) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
// Check if ROM is v3
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record original feature flags
auto orig_main_palettes = rom->ReadByte(kMainPalettesFlag);
auto orig_area_bg = rom->ReadByte(kAreaBgFlag);
auto orig_subscreen = rom->ReadByte(kSubscreenOverlayFlag);
auto orig_animated = rom->ReadByte(kAnimatedGfxFlag);
auto orig_custom_tiles = rom->ReadByte(kCustomTilesFlag);
auto orig_mosaic = rom->ReadByte(kMosaicFlag);
// Toggle some flags
ASSERT_OK(rom->WriteByte(kMainPalettesFlag,
orig_main_palettes.ok() ? (*orig_main_palettes ^ 0x01) : 0x01));
ASSERT_OK(rom->WriteByte(kAnimatedGfxFlag,
orig_animated.ok() ? (*orig_animated ^ 0x01) : 0x01));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto new_main_palettes = reloaded->ReadByte(kMainPalettesFlag);
auto new_animated = reloaded->ReadByte(kAnimatedGfxFlag);
ASSERT_TRUE(new_main_palettes.ok());
ASSERT_TRUE(new_animated.ok());
if (orig_main_palettes.ok()) {
EXPECT_EQ(*new_main_palettes, (*orig_main_palettes ^ 0x01))
<< "Main palettes flag toggle should persist";
}
if (orig_animated.ok()) {
EXPECT_EQ(*new_animated, (*orig_animated ^ 0x01))
<< "Animated GFX flag toggle should persist";
}
}
// Test 3: Expanded tile16 detection and save
TEST_F(ZSCustomOverworldSaveTest, ExpandedTile16_Detection) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
if (!version_info.is_expanded_tile16) {
GTEST_SKIP() << "ROM does not have expanded tile16";
}
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()));
EXPECT_TRUE(overworld.expanded_tile16())
<< "Overworld should detect expanded tile16";
// Verify we can access expanded tile16 data
const auto& tiles16 = overworld.tiles16();
EXPECT_GT(tiles16.size(), 0) << "Should have tile16 data loaded";
}
// Test 4: Expanded tile32 detection and save
TEST_F(ZSCustomOverworldSaveTest, ExpandedTile32_Detection) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version_info = DetectRomVersion(*rom);
if (!version_info.is_expanded_tile32) {
GTEST_SKIP() << "ROM does not have expanded tile32";
}
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()));
EXPECT_TRUE(overworld.expanded_tile32())
<< "Overworld should detect expanded tile32";
}
// Test 5: Area-specific BG colors save
TEST_F(ZSCustomOverworldSaveTest, AreaBgColors_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record original BG color data
std::vector<uint16_t> original_colors(64); // 64 maps worth
for (int i = 0; i < 64; ++i) {
auto color = rom->ReadWord(kExpandedBgColors + (i * 2));
original_colors[i] = color.ok() ? *color : 0;
}
// Modify some BG colors
const int test_map = 5;
uint16_t new_color = (original_colors[test_map] + 0x0421) & 0x7FFF;
ASSERT_OK(rom->WriteWord(kExpandedBgColors + (test_map * 2), new_color));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_color = reloaded->ReadWord(kExpandedBgColors + (test_map * 2));
ASSERT_TRUE(reloaded_color.ok());
EXPECT_EQ(*reloaded_color, new_color)
<< "Area BG color modification should persist";
// Verify other colors weren't corrupted
for (int i = 0; i < 64; ++i) {
if (i == test_map) continue;
auto color = reloaded->ReadWord(kExpandedBgColors + (i * 2));
if (color.ok()) {
EXPECT_EQ(*color, original_colors[i])
<< "BG color for map " << i << " should not be corrupted";
}
}
}
// Test 6: Custom tile GFX groups save
TEST_F(ZSCustomOverworldSaveTest, CustomTileGfxGroups_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record original custom tile data
std::vector<uint8_t> original_tiles(64);
for (int i = 0; i < 64; ++i) {
auto tile = rom->ReadByte(kExpandedCustomTiles + i);
original_tiles[i] = tile.ok() ? *tile : 0;
}
// Modify custom tile assignments
const int test_map = 10;
uint8_t new_tile_group = (original_tiles[test_map] + 5) % 256;
ASSERT_OK(rom->WriteByte(kExpandedCustomTiles + test_map, new_tile_group));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_tile = reloaded->ReadByte(kExpandedCustomTiles + test_map);
ASSERT_TRUE(reloaded_tile.ok());
EXPECT_EQ(*reloaded_tile, new_tile_group)
<< "Custom tile GFX group modification should persist";
}
// Test 7: Animated GFX data save
TEST_F(ZSCustomOverworldSaveTest, AnimatedGfx_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record original animated GFX data
std::vector<uint8_t> original_anim(64);
for (int i = 0; i < 64; ++i) {
auto anim = rom->ReadByte(kExpandedAnimatedGfx + i);
original_anim[i] = anim.ok() ? *anim : 0;
}
// Modify animated GFX assignments
const int test_map = 15;
uint8_t new_anim = (original_anim[test_map] + 3) % 256;
ASSERT_OK(rom->WriteByte(kExpandedAnimatedGfx + test_map, new_anim));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_anim = reloaded->ReadByte(kExpandedAnimatedGfx + test_map);
ASSERT_TRUE(reloaded_anim.ok());
EXPECT_EQ(*reloaded_anim, new_anim)
<< "Animated GFX modification should persist";
}
// Test 8: Main palette data save (expanded)
TEST_F(ZSCustomOverworldSaveTest, ExpandedMainPalettes_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record and modify main palette data
auto original = rom->ReadByte(kExpandedMainPalettes);
if (!original.ok()) {
GTEST_SKIP() << "Cannot read expanded main palette data";
}
uint8_t modified = (*original + 7) % 256;
ASSERT_OK(rom->WriteByte(kExpandedMainPalettes, modified));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_pal = reloaded->ReadByte(kExpandedMainPalettes);
ASSERT_TRUE(reloaded_pal.ok());
EXPECT_EQ(*reloaded_pal, modified)
<< "Expanded main palette modification should persist";
}
// Test 9: Overworld Save with v3 expanded data
TEST_F(ZSCustomOverworldSaveTest, OverworldSave_V3Expanded) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Load overworld
zelda3::Overworld overworld(rom.get());
ASSERT_OK(overworld.Load(rom.get()));
// Verify expanded features are detected
if (!overworld.expanded_tile16() && !overworld.expanded_tile32()) {
GTEST_SKIP() << "No expanded features detected in v3 ROM";
}
// Modify a map property
auto* map5 = overworld.mutable_overworld_map(5);
uint8_t original_gfx = map5->area_graphics();
map5->set_area_graphics((original_gfx + 1) % 256);
// Save through Overworld
ASSERT_OK(overworld.SaveMapProperties());
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
zelda3::Overworld reloaded_ow(reloaded.get());
ASSERT_OK(reloaded_ow.Load(reloaded.get()));
EXPECT_EQ(reloaded_ow.overworld_map(5)->area_graphics(),
(original_gfx + 1) % 256)
<< "v3 overworld map modification should persist";
}
// Test 10: Area sizes (v3 feature) save
TEST_F(ZSCustomOverworldSaveTest, AreaSizes_SaveAndReload) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Record original area sizes
std::vector<uint8_t> original_sizes(64);
for (int i = 0; i < 64; ++i) {
auto size = rom->ReadByte(kExpandedAreaSizes + i);
original_sizes[i] = size.ok() ? *size : 0;
}
// Modify an area size
const int test_map = 20;
uint8_t new_size = (original_sizes[test_map] + 1) % 4; // 0-3 valid sizes
ASSERT_OK(rom->WriteByte(kExpandedAreaSizes + test_map, new_size));
// Save ROM
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
auto reloaded_size = reloaded->ReadByte(kExpandedAreaSizes + test_map);
ASSERT_TRUE(reloaded_size.ok());
EXPECT_EQ(*reloaded_size, new_size)
<< "Area size modification should persist";
// Verify other sizes weren't corrupted
for (int i = 0; i < 64; ++i) {
if (i == test_map) continue;
auto size = reloaded->ReadByte(kExpandedAreaSizes + i);
if (size.ok()) {
EXPECT_EQ(*size, original_sizes[i])
<< "Area size for map " << i << " should not be corrupted";
}
}
}
// Test 11: Full v3 data round-trip
TEST_F(ZSCustomOverworldSaveTest, FullV3RoundTrip) {
std::unique_ptr<Rom> rom;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, rom));
auto version = rom->ReadByte(kVersionFlag);
if (!version.ok() || *version < 0x03) {
GTEST_SKIP() << "Not a v3 ROM";
}
// Take snapshots of all v3 data regions
auto bg_snapshot = TakeSnapshot(*rom, kExpandedBgColors, 128);
auto pal_snapshot = TakeSnapshot(*rom, kExpandedMainPalettes, 160);
auto anim_snapshot = TakeSnapshot(*rom, kExpandedAnimatedGfx, 64);
auto sub_snapshot = TakeSnapshot(*rom, kExpandedSubscreenOverlays, 128);
auto tile_snapshot = TakeSnapshot(*rom, kExpandedCustomTiles, 160);
// Save ROM without modifications
ASSERT_OK(SaveRomToFile(rom.get(), test_rom_path_));
// Reload and verify all regions are preserved
std::unique_ptr<Rom> reloaded;
ASSERT_OK(LoadAndVerifyRom(test_rom_path_, reloaded));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, bg_snapshot, "BG Colors"));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, pal_snapshot, "Main Palettes"));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, anim_snapshot, "Animated GFX"));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, sub_snapshot, "Subscreen Overlays"));
EXPECT_TRUE(VerifyNoCorruption(*reloaded, tile_snapshot, "Custom Tiles"));
}
} // namespace test
} // namespace yaze