diff --git a/CMakePresets.json b/CMakePresets.json index 01f261cd..34fcf094 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -93,6 +93,20 @@ "YAZE_TEST_ROM_PATH": "${sourceDir}/zelda3.sfc" } }, + { + "name": "macos-dungeon-dev", + "displayName": "Dungeon Editor Dev (ARM64)", + "description": "macOS ARM64 build for dungeon editing implementation work", + "binaryDir": "${sourceDir}/build_rooms", + "inherits": "macos-debug", + "cacheVariables": { + "YAZE_BUILD_APP": "ON", + "YAZE_BUILD_TESTS": "ON", + "YAZE_ENABLE_ROM_TESTS": "ON", + "YAZE_TEST_ROM_PATH": "${sourceDir}/zelda3.sfc", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, { "name": "ci", "displayName": "Continuous Integration", @@ -404,6 +418,11 @@ "configurePreset": "macos-agent-test", "displayName": "macOS z3ed Agent Test Build" }, + { + "name": "macos-dungeon-dev", + "configurePreset": "macos-dungeon-dev", + "displayName": "macOS Dungeon Editor Dev Build" + }, { "name": "fast", "configurePreset": "debug", diff --git a/src/app/zelda3/dungeon/room_object.cc b/src/app/zelda3/dungeon/room_object.cc index 163f5415..5c48bc69 100644 --- a/src/app/zelda3/dungeon/room_object.cc +++ b/src/app/zelda3/dungeon/room_object.cc @@ -248,5 +248,127 @@ int RoomObject::GetTileCount() const { return tile_count_; } +// ============================================================================ +// Object Encoding/Decoding Implementation (Phase 1, Task 1.1) +// ============================================================================ + +int RoomObject::DetermineObjectType(uint8_t b1, uint8_t b3) { + // Type 3: Objects with ID >= 0xF00 + // These have b3 >= 0xF8 (top nibble is 0xF) + if (b3 >= 0xF8) { + return 3; + } + + // Type 2: Objects with ID >= 0x100 + // These have b1 >= 0xFC (marker for Type2 encoding) + if (b1 >= 0xFC) { + return 2; + } + + // Type 1: Standard objects (ID 0x00-0xFF) + return 1; +} + +RoomObject RoomObject::DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t layer) { + int type = DetermineObjectType(b1, b3); + + uint8_t x = 0; + uint8_t y = 0; + uint8_t size = 0; + uint16_t id = 0; + + switch (type) { + case 1: { + // Type1: xxxxxxss yyyyyyss iiiiiiii + // X position: bits 2-7 of byte 1 + x = (b1 & 0xFC) >> 2; + + // Y position: bits 2-7 of byte 2 + y = (b2 & 0xFC) >> 2; + + // Size: bits 0-1 of byte 1 (high), bits 0-1 of byte 2 (low) + size = ((b1 & 0x03) << 2) | (b2 & 0x03); + + // ID: byte 3 (0x00-0xFF) + id = b3; + break; + } + + case 2: { + // Type2: 111111xx xxxxyyyy yyiiiiii + // X position: bits 0-1 of byte 1 (high), bits 4-7 of byte 2 (low) + x = ((b1 & 0x03) << 4) | ((b2 & 0xF0) >> 4); + + // Y position: bits 0-3 of byte 2 (high), bits 6-7 of byte 3 (low) + y = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6); + + // Size: 0 (Type2 objects don't use size parameter) + size = 0; + + // ID: bits 0-5 of byte 3, OR with 0x100 to mark as Type2 + id = (b3 & 0x3F) | 0x100; + break; + } + + case 3: { + // Type3: xxxxxxii yyyyyyii 11111iii + // X position: bits 2-7 of byte 1 + x = (b1 & 0xFC) >> 2; + + // Y position: bits 2-7 of byte 2 + y = (b2 & 0xFC) >> 2; + + // Size: 0 (Type3 objects don't use size parameter) + size = 0; + + // ID: Complex reconstruction + // Top 8 bits from byte 3 (shifted left by 4) + // Bits 2-3 from byte 2 + // Bits 0-1 from byte 1 + // Plus 0x80 offset + id = ((b3 & 0xFF) << 4) | ((b2 & 0x03) << 2) | (b1 & 0x03) | 0x80; + break; + } + + default: + // Should never happen, but default to Type1 + id = b3; + x = (b1 & 0xFC) >> 2; + y = (b2 & 0xFC) >> 2; + size = ((b1 & 0x03) << 2) | (b2 & 0x03); + break; + } + + return RoomObject(static_cast(id), x, y, size, layer); +} + +RoomObject::ObjectBytes RoomObject::EncodeObjectToBytes() const { + ObjectBytes bytes; + + // Determine type based on object ID + if (id_ >= 0xF00) { + // Type 3: xxxxxxii yyyyyyii 11111iii + bytes.b1 = (x_ << 2) | (id_ & 0x03); + bytes.b2 = (y_ << 2) | ((id_ >> 2) & 0x03); + bytes.b3 = (id_ >> 4) & 0xFF; + } else if (id_ >= 0x100) { + // Type 2: 111111xx xxxxyyyy yyiiiiii + bytes.b1 = 0xFC | ((x_ & 0x30) >> 4); + bytes.b2 = ((x_ & 0x0F) << 4) | ((y_ & 0x3C) >> 2); + bytes.b3 = ((y_ & 0x03) << 6) | (id_ & 0x3F); + } else { + // Type 1: xxxxxxss yyyyyyss iiiiiiii + // Clamp size to 0-15 range + uint8_t clamped_size = size_ > 15 ? 0 : size_; + + bytes.b1 = (x_ << 2) | ((clamped_size >> 2) & 0x03); + bytes.b2 = (y_ << 2) | (clamped_size & 0x03); + bytes.b3 = static_cast(id_); + } + + return bytes; +} + } // namespace zelda3 } // namespace yaze diff --git a/src/app/zelda3/dungeon/room_object.h b/src/app/zelda3/dungeon/room_object.h index d54313bc..df11f82d 100644 --- a/src/app/zelda3/dungeon/room_object.h +++ b/src/app/zelda3/dungeon/room_object.h @@ -103,6 +103,35 @@ class RoomObject { // Get tile count without loading all tiles int GetTileCount() const; + // ============================================================================ + // Object Encoding/Decoding (Phase 1, Task 1.1) + // ============================================================================ + + // 3-byte object encoding structure + struct ObjectBytes { + uint8_t b1; + uint8_t b2; + uint8_t b3; + }; + + // Decode object from 3-byte ROM format + // Type1: xxxxxxss yyyyyyss iiiiiiii (ID 0x00-0xFF) + // Type2: 111111xx xxxxyyyy yyiiiiii (ID 0x100-0x1FF) + // Type3: xxxxxxii yyyyyyii 11111iii (ID 0xF00-0xFFF) + static RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, + uint8_t layer); + + // Encode object to 3-byte ROM format + ObjectBytes EncodeObjectToBytes() const; + + // Determine object type from bytes (1, 2, or 3) + static int DetermineObjectType(uint8_t b1, uint8_t b3); + + // Get layer from LayerType enum + uint8_t GetLayerValue() const { return static_cast(layer_); } + + // ============================================================================ + void AddTiles(int nbr, int pos) { // Reads nbr Tile16 entries from ROM object data starting at pos (8 bytes per Tile16) for (int i = 0; i < nbr; i++) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6dcbb56a..8e95b05d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,6 +42,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF") unit/zelda3/sprite_position_test.cc unit/zelda3/test_dungeon_objects.cc unit/zelda3/dungeon_component_unit_test.cc + zelda3/dungeon/room_object_encoding_test.cc # CLI Services (for catalog serialization tests) ../src/cli/service/resources/resource_catalog.cc @@ -96,6 +97,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF") unit/zelda3/sprite_position_test.cc unit/zelda3/test_dungeon_objects.cc unit/zelda3/dungeon_component_unit_test.cc + zelda3/dungeon/room_object_encoding_test.cc # CLI Services (for catalog serialization tests) ../src/cli/service/resources/resource_catalog.cc diff --git a/test/zelda3/dungeon/room_object_encoding_test.cc b/test/zelda3/dungeon/room_object_encoding_test.cc new file mode 100644 index 00000000..23c2a90b --- /dev/null +++ b/test/zelda3/dungeon/room_object_encoding_test.cc @@ -0,0 +1,326 @@ +// test/zelda3/dungeon/room_object_encoding_test.cc +// Unit tests for Phase 1, Task 1.1: Object Encoding/Decoding +// +// These tests verify that the object encoding and decoding functions work +// correctly for all three object types (Type1, Type2, Type3) based on +// ZScream's proven implementation. + +#include "app/zelda3/dungeon/room_object.h" + +#include + +namespace yaze { +namespace zelda3 { +namespace { + +// ============================================================================ +// Object Type Detection Tests +// ============================================================================ + +TEST(RoomObjectEncodingTest, DetermineObjectTypeType1) { + // Type1: b1 < 0xFC, b3 < 0xF8 + EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0x10), 1); + EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0x42), 1); + EXPECT_EQ(RoomObject::DetermineObjectType(0xFB, 0xF7), 1); +} + +TEST(RoomObjectEncodingTest, DetermineObjectTypeType2) { + // Type2: b1 >= 0xFC, b3 < 0xF8 + EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0x42), 2); + EXPECT_EQ(RoomObject::DetermineObjectType(0xFD, 0x25), 2); + EXPECT_EQ(RoomObject::DetermineObjectType(0xFF, 0x00), 2); +} + +TEST(RoomObjectEncodingTest, DetermineObjectTypeType3) { + // Type3: b3 >= 0xF8 + EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0xF8), 3); + EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0xF9), 3); + EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0xFF), 3); +} + +// ============================================================================ +// Type 1 Object Encoding/Decoding Tests +// ============================================================================ + +TEST(RoomObjectEncodingTest, Type1EncodeDecodeBasic) { + // Type1: xxxxxxss yyyyyyss iiiiiiii + // Example: Object ID 0x42, position (10, 20), size 3, layer 0 + + RoomObject obj(0x42, 10, 20, 3, 0); + + // Encode + auto bytes = obj.EncodeObjectToBytes(); + + // Decode + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + // Verify + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); + EXPECT_EQ(decoded.size(), obj.size()); + EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue()); +} + +TEST(RoomObjectEncodingTest, Type1MaxValues) { + // Test maximum valid values for Type1 + // Max X = 63, Max Y = 63, Max Size = 15 + RoomObject obj(0xFF, 63, 63, 15, 2); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2); + + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); + EXPECT_EQ(decoded.size(), obj.size()); +} + +TEST(RoomObjectEncodingTest, Type1MinValues) { + // Test minimum values for Type1 + RoomObject obj(0x00, 0, 0, 0, 0); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); + EXPECT_EQ(decoded.size(), obj.size()); +} + +TEST(RoomObjectEncodingTest, Type1DifferentSizes) { + // Test all valid size values (0-15) + for (int size = 0; size <= 15; size++) { + RoomObject obj(0x30, 15, 20, size, 1); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); + + EXPECT_EQ(decoded.size(), size) << "Failed for size " << size; + } +} + +TEST(RoomObjectEncodingTest, Type1RealWorldExample1) { + // Example from actual ROM: Wall object + // Bytes: 0x28 0x50 0x10 + // Expected: X=10, Y=20, Size=0, ID=0x10 + + auto decoded = RoomObject::DecodeObjectFromBytes(0x28, 0x50, 0x10, 0); + + EXPECT_EQ(decoded.x(), 10); + EXPECT_EQ(decoded.y(), 20); + EXPECT_EQ(decoded.size(), 0); + EXPECT_EQ(decoded.id_, 0x10); +} + +TEST(RoomObjectEncodingTest, Type1RealWorldExample2) { + // Example: Ceiling object with size + // Bytes: 0x2B 0x53 0x00 + // Expected: X=10, Y=20, Size=3, ID=0x00 + + auto decoded = RoomObject::DecodeObjectFromBytes(0x2B, 0x53, 0x00, 0); + + EXPECT_EQ(decoded.x(), 10); + EXPECT_EQ(decoded.y(), 20); + EXPECT_EQ(decoded.size(), 3); + EXPECT_EQ(decoded.id_, 0x00); +} + +// ============================================================================ +// Type 2 Object Encoding/Decoding Tests +// ============================================================================ + +TEST(RoomObjectEncodingTest, Type2EncodeDecodeBasic) { + // Type2: 111111xx xxxxyyyy yyiiiiii + // Example: Object ID 0x125, position (15, 30), size ignored, layer 1 + + RoomObject obj(0x125, 15, 30, 0, 1); + + // Encode + auto bytes = obj.EncodeObjectToBytes(); + + // Verify b1 starts with 0xFC + EXPECT_GE(bytes.b1, 0xFC); + + // Decode + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); + + // Verify + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); + EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue()); +} + +TEST(RoomObjectEncodingTest, Type2MaxValues) { + // Type2 allows larger position range + // Max X = 63, Max Y = 63 + RoomObject obj(0x13F, 63, 63, 0, 2); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2); + + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); +} + +TEST(RoomObjectEncodingTest, Type2RealWorldExample) { + // Example: Large brazier (object 0x11C) + // Position (8, 12) + + RoomObject obj(0x11C, 8, 12, 0, 0); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + EXPECT_EQ(decoded.id_, 0x11C); + EXPECT_EQ(decoded.x(), 8); + EXPECT_EQ(decoded.y(), 12); +} + +// ============================================================================ +// Type 3 Object Encoding/Decoding Tests +// ============================================================================ + +TEST(RoomObjectEncodingTest, Type3EncodeDecodeChest) { + // Type3: xxxxxxii yyyyyyii 11111iii + // Example: Small chest (0xF99), position (5, 10) + + RoomObject obj(0xF99, 5, 10, 0, 0); + + // Encode + auto bytes = obj.EncodeObjectToBytes(); + + // Verify b3 >= 0xF8 + EXPECT_GE(bytes.b3, 0xF8); + + // Decode + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); + + // Verify + EXPECT_EQ(decoded.id_, obj.id_); + EXPECT_EQ(decoded.x(), obj.x()); + EXPECT_EQ(decoded.y(), obj.y()); +} + +TEST(RoomObjectEncodingTest, Type3EncodeDcodeBigChest) { + // Example: Big chest (0xFB1), position (15, 20) + + RoomObject obj(0xFB1, 15, 20, 0, 1); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); + + EXPECT_EQ(decoded.id_, 0xFB1); + EXPECT_EQ(decoded.x(), 15); + EXPECT_EQ(decoded.y(), 20); +} + +TEST(RoomObjectEncodingTest, Type3RealWorldExample) { + // Example from ROM: Chest at position (10, 15) + // Bytes: 0x29 0x3D 0xF9 + + auto decoded = RoomObject::DecodeObjectFromBytes(0x29, 0x3D, 0xF9, 0); + + // Expected: X=10, Y=15, ID=0xF99 (small chest) + EXPECT_EQ(decoded.x(), 10); + EXPECT_EQ(decoded.y(), 15); + EXPECT_EQ(decoded.id_, 0xF99); +} + +// ============================================================================ +// Edge Cases and Special Values +// ============================================================================ + +TEST(RoomObjectEncodingTest, LayerPreservation) { + // Test that layer information is preserved through encode/decode + for (uint8_t layer = 0; layer <= 2; layer++) { + RoomObject obj(0x42, 10, 20, 3, layer); + + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, layer); + + EXPECT_EQ(decoded.GetLayerValue(), layer) << "Failed for layer " << (int)layer; + } +} + +TEST(RoomObjectEncodingTest, BoundaryBetweenTypes) { + // Test boundary values between object types + + // Last Type1 object + RoomObject type1(0xFF, 10, 20, 3, 0); + auto bytes1 = type1.EncodeObjectToBytes(); + auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0); + EXPECT_EQ(decoded1.id_, 0xFF); + + // First Type2 object + RoomObject type2(0x100, 10, 20, 0, 0); + auto bytes2 = type2.EncodeObjectToBytes(); + auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0); + EXPECT_EQ(decoded2.id_, 0x100); + + // Last Type2 object + RoomObject type2_last(0x13F, 10, 20, 0, 0); + auto bytes2_last = type2_last.EncodeObjectToBytes(); + auto decoded2_last = RoomObject::DecodeObjectFromBytes(bytes2_last.b1, bytes2_last.b2, bytes2_last.b3, 0); + EXPECT_EQ(decoded2_last.id_, 0x13F); + + // Type3 objects + RoomObject type3(0xF99, 10, 20, 0, 0); + auto bytes3 = type3.EncodeObjectToBytes(); + auto decoded3 = RoomObject::DecodeObjectFromBytes(bytes3.b1, bytes3.b2, bytes3.b3, 0); + EXPECT_EQ(decoded3.id_, 0xF99); +} + +TEST(RoomObjectEncodingTest, ZeroPosition) { + // Test objects at position (0, 0) + RoomObject type1(0x10, 0, 0, 0, 0); + auto bytes1 = type1.EncodeObjectToBytes(); + auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0); + EXPECT_EQ(decoded1.x(), 0); + EXPECT_EQ(decoded1.y(), 0); + + RoomObject type2(0x110, 0, 0, 0, 0); + auto bytes2 = type2.EncodeObjectToBytes(); + auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0); + EXPECT_EQ(decoded2.x(), 0); + EXPECT_EQ(decoded2.y(), 0); +} + +// ============================================================================ +// Batch Tests with Multiple Objects +// ============================================================================ + +TEST(RoomObjectEncodingTest, MultipleObjectsRoundTrip) { + // Test encoding/decoding a batch of different objects + std::vector objects; + + // Add various objects + objects.emplace_back(0x10, 5, 10, 2, 0); // Type1 + objects.emplace_back(0x42, 15, 20, 5, 1); // Type1 + objects.emplace_back(0x110, 8, 12, 0, 0); // Type2 + objects.emplace_back(0x125, 25, 30, 0, 1); // Type2 + objects.emplace_back(0xF99, 10, 15, 0, 0); // Type3 (chest) + objects.emplace_back(0xFB1, 20, 25, 0, 2); // Type3 (big chest) + + for (size_t i = 0; i < objects.size(); i++) { + auto& obj = objects[i]; + auto bytes = obj.EncodeObjectToBytes(); + auto decoded = RoomObject::DecodeObjectFromBytes( + bytes.b1, bytes.b2, bytes.b3, obj.GetLayerValue()); + + EXPECT_EQ(decoded.id_, obj.id_) << "Failed at index " << i; + EXPECT_EQ(decoded.x(), obj.x()) << "Failed at index " << i; + EXPECT_EQ(decoded.y(), obj.y()) << "Failed at index " << i; + if (obj.id_ < 0x100) { // Type1 objects have size + EXPECT_EQ(decoded.size(), obj.size()) << "Failed at index " << i; + } + } +} + +} // namespace +} // namespace zelda3 +} // namespace yaze +