feat: Enhance DungeonObjectEditor with visual feedback and object manipulation features, including drag-and-drop support and property panel integration

This commit is contained in:
scawful
2025-10-04 00:42:48 -04:00
parent 9bfc95de37
commit 2ea811eeb2
7 changed files with 1026 additions and 56 deletions

View File

@@ -0,0 +1,327 @@
// Integration tests for Room object load/save cycle with real ROM data
// Phase 1, Task 2.1: Full round-trip verification
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
// Helper function for SNES to PC address conversion
inline int SnesToPc(int addr) {
int temp = (addr & 0x7FFF) + ((addr / 2) & 0xFF8000);
return (temp + 0x0);
}
namespace yaze {
namespace zelda3 {
namespace test {
class RoomIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Load the ROM file
rom_ = std::make_unique<Rom>();
// Check if ROM file exists
const char* rom_path = std::getenv("YAZE_TEST_ROM_PATH");
if (!rom_path) {
rom_path = "zelda3.sfc";
}
auto status = rom_->LoadFromFile(rom_path);
if (!status.ok()) {
GTEST_SKIP() << "ROM file not available: " << status.message();
}
// Create backup of ROM data for restoration after tests
original_rom_data_ = rom_->vector();
}
void TearDown() override {
// Restore original ROM data
if (rom_ && !original_rom_data_.empty()) {
for (size_t i = 0; i < original_rom_data_.size(); i++) {
rom_->WriteByte(i, original_rom_data_[i]);
}
}
}
std::unique_ptr<Rom> rom_;
std::vector<uint8_t> original_rom_data_;
};
// ============================================================================
// Test 1: Basic Load/Save Round-Trip
// ============================================================================
TEST_F(RoomIntegrationTest, BasicLoadSaveRoundTrip) {
// Load room 0 (Hyrule Castle Entrance)
Room room1(0x00, rom_.get());
// Get original object count
size_t original_count = room1.GetTileObjects().size();
ASSERT_GT(original_count, 0) << "Room should have objects";
// Store original objects
auto original_objects = room1.GetTileObjects();
// Save the room (should write same data back)
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Load the room again
Room room2(0x00, rom_.get());
// Verify object count matches
EXPECT_EQ(room2.GetTileObjects().size(), original_count);
// Verify each object matches
auto reloaded_objects = room2.GetTileObjects();
ASSERT_EQ(reloaded_objects.size(), original_objects.size());
for (size_t i = 0; i < original_objects.size(); i++) {
SCOPED_TRACE("Object " + std::to_string(i));
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_) << "ID mismatch";
EXPECT_EQ(reload.x(), orig.x()) << "X position mismatch";
EXPECT_EQ(reload.y(), orig.y()) << "Y position mismatch";
EXPECT_EQ(reload.size(), orig.size()) << "Size mismatch";
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue()) << "Layer mismatch";
}
}
// ============================================================================
// Test 2: Multi-Room Verification
// ============================================================================
TEST_F(RoomIntegrationTest, MultiRoomLoadSaveRoundTrip) {
// Test several different rooms to ensure broad coverage
std::vector<int> test_rooms = {0x00, 0x01, 0x02, 0x10, 0x20};
for (int room_id : test_rooms) {
SCOPED_TRACE("Room " + std::to_string(room_id));
// Load room
Room room1(room_id, rom_.get());
auto original_objects = room1.GetTileObjects();
if (original_objects.empty()) {
continue; // Skip empty rooms
}
// Save objects
auto save_status = room1.SaveObjects();
ASSERT_TRUE(save_status.ok()) << save_status.message();
// Reload and verify
Room room2(room_id, rom_.get());
auto reloaded_objects = room2.GetTileObjects();
EXPECT_EQ(reloaded_objects.size(), original_objects.size());
// Verify objects match
for (size_t i = 0; i < std::min(original_objects.size(), reloaded_objects.size()); i++) {
const auto& orig = original_objects[i];
const auto& reload = reloaded_objects[i];
EXPECT_EQ(reload.id_, orig.id_);
EXPECT_EQ(reload.x(), orig.x());
EXPECT_EQ(reload.y(), orig.y());
EXPECT_EQ(reload.size(), orig.size());
EXPECT_EQ(reload.GetLayerValue(), orig.GetLayerValue());
}
}
}
// ============================================================================
// Test 3: Layer Verification
// ============================================================================
TEST_F(RoomIntegrationTest, LayerPreservation) {
// Load a room known to have multiple layers
Room room(0x01, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count objects per layer
int layer0_count = 0, layer1_count = 0, layer2_count = 0;
for (const auto& obj : objects) {
switch (obj.GetLayerValue()) {
case 0: layer0_count++; break;
case 1: layer1_count++; break;
case 2: layer2_count++; break;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x01, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify layer counts match
int reload_layer0 = 0, reload_layer1 = 0, reload_layer2 = 0;
for (const auto& obj : reloaded) {
switch (obj.GetLayerValue()) {
case 0: reload_layer0++; break;
case 1: reload_layer1++; break;
case 2: reload_layer2++; break;
}
}
EXPECT_EQ(reload_layer0, layer0_count);
EXPECT_EQ(reload_layer1, layer1_count);
EXPECT_EQ(reload_layer2, layer2_count);
}
// ============================================================================
// Test 4: Object Type Distribution
// ============================================================================
TEST_F(RoomIntegrationTest, ObjectTypeDistribution) {
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0);
// Count object types
int type1_count = 0; // ID < 0x100
int type2_count = 0; // ID 0x100-0x13F
int type3_count = 0; // ID >= 0xF00
for (const auto& obj : objects) {
if (obj.id_ >= 0xF00) {
type3_count++;
} else if (obj.id_ >= 0x100) {
type2_count++;
} else {
type1_count++;
}
}
// Save and reload
ASSERT_TRUE(room.SaveObjects().ok());
Room room2(0x00, rom_.get());
auto reloaded = room2.GetTileObjects();
// Verify type distribution matches
int reload_type1 = 0, reload_type2 = 0, reload_type3 = 0;
for (const auto& obj : reloaded) {
if (obj.id_ >= 0xF00) {
reload_type3++;
} else if (obj.id_ >= 0x100) {
reload_type2++;
} else {
reload_type1++;
}
}
EXPECT_EQ(reload_type1, type1_count);
EXPECT_EQ(reload_type2, type2_count);
EXPECT_EQ(reload_type3, type3_count);
}
// ============================================================================
// Test 5: Binary Data Verification
// ============================================================================
TEST_F(RoomIntegrationTest, BinaryDataExactMatch) {
// This test verifies that saving doesn't change ROM data
// when no modifications are made
Room room(0x02, rom_.get());
// Get the ROM location where objects are stored
auto rom_data = rom_->vector();
int object_pointer = (rom_data[0x874C + 2] << 16) +
(rom_data[0x874C + 1] << 8) +
(rom_data[0x874C]);
object_pointer = SnesToPc(object_pointer);
int room_address = object_pointer + (0x02 * 3);
int tile_address = (rom_data[room_address + 2] << 16) +
(rom_data[room_address + 1] << 8) +
rom_data[room_address];
int objects_location = SnesToPc(tile_address);
// Read original bytes (up to 500 bytes should cover most rooms)
std::vector<uint8_t> original_bytes;
for (int i = 0; i < 500 && objects_location + i < (int)rom_data.size(); i++) {
original_bytes.push_back(rom_data[objects_location + i]);
// Stop at final terminator
if (i > 0 && original_bytes[i] == 0xFF && original_bytes[i-1] == 0xFF) {
// Check if this is the final terminator (3rd layer end)
bool might_be_final = true;
for (int j = i - 10; j < i - 1; j += 2) {
if (j >= 0 && original_bytes[j] == 0xFF && original_bytes[j+1] == 0xFF) {
// Found another FF FF marker, keep going
break;
}
}
if (might_be_final) break;
}
}
// Save objects (should write identical data)
ASSERT_TRUE(room.SaveObjects().ok());
// Read bytes after save
rom_data = rom_->vector();
std::vector<uint8_t> saved_bytes;
for (size_t i = 0; i < original_bytes.size() && objects_location + i < rom_data.size(); i++) {
saved_bytes.push_back(rom_data[objects_location + i]);
}
// Verify binary match
ASSERT_EQ(saved_bytes.size(), original_bytes.size());
for (size_t i = 0; i < original_bytes.size(); i++) {
EXPECT_EQ(saved_bytes[i], original_bytes[i])
<< "Byte mismatch at offset " << i;
}
}
// ============================================================================
// Test 6: Known Room Data Verification
// ============================================================================
TEST_F(RoomIntegrationTest, KnownRoomData) {
// Room 0x00 (Hyrule Castle Entrance) - verify known objects exist
Room room(0x00, rom_.get());
auto objects = room.GetTileObjects();
ASSERT_GT(objects.size(), 0) << "Room 0x00 should have objects";
// Verify we can find common object types
bool found_type1 = false;
bool found_layer0 = false;
bool found_layer1 = false;
for (const auto& obj : objects) {
if (obj.id_ < 0x100) found_type1 = true;
if (obj.GetLayerValue() == 0) found_layer0 = true;
if (obj.GetLayerValue() == 1) found_layer1 = true;
}
EXPECT_TRUE(found_type1) << "Should have Type 1 objects";
EXPECT_TRUE(found_layer0) << "Should have Layer 0 objects";
// Verify coordinates are in valid range (0-63)
for (const auto& obj : objects) {
EXPECT_GE(obj.x(), 0);
EXPECT_LE(obj.x(), 63);
EXPECT_GE(obj.y(), 0);
EXPECT_LE(obj.y(), 63);
}
}
} // namespace test
} // namespace zelda3
} // namespace yaze

View File

@@ -0,0 +1,169 @@
// Tests for Room object manipulation methods (Phase 3)
#include <gtest/gtest.h>
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_object.h"
namespace yaze {
namespace zelda3 {
namespace test {
class RoomManipulationTest : public ::testing::Test {
protected:
void SetUp() override {
rom_ = std::make_unique<Rom>();
// Create a minimal ROM for testing
std::vector<uint8_t> dummy_data(0x200000, 0);
rom_->LoadFromData(dummy_data, false);
room_ = std::make_unique<Room>(0, rom_.get());
}
std::unique_ptr<Rom> rom_;
std::unique_ptr<Room> room_;
};
TEST_F(RoomManipulationTest, AddObject) {
RoomObject obj(0x10, 10, 20, 3, 0);
auto status = room_->AddObject(obj);
ASSERT_TRUE(status.ok());
auto objects = room_->GetTileObjects();
EXPECT_EQ(objects.size(), 1);
EXPECT_EQ(objects[0].id_, 0x10);
EXPECT_EQ(objects[0].x(), 10);
EXPECT_EQ(objects[0].y(), 20);
}
TEST_F(RoomManipulationTest, AddInvalidObject) {
// Invalid X position (> 63)
RoomObject obj(0x10, 100, 20, 3, 0);
auto status = room_->AddObject(obj);
EXPECT_FALSE(status.ok());
EXPECT_EQ(room_->GetTileObjects().size(), 0);
}
TEST_F(RoomManipulationTest, RemoveObject) {
RoomObject obj1(0x10, 10, 20, 3, 0);
RoomObject obj2(0x20, 15, 25, 2, 1);
room_->AddObject(obj1);
room_->AddObject(obj2);
EXPECT_EQ(room_->GetTileObjects().size(), 2);
auto status = room_->RemoveObject(0);
ASSERT_TRUE(status.ok());
auto objects = room_->GetTileObjects();
EXPECT_EQ(objects.size(), 1);
EXPECT_EQ(objects[0].id_, 0x20);
}
TEST_F(RoomManipulationTest, RemoveInvalidIndex) {
auto status = room_->RemoveObject(0);
EXPECT_FALSE(status.ok());
}
TEST_F(RoomManipulationTest, UpdateObject) {
RoomObject obj(0x10, 10, 20, 3, 0);
room_->AddObject(obj);
RoomObject updated(0x20, 15, 25, 5, 1);
auto status = room_->UpdateObject(0, updated);
ASSERT_TRUE(status.ok());
auto objects = room_->GetTileObjects();
EXPECT_EQ(objects[0].id_, 0x20);
EXPECT_EQ(objects[0].x(), 15);
EXPECT_EQ(objects[0].y(), 25);
}
TEST_F(RoomManipulationTest, FindObjectAt) {
RoomObject obj1(0x10, 10, 20, 3, 0);
RoomObject obj2(0x20, 15, 25, 2, 1);
room_->AddObject(obj1);
room_->AddObject(obj2);
auto result = room_->FindObjectAt(15, 25, 1);
ASSERT_TRUE(result.ok());
EXPECT_EQ(result.value(), 1);
auto not_found = room_->FindObjectAt(99, 99, 0);
EXPECT_FALSE(not_found.ok());
}
TEST_F(RoomManipulationTest, ValidateObject) {
// Valid Type 1 object
RoomObject valid1(0x10, 10, 20, 3, 0);
EXPECT_TRUE(room_->ValidateObject(valid1));
// Valid Type 2 object
RoomObject valid2(0x110, 30, 40, 0, 1);
EXPECT_TRUE(room_->ValidateObject(valid2));
// Invalid X (> 63)
RoomObject invalid_x(0x10, 100, 20, 3, 0);
EXPECT_FALSE(room_->ValidateObject(invalid_x));
// Invalid layer (> 2)
RoomObject invalid_layer(0x10, 10, 20, 3, 5);
EXPECT_FALSE(room_->ValidateObject(invalid_layer));
// Invalid size for Type 1 (> 15)
RoomObject invalid_size(0x10, 10, 20, 20, 0);
EXPECT_FALSE(room_->ValidateObject(invalid_size));
}
TEST_F(RoomManipulationTest, MultipleOperations) {
// Add several objects
for (int i = 0; i < 5; i++) {
RoomObject obj(0x10 + i, i * 5, i * 6, i, 0);
ASSERT_TRUE(room_->AddObject(obj).ok());
}
EXPECT_EQ(room_->GetTileObjects().size(), 5);
// Update middle object
RoomObject updated(0x99, 30, 35, 7, 1);
ASSERT_TRUE(room_->UpdateObject(2, updated).ok());
// Verify update
auto objects = room_->GetTileObjects();
EXPECT_EQ(objects[2].id_, 0x99);
// Remove first object
ASSERT_TRUE(room_->RemoveObject(0).ok());
EXPECT_EQ(room_->GetTileObjects().size(), 4);
// Verify first object is now what was second
EXPECT_EQ(room_->GetTileObjects()[0].id_, 0x11);
}
TEST_F(RoomManipulationTest, LayerOrganization) {
// Add objects to different layers
RoomObject layer0_obj(0x10, 10, 10, 2, 0);
RoomObject layer1_obj(0x20, 20, 20, 3, 1);
RoomObject layer2_obj(0x30, 30, 30, 4, 2);
room_->AddObject(layer0_obj);
room_->AddObject(layer1_obj);
room_->AddObject(layer2_obj);
// Verify can find by layer
EXPECT_TRUE(room_->FindObjectAt(10, 10, 0).ok());
EXPECT_TRUE(room_->FindObjectAt(20, 20, 1).ok());
EXPECT_TRUE(room_->FindObjectAt(30, 30, 2).ok());
// Wrong layer should not find
EXPECT_FALSE(room_->FindObjectAt(10, 10, 1).ok());
}
} // namespace test
} // namespace zelda3
} // namespace yaze