backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
665
test/unit/zelda3/dungeon/object_drawing_comprehensive_test.cc
Normal file
665
test/unit/zelda3/dungeon/object_drawing_comprehensive_test.cc
Normal file
@@ -0,0 +1,665 @@
|
||||
/**
|
||||
* @file object_drawing_comprehensive_test.cc
|
||||
* @brief Comprehensive tests for object drawing, parsing, and routine mapping
|
||||
*
|
||||
* Tests the following areas:
|
||||
* 1. Object type detection (Type 1: 0x00-0xFF, Type 2: 0x100-0x1FF, Type 3: 0xF80-0xFFF)
|
||||
* 2. Tile count lookup table (kSubtype1TileLengths)
|
||||
* 3. Draw routine mapping completeness
|
||||
* 4. Type 3 object index calculation
|
||||
* 5. Special size handling (size=0 cases)
|
||||
* 6. BothBG flag propagation
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "rom/rom.h"
|
||||
#include "zelda3/dungeon/object_drawer.h"
|
||||
#include "zelda3/dungeon/object_parser.h"
|
||||
#include "zelda3/dungeon/room_object.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace zelda3 {
|
||||
|
||||
// Expected tile counts from kSubtype1TileLengths table in object_parser.cc
|
||||
// clang-format off
|
||||
static constexpr uint8_t kExpectedTileCounts[0xF8] = {
|
||||
4, 8, 8, 8, 8, 8, 8, 4, 4, 5, 5, 5, 5, 5, 5, 5, // 0x00-0x0F
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 0x10-0x1F
|
||||
5, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, // 0x20-0x2F
|
||||
6, 1, 1, 16, 1, 1, 16, 16, 6, 8, 12, 12, 4, 8, 4, 3, // 0x30-0x3F
|
||||
3, 3, 3, 3, 3, 3, 3, 0, 0, 8, 8, 4, 9, 16, 16, 16, // 0x40-0x4F
|
||||
1, 18, 18, 4, 1, 8, 8, 1, 1, 1, 1, 18, 18, 15, 4, 3, // 0x50-0x5F
|
||||
4, 8, 8, 8, 8, 8, 8, 4, 4, 3, 1, 1, 6, 6, 1, 1, // 0x60-0x6F
|
||||
16, 1, 1, 16, 16, 8, 16, 16, 4, 1, 1, 4, 1, 4, 1, 8, // 0x70-0x7F
|
||||
8, 12, 12, 12, 12, 18, 18, 8, 12, 4, 3, 3, 3, 1, 1, 6, // 0x80-0x8F
|
||||
8, 8, 4, 4, 16, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x90-0x9F
|
||||
1, 1, 1, 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xA0-0xAF
|
||||
1, 1, 16, 3, 3, 8, 8, 8, 4, 4, 16, 4, 4, 4, 1, 1, // 0xB0-0xBF
|
||||
1, 68, 1, 1, 8, 8, 8, 8, 8, 8, 8, 1, 1, 28, 28, 1, // 0xC0-0xCF
|
||||
1, 8, 8, 0, 0, 0, 0, 1, 8, 8, 8, 8, 21, 16, 4, 8, // 0xD0-0xDF
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, // 0xE0-0xEF
|
||||
1, 1, 1, 1, 1, 1, 1, 1 // 0xF0-0xF7
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
class ObjectDrawingComprehensiveTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
rom_ = std::make_unique<Rom>();
|
||||
std::vector<uint8_t> mock_rom_data(1024 * 1024, 0);
|
||||
rom_->LoadFromData(mock_rom_data);
|
||||
}
|
||||
|
||||
std::unique_ptr<Rom> rom_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Type Detection Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DetectsType1Objects) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Type 1: 0x00-0xFF (first 248 objects 0x00-0xF7 per spec)
|
||||
for (int id = 0; id <= 0xF7; ++id) {
|
||||
auto info = parser.GetObjectSubtype(id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << id;
|
||||
EXPECT_EQ(info->subtype, 1) << "ID 0x" << std::hex << id << " should be Type 1";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DetectsType2Objects) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Type 2: 0x100-0x1FF (64 fixed-size objects)
|
||||
for (int id = 0x100; id <= 0x1FF; ++id) {
|
||||
auto info = parser.GetObjectSubtype(id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << id;
|
||||
EXPECT_EQ(info->subtype, 2) << "ID 0x" << std::hex << id << " should be Type 2";
|
||||
EXPECT_EQ(info->max_tile_count, 8) << "Type 2 objects should have 8 tiles";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DetectsType3Objects) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Type 3: 0xF80-0xFFF (128 special objects)
|
||||
for (int id = 0xF80; id <= 0xFFF; ++id) {
|
||||
auto info = parser.GetObjectSubtype(id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << id;
|
||||
EXPECT_EQ(info->subtype, 3) << "ID 0x" << std::hex << id << " should be Type 3";
|
||||
EXPECT_EQ(info->max_tile_count, 8) << "Type 3 objects should have 8 tiles";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type 3 Index Calculation Tests (Critical Bug Fix Verification)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, Type3IndexCalculation_BoundaryValues) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Verify Type 3 index calculation: index = (object_id - 0xF80) & 0x7F
|
||||
// This maps 0xF80-0xFFF to table indices 0-127
|
||||
|
||||
struct TestCase {
|
||||
int object_id;
|
||||
int expected_index;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
{0xF80, 0}, // First Type 3 object -> index 0
|
||||
{0xF81, 1}, // Second Type 3 object -> index 1
|
||||
{0xF8F, 15}, // Index 15
|
||||
{0xF90, 16}, // Index 16
|
||||
{0xFA0, 32}, // Index 32
|
||||
{0xFBF, 63}, // Index 63
|
||||
{0xFC0, 64}, // Index 64
|
||||
{0xFFF, 127}, // Last Type 3 object -> index 127
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
auto info = parser.GetObjectSubtype(tc.object_id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << tc.object_id;
|
||||
|
||||
// Verify subtype_ptr points to correct table offset
|
||||
// kRoomObjectSubtype3 = 0x84F0 (from room_object.h)
|
||||
// Expected ptr = 0x84F0 + (index * 2)
|
||||
int expected_ptr = 0x84F0 + (tc.expected_index * 2);
|
||||
EXPECT_EQ(info->subtype_ptr, expected_ptr)
|
||||
<< "ID 0x" << std::hex << tc.object_id
|
||||
<< " expected ptr 0x" << expected_ptr
|
||||
<< " got 0x" << info->subtype_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, Type3IndexCalculation_AllIndicesInRange) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Verify all Type 3 objects produce indices in valid range (0-127)
|
||||
for (int id = 0xF80; id <= 0xFFF; ++id) {
|
||||
auto info = parser.GetObjectSubtype(id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << id;
|
||||
|
||||
// Calculate what index was used
|
||||
// subtype_ptr = kRoomObjectSubtype3 + (index * 2)
|
||||
// index = (subtype_ptr - kRoomObjectSubtype3) / 2
|
||||
int index = (info->subtype_ptr - 0x84F0) / 2;
|
||||
|
||||
EXPECT_GE(index, 0) << "Index for 0x" << std::hex << id << " is negative";
|
||||
EXPECT_LE(index, 127) << "Index for 0x" << std::hex << id << " exceeds 127";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type 2 Index Calculation Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, Type2IndexCalculation_BoundaryValues) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Verify Type 2 index calculation: index = (object_id - 0x100) & 0xFF
|
||||
// This maps 0x100-0x1FF to table indices 0-255
|
||||
|
||||
struct TestCase {
|
||||
int object_id;
|
||||
int expected_index;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
{0x100, 0}, // First Type 2 object -> index 0
|
||||
{0x101, 1}, // Second Type 2 object -> index 1
|
||||
{0x10F, 15}, // Index 15
|
||||
{0x13F, 63}, // Last commonly used Type 2 object
|
||||
{0x1FF, 255}, // Last possible Type 2 object
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
auto info = parser.GetObjectSubtype(tc.object_id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << tc.object_id;
|
||||
|
||||
// Verify subtype_ptr points to correct table offset
|
||||
// kRoomObjectSubtype2 = 0x83F0 (from room_object.h)
|
||||
// Expected ptr = 0x83F0 + (index * 2)
|
||||
int expected_ptr = 0x83F0 + (tc.expected_index * 2);
|
||||
EXPECT_EQ(info->subtype_ptr, expected_ptr)
|
||||
<< "ID 0x" << std::hex << tc.object_id
|
||||
<< " expected ptr 0x" << expected_ptr
|
||||
<< " got 0x" << info->subtype_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tile Count Lookup Table Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, TileCountLookupTable_VerifyAllEntries) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Verify tile counts match the expected table for Type 1 objects
|
||||
for (int id = 0; id < 0xF8; ++id) {
|
||||
auto info = parser.GetObjectSubtype(id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for ID 0x" << std::hex << id;
|
||||
|
||||
int expected_count = kExpectedTileCounts[id];
|
||||
// Note: Tile count 0 in table means "default to 8"
|
||||
if (expected_count == 0) expected_count = 8;
|
||||
|
||||
EXPECT_EQ(info->max_tile_count, expected_count)
|
||||
<< "ID 0x" << std::hex << id
|
||||
<< " expected " << expected_count
|
||||
<< " tiles, got " << info->max_tile_count;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, TileCountLookupTable_SpecialCases) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
// Test objects with notable tile counts
|
||||
struct TestCase {
|
||||
int object_id;
|
||||
int expected_tiles;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
{0x00, 4, "Floor tile 2x2"},
|
||||
{0x01, 8, "Wall segment 2x4"},
|
||||
{0x33, 16, "Large block 4x4"},
|
||||
{0xA4, 24, "Large special object"},
|
||||
{0xC1, 68, "Very large object"},
|
||||
{0xCD, 28, "Moving wall"},
|
||||
{0xCE, 28, "Moving wall variant"},
|
||||
{0x47, 8, "Waterfall (default)"}, // Table has 0, defaults to 8
|
||||
{0x48, 8, "Waterfall variant (default)"},
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
auto info = parser.GetObjectSubtype(tc.object_id);
|
||||
ASSERT_TRUE(info.ok()) << "Failed for " << tc.description;
|
||||
EXPECT_EQ(info->max_tile_count, tc.expected_tiles)
|
||||
<< tc.description << " (0x" << std::hex << tc.object_id << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Draw Routine Mapping Tests
|
||||
// ============================================================================
|
||||
|
||||
// TODO(Phase 4): Update routine ID range check
|
||||
// Phase 4 added SuperSquare routines (IDs 56-64), so the upper bound should be 64.
|
||||
// Remaining Phase 4 work will add more routines (simple variants, diagonal ceilings,
|
||||
// special/logic-dependent) bringing the total higher.
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DrawRoutineMapping_AllSubtype1ObjectsHaveRoutines) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Verify all Type 1 objects (0x00-0xF7) have valid routine mappings
|
||||
for (int id = 0; id <= 0xF7; ++id) {
|
||||
int routine_id = drawer.GetDrawRoutineId(id);
|
||||
// Should return valid routine (0-82) or -1 for unmapped
|
||||
// Phase 4 added: SuperSquare routines 56-64, Step 2 variants 65-74,
|
||||
// Step 3 diagonal ceilings 75-78, Step 5 special routines 79-82
|
||||
EXPECT_GE(routine_id, -1) << "ID 0x" << std::hex << id;
|
||||
EXPECT_LE(routine_id, 82) << "ID 0x" << std::hex << id;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DrawRoutineMapping_DiagonalWalls) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Diagonal walls 0x09-0x20 have specific routine assignments
|
||||
// Based on bank_01.asm analysis
|
||||
|
||||
// Non-BothBG Acute Diagonals (/)
|
||||
for (int id : {0x0C, 0x0D, 0x10, 0x11, 0x14}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 5)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 5 (DiagonalAcute)";
|
||||
}
|
||||
|
||||
// Non-BothBG Grave Diagonals (\)
|
||||
for (int id : {0x0E, 0x0F, 0x12, 0x13}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 6)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 6 (DiagonalGrave)";
|
||||
}
|
||||
|
||||
// BothBG Acute Diagonals (/)
|
||||
for (int id : {0x15, 0x18, 0x19, 0x1C, 0x1D, 0x20}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 17)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 17 (DiagonalAcute_BothBG)";
|
||||
}
|
||||
|
||||
// BothBG Grave Diagonals (\)
|
||||
for (int id : {0x16, 0x17, 0x1A, 0x1B, 0x1E, 0x1F}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 18)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 18 (DiagonalGrave_BothBG)";
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(Phase 4): Update NothingRoutines test
|
||||
// Phase 4 corrected mappings for several objects that were incorrectly mapped to "Nothing":
|
||||
// - 0xC4 now maps to routine 59 (Draw4x4FloorOneIn4x4SuperSquare)
|
||||
// - 0xCB, 0xCC, 0xCF, 0xD0 need verification against assembly ground truth
|
||||
// - 0xD3-0xD6 are logic-only (CheckIfWallIsMoved) and correctly remain as Nothing
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DrawRoutineMapping_NothingRoutines) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Objects that map to "Nothing" (routine 38) are invisible/logic objects
|
||||
// NOTE: Phase 4 removed some objects from this list as they now have proper routines
|
||||
std::vector<int> nothing_objects = {
|
||||
0x31, 0x32, // Custom/logic
|
||||
0x54, 0x57, 0x58, 0x59, 0x5A, // Logic objects
|
||||
0x6E, 0x6F, // End of vertical section
|
||||
0x72, 0x7E, // Logic objects
|
||||
0xBE, 0xBF, // Logic objects
|
||||
// 0xC4 removed - now maps to Draw4x4FloorOneIn4x4SuperSquare (routine 59)
|
||||
0xCB, 0xCC, 0xCF, 0xD0, // Logic objects (verify against ASM)
|
||||
0xD3, 0xD4, 0xD5, 0xD6, // Wall moved checks (logic-only, no tiles)
|
||||
};
|
||||
|
||||
for (int id : nothing_objects) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 38)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 38 (Nothing)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DrawRoutineMapping_Type2Objects) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Type 2 objects (0x100+) have specific routine assignments
|
||||
|
||||
// 0x100-0x107: 4x4 blocks
|
||||
for (int id = 0x100; id <= 0x107; ++id) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 16)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 16 (4x4)";
|
||||
}
|
||||
|
||||
// 0x108-0x10F: 4x4 Corner BothBG
|
||||
for (int id = 0x108; id <= 0x10F; ++id) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 35)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 35 (4x4 Corner BothBG)";
|
||||
}
|
||||
|
||||
// 0x110-0x113: Weird Corner Bottom
|
||||
for (int id = 0x110; id <= 0x113; ++id) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 36)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 36 (Weird Corner Bottom)";
|
||||
}
|
||||
|
||||
// 0x114-0x117: Weird Corner Top
|
||||
for (int id = 0x114; id <= 0x117; ++id) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 37)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 37 (Weird Corner Top)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DrawRoutineMapping_Type3SpecialObjects) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Type 3 objects (0xF80-0xFFF) - actual decoded IDs from ROM
|
||||
// Index = (object_id - 0xF80) & 0x7F
|
||||
|
||||
// Water Face (indices 0-2)
|
||||
for (int id : {0xF80, 0xF81, 0xF82}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 34)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 34 (Water Face)";
|
||||
}
|
||||
|
||||
// Somaria Line (indices 3-9)
|
||||
for (int id : {0xF83, 0xF84, 0xF85, 0xF86, 0xF87, 0xF88, 0xF89}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 33)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 33 (Somaria Line)";
|
||||
}
|
||||
|
||||
// Chests (indices 23-26 = 0x17-0x1A + 0xF80 = 0xF97-0xF9A)
|
||||
for (int id : {0xF97, 0xF98, 0xF99, 0xF9A}) {
|
||||
EXPECT_EQ(drawer.GetDrawRoutineId(id), 39)
|
||||
<< "ID 0x" << std::hex << id << " should use routine 39 (DrawChest)";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Object Decoding/Encoding Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, ObjectDecoding_Type1) {
|
||||
// Type 1: xxxxxxss yyyyyyss iiiiiiii
|
||||
// b1=0x28 (x=10), b2=0x14 (y=5), b3=0x01 (id=0x01)
|
||||
uint8_t b1 = 0x28; // x=10 (0x28 >> 2 = 0x0A)
|
||||
uint8_t b2 = 0x14; // y=5 (0x14 >> 2 = 0x05)
|
||||
uint8_t b3 = 0x01; // id=1
|
||||
|
||||
auto obj = RoomObject::DecodeObjectFromBytes(b1, b2, b3, 0);
|
||||
|
||||
EXPECT_EQ(obj.id_, 0x01);
|
||||
EXPECT_EQ(obj.x_, 10);
|
||||
EXPECT_EQ(obj.y_, 5);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, ObjectDecoding_Type2) {
|
||||
// Type 2: 111111xx xxxxyyyy yyiiiiii
|
||||
// Discriminator: b1 >= 0xFC
|
||||
// Example: b1=0xFC, b2=0x50, b3=0x05 -> id=0x105
|
||||
uint8_t b1 = 0xFC; // 111111 00
|
||||
uint8_t b2 = 0x50; // xxxx=5, yyyy=0
|
||||
uint8_t b3 = 0x05; // yy=0, iiiiii=5
|
||||
|
||||
auto obj = RoomObject::DecodeObjectFromBytes(b1, b2, b3, 0);
|
||||
|
||||
EXPECT_EQ(obj.id_, 0x105); // 0x100 + 5
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, ObjectDecoding_Type3) {
|
||||
// Type 3: xxxxxxii yyyyyyii 11111iii
|
||||
// Discriminator: b3 >= 0xF8
|
||||
// Example: b1=0x28, b2=0x14, b3=0xF8 -> Type 3
|
||||
uint8_t b1 = 0x28;
|
||||
uint8_t b2 = 0x14;
|
||||
uint8_t b3 = 0xF8;
|
||||
|
||||
auto obj = RoomObject::DecodeObjectFromBytes(b1, b2, b3, 0);
|
||||
|
||||
// Verify it's detected as Type 3 (ID >= 0xF80)
|
||||
EXPECT_GE(obj.id_, 0xF80);
|
||||
EXPECT_LE(obj.id_, 0xFFF);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, ObjectEncoding_Roundtrip) {
|
||||
// Test that encoding and decoding produces consistent results
|
||||
|
||||
// Type 1 object
|
||||
RoomObject obj1(0x05, 10, 20, 3, 0);
|
||||
auto bytes1 = obj1.EncodeObjectToBytes();
|
||||
auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0);
|
||||
EXPECT_EQ(decoded1.id_, obj1.id_);
|
||||
EXPECT_EQ(decoded1.x_, obj1.x_);
|
||||
EXPECT_EQ(decoded1.y_, obj1.y_);
|
||||
|
||||
// Type 2 object
|
||||
RoomObject obj2(0x105, 15, 25, 0, 0);
|
||||
auto bytes2 = obj2.EncodeObjectToBytes();
|
||||
auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0);
|
||||
EXPECT_EQ(decoded2.id_, obj2.id_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BothBG Flag Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, AllBgsFlag_SetDuringDecoding) {
|
||||
// Objects with specific IDs should have all_bgs_ flag set during decoding
|
||||
// Based on DecodeObjectFromBytes logic
|
||||
|
||||
// Routine 3 objects (0x03-0x04)
|
||||
for (int id : {0x03, 0x04}) {
|
||||
RoomObject obj(id, 0, 0, 0, 0);
|
||||
// Create via decoding to trigger all_bgs logic
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x00, 0x00, id, 0);
|
||||
EXPECT_TRUE(decoded.all_bgs_) << "ID 0x" << std::hex << id << " should have all_bgs set";
|
||||
}
|
||||
|
||||
// Routine 9 objects (0x63-0x64)
|
||||
for (int id : {0x63, 0x64}) {
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x00, 0x00, id, 0);
|
||||
EXPECT_TRUE(decoded.all_bgs_) << "ID 0x" << std::hex << id << " should have all_bgs set";
|
||||
}
|
||||
|
||||
// Diagonal BothBG objects
|
||||
std::vector<int> bothbg_diagonals = {
|
||||
0x0C, 0x0D, 0x10, 0x11, 0x14, 0x15, 0x18, 0x19,
|
||||
0x1C, 0x1D, 0x20, 0x0E, 0x0F, 0x12, 0x13, 0x16,
|
||||
0x17, 0x1A, 0x1B, 0x1E, 0x1F
|
||||
};
|
||||
for (int id : bothbg_diagonals) {
|
||||
auto decoded = RoomObject::DecodeObjectFromBytes(0x00, 0x00, id, 0);
|
||||
EXPECT_TRUE(decoded.all_bgs_)
|
||||
<< "Diagonal ID 0x" << std::hex << id << " should have all_bgs set";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dimension Calculation Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DimensionCalculation_HorizontalPatterns) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Test horizontal objects
|
||||
RoomObject obj00_size1(0x00, 0, 0, 1, 0);
|
||||
auto dims = drawer.CalculateObjectDimensions(obj00_size1);
|
||||
EXPECT_EQ(dims.first, 16); // 1 * 16 = 16 pixels wide
|
||||
EXPECT_EQ(dims.second, 16); // 2 tiles = 16 pixels tall
|
||||
|
||||
RoomObject obj00_size5(0x00, 0, 0, 5, 0);
|
||||
dims = drawer.CalculateObjectDimensions(obj00_size5);
|
||||
EXPECT_EQ(dims.first, 80); // 5 * 16 = 80 pixels wide
|
||||
EXPECT_EQ(dims.second, 16);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, DimensionCalculation_DiagonalPatterns) {
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
// Diagonal walls use (size + 6) or (size + 7) count
|
||||
RoomObject diagonal_size0(0x10, 0, 0, 0, 0);
|
||||
auto dims = drawer.CalculateObjectDimensions(diagonal_size0);
|
||||
// Diagonal: (size + 6) * 8 pixels each direction
|
||||
EXPECT_EQ(dims.first, 48); // 6 * 8 = 48
|
||||
EXPECT_EQ(dims.second, 48);
|
||||
|
||||
RoomObject diagonal_size10(0x10, 0, 0, 10, 0);
|
||||
dims = drawer.CalculateObjectDimensions(diagonal_size10);
|
||||
EXPECT_EQ(dims.first, 128); // 16 * 8 = 128
|
||||
EXPECT_EQ(dims.second, 128);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Case Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, EdgeCase_NegativeObjectId) {
|
||||
ObjectParser parser(rom_.get());
|
||||
|
||||
auto result = parser.ParseObject(-1);
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, EdgeCase_NullRom) {
|
||||
ObjectParser parser(nullptr);
|
||||
|
||||
auto result = parser.ParseObject(0x01);
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, EdgeCase_Size0HandledCorrectly) {
|
||||
// Size 0 has special meaning for certain objects
|
||||
// 0x00: size 0 -> 32 repetitions (routine 0 is handled in CalculateObjectDimensions)
|
||||
// 0x01: size 0 -> 26 repetitions (routine 1 NOT handled - falls through to default)
|
||||
|
||||
RoomObject obj00_size0(0x00, 0, 0, 0, 0);
|
||||
|
||||
// Object 0x00 (routine 0) should use special size=32 when input is 0
|
||||
ObjectDrawer drawer(rom_.get(), 0);
|
||||
|
||||
auto dims00 = drawer.CalculateObjectDimensions(obj00_size0);
|
||||
EXPECT_EQ(dims00.first, 512); // 32 * 16 = 512
|
||||
|
||||
// NOTE: Object 0x01 (routine 1) size=0 handling is NOT implemented in
|
||||
// CalculateObjectDimensions. The draw routine uses size=26 when size=0,
|
||||
// but CalculateObjectDimensions falls through to default case.
|
||||
// This is a known limitation - see TODO in CalculateObjectDimensions.
|
||||
RoomObject obj01_size0(0x01, 0, 0, 0, 0);
|
||||
auto dims01 = drawer.CalculateObjectDimensions(obj01_size0);
|
||||
// Current behavior: falls through to default, size_h=0, width=(0+1)*8=8
|
||||
EXPECT_EQ(dims01.first, 8); // Known limitation: should be 416 (26*16)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transparency and Pixel Rendering Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, TransparencyHandling_Pixel0IsSkipped) {
|
||||
// Verify that pixel value 0 is treated as transparent
|
||||
// According to SNES/ALTTP conventions, palette index 0 is transparent
|
||||
|
||||
// This test verifies the design principle from DrawTileToBitmap:
|
||||
// "if (pixel != 0) { ... }" means pixel 0 is skipped
|
||||
|
||||
// Create a mock graphics buffer with known values
|
||||
std::vector<uint8_t> test_gfx(0x10000, 0); // All transparent initially
|
||||
|
||||
// Set up a test tile at ID 0 (row 0, col 0)
|
||||
// Tile spans bytes 0-7 in first row of the tile, then 128-135 for second row, etc.
|
||||
// Put some non-zero pixels to verify they get drawn
|
||||
test_gfx[0] = 0; // pixel (0,0) = transparent
|
||||
test_gfx[1] = 1; // pixel (1,0) = color index 0 (1-1=0)
|
||||
test_gfx[2] = 2; // pixel (2,0) = color index 1 (2-1=1)
|
||||
test_gfx[128] = 3; // pixel (0,1) = color index 2 (3-1=2)
|
||||
|
||||
ObjectDrawer drawer(rom_.get(), 0, test_gfx.data());
|
||||
|
||||
gfx::BackgroundBuffer bg(64, 64);
|
||||
std::vector<uint8_t> bg_data(64 * 64, 0xFF); // Fill with sentinel value
|
||||
bg.bitmap().Create(64, 64, 8, bg_data);
|
||||
|
||||
gfx::TileInfo tile;
|
||||
tile.id_ = 0;
|
||||
tile.palette_ = 0;
|
||||
tile.horizontal_mirror_ = false;
|
||||
tile.vertical_mirror_ = false;
|
||||
|
||||
// Draw tile at position (0,0)
|
||||
drawer.DrawTileToBitmap(bg.bitmap(), tile, 0, 0, test_gfx.data());
|
||||
|
||||
// Verify pixel (0,0) was NOT written (should still be 0xFF sentinel)
|
||||
const auto& data = bg.bitmap().vector();
|
||||
EXPECT_EQ(data[0], 0xFF) << "Transparent pixel (0) should not overwrite bitmap";
|
||||
|
||||
// Verify pixel (1,0) WAS written with value (1-1) + palette_offset = 0
|
||||
EXPECT_EQ(data[1], 0) << "Non-transparent pixel should be written";
|
||||
|
||||
// Verify pixel (2,0) WAS written with value (2-1) + palette_offset = 1
|
||||
EXPECT_EQ(data[2], 1) << "Non-transparent pixel should be written";
|
||||
|
||||
// Verify pixel (0,1) WAS written with value (3-1) + palette_offset = 2
|
||||
EXPECT_EQ(data[64], 2) << "Non-transparent pixel at row 1 should be written";
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, TileInfo_PaletteIndexMappingVerify) {
|
||||
// Verify palette index calculation:
|
||||
// final_color = (pixel - 1) + (palette_ * 15)
|
||||
|
||||
// TileInfo with palette 0
|
||||
gfx::TileInfo tile0;
|
||||
tile0.palette_ = 0;
|
||||
|
||||
// TileInfo with palette 1
|
||||
gfx::TileInfo tile1;
|
||||
tile1.palette_ = 1;
|
||||
|
||||
// Palette 0 should use offsets 0-14
|
||||
// Palette 1 should use offsets 15-29
|
||||
// etc.
|
||||
|
||||
// This is design verification - the actual color lookup happens in DrawTileToBitmap
|
||||
EXPECT_EQ(tile0.palette_ * 15, 0);
|
||||
EXPECT_EQ(tile1.palette_ * 15, 15);
|
||||
|
||||
// Test palette clamping - palettes 6,7 wrap to 0,1
|
||||
gfx::TileInfo tile6;
|
||||
tile6.palette_ = 6;
|
||||
uint8_t clamped6 = tile6.palette_ % 6;
|
||||
EXPECT_EQ(clamped6, 0);
|
||||
|
||||
gfx::TileInfo tile7;
|
||||
tile7.palette_ = 7;
|
||||
uint8_t clamped7 = tile7.palette_ % 6;
|
||||
EXPECT_EQ(clamped7, 1);
|
||||
}
|
||||
|
||||
TEST_F(ObjectDrawingComprehensiveTest, TileInfo_MirroringFlags) {
|
||||
// Verify mirroring flag interpretation
|
||||
|
||||
gfx::TileInfo tile;
|
||||
tile.horizontal_mirror_ = true;
|
||||
tile.vertical_mirror_ = false;
|
||||
|
||||
// For horizontal mirror: src_col = 7 - px
|
||||
// For vertical mirror: src_row = 7 - py
|
||||
|
||||
// These are design verification tests
|
||||
EXPECT_TRUE(tile.horizontal_mirror_);
|
||||
EXPECT_FALSE(tile.vertical_mirror_);
|
||||
|
||||
gfx::TileInfo tile_both;
|
||||
tile_both.horizontal_mirror_ = true;
|
||||
tile_both.vertical_mirror_ = true;
|
||||
|
||||
EXPECT_TRUE(tile_both.horizontal_mirror_);
|
||||
EXPECT_TRUE(tile_both.vertical_mirror_);
|
||||
}
|
||||
|
||||
} // namespace zelda3
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user