backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
439
test/e2e/rom_dependent/palette_editor_save_test.cc
Normal file
439
test/e2e/rom_dependent/palette_editor_save_test.cc
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user