#include #include #include #include #include #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(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(); 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(); 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_; std::unique_ptr 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 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 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 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 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 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 untouched_gfx; const std::vector 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 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 expected_gfx; for (int map_id = 0; map_id < static_cast(overworld.overworld_maps().size()) && static_cast(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 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 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(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(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 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 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