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

519 lines
17 KiB
C++

#include <gtest/gtest.h>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include "e2e/rom_dependent/editor_save_test_base.h"
#include "rom/rom.h"
#include "rom/snes.h"
#include "testing.h"
#include "zelda3/game_data.h"
#include "zelda3/overworld/overworld.h"
#include "zelda3/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:
static int FindPrimaryMapId(const zelda3::Overworld& overworld) {
for (int i = 0; i < static_cast<int>(overworld.overworld_maps().size());
i++) {
if (overworld.overworld_map(i)->parent() == i) {
return i;
}
}
return 0;
}
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 ---
const int map_id = FindPrimaryMapId(overworld);
auto* map5 = overworld.mutable_overworld_map(map_id);
uint8_t original_palette_id = map5->area_palette();
map5->set_area_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(map_id)->area_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 map_id = 0;
map_id < static_cast<int>(overworld.overworld_maps().size()) &&
static_cast<int>(expected_gfx.size()) < num_map_edits;
++map_id) {
if (overworld.overworld_map(map_id)->parent() != map_id) {
continue;
}
auto* map = overworld.mutable_overworld_map(map_id);
expected_gfx[map_id] = (map->area_graphics() + map_id) % 256;
map->set_area_graphics(expected_gfx[map_id]);
}
// 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, static_cast<int>(expected_gfx.size()))
<< "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