diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..34a39e85 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +name: Tests + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get install cmake ninja-build libbz2-dev lcov + - name: Configure + run: cmake -H. -Bbuild -GNinja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DBUILD_INTEGRATION_TESTS=ON -DENABLE_COVERAGE=ON + - name: Build + run: cmake --build build -j + - name: Unit Tests + run: build/torch_tests + - name: Integration Tests + # Integration tests require ROM files not present in CI. + # Tests will self-skip via GTEST_SKIP when ROMs are not found. + run: build/torch_integration_tests + - name: Collect Coverage + run: | + lcov --capture --directory build --output-file coverage.info --ignore-errors mismatch,inconsistent + lcov --remove coverage.info '/usr/*' '*/lib/*' '*/build/_deps/*' '*/tests/*' --output-file coverage.info --ignore-errors unused + genhtml coverage.info --output-directory coverage-report --ignore-errors inconsistent + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage-report/ diff --git a/.gitignore b/.gitignore index 8242768d..ddd84b54 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ tools/ modding/* .cache +# Coverage +coverage-report/ +*.info + # Doxygen output docs/html docs/latex \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 44a70e7b..3f1a3e53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,12 @@ if(ENABLE_ASAN) add_link_options(-fsanitize=address) endif() +option(ENABLE_COVERAGE "Enable code coverage instrumentation" OFF) +if(ENABLE_COVERAGE) + add_compile_options(--coverage -fno-inline) + add_link_options(--coverage) +endif() + # Build if (USE_STANDALONE) add_definitions(-DSTANDALONE) @@ -288,3 +294,52 @@ if(NOT USE_STANDALONE) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(${PROJECT_NAME} PUBLIC ${yaml-cpp_SOURCE_DIR}/include) endif() + +# Unit Tests +option(BUILD_TESTS "Build unit tests" OFF) +if(BUILD_TESTS) + set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 + ) + FetchContent_MakeAvailable(googletest) + + enable_testing() + + file(GLOB TEST_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp) + + set(TEST_SRC_DIR ${SRC_DIR}) + list(FILTER TEST_SRC_DIR EXCLUDE REGEX "main\\.cpp$") + + add_executable(torch_tests ${TEST_FILES} ${TEST_SRC_DIR}) + target_include_directories(torch_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/lib + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + target_link_libraries(torch_tests PRIVATE gtest_main yaml-cpp tinyxml2 N64Graphics BinaryTools spdlog) + add_test(NAME torch_tests COMMAND torch_tests) + + # Integration Tests + option(BUILD_INTEGRATION_TESTS "Build integration tests (require ROM files)" OFF) + if(BUILD_INTEGRATION_TESTS) + file(GLOB INTEGRATION_TEST_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tests/integration/*.cpp) + + add_executable(torch_integration_tests ${INTEGRATION_TEST_FILES} ${TEST_SRC_DIR}) + target_include_directories(torch_integration_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/lib + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/tests/integration + ) + target_compile_definitions(torch_integration_tests PRIVATE + INTEGRATION_TEST_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/integration" + INTEGRATION_ROM_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/roms" + ) + target_link_libraries(torch_integration_tests PRIVATE gtest_main yaml-cpp tinyxml2 N64Graphics BinaryTools spdlog) + add_test(NAME torch_integration_tests COMMAND torch_integration_tests) + endif() +endif() diff --git a/src/factories/sm64/AnimationFactory.cpp b/src/factories/sm64/AnimationFactory.cpp index cde52dda..c1fb8966 100644 --- a/src/factories/sm64/AnimationFactory.cpp +++ b/src/factories/sm64/AnimationFactory.cpp @@ -3,8 +3,6 @@ #include "utils/Decompressor.h" -#define ANIMINDEX_COUNT(boneCount) (((boneCount) + 1) * 6) - ExportResult SM64::AnimationBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { auto writer = LUS::BinaryWriter(); diff --git a/src/factories/sm64/AnimationFactory.h b/src/factories/sm64/AnimationFactory.h index 9819e5f6..72964651 100644 --- a/src/factories/sm64/AnimationFactory.h +++ b/src/factories/sm64/AnimationFactory.h @@ -2,6 +2,8 @@ #include +#define ANIMINDEX_COUNT(boneCount) (((boneCount) + 1) * 6) + namespace SM64 { class AnimationData : public IParsedData { public: diff --git a/tests/BinaryReaderTest.cpp b/tests/BinaryReaderTest.cpp new file mode 100644 index 00000000..db5c7e07 --- /dev/null +++ b/tests/BinaryReaderTest.cpp @@ -0,0 +1,149 @@ +#include +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include "lib/binarytools/Stream.h" +#include +#include + +// Helper to create a BinaryReader from a vector of bytes +static LUS::BinaryReader MakeReader(std::vector& buf, Torch::Endianness endianness = Torch::Endianness::Big) { + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(endianness); + return reader; +} + +TEST(BinaryReaderTest, ReadInt8) { + std::vector buf = {0x7F}; + auto reader = MakeReader(buf); + EXPECT_EQ(reader.ReadInt8(), 0x7F); +} + +TEST(BinaryReaderTest, ReadInt8Negative) { + std::vector buf = {0x80}; + auto reader = MakeReader(buf); + EXPECT_EQ(reader.ReadInt8(), -128); +} + +TEST(BinaryReaderTest, ReadUByte) { + std::vector buf = {0xFF}; + auto reader = MakeReader(buf); + EXPECT_EQ(reader.ReadUByte(), 0xFF); +} + +TEST(BinaryReaderTest, ReadInt16BigEndian) { + std::vector buf = {0x01, 0x00}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadInt16(), 256); +} + +TEST(BinaryReaderTest, ReadUInt16BigEndian) { + std::vector buf = {0xFF, 0xFE}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUInt16(), 0xFFFE); +} + +TEST(BinaryReaderTest, ReadInt32BigEndian) { + std::vector buf = {0x00, 0x01, 0x00, 0x00}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadInt32(), 0x00010000); +} + +TEST(BinaryReaderTest, ReadUInt32BigEndian) { + std::vector buf = {0xDE, 0xAD, 0xBE, 0xEF}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUInt32(), 0xDEADBEEF); +} + +TEST(BinaryReaderTest, ReadUInt64BigEndian) { + std::vector buf = {0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUInt64(), 0x00000000DEADBEEF); +} + +TEST(BinaryReaderTest, ReadFloat) { + // IEEE 754: 1.0f = 0x3F800000 + std::vector buf = {0x3F, 0x80, 0x00, 0x00}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_FLOAT_EQ(reader.ReadFloat(), 1.0f); +} + +TEST(BinaryReaderTest, GetLength) { + std::vector buf = {0x01, 0x02, 0x03, 0x04}; + auto reader = MakeReader(buf); + EXPECT_EQ(reader.GetLength(), 4u); +} + +TEST(BinaryReaderTest, ReadCString) { + // "Hi" + null terminator + std::vector buf = {'H', 'i', '\0'}; + auto reader = MakeReader(buf); + std::string result = reader.ReadCString(); + // ReadCString includes the null terminator in the string + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], 'H'); + EXPECT_EQ(result[1], 'i'); + EXPECT_EQ(result[2], '\0'); +} + +TEST(BinaryReaderTest, ReadString) { + // ReadString reads a big-endian int32 length prefix, then that many chars + // Length = 3, then "abc" + std::vector buf = {0x00, 0x00, 0x00, 0x03, 'a', 'b', 'c'}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadString(), "abc"); +} + +TEST(BinaryReaderTest, SeekFromStart) { + std::vector buf = {0xAA, 0xBB, 0xCC, 0xDD}; + auto reader = MakeReader(buf); + reader.Seek(2, LUS::SeekOffsetType::Start); + EXPECT_EQ(reader.ReadUByte(), 0xCC); +} + +TEST(BinaryReaderTest, SeekFromCurrent) { + std::vector buf = {0xAA, 0xBB, 0xCC, 0xDD}; + auto reader = MakeReader(buf); + reader.ReadUByte(); // now at offset 1 + reader.Seek(1, LUS::SeekOffsetType::Current); // skip to offset 2 + EXPECT_EQ(reader.ReadUByte(), 0xCC); +} + +TEST(BinaryReaderTest, GetBaseAddress) { + std::vector buf = {0xAA, 0xBB, 0xCC, 0xDD}; + auto reader = MakeReader(buf); + EXPECT_EQ(reader.GetBaseAddress(), 0u); + reader.ReadUByte(); + EXPECT_EQ(reader.GetBaseAddress(), 1u); + reader.ReadUByte(); + EXPECT_EQ(reader.GetBaseAddress(), 2u); +} + +TEST(BinaryReaderTest, EndiannessSwitching) { + // 0x0102 as bytes + std::vector buf = {0x01, 0x02}; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUInt16(), 0x0102); + + // Reset and read as little-endian + reader.Seek(0, LUS::SeekOffsetType::Start); + reader.SetEndianness(Torch::Endianness::Native); + uint16_t native = reader.ReadUInt16(); + // On little-endian (x86), native should read 0x0201 + // On big-endian, native should read 0x0102 + // Either way, it should differ from Big on little-endian platforms + EXPECT_EQ(reader.GetEndianness(), Torch::Endianness::Native); + (void)native; // value is platform-dependent +} + +TEST(BinaryReaderTest, ReadMultipleValues) { + // Read a sequence: uint8, uint16, uint32 + std::vector buf = { + 0xFF, // uint8 + 0x00, 0x0A, // uint16 = 10 + 0x00, 0x00, 0x01, 0x00 // uint32 = 256 + }; + auto reader = MakeReader(buf, Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUByte(), 0xFF); + EXPECT_EQ(reader.ReadUInt16(), 10); + EXPECT_EQ(reader.ReadUInt32(), 256); +} diff --git a/tests/BinaryWriterTest.cpp b/tests/BinaryWriterTest.cpp new file mode 100644 index 00000000..6a958c00 --- /dev/null +++ b/tests/BinaryWriterTest.cpp @@ -0,0 +1,119 @@ +#include +#include "lib/binarytools/BinaryWriter.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +TEST(BinaryWriterTest, WriteInt8) { + LUS::BinaryWriter writer; + writer.Write(static_cast(0x7F)); + auto vec = writer.ToVector(); + ASSERT_EQ(vec.size(), 1u); + EXPECT_EQ(static_cast(vec[0]), 0x7F); +} + +TEST(BinaryWriterTest, WriteUInt8) { + LUS::BinaryWriter writer; + writer.Write(static_cast(0xFF)); + auto vec = writer.ToVector(); + ASSERT_EQ(vec.size(), 1u); + EXPECT_EQ(static_cast(vec[0]), 0xFF); +} + +TEST(BinaryWriterTest, WriteInt16BigEndian) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(static_cast(0x0102)); + auto vec = writer.ToVector(); + ASSERT_EQ(vec.size(), 2u); + EXPECT_EQ(static_cast(vec[0]), 0x01); + EXPECT_EQ(static_cast(vec[1]), 0x02); +} + +TEST(BinaryWriterTest, WriteUInt32BigEndian) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(static_cast(0xDEADBEEF)); + auto vec = writer.ToVector(); + ASSERT_EQ(vec.size(), 4u); + EXPECT_EQ(static_cast(vec[0]), 0xDE); + EXPECT_EQ(static_cast(vec[1]), 0xAD); + EXPECT_EQ(static_cast(vec[2]), 0xBE); + EXPECT_EQ(static_cast(vec[3]), 0xEF); +} + +TEST(BinaryWriterTest, WriteFloat) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(1.0f); + auto vec = writer.ToVector(); + ASSERT_EQ(vec.size(), 4u); + // IEEE 754: 1.0f = 0x3F800000 big-endian + EXPECT_EQ(static_cast(vec[0]), 0x3F); + EXPECT_EQ(static_cast(vec[1]), 0x80); + EXPECT_EQ(static_cast(vec[2]), 0x00); + EXPECT_EQ(static_cast(vec[3]), 0x00); +} + +TEST(BinaryWriterTest, RoundTripInt32) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(static_cast(123456)); + auto vec = writer.ToVector(); + + std::vector buf(vec.begin(), vec.end()); + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + EXPECT_EQ(reader.ReadInt32(), 123456); +} + +TEST(BinaryWriterTest, RoundTripUInt32) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(static_cast(0xCAFEBABE)); + auto vec = writer.ToVector(); + + std::vector buf(vec.begin(), vec.end()); + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUInt32(), 0xCAFEBABE); +} + +TEST(BinaryWriterTest, RoundTripFloat) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(3.14f); + auto vec = writer.ToVector(); + + std::vector buf(vec.begin(), vec.end()); + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + EXPECT_FLOAT_EQ(reader.ReadFloat(), 3.14f); +} + +TEST(BinaryWriterTest, RoundTripMultipleValues) { + LUS::BinaryWriter writer; + writer.SetEndianness(Torch::Endianness::Big); + writer.Write(static_cast(0xAA)); + writer.Write(static_cast(0x1234)); + writer.Write(static_cast(0xDEADBEEF)); + auto vec = writer.ToVector(); + + ASSERT_EQ(vec.size(), 7u); // 1 + 2 + 4 + + std::vector buf(vec.begin(), vec.end()); + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + EXPECT_EQ(reader.ReadUByte(), 0xAA); + EXPECT_EQ(reader.ReadUInt16(), 0x1234); + EXPECT_EQ(reader.ReadUInt32(), 0xDEADBEEF); +} + +TEST(BinaryWriterTest, GetLength) { + LUS::BinaryWriter writer; + EXPECT_EQ(writer.GetLength(), 0u); + writer.Write(static_cast(0)); + EXPECT_EQ(writer.GetLength(), 4u); + writer.Write(static_cast(0)); + EXPECT_EQ(writer.GetLength(), 5u); +} diff --git a/tests/CommandMacrosTest.cpp b/tests/CommandMacrosTest.cpp new file mode 100644 index 00000000..6ca176b0 --- /dev/null +++ b/tests/CommandMacrosTest.cpp @@ -0,0 +1,66 @@ +#include +#include "n64/CommandMacros.h" + +// _SHIFTL: shift left and mask to width bits +TEST(CommandMacrosTest, ShiftLBasic) { + // _SHIFTL(0xFF, 8, 8) = (0xFF & 0xFF) << 8 = 0xFF00 + EXPECT_EQ(_SHIFTL(0xFF, 8, 8), 0x0000FF00u); +} + +TEST(CommandMacrosTest, ShiftLMask) { + // _SHIFTL(0x1FF, 0, 8) = (0x1FF & 0xFF) << 0 = 0xFF (masks to 8 bits) + EXPECT_EQ(_SHIFTL(0x1FF, 0, 8), 0xFFu); +} + +TEST(CommandMacrosTest, ShiftLHighBits) { + // _SHIFTL(0xDE, 24, 8) = 0xDE000000 + EXPECT_EQ(_SHIFTL(0xDE, 24, 8), 0xDE000000u); +} + +TEST(CommandMacrosTest, ShiftLSixteenBit) { + // _SHIFTL(0xABCD, 16, 16) = 0xABCD0000 + EXPECT_EQ(_SHIFTL(0xABCD, 16, 16), 0xABCD0000u); +} + +// _SHIFTR: shift right and mask to width bits +TEST(CommandMacrosTest, ShiftRBasic) { + // _SHIFTR(0xFF00, 8, 8) = (0xFF00 >> 8) & 0xFF = 0xFF + EXPECT_EQ(_SHIFTR(0xFF00, 8, 8), 0xFFu); +} + +TEST(CommandMacrosTest, ShiftRHighByte) { + // _SHIFTR(0xDE000000, 24, 8) = 0xDE + EXPECT_EQ(_SHIFTR(0xDE000000, 24, 8), 0xDEu); +} + +// CMD_SIZE_SHIFT: sizeof(uint32_t) >> 3 = 4 >> 3 = 0 +TEST(CommandMacrosTest, CmdSizeShift) { + EXPECT_EQ(CMD_SIZE_SHIFT, 0u); +} + +// CMD_PROCESS_OFFSET: with CMD_SIZE_SHIFT=0, should be identity +TEST(CommandMacrosTest, CmdProcessOffsetIdentity) { + for (int i = 0; i < 16; i++) { + EXPECT_EQ(CMD_PROCESS_OFFSET(i), (uint32_t)i) << "offset " << i; + } +} + +// CMD_BBH: pack 2 bytes + 1 halfword +TEST(CommandMacrosTest, CmdBBH) { + // CMD_BBH(0x00, 0x19, 0x0004) + // = _SHIFTL(0x00,0,8) | _SHIFTL(0x19,8,8) | _SHIFTL(0x0004,16,16) + // = 0x00 | 0x1900 | 0x00040000 + // = 0x00041900 + EXPECT_EQ(CMD_BBH(0x00, 0x19, 0x0004), 0x00041900u); +} + +// CMD_HH: pack 2 halfwords +TEST(CommandMacrosTest, CmdHH) { + // CMD_HH(0x1234, 0x5678) = 0x56781234 + EXPECT_EQ(CMD_HH(0x1234, 0x5678), 0x56781234u); +} + +// CMD_W: identity +TEST(CommandMacrosTest, CmdW) { + EXPECT_EQ(CMD_W(0xDEADBEEF), 0xDEADBEEFu); +} diff --git a/tests/CompanionFactoryRegistrationTest.cpp b/tests/CompanionFactoryRegistrationTest.cpp new file mode 100644 index 00000000..2d705830 --- /dev/null +++ b/tests/CompanionFactoryRegistrationTest.cpp @@ -0,0 +1,68 @@ +#include +#include "Companion.h" +#include "factories/VtxFactory.h" +#include "factories/MtxFactory.h" +#include "factories/FloatFactory.h" + +class CompanionRegistrationTest : public ::testing::Test { +protected: + void SetUp() override { + // Construct Companion with empty ROM, no archive, no debug + std::vector emptyRom; + companion = std::make_unique(emptyRom, ArchiveType::None, false, false); + } + + std::unique_ptr companion; +}; + +TEST_F(CompanionRegistrationTest, RegisterAndGetFactory) { + auto factory = std::make_shared(); + companion->RegisterFactory("TEST_VTX", factory); + auto result = companion->GetFactory("TEST_VTX"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().get(), factory.get()); +} + +TEST_F(CompanionRegistrationTest, GetFactoryMissing) { + auto result = companion->GetFactory("NONEXISTENT"); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(CompanionRegistrationTest, RegisterMultipleFactories) { + companion->RegisterFactory("A", std::make_shared()); + companion->RegisterFactory("B", std::make_shared()); + companion->RegisterFactory("C", std::make_shared()); + + EXPECT_TRUE(companion->GetFactory("A").has_value()); + EXPECT_TRUE(companion->GetFactory("B").has_value()); + EXPECT_TRUE(companion->GetFactory("C").has_value()); + EXPECT_FALSE(companion->GetFactory("D").has_value()); +} + +TEST_F(CompanionRegistrationTest, RegisterOverwritesPrevious) { + auto factory1 = std::make_shared(); + auto factory2 = std::make_shared(); + companion->RegisterFactory("SAME", factory1); + companion->RegisterFactory("SAME", factory2); + auto result = companion->GetFactory("SAME"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().get(), factory2.get()); +} + +TEST_F(CompanionRegistrationTest, FactoryExportersAccessible) { + auto factory = std::make_shared(); + companion->RegisterFactory("VTX", factory); + auto result = companion->GetFactory("VTX"); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result.value()->GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(result.value()->GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(result.value()->GetExporter(ExportType::Binary).has_value()); +} + +TEST_F(CompanionRegistrationTest, FactoryAlignmentAccessible) { + auto factory = std::make_shared(); + companion->RegisterFactory("VTX", factory); + auto result = companion->GetFactory("VTX"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value()->GetAlignment(), 8u); +} diff --git a/tests/DecompressorTest.cpp b/tests/DecompressorTest.cpp new file mode 100644 index 00000000..4c02cadc --- /dev/null +++ b/tests/DecompressorTest.cpp @@ -0,0 +1,87 @@ +#include +#include "utils/Decompressor.h" +#include "factories/BaseFactory.h" +#include + +// GetCompressionType — reads a 4-byte magic string at the given offset + +TEST(DecompressorTest, GetCompressionTypeMIO0) { + std::vector buf = {'M', 'I', 'O', '0', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 0), CompressionType::None); + // offset=0 returns None unconditionally (the implementation checks `if (offset)`) +} + +TEST(DecompressorTest, GetCompressionTypeMIO0AtOffset) { + // Place "MIO0" at offset 4 + std::vector buf = {0, 0, 0, 0, 'M', 'I', 'O', '0', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 4), CompressionType::MIO0); +} + +TEST(DecompressorTest, GetCompressionTypeYay0) { + std::vector buf = {0, 'Y', 'a', 'y', '0', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 1), CompressionType::YAY0); +} + +TEST(DecompressorTest, GetCompressionTypePERS) { + // "PERS" is also treated as YAY0 + std::vector buf = {0, 'P', 'E', 'R', 'S', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 1), CompressionType::YAY0); +} + +TEST(DecompressorTest, GetCompressionTypeYay1) { + std::vector buf = {0, 'Y', 'a', 'y', '1', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 1), CompressionType::YAY1); +} + +TEST(DecompressorTest, GetCompressionTypeYaz0) { + std::vector buf = {0, 'Y', 'a', 'z', '0', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 1), CompressionType::YAZ0); +} + +TEST(DecompressorTest, GetCompressionTypeUnknown) { + std::vector buf = {0, 'X', 'Y', 'Z', 'W', '\0'}; + EXPECT_EQ(Decompressor::GetCompressionType(buf, 1), CompressionType::None); +} + +// IS_SEGMENTED macro tests (pure bit logic, no Companion dependency) + +TEST(DecompressorTest, IsSegmentedMacroTrue) { + // Segment 0x06, offset 0x001000 → segmented + EXPECT_TRUE(IS_SEGMENTED(0x06001000)); +} + +TEST(DecompressorTest, IsSegmentedMacroFalse) { + // Physical address (high byte 0x00) → not segmented + EXPECT_FALSE(IS_SEGMENTED(0x00100000)); +} + +TEST(DecompressorTest, IsSegmentedMacroHighSegment) { + // Segment 0x20 is out of range (must be < 0x20) + EXPECT_FALSE(IS_SEGMENTED(0x20000000)); +} + +TEST(DecompressorTest, SegmentNumberMacro) { + EXPECT_EQ(SEGMENT_NUMBER(0x06001000), 0x06u); + EXPECT_EQ(SEGMENT_NUMBER(0x0E123456), 0x0Eu); +} + +TEST(DecompressorTest, SegmentOffsetMacro) { + EXPECT_EQ(SEGMENT_OFFSET(0x06001000), 0x001000u); + EXPECT_EQ(SEGMENT_OFFSET(0x0EFFFFFF), 0xFFFFFFu); +} + +TEST(DecompressorTest, AssetPtrMacroSegmented) { + // Segmented address → returns just the offset portion + EXPECT_EQ(ASSET_PTR(0x06001000), 0x001000u); +} + +TEST(DecompressorTest, AssetPtrMacroPhysical) { + // Physical address → returns the address unchanged + EXPECT_EQ(ASSET_PTR(0x00100000), 0x00100000u); +} + +// ClearCache — just verify it doesn't crash +TEST(DecompressorTest, ClearCacheNoCrash) { + Decompressor::ClearCache(); + // If we get here without crashing, the test passes +} diff --git a/tests/DisplayListFactoryTest.cpp b/tests/DisplayListFactoryTest.cpp new file mode 100644 index 00000000..43667e99 --- /dev/null +++ b/tests/DisplayListFactoryTest.cpp @@ -0,0 +1,79 @@ +#include +#include "factories/DisplayListFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +TEST(DisplayListFactoryTest, DListDataDefaultConstruction) { + DListData data; + EXPECT_TRUE(data.mGfxs.empty()); +} + +TEST(DisplayListFactoryTest, DListDataConstruction) { + std::vector gfxs = {0xDE010000, 0x06001000, 0xDF000000, 0x00000000}; + DListData data(gfxs); + ASSERT_EQ(data.mGfxs.size(), 4u); + EXPECT_EQ(data.mGfxs[0], 0xDE010000u); + EXPECT_EQ(data.mGfxs[1], 0x06001000u); + EXPECT_EQ(data.mGfxs[2], 0xDF000000u); + EXPECT_EQ(data.mGfxs[3], 0x00000000u); +} + +TEST(DisplayListFactoryTest, Alignment) { + DListFactory factory; + EXPECT_EQ(factory.GetAlignment(), 8u); +} + +TEST(DisplayListFactoryTest, HasExpectedExporters) { + DListFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +// GBI opcode extraction: the high byte of word 0 is the opcode +TEST(DisplayListFactoryTest, GBIOpcodeExtraction) { + // gsSPEndDisplayList: w0 = 0xDF000000 + uint32_t endDL = 0xDF000000; + EXPECT_EQ((endDL >> 24) & 0xFF, 0xDFu); + + // gsSPDisplayList (branch): w0 = 0xDE010000 + uint32_t branchDL = 0xDE010000; + EXPECT_EQ((branchDL >> 24) & 0xFF, 0xDEu); + + // gsSPVertex: w0 = 0x01012010 (F3DEX2 opcode 0x01) + uint32_t spVertex = 0x01012010; + EXPECT_EQ((spVertex >> 24) & 0xFF, 0x01u); +} + +// Parse GBI command pairs from a big-endian binary buffer +TEST(DisplayListFactoryTest, ManualGBICommandParse) { + // Minimal display list: gsSPVertex + gsSPEndDisplayList + // Each command is a pair of 32-bit words (w0, w1) + std::vector buf = { + // gsSPVertex: w0=0x01020030 (opcode=0x01, n=2, v0+n=0, size=0x30) + 0x01, 0x02, 0x00, 0x30, + // w1=0x06001000 (segment address of vertex data) + 0x06, 0x00, 0x10, 0x00, + // gsSPEndDisplayList: w0=0xDF000000, w1=0x00000000 + 0xDF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector gfxs; + // Read 2 commands (4 words) + for (int i = 0; i < 4; i++) { + gfxs.push_back(reader.ReadUInt32()); + } + + ASSERT_EQ(gfxs.size(), 4u); + // First command: gsSPVertex + EXPECT_EQ((gfxs[0] >> 24) & 0xFF, 0x01u); // opcode + EXPECT_EQ(gfxs[1], 0x06001000u); // vertex address + // Second command: gsSPEndDisplayList + EXPECT_EQ((gfxs[2] >> 24) & 0xFF, 0xDFu); // opcode + EXPECT_EQ(gfxs[3], 0x00000000u); +} diff --git a/tests/FZXCourseTest.cpp b/tests/FZXCourseTest.cpp new file mode 100644 index 00000000..b92e6b3b --- /dev/null +++ b/tests/FZXCourseTest.cpp @@ -0,0 +1,108 @@ +#include +#include "factories/fzerox/CourseFactory.h" +#include "factories/fzerox/course/Course.h" +#include + +// Struct sizes +TEST(FZXCourseTest, ControlPointSize) { + EXPECT_EQ(sizeof(FZX::ControlPoint), 0x14u); +} + +TEST(FZXCourseTest, CourseRawDataSize) { + EXPECT_EQ(sizeof(FZX::CourseRawData), 0x7E0u); +} + +// CourseData construction +TEST(FZXCourseTest, CourseDataConstruction) { + FZX::ControlPointInfo cpi = {}; + cpi.controlPoint.pos = {100.0f, 200.0f, 300.0f}; + cpi.controlPoint.radiusLeft = 50; + cpi.controlPoint.radiusRight = 60; + cpi.controlPoint.trackSegmentInfo = 0; + cpi.bankAngle = 10; + cpi.pit = -1; + cpi.dash = -1; + cpi.dirt = -1; + cpi.ice = -1; + cpi.jump = -1; + cpi.landmine = -1; + cpi.gate = -1; + cpi.building = -1; + cpi.sign = -1; + + std::vector cpis = {cpi}; + std::vector fileName(23, '\0'); + fileName[0] = 'T'; fileName[1] = 'e'; fileName[2] = 's'; fileName[3] = 't'; + + FZX::CourseData course(4, 0, 1, 0, fileName, 5, cpis); + EXPECT_EQ(course.mCreatorId, 4); + EXPECT_EQ(course.mVenue, 0); + EXPECT_EQ(course.mSkybox, 1); + EXPECT_EQ(course.mFlag, 0); + EXPECT_EQ(course.mBgm, 5); + ASSERT_EQ(course.mControlPointInfos.size(), 1u); + EXPECT_FLOAT_EQ(course.mControlPointInfos[0].controlPoint.pos.x, 100.0f); +} + +TEST(FZXCourseTest, CourseDataEmpty) { + std::vector empty; + std::vector fileName(23, '\0'); + FZX::CourseData course(0, 0, 0, 0, fileName, 0, empty); + EXPECT_TRUE(course.mControlPointInfos.empty()); +} + +// CalculateChecksum +TEST(FZXCourseTest, ChecksumEmptyCourse) { + std::vector empty; + std::vector fileName(23, '\0'); + FZX::CourseData course(0, 0, 0, 0, fileName, 0, empty); + uint32_t checksum = course.CalculateChecksum(); + // checksum starts with mControlPointInfos.size() = 0 + EXPECT_EQ(checksum, 0u); +} + +TEST(FZXCourseTest, ChecksumDeterministic) { + FZX::ControlPointInfo cpi = {}; + cpi.controlPoint.pos = {10.0f, 20.0f, 30.0f}; + cpi.controlPoint.radiusLeft = 100; + cpi.controlPoint.radiusRight = 100; + cpi.controlPoint.trackSegmentInfo = 0x3F; // TRACK_TYPE_NONE + cpi.bankAngle = 0; + cpi.pit = 0; + cpi.dash = 0; + cpi.dirt = 0; + cpi.ice = 0; + cpi.jump = 0; + cpi.landmine = 0; + cpi.gate = 0; + cpi.building = 0; + cpi.sign = 0; + + std::vector cpis = {cpi}; + std::vector fileName(23, '\0'); + FZX::CourseData course(4, 0, 0, 0, fileName, 0, cpis); + + uint32_t checksum1 = course.CalculateChecksum(); + uint32_t checksum2 = course.CalculateChecksum(); + EXPECT_EQ(checksum1, checksum2); + EXPECT_NE(checksum1, 0u); // with non-zero position, checksum should be non-zero +} + +// Track mask constants +TEST(FZXCourseTest, TrackMaskConstants) { + EXPECT_EQ(TRACK_JOIN_MASK, 0x600); + EXPECT_EQ(TRACK_FORM_MASK, 0x00038000); + EXPECT_EQ(TRACK_FLAG_CONTINUOUS, 0x40000000); + EXPECT_EQ(TRACK_TYPE_MASK, 0x3F); + EXPECT_EQ(TRACK_SHAPE_MASK, 0x1C0); +} + +// Factory +TEST(FZXCourseTest, CourseFactoryExporters) { + FZX::CourseFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Modding).has_value()); + EXPECT_TRUE(factory.SupportModdedAssets()); +} diff --git a/tests/FZXDataTest.cpp b/tests/FZXDataTest.cpp new file mode 100644 index 00000000..a605bc8f --- /dev/null +++ b/tests/FZXDataTest.cpp @@ -0,0 +1,269 @@ +#include +#include "factories/fzerox/EADAnimationFactory.h" +#include "factories/fzerox/EADLimbFactory.h" +#include "factories/fzerox/SoundFontFactory.h" +#include "factories/fzerox/SequenceFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +// EADAnimationData +TEST(FZXDataTest, EADAnimationDataConstruction) { + FZX::EADAnimationData anim(30, 10, 0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000); + EXPECT_EQ(anim.mFrameCount, 30); + EXPECT_EQ(anim.mLimbCount, 10); + EXPECT_EQ(anim.mScaleData, 0x1000u); + EXPECT_EQ(anim.mScaleInfo, 0x2000u); + EXPECT_EQ(anim.mRotationData, 0x3000u); + EXPECT_EQ(anim.mRotationInfo, 0x4000u); + EXPECT_EQ(anim.mPositionData, 0x5000u); + EXPECT_EQ(anim.mPositionInfo, 0x6000u); +} + +TEST(FZXDataTest, EADAnimationFactoryExporters) { + FZX::EADAnimationFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +// EADLimbData +TEST(FZXDataTest, EADLimbDataConstruction) { + FZX::EADLimbData limb( + 0x06001000, + Vec3f(1.0f, 2.0f, 3.0f), + Vec3f(10.0f, 20.0f, 30.0f), + Vec3s(100, 200, 300), + 0x06002000, 0x06003000, 0x06004000, 0x06005000, 5); + + EXPECT_EQ(limb.mDl, 0x06001000u); + EXPECT_FLOAT_EQ(limb.mScale.x, 1.0f); + EXPECT_FLOAT_EQ(limb.mScale.z, 3.0f); + EXPECT_FLOAT_EQ(limb.mPos.y, 20.0f); + EXPECT_EQ(limb.mRot.x, 100); + EXPECT_EQ(limb.mRot.z, 300); + EXPECT_EQ(limb.mNextLimb, 0x06002000u); + EXPECT_EQ(limb.mChildLimb, 0x06003000u); + EXPECT_EQ(limb.mAssociatedLimb, 0x06004000u); + EXPECT_EQ(limb.mAssociatedLimbDL, 0x06005000u); + EXPECT_EQ(limb.mLimbId, 5); +} + +TEST(FZXDataTest, EADLimbFactoryExporters) { + FZX::EADLimbFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +// SoundFont types +TEST(FZXDataTest, TunedSampleConstruction) { + FZX::TunedSample ts = {"sample_ref", 1.5f}; + EXPECT_EQ(ts.sampleRef, "sample_ref"); + EXPECT_FLOAT_EQ(ts.tuning, 1.5f); +} + +TEST(FZXDataTest, DrumConstruction) { + FZX::Drum drum = {10, 64, 0, {"sample", 1.0f}, "env_ref"}; + EXPECT_EQ(drum.adsrDecayIndex, 10); + EXPECT_EQ(drum.pan, 64); + EXPECT_EQ(drum.isRelocated, 0); + EXPECT_EQ(drum.tunedSample.sampleRef, "sample"); + EXPECT_EQ(drum.envelopeRef, "env_ref"); +} + +TEST(FZXDataTest, InstrumentConstruction) { + FZX::Instrument inst = { + 0, 20, 100, 5, "env", + {"low_sample", 0.5f}, + {"normal_sample", 1.0f}, + {"high_sample", 2.0f} + }; + EXPECT_EQ(inst.normalRangeLo, 20); + EXPECT_EQ(inst.normalRangeHi, 100); + EXPECT_EQ(inst.adsrDecayIndex, 5); + EXPECT_FLOAT_EQ(inst.lowPitchTunedSample.tuning, 0.5f); + EXPECT_FLOAT_EQ(inst.normalPitchTunedSample.tuning, 1.0f); + EXPECT_FLOAT_EQ(inst.highPitchTunedSample.tuning, 2.0f); +} + +TEST(FZXDataTest, AdpcmBookConstruction) { + FZX::AdpcmBook book = {2, 4, {1, 2, 3, 4, 5, 6, 7, 8}}; + EXPECT_EQ(book.order, 2); + EXPECT_EQ(book.numPredictors, 4); + ASSERT_EQ(book.book.size(), 8u); + EXPECT_EQ(book.book[0], 1); +} + +TEST(FZXDataTest, AdpcmLoopConstruction) { + FZX::AdpcmLoop loop = {100, 5000, 3, {10, 20, 30}}; + EXPECT_EQ(loop.start, 100u); + EXPECT_EQ(loop.end, 5000u); + EXPECT_EQ(loop.count, 3u); + ASSERT_EQ(loop.predictorState.size(), 3u); +} + +TEST(FZXDataTest, SoundFontDataConstruction) { + FZX::SoundFontEntry entry = { + "drum_0", FZX::DataType::Drum, + FZX::Drum{0, 64, 0, {"sample", 1.0f}, "env"} + }; + std::vector entries = {entry}; + FZX::SoundFontData data(entries, true); + ASSERT_EQ(data.mEntries.size(), 1u); + EXPECT_EQ(data.mEntries[0].name, "drum_0"); + EXPECT_EQ(data.mEntries[0].type, FZX::DataType::Drum); + EXPECT_TRUE(data.mSupportSfx); +} + +TEST(FZXDataTest, SoundFontFactoryExporters) { + FZX::SoundFontFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +// Sequence types +TEST(FZXDataTest, SeqCommandComparison) { + FZX::SeqCommand cmd1 = {0x90, 0, FZX::SequenceState::player, 0, 0, 1, {}}; + FZX::SeqCommand cmd2 = {0x91, 10, FZX::SequenceState::channel, 1, 0, 1, {}}; + EXPECT_TRUE(cmd1 < cmd2); + EXPECT_FALSE(cmd2 < cmd1); +} + +TEST(FZXDataTest, SeqLabelInfoConstruction) { + FZX::SeqLabelInfo label(0x100, FZX::SequenceState::layer, 2, 1, true); + EXPECT_EQ(label.pos, 0x100u); + EXPECT_EQ(label.state, FZX::SequenceState::layer); + EXPECT_EQ(label.channel, 2u); + EXPECT_EQ(label.layer, 1u); + EXPECT_TRUE(label.largeNotes); +} + +TEST(FZXDataTest, SequenceDataConstruction) { + FZX::SeqCommand cmd = {0xFE, 0, FZX::SequenceState::player, -1, -1, 1, {}}; + std::vector cmds = {cmd}; + std::unordered_set labels = {0, 100, 200}; + FZX::SequenceData data(cmds, labels, true); + ASSERT_EQ(data.mCmds.size(), 1u); + EXPECT_EQ(data.mCmds[0].cmd, 0xFE); + EXPECT_EQ(data.mLabels.size(), 3u); + EXPECT_TRUE(data.mHasFooter); +} + +TEST(FZXDataTest, SequenceFactoryExporters) { + FZX::SequenceFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +// BinaryReader parse test — EADAnimation header format +TEST(FZXDataTest, ManualEADAnimationHeaderParse) { + // 28 bytes: int16_t frameCount + int16_t limbCount + 6×uint32_t pointers + std::vector buf = { + 0x00, 0x1E, // frameCount = 30 + 0x00, 0x0A, // limbCount = 10 + 0x00, 0x00, 0x10, 0x00, // scaleData = 0x1000 + 0x00, 0x00, 0x20, 0x00, // scaleInfo = 0x2000 + 0x00, 0x00, 0x30, 0x00, // rotationData = 0x3000 + 0x00, 0x00, 0x40, 0x00, // rotationInfo = 0x4000 + 0x00, 0x00, 0x50, 0x00, // positionData = 0x5000 + 0x00, 0x00, 0x60, 0x00, // positionInfo = 0x6000 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto frameCount = reader.ReadInt16(); + auto limbCount = reader.ReadInt16(); + auto scaleData = reader.ReadUInt32(); + auto scaleInfo = reader.ReadUInt32(); + auto rotationData = reader.ReadUInt32(); + auto rotationInfo = reader.ReadUInt32(); + auto positionData = reader.ReadUInt32(); + auto positionInfo = reader.ReadUInt32(); + + FZX::EADAnimationData anim(frameCount, limbCount, scaleData, scaleInfo, + rotationData, rotationInfo, positionData, positionInfo); + + EXPECT_EQ(anim.mFrameCount, 30); + EXPECT_EQ(anim.mLimbCount, 10); + EXPECT_EQ(anim.mScaleData, 0x1000u); + EXPECT_EQ(anim.mScaleInfo, 0x2000u); + EXPECT_EQ(anim.mRotationData, 0x3000u); + EXPECT_EQ(anim.mRotationInfo, 0x4000u); + EXPECT_EQ(anim.mPositionData, 0x5000u); + EXPECT_EQ(anim.mPositionInfo, 0x6000u); +} + +// BinaryReader parse test — EADLimb format +TEST(FZXDataTest, ManualEADLimbParse) { + // 52 bytes: u32 dl + 3×float scale + 3×float pos + 3×int16 rot + i16 pad + // + u32 next + u32 child + u32 assocLimb + u32 assocDL + i16 limbId + std::vector buf = { + 0x06, 0x00, 0x10, 0x00, // dl = 0x06001000 + 0x3F, 0x80, 0x00, 0x00, // scale.x = 1.0f + 0x40, 0x00, 0x00, 0x00, // scale.y = 2.0f + 0x40, 0x40, 0x00, 0x00, // scale.z = 3.0f + 0x41, 0x20, 0x00, 0x00, // pos.x = 10.0f + 0x41, 0xA0, 0x00, 0x00, // pos.y = 20.0f + 0x41, 0xF0, 0x00, 0x00, // pos.z = 30.0f + 0x00, 0x64, // rot.x = 100 + 0x00, 0xC8, // rot.y = 200 + 0x01, 0x2C, // rot.z = 300 + 0x00, 0x00, // pad + 0x06, 0x00, 0x20, 0x00, // nextLimb = 0x06002000 + 0x06, 0x00, 0x30, 0x00, // childLimb = 0x06003000 + 0x06, 0x00, 0x40, 0x00, // associatedLimb = 0x06004000 + 0x06, 0x00, 0x50, 0x00, // associatedLimbDL = 0x06005000 + 0x00, 0x05, // limbId = 5 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto dl = reader.ReadUInt32(); + auto sx = reader.ReadFloat(); auto sy = reader.ReadFloat(); auto sz = reader.ReadFloat(); + Vec3f scale(sx, sy, sz); + auto px = reader.ReadFloat(); auto py = reader.ReadFloat(); auto pz = reader.ReadFloat(); + Vec3f pos(px, py, pz); + auto rx = reader.ReadInt16(); auto ry = reader.ReadInt16(); auto rz = reader.ReadInt16(); + Vec3s rot(rx, ry, rz); + reader.ReadInt16(); // pad + auto nextLimb = reader.ReadUInt32(); + auto childLimb = reader.ReadUInt32(); + auto assocLimb = reader.ReadUInt32(); + auto assocDL = reader.ReadUInt32(); + auto limbId = reader.ReadInt16(); + + FZX::EADLimbData limb(dl, scale, pos, rot, nextLimb, childLimb, assocLimb, assocDL, limbId); + + EXPECT_EQ(limb.mDl, 0x06001000u); + EXPECT_FLOAT_EQ(limb.mScale.x, 1.0f); + EXPECT_FLOAT_EQ(limb.mScale.y, 2.0f); + EXPECT_FLOAT_EQ(limb.mScale.z, 3.0f); + EXPECT_FLOAT_EQ(limb.mPos.x, 10.0f); + EXPECT_FLOAT_EQ(limb.mPos.y, 20.0f); + EXPECT_FLOAT_EQ(limb.mPos.z, 30.0f); + EXPECT_EQ(limb.mRot.x, 100); + EXPECT_EQ(limb.mRot.y, 200); + EXPECT_EQ(limb.mRot.z, 300); + EXPECT_EQ(limb.mNextLimb, 0x06002000u); + EXPECT_EQ(limb.mChildLimb, 0x06003000u); + EXPECT_EQ(limb.mAssociatedLimb, 0x06004000u); + EXPECT_EQ(limb.mAssociatedLimbDL, 0x06005000u); + EXPECT_EQ(limb.mLimbId, 5); +} + +// DataType enum coverage +TEST(FZXDataTest, DataTypeValues) { + EXPECT_NE(FZX::DataType::Drum, FZX::DataType::Sfx); + EXPECT_NE(FZX::DataType::Instrument, FZX::DataType::Envelope); + EXPECT_NE(FZX::DataType::Sample, FZX::DataType::Book); + EXPECT_NE(FZX::DataType::Loop, FZX::DataType::DrumOffsets); +} diff --git a/tests/FZXGhostRecordTest.cpp b/tests/FZXGhostRecordTest.cpp new file mode 100644 index 00000000..c6535b72 --- /dev/null +++ b/tests/FZXGhostRecordTest.cpp @@ -0,0 +1,105 @@ +#include +#include "factories/fzerox/GhostRecordFactory.h" +#include +#include + +// Helper to create a minimal GhostRecordData +static FZX::GhostRecordData MakeGhostRecord(std::vector replayData = {}, std::vector lapTimes = {}) { + FZX::GhostMachineInfo info = {}; + return FZX::GhostRecordData( + 0, 0, 0, 0, 0, 0, "", info, + 0, std::move(lapTimes), 0, 0, std::move(replayData)); +} + +// Save_CalculateChecksum +TEST(FZXGhostRecordTest, SaveChecksumKnownBuffer) { + auto ghost = MakeGhostRecord(); + uint8_t data[] = {0x10, 0x20, 0x30, 0x40}; + uint16_t result = ghost.Save_CalculateChecksum(data, 4); + EXPECT_EQ(result, 0x10 + 0x20 + 0x30 + 0x40); +} + +TEST(FZXGhostRecordTest, SaveChecksumEmpty) { + auto ghost = MakeGhostRecord(); + uint8_t data[] = {0}; + uint16_t result = ghost.Save_CalculateChecksum(data, 0); + EXPECT_EQ(result, 0u); +} + +TEST(FZXGhostRecordTest, SaveChecksumSingleByte) { + auto ghost = MakeGhostRecord(); + uint8_t data[] = {0xAB}; + uint16_t result = ghost.Save_CalculateChecksum(data, 1); + EXPECT_EQ(result, 0xABu); +} + +// CalculateReplayChecksum +TEST(FZXGhostRecordTest, ReplayChecksum4Bytes) { + // 4 bytes = one int32: 0x01020304 + auto ghost = MakeGhostRecord({0x01, 0x02, 0x03, 0x04}); + int32_t result = ghost.CalculateReplayChecksum(); + // byte0 << 24 | byte1 << 16 | byte2 << 8 | byte3 + EXPECT_EQ(result, 0x01020304); +} + +TEST(FZXGhostRecordTest, ReplayChecksum8Bytes) { + auto ghost = MakeGhostRecord({0x01, 0x02, 0x03, 0x04, 0x10, 0x20, 0x30, 0x40}); + int32_t result = ghost.CalculateReplayChecksum(); + EXPECT_EQ(result, 0x01020304 + 0x10203040); +} + +TEST(FZXGhostRecordTest, ReplayChecksumNonAligned) { + // 5 bytes: first 4 form a word, the 5th byte starts accumulating but + // never completes (i never hits 0 again), so only the first word counts + auto ghost = MakeGhostRecord({0x01, 0x02, 0x03, 0x04, 0xFF}); + int32_t result = ghost.CalculateReplayChecksum(); + EXPECT_EQ(result, 0x01020304); +} + +// GhostMachineInfo +TEST(FZXGhostRecordTest, GhostMachineInfoConstruction) { + FZX::GhostMachineInfo info = { + 1, 2, 3, 4, 5, 6, 7, 8, // character through decal + 0xAA, 0xBB, 0xCC, // bodyR/G/B + 0x11, 0x22, 0x33, // numberR/G/B + 0x44, 0x55, 0x66, // decalR/G/B + 0x77, 0x88, 0x99 // cockpitR/G/B + }; + EXPECT_EQ(info.character, 1); + EXPECT_EQ(info.bodyR, 0xAA); + EXPECT_EQ(info.cockpitB, 0x99); +} + +// GhostRecordData +TEST(FZXGhostRecordTest, GhostRecordDataConstruction) { + FZX::GhostMachineInfo info = {}; + std::vector lapTimes = {60000, 61000, 62000}; + std::vector replay = {1, 2, 3, 4, 5}; + FZX::GhostRecordData ghost( + 0x1234, 1, 0x56789ABC, 42, 183000, 0, + "CustomTrack", info, + 0x5678, lapTimes, -1, 5, replay); + + EXPECT_EQ(ghost.mRecordChecksum, 0x1234u); + EXPECT_EQ(ghost.mGhostType, 1u); + EXPECT_EQ(ghost.mReplayChecksum, 0x56789ABC); + EXPECT_EQ(ghost.mCourseEncoding, 42); + EXPECT_EQ(ghost.mRaceTime, 183000); + EXPECT_EQ(ghost.mTrackName, "CustomTrack"); + EXPECT_EQ(ghost.mDataChecksum, 0x5678u); + ASSERT_EQ(ghost.mLapTimes.size(), 3u); + EXPECT_EQ(ghost.mLapTimes[0], 60000); + EXPECT_EQ(ghost.mReplayEnd, -1); + EXPECT_EQ(ghost.mReplaySize, 5u); + ASSERT_EQ(ghost.mReplayData.size(), 5u); +} + +// Factory +TEST(FZXGhostRecordTest, GhostRecordFactoryExporters) { + FZX::GhostRecordFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Modding).has_value()); + EXPECT_TRUE(factory.SupportModdedAssets()); +} diff --git a/tests/FloatFactoryTest.cpp b/tests/FloatFactoryTest.cpp new file mode 100644 index 00000000..ae718a2d --- /dev/null +++ b/tests/FloatFactoryTest.cpp @@ -0,0 +1,62 @@ +#include +#include "factories/FloatFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include +#include + +TEST(FloatFactoryTest, FloatDataConstruction) { + std::vector floats = {1.0f, -2.5f, 3.14159f, 0.0f}; + FloatData data(floats); + ASSERT_EQ(data.mFloats.size(), 4u); + EXPECT_FLOAT_EQ(data.mFloats[0], 1.0f); + EXPECT_FLOAT_EQ(data.mFloats[1], -2.5f); + EXPECT_FLOAT_EQ(data.mFloats[2], 3.14159f); + EXPECT_FLOAT_EQ(data.mFloats[3], 0.0f); +} + +TEST(FloatFactoryTest, FloatDataEmpty) { + std::vector floats; + FloatData data(floats); + EXPECT_TRUE(data.mFloats.empty()); +} + +TEST(FloatFactoryTest, HasExpectedExporters) { + FloatFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +TEST(FloatFactoryTest, ParseModdingReturnsNullopt) { + FloatFactory factory; + std::vector buf; + YAML::Node node; + EXPECT_EQ(factory.parse_modding(buf, node), std::nullopt); +} + +TEST(FloatFactoryTest, ManualFloatParse) { + // Big-endian IEEE 754 floats: + // 1.0f = 0x3F800000 + // -2.5f = 0xC0200000 + // 0.0f = 0x00000000 + std::vector buf = { + 0x3F, 0x80, 0x00, 0x00, // 1.0f + 0xC0, 0x20, 0x00, 0x00, // -2.5f + 0x00, 0x00, 0x00, 0x00, // 0.0f + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector floats; + for (int i = 0; i < 3; i++) { + floats.push_back(reader.ReadFloat()); + } + + ASSERT_EQ(floats.size(), 3u); + EXPECT_FLOAT_EQ(floats[0], 1.0f); + EXPECT_FLOAT_EQ(floats[1], -2.5f); + EXPECT_FLOAT_EQ(floats[2], 0.0f); +} diff --git a/tests/LightsFactoryTest.cpp b/tests/LightsFactoryTest.cpp new file mode 100644 index 00000000..d1725fa9 --- /dev/null +++ b/tests/LightsFactoryTest.cpp @@ -0,0 +1,68 @@ +#include +#include "factories/LightsFactory.h" +#include + +TEST(LightsFactoryTest, StructSizes) { + EXPECT_EQ(sizeof(AmbientRaw), 8u); + EXPECT_EQ(sizeof(LightRaw), 16u); + EXPECT_EQ(sizeof(Lights1Raw), 24u); +} + +TEST(LightsFactoryTest, LightsDataConstruction) { + Lights1Raw raw = {}; + // Ambient: bright white + raw.a.l.col[0] = 64; raw.a.l.col[1] = 64; raw.a.l.col[2] = 64; + raw.a.l.colc[0] = 64; raw.a.l.colc[1] = 64; raw.a.l.colc[2] = 64; + // Diffuse: red light pointing down + raw.l[0].l.col[0] = 255; raw.l[0].l.col[1] = 0; raw.l[0].l.col[2] = 0; + raw.l[0].l.colc[0] = 255; raw.l[0].l.colc[1] = 0; raw.l[0].l.colc[2] = 0; + raw.l[0].l.dir[0] = 0; raw.l[0].l.dir[1] = 127; raw.l[0].l.dir[2] = 0; + + LightsData data(raw); + EXPECT_EQ(data.mLights.a.l.col[0], 64); + EXPECT_EQ(data.mLights.a.l.col[1], 64); + EXPECT_EQ(data.mLights.a.l.col[2], 64); + EXPECT_EQ(data.mLights.l[0].l.col[0], 255); + EXPECT_EQ(data.mLights.l[0].l.col[1], 0); + EXPECT_EQ(data.mLights.l[0].l.col[2], 0); + EXPECT_EQ(data.mLights.l[0].l.dir[0], 0); + EXPECT_EQ(data.mLights.l[0].l.dir[1], 127); + EXPECT_EQ(data.mLights.l[0].l.dir[2], 0); +} + +TEST(LightsFactoryTest, HasExpectedExporters) { + LightsFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +TEST(LightsFactoryTest, ManualLightsParse) { + // Build a 24-byte buffer representing Lights1Raw: + // Ambient (8 bytes): col={100,150,200}, pad1=0, colc={100,150,200}, pad2=0 + // Light (16 bytes): col={255,128,64}, pad1=0, colc={255,128,64}, pad2=0, dir={40,-50,73}, pad3=0 + uint8_t buf[24] = { + // AmbientRaw (8 bytes) + 100, 150, 200, 0, // col[3] + pad1 + 100, 150, 200, 0, // colc[3] + pad2 + // LightRaw (16 bytes) + 255, 128, 64, 0, // col[3] + pad1 + 255, 128, 64, 0, // colc[3] + pad2 + 40, static_cast(-50), 73, 0, // dir[3] + pad3 + 0, 0, 0, 0, // padding for alignment + }; + + Lights1Raw lights; + std::memcpy(&lights, buf, sizeof(Lights1Raw)); + + EXPECT_EQ(lights.a.l.col[0], 100); + EXPECT_EQ(lights.a.l.col[1], 150); + EXPECT_EQ(lights.a.l.col[2], 200); + EXPECT_EQ(lights.l[0].l.col[0], 255); + EXPECT_EQ(lights.l[0].l.col[1], 128); + EXPECT_EQ(lights.l[0].l.col[2], 64); + EXPECT_EQ(lights.l[0].l.dir[0], 40); + EXPECT_EQ(lights.l[0].l.dir[1], -50); + EXPECT_EQ(lights.l[0].l.dir[2], 73); +} diff --git a/tests/MK64DataTest.cpp b/tests/MK64DataTest.cpp new file mode 100644 index 00000000..20d888be --- /dev/null +++ b/tests/MK64DataTest.cpp @@ -0,0 +1,363 @@ +#include +#include "factories/mk64/CourseVtx.h" +#include "factories/mk64/Paths.h" +#include "factories/mk64/TrackSections.h" +#include "factories/mk64/SpawnData.h" +#include "factories/mk64/DrivingBehaviour.h" +#include "factories/mk64/ItemCurve.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +// CourseVtx +TEST(MK64DataTest, CourseVtxConstruction) { + MK64::CourseVtx vtx = {{100, 200, 300}, {10, 20}, {255, 128, 64, 255}}; + EXPECT_EQ(vtx.ob[0], 100); + EXPECT_EQ(vtx.ob[1], 200); + EXPECT_EQ(vtx.ob[2], 300); + EXPECT_EQ(vtx.tc[0], 10); + EXPECT_EQ(vtx.tc[1], 20); + EXPECT_EQ(vtx.cn[0], 255); + EXPECT_EQ(vtx.cn[1], 128); + EXPECT_EQ(vtx.cn[2], 64); + EXPECT_EQ(vtx.cn[3], 255); +} + +TEST(MK64DataTest, CourseVtxDataConstruction) { + std::vector vtxs = { + {{0, 0, 0}, {0, 0}, {0, 0, 0, 0}}, + {{100, -50, 200}, {32, 64}, {255, 255, 255, 255}}, + }; + MK64::CourseVtxData data(vtxs); + ASSERT_EQ(data.mVtxs.size(), 2u); + EXPECT_EQ(data.mVtxs[0].ob[0], 0); + EXPECT_EQ(data.mVtxs[1].ob[1], -50); + EXPECT_EQ(data.mVtxs[1].cn[0], 255); +} + +TEST(MK64DataTest, CourseVtxFactoryExporters) { + MK64::CourseVtxFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.SupportModdedAssets()); +} + +// TrackPath +TEST(MK64DataTest, TrackPathConstruction) { + MK64::TrackPath path = {100, -200, 300, 5}; + EXPECT_EQ(path.posX, 100); + EXPECT_EQ(path.posY, -200); + EXPECT_EQ(path.posZ, 300); + EXPECT_EQ(path.trackSegment, 5u); +} + +TEST(MK64DataTest, PathDataConstruction) { + std::vector paths = { + {0, 0, 0, 0}, + {1000, -500, 2000, 3}, + }; + MK64::PathData data(paths); + ASSERT_EQ(data.mPaths.size(), 2u); + EXPECT_EQ(data.mPaths[1].posX, 1000); + EXPECT_EQ(data.mPaths[1].trackSegment, 3u); +} + +TEST(MK64DataTest, PathsFactoryExporters) { + MK64::PathsFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.SupportModdedAssets()); +} + +// TrackSections +TEST(MK64DataTest, TrackSectionsConstruction) { + MK64::TrackSections sec = {0xDEADBEEF12345678ULL, 2, 7, 0x00FF}; + EXPECT_EQ(sec.crc, 0xDEADBEEF12345678ULL); + EXPECT_EQ(sec.surfaceType, 2); + EXPECT_EQ(sec.sectionId, 7); + EXPECT_EQ(sec.flags, 0x00FFu); +} + +TEST(MK64DataTest, TrackSectionsDataConstruction) { + std::vector secs = { + {0x1111, 0, 0, 0}, + {0x2222, 1, 1, 0x8000}, + }; + MK64::TrackSectionsData data(secs); + ASSERT_EQ(data.mSecs.size(), 2u); + EXPECT_EQ(data.mSecs[0].crc, 0x1111u); + EXPECT_EQ(data.mSecs[1].flags, 0x8000u); +} + +TEST(MK64DataTest, TrackSectionsFactoryExporters) { + MK64::TrackSectionsFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.SupportModdedAssets()); +} + +// ActorSpawnData +TEST(MK64DataTest, ActorSpawnDataConstruction) { + MK64::ActorSpawnData spawn = {100, -200, 300, 42}; + EXPECT_EQ(spawn.x, 100); + EXPECT_EQ(spawn.y, -200); + EXPECT_EQ(spawn.z, 300); + EXPECT_EQ(spawn.id, 42u); +} + +TEST(MK64DataTest, SpawnDataDataConstruction) { + std::vector spawns = { + {0, 0, 0, 1}, + {500, 100, -300, 10}, + }; + MK64::SpawnDataData data(spawns); + ASSERT_EQ(data.mSpawns.size(), 2u); + EXPECT_EQ(data.mSpawns[1].id, 10u); +} + +TEST(MK64DataTest, SpawnDataFactoryExporters) { + MK64::SpawnDataFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.SupportModdedAssets()); +} + +// DrivingBehaviour +TEST(MK64DataTest, BhvRawConstruction) { + MK64::BhvRaw bhv = {10, 20, 0x12345678}; + EXPECT_EQ(bhv.waypoint1, 10); + EXPECT_EQ(bhv.waypoint2, 20); + EXPECT_EQ(bhv.bhv, 0x12345678); +} + +TEST(MK64DataTest, DrivingDataConstruction) { + std::vector bhvs = { + {0, 5, 100}, + {5, 10, -1}, + }; + MK64::DrivingData data(bhvs); + ASSERT_EQ(data.mBhvs.size(), 2u); + EXPECT_EQ(data.mBhvs[0].waypoint2, 5); + EXPECT_EQ(data.mBhvs[1].bhv, -1); +} + +TEST(MK64DataTest, DrivingBehaviourFactoryExporters) { + MK64::DrivingBehaviourFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); +} + +// ItemCurve +TEST(MK64DataTest, ItemCurveDataConstruction) { + std::vector items(100, 0x0A); // 10x10 probability array + MK64::ItemCurveData data(items); + ASSERT_EQ(data.mItems.size(), 100u); + EXPECT_EQ(data.mItems[0], 0x0A); + EXPECT_EQ(data.mItems[99], 0x0A); +} + +TEST(MK64DataTest, ItemCurveFactoryExporters) { + MK64::ItemCurveFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.SupportModdedAssets()); +} + +// ============================================================ +// BinaryReader parse tests — replicate factory parse logic +// ============================================================ + +// CourseVtx parse: flag extraction from color bytes +// cn1 low 2 bits and cn2 bits 2-3 encode flags; colors are masked +TEST(MK64DataTest, ManualCourseVtxFlagExtraction) { + // Craft a 14-byte big-endian CourseVtx: + // ob: (50, -100, 200), tc: (10, 20), cn: (0x07, 0x0B, 0xAA, 0xFF) + // Expected flags: (0x07 & 3) | ((0x0B << 2) & 0xC) = 3 | 0xC = 0xF + // Expected colors: (0x07 & 0xFC)=0x04, (0x0B & 0xFC)=0x08, 0xAA, 0xFF→0xFF + std::vector buf = { + 0x00, 0x32, // ob[0] = 50 + 0xFF, 0x9C, // ob[1] = -100 + 0x00, 0xC8, // ob[2] = 200 + 0x00, 0x0A, // tc[0] = 10 + 0x00, 0x14, // tc[1] = 20 + 0x07, // cn[0] = 0x07 + 0x0B, // cn[1] = 0x0B + 0xAA, // cn[2] = 0xAA + 0xFF, // cn[3] = 0xFF + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto x = reader.ReadInt16(); + auto y = reader.ReadInt16(); + auto z = reader.ReadInt16(); + auto tc1 = reader.ReadInt16(); + auto tc2 = reader.ReadInt16(); + auto cn1 = reader.ReadUByte(); + auto cn2 = reader.ReadUByte(); + auto cn3 = reader.ReadUByte(); + auto cn4 = reader.ReadUByte(); + + uint16_t flags = cn1 & 3; + flags |= (cn2 << 2) & 0xC; + + VtxRaw vtx = {{ x, y, z }, flags, { tc1, tc2 }, { (uint8_t)(cn1 & 0xfc), (uint8_t)(cn2 & 0xfc), cn3, 0xff }}; + + EXPECT_EQ(vtx.ob[0], 50); + EXPECT_EQ(vtx.ob[1], -100); + EXPECT_EQ(vtx.ob[2], 200); + EXPECT_EQ(vtx.flag, 0xFu); // flags = 3 | 0xC + EXPECT_EQ(vtx.cn[0], 0x04); // 0x07 & 0xFC + EXPECT_EQ(vtx.cn[1], 0x08); // 0x0B & 0xFC + EXPECT_EQ(vtx.cn[2], 0xAA); + EXPECT_EQ(vtx.cn[3], 0xFF); // always 0xFF +} + +// DrivingBehaviour parse: sentinel-terminated reading +TEST(MK64DataTest, ManualDrivingBehaviourSentinel) { + // Two normal entries + sentinel (w1=-1, w2=-1, id=0) + // Each entry: int16_t w1, int16_t w2, int32_t id = 8 bytes + std::vector buf = { + // Entry 0: w1=5, w2=10, id=0x100 + 0x00, 0x05, 0x00, 0x0A, 0x00, 0x00, 0x01, 0x00, + // Entry 1: w1=15, w2=20, id=0x200 + 0x00, 0x0F, 0x00, 0x14, 0x00, 0x00, 0x02, 0x00, + // Sentinel: w1=-1, w2=-1, id=0 + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector behaviours; + while (true) { + auto w1 = reader.ReadInt16(); + auto w2 = reader.ReadInt16(); + auto id = reader.ReadInt32(); + behaviours.push_back(MK64::BhvRaw({ w1, w2, id })); + if ((w1 == -1) && (w2 == -1)) { + break; + } + } + + ASSERT_EQ(behaviours.size(), 3u); + EXPECT_EQ(behaviours[0].waypoint1, 5); + EXPECT_EQ(behaviours[0].waypoint2, 10); + EXPECT_EQ(behaviours[0].bhv, 0x100); + EXPECT_EQ(behaviours[1].waypoint1, 15); + EXPECT_EQ(behaviours[1].bhv, 0x200); + EXPECT_EQ(behaviours[2].waypoint1, -1); + EXPECT_EQ(behaviours[2].waypoint2, -1); +} + +// TrackSections parse: on-disk 8 bytes vs struct 16 bytes (u64 crc field) +TEST(MK64DataTest, ManualTrackSectionsParse) { + // On-disk per entry: u32 addr + i8 surf + i8 sect + u16 flags = 8 bytes + // The u32 addr maps to the u64 crc field in the struct + std::vector buf = { + // Entry 0: addr=0xDEADBEEF, surf=2, sect=7, flags=0x00FF + 0xDE, 0xAD, 0xBE, 0xEF, 0x02, 0x07, 0x00, 0xFF, + // Entry 1: addr=0x12345678, surf=-1, sect=3, flags=0x8000 + 0x12, 0x34, 0x56, 0x78, 0xFF, 0x03, 0x80, 0x00, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector sections; + for (int i = 0; i < 2; i++) { + auto addr = reader.ReadUInt32(); + auto surf = reader.ReadInt8(); + auto sect = reader.ReadInt8(); + auto flags = reader.ReadUInt16(); + sections.push_back(MK64::TrackSections({ addr, surf, sect, flags })); + } + + ASSERT_EQ(sections.size(), 2u); + EXPECT_EQ(sections[0].crc, 0xDEADBEEFu); + EXPECT_EQ(sections[0].surfaceType, 2); + EXPECT_EQ(sections[0].sectionId, 7); + EXPECT_EQ(sections[0].flags, 0x00FFu); + EXPECT_EQ(sections[1].crc, 0x12345678u); + EXPECT_EQ(sections[1].surfaceType, -1); + EXPECT_EQ(sections[1].sectionId, 3); + EXPECT_EQ(sections[1].flags, 0x8000u); +} + +// Paths parse: 8 bytes per entry (3×int16_t + uint16_t) +TEST(MK64DataTest, ManualPathsParse) { + std::vector buf = { + // Entry 0: x=1000, y=-500, z=2000, segment=3 + 0x03, 0xE8, 0xFE, 0x0C, 0x07, 0xD0, 0x00, 0x03, + // Entry 1: x=0, y=0, z=0, segment=0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector paths; + for (int i = 0; i < 2; i++) { + auto x = reader.ReadInt16(); + auto y = reader.ReadInt16(); + auto z = reader.ReadInt16(); + auto seg = reader.ReadUInt16(); + paths.push_back(MK64::TrackPath({ x, y, z, seg })); + } + + ASSERT_EQ(paths.size(), 2u); + EXPECT_EQ(paths[0].posX, 1000); + EXPECT_EQ(paths[0].posY, -500); + EXPECT_EQ(paths[0].posZ, 2000); + EXPECT_EQ(paths[0].trackSegment, 3u); + EXPECT_EQ(paths[1].posX, 0); +} + +// SpawnData parse: 8 bytes per entry (3×int16_t + uint16_t) +TEST(MK64DataTest, ManualSpawnDataParse) { + std::vector buf = { + // x=500, y=100, z=-300, id=42 + 0x01, 0xF4, 0x00, 0x64, 0xFE, 0xD4, 0x00, 0x2A, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto x = reader.ReadInt16(); + auto y = reader.ReadInt16(); + auto z = reader.ReadInt16(); + auto id = reader.ReadUInt16(); + MK64::ActorSpawnData spawn = { x, y, z, id }; + + EXPECT_EQ(spawn.x, 500); + EXPECT_EQ(spawn.y, 100); + EXPECT_EQ(spawn.z, -300); + EXPECT_EQ(spawn.id, 42u); +} + +// ItemCurve parse: 100 bytes of uint8_t +TEST(MK64DataTest, ManualItemCurveParse) { + // 10x10 probability array + std::vector buf(100); + for (int i = 0; i < 100; i++) { + buf[i] = static_cast(i); + } + + LUS::BinaryReader reader(buf.data(), buf.size()); + std::vector items; + for (int i = 0; i < 100; i++) { + items.push_back(reader.ReadUByte()); + } + + ASSERT_EQ(items.size(), 100u); + EXPECT_EQ(items[0], 0); + EXPECT_EQ(items[50], 50); + EXPECT_EQ(items[99], 99); +} diff --git a/tests/MtxFactoryTest.cpp b/tests/MtxFactoryTest.cpp new file mode 100644 index 00000000..a8599a5c --- /dev/null +++ b/tests/MtxFactoryTest.cpp @@ -0,0 +1,117 @@ +#include +#include "factories/MtxFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +// Replicate the FIXTOF macro locally for testing +#define TEST_FIXTOF(x) ((float)((x) / 65536.0f)) + +TEST(MtxFactoryTest, MtxSSize) { + // MtxS: 16 uint16_t intParts + 16 uint16_t fracParts = 64 bytes + EXPECT_EQ(sizeof(MtxS), 64u); +} + +TEST(MtxFactoryTest, MtxDataConstruction) { + MtxRaw raw = {}; + // Set identity matrix diagonal + raw.mtx[0] = 1.0f; raw.mtx[5] = 1.0f; raw.mtx[10] = 1.0f; raw.mtx[15] = 1.0f; + std::vector mtxs = {raw}; + MtxData data(mtxs); + ASSERT_EQ(data.mMtxs.size(), 1u); + EXPECT_FLOAT_EQ(data.mMtxs[0].mtx[0], 1.0f); + EXPECT_FLOAT_EQ(data.mMtxs[0].mtx[5], 1.0f); + EXPECT_FLOAT_EQ(data.mMtxs[0].mtx[10], 1.0f); + EXPECT_FLOAT_EQ(data.mMtxs[0].mtx[15], 1.0f); + EXPECT_FLOAT_EQ(data.mMtxs[0].mtx[1], 0.0f); +} + +TEST(MtxFactoryTest, HasExpectedExporters) { + MtxFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +TEST(MtxFactoryTest, FixedPointConversionIdentity) { + // 1.0 in N64 fixed-point: int=0x0001, frac=0x0000 + // (0x0001 << 16) | 0x0000 = 0x00010000 = 65536 + // FIXTOF(65536) = 65536 / 65536.0 = 1.0 + uint16_t int_part = 0x0001; + uint16_t frac_part = 0x0000; + float result = TEST_FIXTOF((int32_t)((int_part << 16) | frac_part)); + EXPECT_FLOAT_EQ(result, 1.0f); +} + +TEST(MtxFactoryTest, FixedPointConversionZero) { + uint16_t int_part = 0x0000; + uint16_t frac_part = 0x0000; + float result = TEST_FIXTOF((int32_t)((int_part << 16) | frac_part)); + EXPECT_FLOAT_EQ(result, 0.0f); +} + +TEST(MtxFactoryTest, FixedPointConversionHalf) { + // 0.5 in fixed-point: int=0x0000, frac=0x8000 + // (0x0000 << 16) | 0x8000 = 0x00008000 = 32768 + // FIXTOF(32768) = 32768 / 65536.0 = 0.5 + uint16_t int_part = 0x0000; + uint16_t frac_part = 0x8000; + float result = TEST_FIXTOF((int32_t)((int_part << 16) | frac_part)); + EXPECT_FLOAT_EQ(result, 0.5f); +} + +TEST(MtxFactoryTest, FixedPointConversionNegative) { + // -1.0 in fixed-point: int=0xFFFF, frac=0x0000 + // (0xFFFF << 16) | 0x0000 = 0xFFFF0000 interpreted as int32_t = -65536 + // FIXTOF(-65536) = -65536 / 65536.0 = -1.0 + uint16_t int_part = 0xFFFF; + uint16_t frac_part = 0x0000; + float result = TEST_FIXTOF((int32_t)((int_part << 16) | frac_part)); + EXPECT_FLOAT_EQ(result, -1.0f); +} + +TEST(MtxFactoryTest, ManualMatrixParse) { + // Build a 64-byte big-endian buffer for a 4x4 identity matrix in N64 fixed-point: + // First 32 bytes: 16 uint16_t integer parts (row-major) + // Next 32 bytes: 16 uint16_t fractional parts + // Identity: diagonal int=1, frac=0; off-diagonal int=0, frac=0 + std::vector buf = { + // Integer parts (16 uint16_t, big-endian) + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // row 0: 1,0,0,0 + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // row 1: 0,1,0,0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // row 2: 0,0,1,0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // row 3: 0,0,0,1 + // Fractional parts (16 uint16_t, all zero) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + // Read 16 int parts then 16 frac parts, convert to float + uint16_t intParts[16]; + uint16_t fracParts[16]; + for (int i = 0; i < 16; i++) { + intParts[i] = reader.ReadUInt16(); + } + for (int i = 0; i < 16; i++) { + fracParts[i] = reader.ReadUInt16(); + } + + float mtx[16]; + for (int i = 0; i < 16; i++) { + mtx[i] = TEST_FIXTOF((int32_t)((intParts[i] << 16) | fracParts[i])); + } + + // Verify identity matrix + for (int i = 0; i < 16; i++) { + float expected = (i % 5 == 0) ? 1.0f : 0.0f; // diagonal elements + EXPECT_FLOAT_EQ(mtx[i], expected) << "element " << i; + } +} + +#undef TEST_FIXTOF diff --git a/tests/N64GraphicsTest.cpp b/tests/N64GraphicsTest.cpp new file mode 100644 index 00000000..cfedc16a --- /dev/null +++ b/tests/N64GraphicsTest.cpp @@ -0,0 +1,231 @@ +#include + +extern "C" { +#include "n64graphics/n64graphics.h" +} + +#include +#include + +// RGBA16: 5-5-5-1 format, 2 bytes per pixel (big-endian) +// Bits: RRRRR GGGGG BBBBB A +TEST(N64GraphicsTest, Raw2RGBA16SinglePixel) { + // White with alpha: R=31, G=31, B=31, A=1 + // Binary: 11111 11111 11111 1 = 0xFFFF + uint8_t raw[] = {0xFF, 0xFF}; + rgba* img = raw2rgba(raw, 1, 1, 16); + ASSERT_NE(img, nullptr); + // SCALE_5_8(31) = (31 * 255) / 31 = 255 + EXPECT_EQ(img[0].red, 255); + EXPECT_EQ(img[0].green, 255); + EXPECT_EQ(img[0].blue, 255); + EXPECT_EQ(img[0].alpha, 255); + free(img); +} + +TEST(N64GraphicsTest, Raw2RGBA16Black) { + // Black with no alpha: R=0, G=0, B=0, A=0 + uint8_t raw[] = {0x00, 0x00}; + rgba* img = raw2rgba(raw, 1, 1, 16); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].red, 0); + EXPECT_EQ(img[0].green, 0); + EXPECT_EQ(img[0].blue, 0); + EXPECT_EQ(img[0].alpha, 0); + free(img); +} + +TEST(N64GraphicsTest, Raw2RGBA32SinglePixel) { + // RGBA32: 8-8-8-8, 4 bytes per pixel — direct passthrough + uint8_t raw[] = {0xAA, 0xBB, 0xCC, 0xDD}; + rgba* img = raw2rgba(raw, 1, 1, 32); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].red, 0xAA); + EXPECT_EQ(img[0].green, 0xBB); + EXPECT_EQ(img[0].blue, 0xCC); + EXPECT_EQ(img[0].alpha, 0xDD); + free(img); +} + +TEST(N64GraphicsTest, Raw2RGBA32MultiplePixels) { + uint8_t raw[] = { + 255, 0, 0, 255, // red + 0, 255, 0, 255, // green + 0, 0, 255, 255, // blue + 0, 0, 0, 0, // transparent black + }; + rgba* img = raw2rgba(raw, 2, 2, 32); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].red, 255); + EXPECT_EQ(img[1].green, 255); + EXPECT_EQ(img[2].blue, 255); + EXPECT_EQ(img[3].alpha, 0); + free(img); +} + +TEST(N64GraphicsTest, RGBA2RawRGBA32) { + rgba img[] = {{0xAA, 0xBB, 0xCC, 0xDD}}; + uint8_t raw[4]; + int result = rgba2raw(raw, img, 1, 1, 32); + EXPECT_EQ(result, 4); + EXPECT_EQ(raw[0], 0xAA); + EXPECT_EQ(raw[1], 0xBB); + EXPECT_EQ(raw[2], 0xCC); + EXPECT_EQ(raw[3], 0xDD); +} + +TEST(N64GraphicsTest, RGBA32RoundTrip) { + uint8_t original[] = {100, 150, 200, 255}; + rgba* img = raw2rgba(original, 1, 1, 32); + ASSERT_NE(img, nullptr); + + uint8_t output[4]; + rgba2raw(output, img, 1, 1, 32); + EXPECT_EQ(output[0], 100); + EXPECT_EQ(output[1], 150); + EXPECT_EQ(output[2], 200); + EXPECT_EQ(output[3], 255); + free(img); +} + +TEST(N64GraphicsTest, RGBA2RawRGBA16) { + // Pure white with alpha + rgba img[] = {{255, 255, 255, 255}}; + uint8_t raw[2]; + int result = rgba2raw(raw, img, 1, 1, 16); + EXPECT_EQ(result, 2); + EXPECT_EQ(raw[0], 0xFF); + EXPECT_EQ(raw[1], 0xFF); +} + +// IA format tests + +TEST(N64GraphicsTest, Raw2IA16SinglePixel) { + // IA16: 8-bit intensity, 8-bit alpha — 2 bytes per pixel + uint8_t raw[] = {0x80, 0xFF}; + ia* img = raw2ia(raw, 1, 1, 16); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].intensity, 0x80); + EXPECT_EQ(img[0].alpha, 0xFF); + free(img); +} + +TEST(N64GraphicsTest, Raw2IA8SinglePixel) { + // IA8: 4-bit intensity, 4-bit alpha — 1 byte per pixel + // 0xF0 = intensity nibble 0xF, alpha nibble 0x0 + // SCALE_4_8(0xF) = 0xF * 0x11 = 0xFF + // SCALE_4_8(0x0) = 0 + uint8_t raw[] = {0xF0}; + ia* img = raw2ia(raw, 1, 1, 8); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].intensity, 0xFF); + EXPECT_EQ(img[0].alpha, 0x00); + free(img); +} + +TEST(N64GraphicsTest, Raw2IA8FullAlpha) { + // 0xFF = intensity 0xF, alpha 0xF + uint8_t raw[] = {0xFF}; + ia* img = raw2ia(raw, 1, 1, 8); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].intensity, 0xFF); + EXPECT_EQ(img[0].alpha, 0xFF); + free(img); +} + +TEST(N64GraphicsTest, IA16RoundTrip) { + uint8_t original[] = {0x80, 0xC0}; + ia* img = raw2ia(original, 1, 1, 16); + ASSERT_NE(img, nullptr); + + uint8_t output[2]; + ia2raw(output, img, 1, 1, 16); + EXPECT_EQ(output[0], 0x80); + EXPECT_EQ(output[1], 0xC0); + free(img); +} + +// I (intensity-only) format tests + +TEST(N64GraphicsTest, Raw2I8SinglePixel) { + // I8: 8-bit intensity, alpha is always 0xFF + uint8_t raw[] = {0x80}; + ia* img = raw2i(raw, 1, 1, 8); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].intensity, 0x80); + EXPECT_EQ(img[0].alpha, 0xFF); + free(img); +} + +TEST(N64GraphicsTest, Raw2I4TwoPixels) { + // I4: 4-bit intensity packed, 2 pixels per byte + // 0xF0 = pixel0=0xF, pixel1=0x0 + // SCALE_4_8(0xF) = 0xFF, SCALE_4_8(0x0) = 0x00 + uint8_t raw[] = {0xF0}; + ia* img = raw2i(raw, 2, 1, 4); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].intensity, 0xFF); + EXPECT_EQ(img[0].alpha, 0xFF); + EXPECT_EQ(img[1].intensity, 0x00); + EXPECT_EQ(img[1].alpha, 0xFF); + free(img); +} + +TEST(N64GraphicsTest, I8RoundTrip) { + uint8_t original[] = {0x40}; + ia* img = raw2i(original, 1, 1, 8); + ASSERT_NE(img, nullptr); + + uint8_t output[1]; + i2raw(output, img, 1, 1, 8); + EXPECT_EQ(output[0], 0x40); + free(img); +} + +// CI (color-indexed) format tests + +TEST(N64GraphicsTest, Raw2CI8SinglePixel) { + // CI8: 1 byte per pixel, index passthrough + uint8_t raw[] = {42}; + ci* img = raw2ci_torch(raw, 1, 1, 8); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].index, 42); + free(img); +} + +TEST(N64GraphicsTest, Raw2CI4TwoPixels) { + // CI4: 4-bit indices packed, 2 pixels per byte + // 0xA3 = pixel0=0xA, pixel1=0x3 + uint8_t raw[] = {0xA3}; + ci* img = raw2ci_torch(raw, 2, 1, 4); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img[0].index, 0x0A); + EXPECT_EQ(img[1].index, 0x03); + free(img); +} + +TEST(N64GraphicsTest, CI8RoundTrip) { + uint8_t original[] = {0, 1, 2, 255}; + ci* img = raw2ci_torch(original, 4, 1, 8); + ASSERT_NE(img, nullptr); + + uint8_t output[4]; + ci2raw_torch(output, img, 4, 1, 8); + EXPECT_EQ(output[0], 0); + EXPECT_EQ(output[1], 1); + EXPECT_EQ(output[2], 2); + EXPECT_EQ(output[3], 255); + free(img); +} + +TEST(N64GraphicsTest, CI4RoundTrip) { + // 0x5A = pixel0=5, pixel1=A + uint8_t original[] = {0x5A}; + ci* img = raw2ci_torch(original, 2, 1, 4); + ASSERT_NE(img, nullptr); + + uint8_t output[1]; + ci2raw_torch(output, img, 2, 1, 4); + EXPECT_EQ(output[0], 0x5A); + free(img); +} diff --git a/tests/NAudioContextTest.cpp b/tests/NAudioContextTest.cpp new file mode 100644 index 00000000..d6aa422f --- /dev/null +++ b/tests/NAudioContextTest.cpp @@ -0,0 +1,30 @@ +#include +#include "factories/naudio/v1/AudioContext.h" +#include + +// GetMediumStr tests +TEST(NAudioContextTest, MediumRam) { EXPECT_STREQ(AudioContext::GetMediumStr(0), "Ram"); } +TEST(NAudioContextTest, MediumUnk) { EXPECT_STREQ(AudioContext::GetMediumStr(1), "Unk"); } +TEST(NAudioContextTest, MediumCart) { EXPECT_STREQ(AudioContext::GetMediumStr(2), "Cart"); } +TEST(NAudioContextTest, MediumDisk) { EXPECT_STREQ(AudioContext::GetMediumStr(3), "Disk"); } +TEST(NAudioContextTest, MediumGap) { EXPECT_STREQ(AudioContext::GetMediumStr(4), "ERROR"); } +TEST(NAudioContextTest, MediumRamUnloaded) { EXPECT_STREQ(AudioContext::GetMediumStr(5), "RamUnloaded"); } +TEST(NAudioContextTest, MediumOutOfRange) { EXPECT_STREQ(AudioContext::GetMediumStr(255), "ERROR"); } + +// GetCachePolicyStr tests +TEST(NAudioContextTest, CacheTemporary) { EXPECT_STREQ(AudioContext::GetCachePolicyStr(0), "Temporary"); } +TEST(NAudioContextTest, CachePersistent) { EXPECT_STREQ(AudioContext::GetCachePolicyStr(1), "Persistent"); } +TEST(NAudioContextTest, CacheEither) { EXPECT_STREQ(AudioContext::GetCachePolicyStr(2), "Either"); } +TEST(NAudioContextTest, CachePermanent) { EXPECT_STREQ(AudioContext::GetCachePolicyStr(3), "Permanent"); } +TEST(NAudioContextTest, CacheError) { EXPECT_STREQ(AudioContext::GetCachePolicyStr(4), "ERROR"); } + +// GetCodecStr tests +TEST(NAudioContextTest, CodecADPCM) { EXPECT_STREQ(AudioContext::GetCodecStr(0), "ADPCM"); } +TEST(NAudioContextTest, CodecS8) { EXPECT_STREQ(AudioContext::GetCodecStr(1), "S8"); } +TEST(NAudioContextTest, CodecS16MEM) { EXPECT_STREQ(AudioContext::GetCodecStr(2), "S16MEM"); } +TEST(NAudioContextTest, CodecADPCMSMALL) { EXPECT_STREQ(AudioContext::GetCodecStr(3), "ADPCMSMALL"); } +TEST(NAudioContextTest, CodecREVERB) { EXPECT_STREQ(AudioContext::GetCodecStr(4), "REVERB"); } +TEST(NAudioContextTest, CodecS16) { EXPECT_STREQ(AudioContext::GetCodecStr(5), "S16"); } +TEST(NAudioContextTest, CodecUNK6) { EXPECT_STREQ(AudioContext::GetCodecStr(6), "UNK6"); } +TEST(NAudioContextTest, CodecUNK7) { EXPECT_STREQ(AudioContext::GetCodecStr(7), "UNK7"); } +TEST(NAudioContextTest, CodecError) { EXPECT_STREQ(AudioContext::GetCodecStr(8), "ERROR"); } diff --git a/tests/NAudioDataTest.cpp b/tests/NAudioDataTest.cpp new file mode 100644 index 00000000..80be2936 --- /dev/null +++ b/tests/NAudioDataTest.cpp @@ -0,0 +1,135 @@ +#include +#include "factories/naudio/v1/AudioTableFactory.h" +#include "factories/naudio/v1/EnvelopeFactory.h" +#include "factories/naudio/v1/AudioContext.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +// AudioTableEntry +TEST(NAudioDataTest, AudioTableEntryConstruction) { + AudioTableEntry entry = {0x1000, 0x2000, 2, 1, 100, 200, 300, 0xDEADBEEF}; + EXPECT_EQ(entry.addr, 0x1000u); + EXPECT_EQ(entry.size, 0x2000u); + EXPECT_EQ(entry.medium, 2); + EXPECT_EQ(entry.cachePolicy, 1); + EXPECT_EQ(entry.shortData1, 100); + EXPECT_EQ(entry.shortData2, 200); + EXPECT_EQ(entry.shortData3, 300); + EXPECT_EQ(entry.crc, 0xDEADBEEFu); +} + +// AudioTableData +TEST(NAudioDataTest, AudioTableDataConstruction) { + std::vector entries = { + {0x1000, 0x100, 0, 0, 0, 0, 0, 0}, + {0x2000, 0x200, 2, 1, 0, 0, 0, 0}, + }; + AudioTableData data(3, 0x5000, AudioTableType::FONT_TABLE, entries); + EXPECT_EQ(data.medium, 3); + EXPECT_EQ(data.addr, 0x5000u); + EXPECT_EQ(data.type, AudioTableType::FONT_TABLE); + ASSERT_EQ(data.entries.size(), 2u); + EXPECT_EQ(data.entries[0].addr, 0x1000u); + EXPECT_EQ(data.entries[1].size, 0x200u); +} + +// AudioTableType enum +TEST(NAudioDataTest, AudioTableTypeValues) { + // Just verify the enum values exist and are distinct + EXPECT_NE(AudioTableType::SAMPLE_TABLE, AudioTableType::SEQ_TABLE); + EXPECT_NE(AudioTableType::SEQ_TABLE, AudioTableType::FONT_TABLE); + EXPECT_NE(AudioTableType::SAMPLE_TABLE, AudioTableType::FONT_TABLE); +} + +// EnvelopePoint +TEST(NAudioDataTest, EnvelopePointConstruction) { + EnvelopePoint pt = {100, -50}; + EXPECT_EQ(pt.delay, 100); + EXPECT_EQ(pt.arg, -50); +} + +// EnvelopeData +TEST(NAudioDataTest, EnvelopeDataConstruction) { + std::vector points = {{10, 127}, {20, 0}, {-1, 0}}; + EnvelopeData data(points); + ASSERT_EQ(data.points.size(), 3u); + EXPECT_EQ(data.points[0].delay, 10); + EXPECT_EQ(data.points[0].arg, 127); + EXPECT_EQ(data.points[2].delay, -1); +} + +// TunedSample +TEST(NAudioDataTest, TunedSampleConstruction) { + TunedSample ts = {0x1234, 0, 1.0f}; + EXPECT_EQ(ts.sample, 0x1234u); + EXPECT_EQ(ts.sampleBankId, 0u); + EXPECT_FLOAT_EQ(ts.tuning, 1.0f); +} + +// BinaryReader parse test — AudioTable on-disk format +// Header: i16 count + i16 medium + u32 addr + 8 pad bytes = 16 bytes +// Per-entry: u32 addr + u32 size + i8 medium + i8 policy + i16 sd1 + i16 sd2 + i16 sd3 = 16 bytes +TEST(NAudioDataTest, ManualAudioTableParse) { + std::vector buf = { + // Header + 0x00, 0x02, // count = 2 + 0x00, 0x03, // medium = 3 + 0x00, 0x00, 0x50, 0x00, // addr = 0x5000 + 0x00, 0x00, 0x00, 0x00, // pad + 0x00, 0x00, 0x00, 0x00, // pad + // Entry 0: addr=0x1000, size=0x100, medium=0, policy=0, sd1=0, sd2=0, sd3=0 + 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Entry 1: addr=0x2000, size=0x200, medium=2, policy=1, sd1=10, sd2=20, sd3=30 + 0x00, 0x00, 0x20, 0x00, + 0x00, 0x00, 0x02, 0x00, + 0x02, 0x01, + 0x00, 0x0A, 0x00, 0x14, 0x00, 0x1E, + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + // Read header + auto count = reader.ReadInt16(); + auto medium = reader.ReadInt16(); + auto addr = reader.ReadUInt32(); + reader.Seek(16, LUS::SeekOffsetType::Start); // skip pad to offset 16 + + EXPECT_EQ(count, 2); + EXPECT_EQ(medium, 3); + EXPECT_EQ(addr, 0x5000u); + + // Read entries + std::vector entries; + for (int i = 0; i < count; i++) { + auto eAddr = reader.ReadUInt32(); + auto eSize = reader.ReadUInt32(); + auto eMedium = reader.ReadInt8(); + auto ePolicy = reader.ReadInt8(); + auto eSd1 = reader.ReadInt16(); + auto eSd2 = reader.ReadInt16(); + auto eSd3 = reader.ReadInt16(); + entries.push_back(AudioTableEntry{ eAddr, eSize, eMedium, ePolicy, eSd1, eSd2, eSd3, 0 }); + } + + ASSERT_EQ(entries.size(), 2u); + EXPECT_EQ(entries[0].addr, 0x1000u); + EXPECT_EQ(entries[0].size, 0x100u); + EXPECT_EQ(entries[1].addr, 0x2000u); + EXPECT_EQ(entries[1].size, 0x200u); + EXPECT_EQ(entries[1].medium, 2); + EXPECT_EQ(entries[1].cachePolicy, 1); + EXPECT_EQ(entries[1].shortData1, 10); + EXPECT_EQ(entries[1].shortData2, 20); + EXPECT_EQ(entries[1].shortData3, 30); +} + +// NAudioDrivers enum +TEST(NAudioDataTest, NAudioDriversValues) { + EXPECT_NE(NAudioDrivers::SF64, NAudioDrivers::FZEROX); + EXPECT_NE(NAudioDrivers::FZEROX, NAudioDrivers::UNKNOWN); +} diff --git a/tests/PM64DataTest.cpp b/tests/PM64DataTest.cpp new file mode 100644 index 00000000..74a81ddb --- /dev/null +++ b/tests/PM64DataTest.cpp @@ -0,0 +1,66 @@ +#include +#include "factories/pm64/ShapeFactory.h" +#include "factories/pm64/EntityGfxFactory.h" +#include + +// PM64DisplayListInfo +TEST(PM64DataTest, DisplayListInfoConstruction) { + PM64DisplayListInfo dlInfo = {0x1000, {0xDE000000, 0x06001000, 0xDF000000, 0x00000000}}; + EXPECT_EQ(dlInfo.offset, 0x1000u); + ASSERT_EQ(dlInfo.commands.size(), 4u); + EXPECT_EQ(dlInfo.commands[0], 0xDE000000u); +} + +// PM64ShapeData +TEST(PM64DataTest, ShapeDataConstruction) { + std::vector buffer = {0x00, 0x01, 0x02, 0x03}; + std::vector dls = { + {0x100, {0xDE000000, 0x00000000}}, + }; + PM64ShapeData shape(std::move(buffer), std::move(dls), 0x200, 0x80); + + ASSERT_EQ(shape.mBuffer.size(), 4u); + EXPECT_EQ(shape.mBuffer[0], 0x00); + ASSERT_EQ(shape.mDisplayLists.size(), 1u); + EXPECT_EQ(shape.mDisplayLists[0].offset, 0x100u); + EXPECT_EQ(shape.mVertexTableOffset, 0x200u); + EXPECT_EQ(shape.mVertexDataSize, 0x80u); +} + +TEST(PM64DataTest, ShapeFactoryExporters) { + PM64ShapeFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +// PM64EntityDisplayListInfo +TEST(PM64DataTest, EntityDisplayListInfoConstruction) { + PM64EntityDisplayListInfo dlInfo = {0x500, {0xE7000000, 0x00000000}}; + EXPECT_EQ(dlInfo.offset, 0x500u); + ASSERT_EQ(dlInfo.commands.size(), 2u); +} + +// PM64EntityGfxData +TEST(PM64DataTest, EntityGfxDataConstruction) { + std::vector buffer(64, 0xAA); + std::vector dls = { + {0, {0xDE000000, 0x06001000}}, + {0x40, {0xDF000000, 0x00000000}}, + }; + PM64EntityGfxData gfx(std::move(buffer), std::move(dls)); + + ASSERT_EQ(gfx.mBuffer.size(), 64u); + ASSERT_EQ(gfx.mDisplayLists.size(), 2u); + EXPECT_EQ(gfx.mDisplayLists[1].offset, 0x40u); + EXPECT_TRUE(gfx.mStandaloneMtx.empty()); +} + +TEST(PM64DataTest, EntityGfxFactoryExporters) { + PM64EntityGfxFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} diff --git a/tests/SF64DataTest.cpp b/tests/SF64DataTest.cpp new file mode 100644 index 00000000..ff424d3e --- /dev/null +++ b/tests/SF64DataTest.cpp @@ -0,0 +1,147 @@ +#include +#include "factories/sf64/SkeletonFactory.h" +#include "factories/sf64/MessageFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +// SF64 LimbData +TEST(SF64DataTest, LimbDataConstruction) { + SF64::LimbData limb(0x06001000, 0x06002000, Vec3f(1.0f, 2.0f, 3.0f), Vec3s(10, 20, 30), 0x06003000, 0x06004000, 5); + EXPECT_EQ(limb.mAddr, 0x06001000u); + EXPECT_EQ(limb.mDList, 0x06002000u); + EXPECT_FLOAT_EQ(limb.mTrans.x, 1.0f); + EXPECT_FLOAT_EQ(limb.mTrans.y, 2.0f); + EXPECT_FLOAT_EQ(limb.mTrans.z, 3.0f); + EXPECT_EQ(limb.mRot.x, 10); + EXPECT_EQ(limb.mRot.y, 20); + EXPECT_EQ(limb.mRot.z, 30); + EXPECT_EQ(limb.mSibling, 0x06003000u); + EXPECT_EQ(limb.mChild, 0x06004000u); + EXPECT_EQ(limb.mIndex, 5); +} + +// SF64 SkeletonData +TEST(SF64DataTest, SkeletonDataConstruction) { + std::vector limbs; + limbs.emplace_back(0x1000, 0x2000, Vec3f(0, 0, 0), Vec3s(0, 0, 0), 0, 0x1001, 0); + limbs.emplace_back(0x1001, 0x2001, Vec3f(10.0f, 0, 0), Vec3s(0, 0, 0), 0, 0, 1); + + SF64::SkeletonData skel(limbs); + ASSERT_EQ(skel.mSkeleton.size(), 2u); + EXPECT_EQ(skel.mSkeleton[0].mAddr, 0x1000u); + EXPECT_EQ(skel.mSkeleton[1].mIndex, 1); + EXPECT_FLOAT_EQ(skel.mSkeleton[1].mTrans.x, 10.0f); +} + +TEST(SF64DataTest, SkeletonDataEmpty) { + std::vector limbs; + SF64::SkeletonData skel(limbs); + EXPECT_TRUE(skel.mSkeleton.empty()); +} + +TEST(SF64DataTest, SkeletonFactoryExporters) { + SF64::SkeletonFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); +} + +// SF64 MessageData +TEST(SF64DataTest, MessageDataConstruction) { + std::vector opcodes = {0x20, 0x21, 0x22, 0x01, 0x00}; // "ABC\nEND" + std::string mesgStr = "ABC\n"; + SF64::MessageData msg(opcodes, mesgStr); + ASSERT_EQ(msg.mMessage.size(), 5u); + EXPECT_EQ(msg.mMessage[0], 0x20); + EXPECT_EQ(msg.mMessage[4], 0x00); // END_CODE + EXPECT_EQ(msg.mMesgStr, "ABC\n"); +} + +TEST(SF64DataTest, MessageDataEmpty) { + std::vector opcodes = {0x00}; // just END + SF64::MessageData msg(opcodes, ""); + ASSERT_EQ(msg.mMessage.size(), 1u); + EXPECT_TRUE(msg.mMesgStr.empty()); +} + +TEST(SF64DataTest, MessageFactoryExporters) { + SF64::MessageFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Modding).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::XML).has_value()); +} + +// BinaryReader parse test — SF64 limb data format (0x20 = 32 bytes per limb) +// u32 dList + 3×float trans + 3×int16 rot + i16 pad + u32 sibling + u32 child +TEST(SF64DataTest, ManualLimbParse) { + std::vector buf = { + 0x06, 0x00, 0x20, 0x00, // dList = 0x06002000 + 0x3F, 0x80, 0x00, 0x00, // trans.x = 1.0f + 0x40, 0x00, 0x00, 0x00, // trans.y = 2.0f + 0x40, 0x40, 0x00, 0x00, // trans.z = 3.0f + 0x00, 0x0A, // rot.x = 10 + 0x00, 0x14, // rot.y = 20 + 0x00, 0x1E, // rot.z = 30 + 0x00, 0x00, // pad (rw) + 0x06, 0x00, 0x30, 0x00, // sibling = 0x06003000 + 0x06, 0x00, 0x40, 0x00, // child = 0x06004000 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto dList = reader.ReadUInt32(); + auto tx = reader.ReadFloat(); auto ty = reader.ReadFloat(); auto tz = reader.ReadFloat(); + Vec3f trans(tx, ty, tz); + auto rx = reader.ReadInt16(); auto ry = reader.ReadInt16(); auto rz = reader.ReadInt16(); + Vec3s rot(rx, ry, rz); + reader.ReadInt16(); // pad + auto sibling = reader.ReadUInt32(); + auto child = reader.ReadUInt32(); + + SF64::LimbData limb(0x06001000, dList, trans, rot, sibling, child, 0); + + EXPECT_EQ(limb.mDList, 0x06002000u); + EXPECT_FLOAT_EQ(limb.mTrans.x, 1.0f); + EXPECT_FLOAT_EQ(limb.mTrans.y, 2.0f); + EXPECT_FLOAT_EQ(limb.mTrans.z, 3.0f); + EXPECT_EQ(limb.mRot.x, 10); + EXPECT_EQ(limb.mRot.y, 20); + EXPECT_EQ(limb.mRot.z, 30); + EXPECT_EQ(limb.mSibling, 0x06003000u); + EXPECT_EQ(limb.mChild, 0x06004000u); +} + +// BinaryReader parse test — SF64 message format (uint16_t codes until END_CODE=0) +TEST(SF64DataTest, ManualMessageParse) { + std::vector buf = { + 0x00, 0x20, // code 0x20 ('A') + 0x00, 0x21, // code 0x21 ('B') + 0x00, 0x01, // NEWLINE_CODE + 0x00, 0x00, // END_CODE + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector message; + uint16_t c; + do { + c = reader.ReadUInt16(); + message.push_back(c); + } while (c != 0x0000); + + ASSERT_EQ(message.size(), 4u); + EXPECT_EQ(message[0], 0x20); // 'A' + EXPECT_EQ(message[1], 0x21); // 'B' + EXPECT_EQ(message[2], 0x01); // NEWLINE + EXPECT_EQ(message[3], 0x00); // END +} + +TEST(SF64DataTest, MessageFactorySupportsModding) { + SF64::MessageFactory factory; + EXPECT_TRUE(factory.SupportModdedAssets()); +} diff --git a/tests/SM64AnimationTest.cpp b/tests/SM64AnimationTest.cpp new file mode 100644 index 00000000..d8fc609d --- /dev/null +++ b/tests/SM64AnimationTest.cpp @@ -0,0 +1,45 @@ +#include +#include "factories/sm64/AnimationFactory.h" +#include + +TEST(SM64AnimationTest, AnimationDataConstruction) { + std::vector indices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + std::vector entries = {100, 200, 300, -100, -200, -300}; + + SM64::AnimationData anim(0, 1, 0, 10, 50, 1, 60, indices, entries); + EXPECT_EQ(anim.mFlags, 0); + EXPECT_EQ(anim.mAnimYTransDivisor, 1); + EXPECT_EQ(anim.mStartFrame, 0); + EXPECT_EQ(anim.mLoopStart, 10); + EXPECT_EQ(anim.mLoopEnd, 50); + EXPECT_EQ(anim.mUnusedBoneCount, 1); + EXPECT_EQ(anim.mLength, 60); + ASSERT_EQ(anim.mIndices.size(), 12u); + ASSERT_EQ(anim.mEntries.size(), 6u); + EXPECT_EQ(anim.mEntries[3], -100); +} + +TEST(SM64AnimationTest, AnimindexCountMacro) { + // ANIMINDEX_COUNT(boneCount) = (boneCount + 1) * 6 + EXPECT_EQ(ANIMINDEX_COUNT(0), 6); + EXPECT_EQ(ANIMINDEX_COUNT(1), 12); + EXPECT_EQ(ANIMINDEX_COUNT(15), 96); + EXPECT_EQ(ANIMINDEX_COUNT(20), 126); +} + +TEST(SM64AnimationTest, AnimationFactoryExporters) { + SM64::AnimationFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_FALSE(factory.GetExporter(ExportType::Modding).has_value()); +} + +TEST(SM64AnimationTest, AnimationDataEmptyVectors) { + std::vector indices; + std::vector entries; + SM64::AnimationData anim(1, 0, 0, 0, 0, 0, 0, indices, entries); + EXPECT_EQ(anim.mFlags, 1); + EXPECT_TRUE(anim.mIndices.empty()); + EXPECT_TRUE(anim.mEntries.empty()); +} diff --git a/tests/SM64BehaviorScriptTest.cpp b/tests/SM64BehaviorScriptTest.cpp new file mode 100644 index 00000000..9029b916 --- /dev/null +++ b/tests/SM64BehaviorScriptTest.cpp @@ -0,0 +1,108 @@ +#include +#include "factories/sm64/BehaviorScriptFactory.h" +#include "lib/binarytools/endianness.h" +#include +#include + +using namespace SM64; + +TEST(SM64BehaviorScriptTest, BehaviorCommandConstruction) { + std::vector args; + BehaviorCommand cmd(BehaviorOpcode::BEGIN, args); + EXPECT_EQ(cmd.opcode, BehaviorOpcode::BEGIN); + EXPECT_TRUE(cmd.arguments.empty()); +} + +TEST(SM64BehaviorScriptTest, BehaviorCommandWithArguments) { + std::vector args; + args.push_back((uint8_t)0x31); // object list + args.push_back((uint32_t)0x13000000); // behavior param + BehaviorCommand cmd(BehaviorOpcode::BEGIN, args); + ASSERT_EQ(cmd.arguments.size(), 2u); + EXPECT_EQ(std::get(cmd.arguments[0]), 0x31); + EXPECT_EQ(std::get(cmd.arguments[1]), 0x13000000u); +} + +TEST(SM64BehaviorScriptTest, BehaviorScriptDataConstruction) { + std::vector cmds; + cmds.emplace_back(BehaviorOpcode::BEGIN, std::vector{(uint8_t)0x00}); + cmds.emplace_back(BehaviorOpcode::SET_INT, std::vector{(uint8_t)0x00, (int16_t)100}); + cmds.emplace_back(BehaviorOpcode::DEACTIVATE, std::vector{}); + + BehaviorScriptData data(cmds); + ASSERT_EQ(data.mCommands.size(), 3u); + EXPECT_EQ(data.mCommands[0].opcode, BehaviorOpcode::BEGIN); + EXPECT_EQ(data.mCommands[1].opcode, BehaviorOpcode::SET_INT); + EXPECT_EQ(data.mCommands[2].opcode, BehaviorOpcode::DEACTIVATE); +} + +TEST(SM64BehaviorScriptTest, HasExpectedExporters) { + BehaviorScriptFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +TEST(SM64BehaviorScriptTest, BehaviorArgumentVariantTypes) { + BehaviorArgument u8_arg = (uint8_t)42; + BehaviorArgument s8_arg = (int8_t)-10; + BehaviorArgument u16_arg = (uint16_t)1000; + BehaviorArgument s16_arg = (int16_t)-500; + BehaviorArgument u32_arg = (uint32_t)0xCAFEBABE; + BehaviorArgument s32_arg = (int32_t)-100000; + BehaviorArgument f32_arg = 3.14f; + BehaviorArgument ptr_arg = (uint64_t)0x8033B000; + + EXPECT_EQ(std::get(u8_arg), 42); + EXPECT_EQ(std::get(s8_arg), -10); + EXPECT_EQ(std::get(u16_arg), 1000); + EXPECT_EQ(std::get(s16_arg), -500); + EXPECT_EQ(std::get(u32_arg), 0xCAFEBABEu); + EXPECT_EQ(std::get(s32_arg), -100000); + EXPECT_FLOAT_EQ(std::get(f32_arg), 3.14f); + EXPECT_EQ(std::get(ptr_arg), 0x8033B000u); +} + +// Byte-level command parsing tests — verify binary format using the same +// BSWAP logic that cur_behavior_cmd_* macros use in BehaviorScriptFactory::parse + +TEST(SM64BehaviorScriptTest, ByteLevelBeginCommand) { + // BEGIN (opcode 0x00): 4 bytes + // [0x00, objList, 0x00, 0x00] + uint8_t cmd[] = { 0x00, 0x31, 0x00, 0x00 }; + + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, BehaviorOpcode::BEGIN); + + // cur_behavior_cmd_u8(0x01) reads object list + uint8_t objList = cmd[0x01]; + EXPECT_EQ(objList, 0x31); +} + +TEST(SM64BehaviorScriptTest, ByteLevelSetIntCommand) { + // SET_INT (BehaviorOpcode::SET_INT = 16 = 0x10): 4 bytes + // [0x10, field, value_hi, value_lo] + uint8_t cmd[] = { 0x10, 0x1C, 0x00, 0x64 }; + + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, BehaviorOpcode::SET_INT); + + uint8_t field = cmd[0x01]; + int16_t value = (int16_t)BSWAP16(*(int16_t*)&cmd[0x02]); + EXPECT_EQ(field, 0x1C); + EXPECT_EQ(value, 100); +} + +TEST(SM64BehaviorScriptTest, ByteLevelCallCommand) { + // CALL (opcode 0x02): 8 bytes + // [0x02, 0x00, 0x00, 0x00, addr3, addr2, addr1, addr0] + uint8_t cmd[] = { 0x02, 0x00, 0x00, 0x00, 0x80, 0x33, 0xB0, 0x00 }; + + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, BehaviorOpcode::CALL); + + // cur_behavior_cmd_u32(0x04) + uint32_t addr = BSWAP32(*(uint32_t*)&cmd[0x04]); + EXPECT_EQ(addr, 0x8033B000u); +} diff --git a/tests/SM64CollisionFactoryTest.cpp b/tests/SM64CollisionFactoryTest.cpp new file mode 100644 index 00000000..8c674596 --- /dev/null +++ b/tests/SM64CollisionFactoryTest.cpp @@ -0,0 +1,88 @@ +#include +#include "factories/sm64/CollisionFactory.h" +#include + +using namespace SM64; + +TEST(SM64CollisionFactoryTest, CollisionVertexConstruction) { + CollisionVertex v(100, -200, 300); + EXPECT_EQ(v.x, 100); + EXPECT_EQ(v.y, -200); + EXPECT_EQ(v.z, 300); +} + +TEST(SM64CollisionFactoryTest, CollisionTriConstruction) { + CollisionTri tri(0, 1, 2, 50); + EXPECT_EQ(tri.x, 0); + EXPECT_EQ(tri.y, 1); + EXPECT_EQ(tri.z, 2); + EXPECT_EQ(tri.force, 50); +} + +TEST(SM64CollisionFactoryTest, CollisionTriNoForce) { + CollisionTri tri(3, 4, 5, 0); + EXPECT_EQ(tri.force, 0); +} + +TEST(SM64CollisionFactoryTest, CollisionSurfaceConstruction) { + std::vector tris = { + {0, 1, 2, 0}, + {2, 1, 3, 0}, + }; + CollisionSurface surface(SurfaceType::SURFACE_DEFAULT, tris); + EXPECT_EQ(surface.surfaceType, SurfaceType::SURFACE_DEFAULT); + ASSERT_EQ(surface.tris.size(), 2u); + EXPECT_EQ(surface.tris[0].x, 0); + EXPECT_EQ(surface.tris[1].z, 3); +} + +TEST(SM64CollisionFactoryTest, SpecialObjectConstruction) { + std::vector params = {90}; + SpecialObject obj(SpecialPresets::special_yellow_coin, 100, 200, 300, params); + EXPECT_EQ(obj.presetId, SpecialPresets::special_yellow_coin); + EXPECT_EQ(obj.x, 100); + EXPECT_EQ(obj.y, 200); + EXPECT_EQ(obj.z, 300); + ASSERT_EQ(obj.extraParams.size(), 1u); + EXPECT_EQ(obj.extraParams[0], 90); +} + +TEST(SM64CollisionFactoryTest, SpecialObjectNoExtraParams) { + std::vector params; + SpecialObject obj(SpecialPresets::special_null_start, 0, 0, 0, params); + EXPECT_TRUE(obj.extraParams.empty()); +} + +TEST(SM64CollisionFactoryTest, EnvRegionBoxConstruction) { + EnvRegionBox box(1, -1000, -2000, 1000, 2000, 500); + EXPECT_EQ(box.id, 1); + EXPECT_EQ(box.x1, -1000); + EXPECT_EQ(box.z1, -2000); + EXPECT_EQ(box.x2, 1000); + EXPECT_EQ(box.z2, 2000); + EXPECT_EQ(box.height, 500); +} + +TEST(SM64CollisionFactoryTest, CollisionFullConstruction) { + std::vector verts = {{0, 0, 0}, {100, 0, 0}, {0, 100, 0}}; + std::vector tris = {{0, 1, 2, 0}}; + std::vector surfaces = {{SurfaceType::SURFACE_DEFAULT, tris}}; + std::vector specials; + std::vector boxes; + + Collision col(verts, surfaces, specials, boxes); + ASSERT_EQ(col.mVertices.size(), 3u); + ASSERT_EQ(col.mSurfaces.size(), 1u); + EXPECT_TRUE(col.mSpecialObjects.empty()); + EXPECT_TRUE(col.mEnvRegionBoxes.empty()); + EXPECT_EQ(col.mVertices[1].x, 100); + EXPECT_EQ(col.mSurfaces[0].surfaceType, SurfaceType::SURFACE_DEFAULT); +} + +TEST(SM64CollisionFactoryTest, HasExpectedExporters) { + CollisionFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} diff --git a/tests/SM64GeoLayoutTest.cpp b/tests/SM64GeoLayoutTest.cpp new file mode 100644 index 00000000..cc8b0718 --- /dev/null +++ b/tests/SM64GeoLayoutTest.cpp @@ -0,0 +1,138 @@ +#include +#include "factories/sm64/GeoLayoutFactory.h" +#include "lib/binarytools/endianness.h" +#include +#include + +using namespace SM64; + +TEST(SM64GeoLayoutTest, GeoCommandConstruction) { + GeoCommand cmd; + cmd.opcode = GeoOpcode::End; + cmd.skipped = false; + EXPECT_EQ(cmd.opcode, GeoOpcode::End); + EXPECT_FALSE(cmd.skipped); + EXPECT_TRUE(cmd.arguments.empty()); +} + +TEST(SM64GeoLayoutTest, GeoCommandWithArguments) { + GeoCommand cmd; + cmd.opcode = GeoOpcode::NodeTranslation; + cmd.skipped = false; + cmd.arguments.push_back((uint8_t)0x01); // drawingLayer + cmd.arguments.push_back(Vec3s(100, 200, 300)); // translation + ASSERT_EQ(cmd.arguments.size(), 2u); + EXPECT_EQ(std::get(cmd.arguments[0]), 0x01); + auto v = std::get(cmd.arguments[1]); + EXPECT_EQ(v.x, 100); + EXPECT_EQ(v.y, 200); + EXPECT_EQ(v.z, 300); +} + +TEST(SM64GeoLayoutTest, GeoLayoutConstruction) { + std::vector cmds; + GeoCommand open; + open.opcode = GeoOpcode::OpenNode; + open.skipped = false; + cmds.push_back(open); + + GeoCommand close; + close.opcode = GeoOpcode::CloseNode; + close.skipped = false; + cmds.push_back(close); + + GeoLayout layout(cmds); + ASSERT_EQ(layout.commands.size(), 2u); + EXPECT_EQ(layout.commands[0].opcode, GeoOpcode::OpenNode); + EXPECT_EQ(layout.commands[1].opcode, GeoOpcode::CloseNode); +} + +TEST(SM64GeoLayoutTest, GeoLayoutSkippedCommand) { + GeoCommand cmd; + cmd.opcode = GeoOpcode::NOP; + cmd.skipped = true; + std::vector cmds = {cmd}; + + GeoLayout layout(cmds); + EXPECT_TRUE(layout.commands[0].skipped); +} + +TEST(SM64GeoLayoutTest, HasExpectedExporters) { + GeoLayoutFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +// Test GeoArgument variant with different types +TEST(SM64GeoLayoutTest, GeoArgumentVariantTypes) { + GeoArgument u8_arg = (uint8_t)42; + GeoArgument s16_arg = (int16_t)-100; + GeoArgument u32_arg = (uint32_t)0xDEADBEEF; + GeoArgument vec3f_arg = Vec3f(1.0f, 2.0f, 3.0f); + GeoArgument str_arg = std::string("test_symbol"); + + EXPECT_EQ(std::get(u8_arg), 42); + EXPECT_EQ(std::get(s16_arg), -100); + EXPECT_EQ(std::get(u32_arg), 0xDEADBEEFu); + EXPECT_FLOAT_EQ(std::get(vec3f_arg).x, 1.0f); + EXPECT_EQ(std::get(str_arg), "test_symbol"); +} + +// Byte-level command parsing tests — verify binary format using the same +// BSWAP logic that cur_geo_cmd_* macros use in GeoLayoutFactory::parse + +TEST(SM64GeoLayoutTest, ByteLevelEndCommand) { + // End command: GeoOpcode::End = 1 + uint8_t cmd[] = { 0x01, 0x00, 0x00, 0x00 }; + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, GeoOpcode::End); +} + +TEST(SM64GeoLayoutTest, ByteLevelOpenCloseNode) { + // OpenNode: opcode 0x04, CloseNode: opcode 0x05 + uint8_t open[] = { 0x04, 0x00, 0x00, 0x00 }; + uint8_t close[] = { 0x05, 0x00, 0x00, 0x00 }; + EXPECT_EQ(static_cast(open[0]), GeoOpcode::OpenNode); + EXPECT_EQ(static_cast(close[0]), GeoOpcode::CloseNode); +} + +TEST(SM64GeoLayoutTest, ByteLevelNodeTranslation) { + // NodeTranslation (GeoOpcode::NodeTranslation = 17 = 0x11): 8 bytes + // [0x11, drawLayer, x_hi, x_lo, y_hi, y_lo, z_hi, z_lo] + // CMD_SIZE_SHIFT is 0, so offsets are direct byte offsets + uint8_t cmd[] = { + 0x11, // opcode (NodeTranslation = 17) + 0x01, // drawingLayer + 0x00, 0x64, // x = 100 (big-endian) + 0xFF, 0x9C, // y = -100 (big-endian) + 0x01, 0xF4, // z = 500 (big-endian) + }; + + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, GeoOpcode::NodeTranslation); + + // Read drawingLayer at offset 1 (same as cur_geo_cmd_u8(0x01)) + uint8_t drawingLayer = cmd[0x01]; + EXPECT_EQ(drawingLayer, 0x01); + + // Read int16_t values using BSWAP16 (same as cur_geo_cmd_s16) + int16_t x = (int16_t)BSWAP16(*(int16_t*)&cmd[0x02]); + int16_t y = (int16_t)BSWAP16(*(int16_t*)&cmd[0x04]); + int16_t z = (int16_t)BSWAP16(*(int16_t*)&cmd[0x06]); + EXPECT_EQ(x, 100); + EXPECT_EQ(y, -100); + EXPECT_EQ(z, 500); +} + +TEST(SM64GeoLayoutTest, ByteLevelAssignAsView) { + // AssignAsView (GeoOpcode::AssignAsView = 6): 4 bytes + // [0x06, 0x00, idx_hi, idx_lo] + uint8_t cmd[] = { 0x06, 0x00, 0x00, 0x0A }; + auto opcode = static_cast(cmd[0]); + EXPECT_EQ(opcode, GeoOpcode::AssignAsView); + + int16_t idx = (int16_t)BSWAP16(*(int16_t*)&cmd[0x02]); + EXPECT_EQ(idx, 10); +} diff --git a/tests/SmokeTest.cpp b/tests/SmokeTest.cpp new file mode 100644 index 00000000..3481596c --- /dev/null +++ b/tests/SmokeTest.cpp @@ -0,0 +1,9 @@ +#include + +TEST(SmokeTest, TrueIsTrue) { + EXPECT_TRUE(true); +} + +TEST(SmokeTest, FalseIsFalse) { + EXPECT_FALSE(false); +} diff --git a/tests/StrHash64Test.cpp b/tests/StrHash64Test.cpp new file mode 100644 index 00000000..5d24c5df --- /dev/null +++ b/tests/StrHash64Test.cpp @@ -0,0 +1,57 @@ +#include +#include "strhash64/StrHash64.h" +#include + +TEST(StrHash64Test, CRC64EmptyString) { + // CRC64("") with no bytes processed returns INITIAL_CRC64 + EXPECT_EQ(CRC64(""), INITIAL_CRC64); +} + +TEST(StrHash64Test, CRC64Deterministic) { + uint64_t hash = CRC64("hello"); + EXPECT_EQ(CRC64("hello"), hash); + EXPECT_NE(hash, 0u); + EXPECT_NE(hash, INITIAL_CRC64); +} + +TEST(StrHash64Test, CRC64CaseSensitive) { + EXPECT_NE(CRC64("hello"), CRC64("Hello")); +} + +TEST(StrHash64Test, CRC64DifferentStrings) { + EXPECT_NE(CRC64("abc"), CRC64("def")); + EXPECT_NE(CRC64("test"), CRC64("tset")); +} + +TEST(StrHash64Test, CRC64SingleChar) { + uint64_t a = CRC64("a"); + uint64_t b = CRC64("b"); + EXPECT_NE(a, b); + EXPECT_NE(a, 0u); +} + +// CRC64 does NOT apply final ~crc, but crc64/update_crc64 DO. +// They are intentionally different functions. + +TEST(StrHash64Test, Crc64BufferDeterministic) { + const char* str = "hello"; + uint64_t hash = crc64(str, strlen(str)); + EXPECT_EQ(crc64(str, strlen(str)), hash); + EXPECT_NE(hash, 0u); +} + +TEST(StrHash64Test, Crc64DifferentFromCRC64) { + // crc64() applies ~crc at the end, CRC64() does not + const char* str = "hello"; + uint64_t hash1 = CRC64(str); + uint64_t hash2 = crc64(str, strlen(str)); + // They should be bitwise complements of each other + EXPECT_EQ(hash1, ~hash2); +} + +TEST(StrHash64Test, Crc64EmptyBuffer) { + // crc64 with len=0 still applies ~INITIAL_CRC64 = 0 + uint64_t hash = crc64("", 0); + EXPECT_EQ(hash, ~INITIAL_CRC64); + EXPECT_EQ(hash, 0u); +} diff --git a/tests/StringHelperTest.cpp b/tests/StringHelperTest.cpp new file mode 100644 index 00000000..5774334e --- /dev/null +++ b/tests/StringHelperTest.cpp @@ -0,0 +1,161 @@ +#include +#include "utils/StringHelper.h" + +// Split +TEST(StringHelperTest, SplitBasic) { + auto result = StringHelper::Split(std::string("a,b,c"), ","); + ASSERT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], "a"); + EXPECT_EQ(result[1], "b"); + EXPECT_EQ(result[2], "c"); +} + +TEST(StringHelperTest, SplitNoDelimiter) { + auto result = StringHelper::Split(std::string("hello"), ","); + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], "hello"); +} + +TEST(StringHelperTest, SplitEmptyString) { + auto result = StringHelper::Split(std::string(""), ","); + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], ""); +} + +TEST(StringHelperTest, SplitMultiCharDelimiter) { + auto result = StringHelper::Split(std::string("a::b::c"), "::"); + ASSERT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], "a"); + EXPECT_EQ(result[1], "b"); + EXPECT_EQ(result[2], "c"); +} + +// Replace +TEST(StringHelperTest, ReplaceBasic) { + EXPECT_EQ(StringHelper::Replace("hello world", "world", "there"), "hello there"); +} + +TEST(StringHelperTest, ReplaceNotFound) { + EXPECT_EQ(StringHelper::Replace("hello", "xyz", "abc"), "hello"); +} + +TEST(StringHelperTest, ReplaceMultipleOccurrences) { + EXPECT_EQ(StringHelper::Replace("aaa", "a", "bb"), "bbbbbb"); +} + +// ReplaceOriginal (in-place) +TEST(StringHelperTest, ReplaceOriginalModifiesString) { + std::string s = "foo bar foo"; + StringHelper::ReplaceOriginal(s, "foo", "baz"); + EXPECT_EQ(s, "baz bar baz"); +} + +// StartsWith / EndsWith / Contains +TEST(StringHelperTest, StartsWithTrue) { + EXPECT_TRUE(StringHelper::StartsWith("hello world", "hello")); +} + +TEST(StringHelperTest, StartsWithFalse) { + EXPECT_FALSE(StringHelper::StartsWith("hello world", "world")); +} + +TEST(StringHelperTest, EndsWithTrue) { + EXPECT_TRUE(StringHelper::EndsWith("hello world", "world")); +} + +TEST(StringHelperTest, EndsWithFalse) { + EXPECT_FALSE(StringHelper::EndsWith("hello world", "hello")); +} + +TEST(StringHelperTest, ContainsTrue) { + EXPECT_TRUE(StringHelper::Contains("hello world", "lo wo")); +} + +TEST(StringHelperTest, ContainsFalse) { + EXPECT_FALSE(StringHelper::Contains("hello world", "xyz")); +} + +// IEquals +TEST(StringHelperTest, IEqualsSameCase) { + EXPECT_TRUE(StringHelper::IEquals("hello", "hello")); +} + +TEST(StringHelperTest, IEqualsDifferentCase) { + EXPECT_TRUE(StringHelper::IEquals("Hello", "hELLO")); +} + +TEST(StringHelperTest, IEqualsNotEqual) { + EXPECT_FALSE(StringHelper::IEquals("hello", "world")); +} + +// HasOnlyDigits +TEST(StringHelperTest, HasOnlyDigitsTrue) { + EXPECT_TRUE(StringHelper::HasOnlyDigits("12345")); +} + +TEST(StringHelperTest, HasOnlyDigitsFalse) { + EXPECT_FALSE(StringHelper::HasOnlyDigits("123a5")); +} + +TEST(StringHelperTest, HasOnlyDigitsEmpty) { + // std::all_of on empty range returns true + EXPECT_TRUE(StringHelper::HasOnlyDigits("")); +} + +// IsValidHex +TEST(StringHelperTest, IsValidHexLowerCase) { + EXPECT_TRUE(StringHelper::IsValidHex(std::string("0xABCD"))); +} + +TEST(StringHelperTest, IsValidHexUpperX) { + EXPECT_TRUE(StringHelper::IsValidHex(std::string("0X1f"))); +} + +TEST(StringHelperTest, IsValidHexNoPrefix) { + EXPECT_FALSE(StringHelper::IsValidHex(std::string("ABCD"))); +} + +TEST(StringHelperTest, IsValidHexTooShort) { + EXPECT_FALSE(StringHelper::IsValidHex(std::string("0x"))); +} + +TEST(StringHelperTest, IsValidHexInvalidChars) { + EXPECT_FALSE(StringHelper::IsValidHex(std::string("0xGHIJ"))); +} + +// IsValidOffset +TEST(StringHelperTest, IsValidOffsetSingleDigit) { + EXPECT_TRUE(StringHelper::IsValidOffset(std::string("0"))); + EXPECT_TRUE(StringHelper::IsValidOffset(std::string("5"))); +} + +TEST(StringHelperTest, IsValidOffsetHex) { + EXPECT_TRUE(StringHelper::IsValidOffset(std::string("0x100"))); +} + +TEST(StringHelperTest, IsValidOffsetInvalid) { + EXPECT_FALSE(StringHelper::IsValidOffset(std::string("abc"))); +} + +// StrToL +TEST(StringHelperTest, StrToLDecimal) { + EXPECT_EQ(StringHelper::StrToL("42"), 42); +} + +TEST(StringHelperTest, StrToLHex) { + EXPECT_EQ(StringHelper::StrToL("FF", 16), 255); +} + +// BoolStr +TEST(StringHelperTest, BoolStrTrue) { + EXPECT_EQ(StringHelper::BoolStr(true), "true"); +} + +TEST(StringHelperTest, BoolStrFalse) { + EXPECT_EQ(StringHelper::BoolStr(false), "false"); +} + +// Sprintf +TEST(StringHelperTest, SprintfBasic) { + EXPECT_EQ(StringHelper::Sprintf("hello %s %d", "world", 42), "hello world 42"); +} diff --git a/tests/TextureFactoryTest.cpp b/tests/TextureFactoryTest.cpp new file mode 100644 index 00000000..fd04aadf --- /dev/null +++ b/tests/TextureFactoryTest.cpp @@ -0,0 +1,71 @@ +#include +#include "factories/TextureFactory.h" +#include + +TEST(TextureFactoryTest, TextureFormatConstruction) { + TextureFormat fmt = {TextureType::RGBA16bpp, 16}; + EXPECT_EQ(fmt.type, TextureType::RGBA16bpp); + EXPECT_EQ(fmt.depth, 16u); +} + +TEST(TextureFactoryTest, TextureFormatDepthValues) { + // Verify expected depth for each format type + TextureFormat rgba32 = {TextureType::RGBA32bpp, 32}; + TextureFormat rgba16 = {TextureType::RGBA16bpp, 16}; + TextureFormat ci4 = {TextureType::Palette4bpp, 4}; + TextureFormat ci8 = {TextureType::Palette8bpp, 8}; + TextureFormat i4 = {TextureType::Grayscale4bpp, 4}; + TextureFormat i8 = {TextureType::Grayscale8bpp, 8}; + TextureFormat ia1 = {TextureType::GrayscaleAlpha1bpp, 1}; + TextureFormat ia4 = {TextureType::GrayscaleAlpha4bpp, 4}; + TextureFormat ia8 = {TextureType::GrayscaleAlpha8bpp, 8}; + TextureFormat ia16 = {TextureType::GrayscaleAlpha16bpp, 16}; + TextureFormat tlut = {TextureType::TLUT, 16}; + + EXPECT_EQ(rgba32.depth, 32u); + EXPECT_EQ(rgba16.depth, 16u); + EXPECT_EQ(ci4.depth, 4u); + EXPECT_EQ(ci8.depth, 8u); + EXPECT_EQ(i4.depth, 4u); + EXPECT_EQ(i8.depth, 8u); + EXPECT_EQ(ia1.depth, 1u); + EXPECT_EQ(ia4.depth, 4u); + EXPECT_EQ(ia8.depth, 8u); + EXPECT_EQ(ia16.depth, 16u); + EXPECT_EQ(tlut.depth, 16u); +} + +TEST(TextureFactoryTest, TextureDataConstruction) { + TextureFormat fmt = {TextureType::RGBA32bpp, 32}; + std::vector buffer = {0xFF, 0x00, 0x00, 0xFF}; // single red pixel + TextureData data(fmt, 1, 1, buffer); + EXPECT_EQ(data.mFormat.type, TextureType::RGBA32bpp); + EXPECT_EQ(data.mFormat.depth, 32u); + EXPECT_EQ(data.mWidth, 1u); + EXPECT_EQ(data.mHeight, 1u); + ASSERT_EQ(data.mBuffer.size(), 4u); + EXPECT_EQ(data.mBuffer[0], 0xFF); +} + +TEST(TextureFactoryTest, TextureDataLargerBuffer) { + TextureFormat fmt = {TextureType::RGBA16bpp, 16}; + // 4x4 RGBA16 = 32 bytes + std::vector buffer(32, 0xAA); + TextureData data(fmt, 4, 4, buffer); + EXPECT_EQ(data.mWidth, 4u); + EXPECT_EQ(data.mHeight, 4u); + EXPECT_EQ(data.mBuffer.size(), 32u); +} + +TEST(TextureFactoryTest, TextureFactoryExporters) { + TextureFactory factory; + EXPECT_TRUE(factory.GetExporter(ExportType::Code).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Header).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Binary).has_value()); + EXPECT_TRUE(factory.GetExporter(ExportType::Modding).has_value()); +} + +TEST(TextureFactoryTest, TextureFactorySupportsModding) { + TextureFactory factory; + EXPECT_TRUE(factory.SupportModdedAssets()); +} diff --git a/tests/TextureUtilsTest.cpp b/tests/TextureUtilsTest.cpp new file mode 100644 index 00000000..6abe8b4c --- /dev/null +++ b/tests/TextureUtilsTest.cpp @@ -0,0 +1,117 @@ +#include +#include "utils/TextureUtils.h" + +// CalculateTextureSize for 32x32 textures + +TEST(TextureUtilsTest, RGBA32bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::RGBA32bpp, 32, 32), 4096u); +} + +TEST(TextureUtilsTest, RGBA16bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::RGBA16bpp, 32, 32), 2048u); +} + +TEST(TextureUtilsTest, GrayscaleAlpha16bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::GrayscaleAlpha16bpp, 32, 32), 2048u); +} + +TEST(TextureUtilsTest, TLUT) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::TLUT, 32, 32), 2048u); +} + +TEST(TextureUtilsTest, Grayscale8bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Grayscale8bpp, 32, 32), 1024u); +} + +TEST(TextureUtilsTest, Palette8bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Palette8bpp, 32, 32), 1024u); +} + +TEST(TextureUtilsTest, GrayscaleAlpha8bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::GrayscaleAlpha8bpp, 32, 32), 1024u); +} + +TEST(TextureUtilsTest, GrayscaleAlpha1bpp) { + // Returns decoded/output buffer size (1 byte per pixel), not raw/encoded size. + // alloc_ia8_text_from_i1 expands 1-bit input to 8-bit output, so the texture + // in memory is width*height bytes. See TODO in TextureUtils.cpp. + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::GrayscaleAlpha1bpp, 32, 32), 1024u); +} + +TEST(TextureUtilsTest, Palette4bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Palette4bpp, 32, 32), 512u); +} + +TEST(TextureUtilsTest, Grayscale4bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Grayscale4bpp, 32, 32), 512u); +} + +TEST(TextureUtilsTest, GrayscaleAlpha4bpp) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::GrayscaleAlpha4bpp, 32, 32), 512u); +} + +TEST(TextureUtilsTest, ErrorTypeReturnsZero) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Error, 32, 32), 0u); +} + +// Non-square and edge cases + +TEST(TextureUtilsTest, NonSquareTexture) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::RGBA32bpp, 64, 16), 64u * 16u * 4u); +} + +TEST(TextureUtilsTest, OneByOneTexture) { + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::RGBA32bpp, 1, 1), 4u); +} + +TEST(TextureUtilsTest, Palette4bppOddDimension) { + // 16x16 / 2 = 128 + EXPECT_EQ(TextureUtils::CalculateTextureSize(TextureType::Palette4bpp, 16, 16), 128u); +} + +// alloc_ia8_text_from_i1 tests +// Converts 1-bit-per-pixel data (packed in big-endian uint16) to 8-bit-per-pixel + +TEST(TextureUtilsTest, AllocIA8AllOnes) { + // All bits set: 0xFFFF in big-endian → all 16 output bytes should be 0xFF + // BSWAP16 will swap the bytes, so we need to store in native endian + // such that after BSWAP16 we get 0xFFFF (which is still 0xFFFF) + uint16_t input = 0xFFFF; + auto result = TextureUtils::alloc_ia8_text_from_i1(&input, 16, 1); + ASSERT_EQ(result.size(), 16u); + for (size_t i = 0; i < 16; i++) { + EXPECT_EQ(result[i], 0xFF) << "byte " << i; + } +} + +TEST(TextureUtilsTest, AllocIA8AllZeros) { + uint16_t input = 0x0000; + auto result = TextureUtils::alloc_ia8_text_from_i1(&input, 16, 1); + ASSERT_EQ(result.size(), 16u); + for (size_t i = 0; i < 16; i++) { + EXPECT_EQ(result[i], 0x00) << "byte " << i; + } +} + +TEST(TextureUtilsTest, AllocIA8NonPalindrome) { + // 0x8000 is NOT a byte-palindrome, so BSWAP16 changes it to 0x0080. + // Bit pattern of 0x0080: only bit 7 is set. + // bitMask iterates from 0x8000 down, so bit 7 is hit at output index 8. + uint16_t input = 0x8000; + auto result = TextureUtils::alloc_ia8_text_from_i1(&input, 16, 1); + ASSERT_EQ(result.size(), 16u); + for (size_t i = 0; i < 16; i++) { + if (i == 8) { + EXPECT_EQ(result[i], 0xFF) << "byte " << i << " should be set"; + } else { + EXPECT_EQ(result[i], 0x00) << "byte " << i << " should be clear"; + } + } +} + +TEST(TextureUtilsTest, AllocIA8OutputSize) { + // 32x16 = 512 pixels, input needs 512/16 = 32 uint16_t values + std::vector input(32, 0); + auto result = TextureUtils::alloc_ia8_text_from_i1(input.data(), 32, 16); + EXPECT_EQ(result.size(), 512u); +} diff --git a/tests/TorchUtilsTest.cpp b/tests/TorchUtilsTest.cpp new file mode 100644 index 00000000..dfd0be34 --- /dev/null +++ b/tests/TorchUtilsTest.cpp @@ -0,0 +1,54 @@ +#include +#include "utils/TorchUtils.h" +#include +#include + +// to_hex +TEST(TorchUtilsTest, ToHexUint32WithPrefix) { + // to_hex uppercases the entire output including the "0x" prefix + EXPECT_EQ(Torch::to_hex(0xDEADBEEF), "0XDEADBEEF"); +} + +TEST(TorchUtilsTest, ToHexUint32WithoutPrefix) { + EXPECT_EQ(Torch::to_hex(0xFF, false), "FF"); +} + +TEST(TorchUtilsTest, ToHexZero) { + EXPECT_EQ(Torch::to_hex(0), "0X0"); +} + +TEST(TorchUtilsTest, ToHexUint16) { + EXPECT_EQ(Torch::to_hex(0x1234), "0X1234"); +} + +TEST(TorchUtilsTest, ToHexUpperCase) { + // Verify output is uppercase + std::string result = Torch::to_hex(0xabcdef); + EXPECT_NE(result.find("ABCDEF"), std::string::npos); +} + +// contains +TEST(TorchUtilsTest, ContainsMapKeyPresent) { + std::map m = {{"a", 1}, {"b", 2}}; + EXPECT_TRUE(Torch::contains(m, std::string("a"))); +} + +TEST(TorchUtilsTest, ContainsMapKeyAbsent) { + std::map m = {{"a", 1}, {"b", 2}}; + EXPECT_FALSE(Torch::contains(m, std::string("c"))); +} + +TEST(TorchUtilsTest, ContainsSetPresent) { + std::set s = {1, 2, 3}; + EXPECT_TRUE(Torch::contains(s, 2)); +} + +TEST(TorchUtilsTest, ContainsSetAbsent) { + std::set s = {1, 2, 3}; + EXPECT_FALSE(Torch::contains(s, 5)); +} + +TEST(TorchUtilsTest, ContainsEmptyContainer) { + std::map m; + EXPECT_FALSE(Torch::contains(m, 0)); +} diff --git a/tests/Vec3DTest.cpp b/tests/Vec3DTest.cpp new file mode 100644 index 00000000..c9ead4d4 --- /dev/null +++ b/tests/Vec3DTest.cpp @@ -0,0 +1,160 @@ +#include +#include "types/Vec3D.h" +#include + +// Vec3f construction +TEST(Vec3DTest, Vec3fConstruction) { + Vec3f v(1.0f, 2.0f, 3.0f); + EXPECT_FLOAT_EQ(v.x, 1.0f); + EXPECT_FLOAT_EQ(v.y, 2.0f); + EXPECT_FLOAT_EQ(v.z, 3.0f); +} + +TEST(Vec3DTest, Vec3fDefaultConstruction) { + Vec3f v; + EXPECT_FLOAT_EQ(v.x, 0.0f); + EXPECT_FLOAT_EQ(v.y, 0.0f); + EXPECT_FLOAT_EQ(v.z, 0.0f); +} + +// Vec3s construction +TEST(Vec3DTest, Vec3sConstruction) { + Vec3s v(100, -200, 300); + EXPECT_EQ(v.x, 100); + EXPECT_EQ(v.y, -200); + EXPECT_EQ(v.z, 300); +} + +TEST(Vec3DTest, Vec3sDefaultConstruction) { + Vec3s v; + EXPECT_EQ(v.x, 0); + EXPECT_EQ(v.y, 0); + EXPECT_EQ(v.z, 0); +} + +// Vec3i construction +TEST(Vec3DTest, Vec3iConstruction) { + Vec3i v(100000, -200000, 300000); + EXPECT_EQ(v.x, 100000); + EXPECT_EQ(v.y, -200000); + EXPECT_EQ(v.z, 300000); +} + +// Vec3iu construction +TEST(Vec3DTest, Vec3iuConstruction) { + Vec3iu v(0xDEAD, 0xBEEF, 0xCAFE); + EXPECT_EQ(v.x, 0xDEADu); + EXPECT_EQ(v.y, 0xBEEFu); + EXPECT_EQ(v.z, 0xCAFEu); +} + +// Vec2f construction — note: second field is `z`, not `y` +TEST(Vec3DTest, Vec2fConstruction) { + Vec2f v(1.5f, 2.5f); + EXPECT_FLOAT_EQ(v.x, 1.5f); + EXPECT_FLOAT_EQ(v.z, 2.5f); +} + +// Vec4f construction +TEST(Vec3DTest, Vec4fConstruction) { + Vec4f v(1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ(v.x, 1.0f); + EXPECT_FLOAT_EQ(v.y, 2.0f); + EXPECT_FLOAT_EQ(v.z, 3.0f); + EXPECT_FLOAT_EQ(v.w, 4.0f); +} + +// Vec4s construction +TEST(Vec3DTest, Vec4sConstruction) { + Vec4s v(10, 20, 30, 40); + EXPECT_EQ(v.x, 10); + EXPECT_EQ(v.y, 20); + EXPECT_EQ(v.z, 30); + EXPECT_EQ(v.w, 40); +} + +// Precision tests +TEST(Vec3DTest, Vec3fPrecisionWholeNumbers) { + Vec3f v(1.0f, 2.0f, 3.0f); + EXPECT_EQ(v.precision(), 0); +} + +TEST(Vec3DTest, Vec3fPrecisionOneDecimal) { + Vec3f v(1.5f, 2.0f, 3.0f); + EXPECT_EQ(v.precision(), 1); +} + +TEST(Vec3DTest, Vec3fPrecisionMaxOfComponents) { + // precision returns the max precision across all components + Vec3f v(1.0f, 2.5f, 3.25f); + EXPECT_EQ(v.precision(), 2); +} + +// Width tests +TEST(Vec3DTest, Vec3fWidthWholeNumbers) { + // width = max_magnitude + 1 + precision + // (1.0, 2.0, 3.0): max magnitude is 1 (for 3.0), precision is 0 + // width = 1 + 1 + 0 = 2 + Vec3f v(1.0f, 2.0f, 3.0f); + EXPECT_EQ(v.width(), 2); +} + +TEST(Vec3DTest, Vec3sWidth) { + // width = max_magnitude (no precision for integers) + // (100, -200, 300): magnitudes are 3, 4 (neg adds 1), 3 → max = 4 + Vec3s v(100, -200, 300); + EXPECT_EQ(v.width(), 4); +} + +TEST(Vec3DTest, Vec3sWidthZeros) { + Vec3s v(0, 0, 0); + EXPECT_EQ(v.width(), 1); +} + +TEST(Vec3DTest, Vec4sWidth) { + Vec4s v(1, 10, 100, 1000); + // magnitudes: 1, 2, 3, 4 → max = 4 + EXPECT_EQ(v.width(), 4); +} + +// Stream output tests +TEST(Vec3DTest, Vec3fStreamOutput) { + Vec3f v(1.0f, 2.0f, 3.0f); + std::ostringstream oss; + oss << v; + std::string result = oss.str(); + EXPECT_NE(result.find("1"), std::string::npos); + EXPECT_NE(result.find("2"), std::string::npos); + EXPECT_NE(result.find("3"), std::string::npos); + EXPECT_NE(result.find("{"), std::string::npos); + EXPECT_NE(result.find("}"), std::string::npos); +} + +TEST(Vec3DTest, Vec3sStreamOutput) { + Vec3s v(10, 20, 30); + std::ostringstream oss; + oss << v; + std::string result = oss.str(); + EXPECT_NE(result.find("10"), std::string::npos); + EXPECT_NE(result.find("20"), std::string::npos); + EXPECT_NE(result.find("30"), std::string::npos); +} + +TEST(Vec3DTest, Vec2fStreamOutput) { + Vec2f v(1.5f, 2.5f); + std::ostringstream oss; + oss << v; + std::string result = oss.str(); + EXPECT_NE(result.find("1.5"), std::string::npos); + EXPECT_NE(result.find("2.5"), std::string::npos); +} + +TEST(Vec3DTest, Vec4fStreamOutput) { + Vec4f v(1.0f, 2.0f, 3.0f, 4.0f); + std::ostringstream oss; + oss << v; + std::string result = oss.str(); + // Should contain all 4 values and braces + EXPECT_NE(result.find("{"), std::string::npos); + EXPECT_NE(result.find("}"), std::string::npos); +} diff --git a/tests/ViewportFactoryTest.cpp b/tests/ViewportFactoryTest.cpp new file mode 100644 index 00000000..928e3a9d --- /dev/null +++ b/tests/ViewportFactoryTest.cpp @@ -0,0 +1,95 @@ +#include +#include "factories/ViewportFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +TEST(ViewportFactoryTest, VpRawSize) { + EXPECT_EQ(sizeof(VpRaw), 16u); +} + +TEST(ViewportFactoryTest, VpDataConstruction) { + VpRaw vp = {{160, 120, 511, 0}, {160, 120, 511, 0}}; + VpData data(vp); + EXPECT_EQ(data.mViewport.vscale[0], 160); + EXPECT_EQ(data.mViewport.vscale[1], 120); + EXPECT_EQ(data.mViewport.vscale[2], 511); + EXPECT_EQ(data.mViewport.vscale[3], 0); + EXPECT_EQ(data.mViewport.vtrans[0], 160); + EXPECT_EQ(data.mViewport.vtrans[1], 120); + EXPECT_EQ(data.mViewport.vtrans[2], 511); + EXPECT_EQ(data.mViewport.vtrans[3], 0); +} + +TEST(ViewportFactoryTest, HasExpectedExporters) { + ViewportFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +TEST(ViewportFactoryTest, ManualViewportParse) { + // Build a 16-byte big-endian viewport buffer: + // vscale: (640, 480, 511, 0) → 0x0280, 0x01E0, 0x01FF, 0x0000 + // vtrans: (640, 480, 511, 0) → 0x0280, 0x01E0, 0x01FF, 0x0000 + std::vector buf = { + 0x02, 0x80, // vscale[0] = 640 + 0x01, 0xE0, // vscale[1] = 480 + 0x01, 0xFF, // vscale[2] = 511 + 0x00, 0x00, // vscale[3] = 0 + 0x02, 0x80, // vtrans[0] = 640 + 0x01, 0xE0, // vtrans[1] = 480 + 0x01, 0xFF, // vtrans[2] = 511 + 0x00, 0x00, // vtrans[3] = 0 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + VpRaw vp; + for (int i = 0; i < 4; i++) { + vp.vscale[i] = reader.ReadInt16(); + } + for (int i = 0; i < 4; i++) { + vp.vtrans[i] = reader.ReadInt16(); + } + + EXPECT_EQ(vp.vscale[0], 640); + EXPECT_EQ(vp.vscale[1], 480); + EXPECT_EQ(vp.vscale[2], 511); + EXPECT_EQ(vp.vscale[3], 0); + EXPECT_EQ(vp.vtrans[0], 640); + EXPECT_EQ(vp.vtrans[1], 480); + EXPECT_EQ(vp.vtrans[2], 511); + EXPECT_EQ(vp.vtrans[3], 0); +} + +TEST(ViewportFactoryTest, NegativeValues) { + // Test with negative scale/translation values + std::vector buf = { + 0xFF, 0x00, // vscale[0] = -256 + 0x80, 0x00, // vscale[1] = -32768 + 0x00, 0x01, // vscale[2] = 1 + 0x00, 0x00, // vscale[3] = 0 + 0x00, 0x00, // vtrans[0] = 0 + 0x00, 0x00, // vtrans[1] = 0 + 0x00, 0x00, // vtrans[2] = 0 + 0x00, 0x00, // vtrans[3] = 0 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + VpRaw vp; + for (int i = 0; i < 4; i++) { + vp.vscale[i] = reader.ReadInt16(); + } + for (int i = 0; i < 4; i++) { + vp.vtrans[i] = reader.ReadInt16(); + } + + EXPECT_EQ(vp.vscale[0], -256); + EXPECT_EQ(vp.vscale[1], -32768); + EXPECT_EQ(vp.vscale[2], 1); +} diff --git a/tests/VtxFactoryTest.cpp b/tests/VtxFactoryTest.cpp new file mode 100644 index 00000000..2ec4c62e --- /dev/null +++ b/tests/VtxFactoryTest.cpp @@ -0,0 +1,84 @@ +#include +#include "factories/VtxFactory.h" +#include "lib/binarytools/BinaryReader.h" +#include "lib/binarytools/endianness.h" +#include + +TEST(VtxFactoryTest, VtxRawSize) { + // Each N64 vertex is exactly 16 bytes + EXPECT_EQ(sizeof(VtxRaw), 16u); +} + +TEST(VtxFactoryTest, VtxDataConstruction) { + VtxRaw v1 = {{1, 2, 3}, 0, {10, 20}, {255, 128, 64, 32}}; + VtxRaw v2 = {{4, 5, 6}, 0, {30, 40}, {0, 0, 0, 255}}; + std::vector vtxs = {v1, v2}; + + VtxData data(vtxs); + ASSERT_EQ(data.mVtxs.size(), 2u); + EXPECT_EQ(data.mVtxs[0].ob[0], 1); + EXPECT_EQ(data.mVtxs[0].ob[1], 2); + EXPECT_EQ(data.mVtxs[0].ob[2], 3); + EXPECT_EQ(data.mVtxs[0].cn[0], 255); + EXPECT_EQ(data.mVtxs[1].ob[0], 4); +} + +TEST(VtxFactoryTest, Alignment) { + VtxFactory factory; + EXPECT_EQ(factory.GetAlignment(), 8u); +} + +TEST(VtxFactoryTest, HasExpectedExporters) { + VtxFactory factory; + auto exporters = factory.GetExporters(); + EXPECT_TRUE(exporters.count(ExportType::Code)); + EXPECT_TRUE(exporters.count(ExportType::Header)); + EXPECT_TRUE(exporters.count(ExportType::Binary)); +} + +// Parse a vertex manually from a BinaryReader (same logic as VtxFactory::parse) +TEST(VtxFactoryTest, ManualVertexParse) { + // Build a 16-byte big-endian vertex buffer: + // ob[3]: (100, -50, 200) → 0x0064, 0xFFCE, 0x00C8 + // flag: 0 + // tc[2]: (512, 1024) → 0x0200, 0x0400 + // cn[4]: (255, 128, 64, 255) + std::vector buf = { + 0x00, 0x64, // ob[0] = 100 + 0xFF, 0xCE, // ob[1] = -50 + 0x00, 0xC8, // ob[2] = 200 + 0x00, 0x00, // flag = 0 + 0x02, 0x00, // tc[0] = 512 + 0x04, 0x00, // tc[1] = 1024 + 0xFF, // cn[0] = 255 + 0x80, // cn[1] = 128 + 0x40, // cn[2] = 64 + 0xFF, // cn[3] = 255 + }; + + LUS::BinaryReader reader(buf.data(), buf.size()); + reader.SetEndianness(Torch::Endianness::Big); + + VtxRaw vtx; + vtx.ob[0] = reader.ReadInt16(); + vtx.ob[1] = reader.ReadInt16(); + vtx.ob[2] = reader.ReadInt16(); + vtx.flag = reader.ReadUInt16(); + vtx.tc[0] = reader.ReadInt16(); + vtx.tc[1] = reader.ReadInt16(); + vtx.cn[0] = reader.ReadUByte(); + vtx.cn[1] = reader.ReadUByte(); + vtx.cn[2] = reader.ReadUByte(); + vtx.cn[3] = reader.ReadUByte(); + + EXPECT_EQ(vtx.ob[0], 100); + EXPECT_EQ(vtx.ob[1], -50); + EXPECT_EQ(vtx.ob[2], 200); + EXPECT_EQ(vtx.flag, 0); + EXPECT_EQ(vtx.tc[0], 512); + EXPECT_EQ(vtx.tc[1], 1024); + EXPECT_EQ(vtx.cn[0], 255); + EXPECT_EQ(vtx.cn[1], 128); + EXPECT_EQ(vtx.cn[2], 64); + EXPECT_EQ(vtx.cn[3], 255); +} diff --git a/tests/integration/IntegrationTestHelpers.cpp b/tests/integration/IntegrationTestHelpers.cpp new file mode 100644 index 00000000..511607af --- /dev/null +++ b/tests/integration/IntegrationTestHelpers.cpp @@ -0,0 +1,83 @@ +#include "IntegrationTestHelpers.h" +#include "Companion.h" + +// Forward-declare miniz C API functions we need (defined in zip_file.hpp, compiled via ZWrapper.cpp) +extern "C" { +typedef unsigned int mz_uint; +typedef int mz_bool; +typedef struct mz_zip_archive_tag mz_zip_archive; + +struct mz_zip_internal_state_tag; + +struct mz_zip_archive_tag { + unsigned long long m_archive_size; + unsigned long long m_central_directory_file_ofs; + mz_uint m_total_files; + int m_zip_mode; + int m_zip_type; + int m_last_error; + unsigned long long m_file_offset_alignment; + void *(*m_pAlloc)(void *, size_t, size_t); + void *(*m_pRealloc)(void *, void *, size_t, size_t); + void (*m_pFree)(void *, void *); + void *m_pAlloc_opaque; + size_t (*m_pRead)(void *, unsigned long long, void *, size_t); + size_t (*m_pWrite)(void *, unsigned long long, const void *, size_t); + void *m_pIO_opaque; + struct mz_zip_internal_state_tag *m_pState; +}; + +mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); +mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip); +mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size); +void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags); +mz_bool mz_zip_reader_end(mz_zip_archive *pZip); +void mz_free(void *p); +} + +std::map> RunPipeline(const std::string& configDir, const std::string& romPath) { + auto tmpDir = fs::temp_directory_path() / "torch_integration_test"; + if (fs::exists(tmpDir)) { + fs::remove_all(tmpDir); + } + fs::create_directories(tmpDir); + + auto instance = Companion::Instance = new Companion( + fs::path(romPath), ArchiveType::O2R, false, configDir, tmpDir.string() + ); + instance->Init(ExportType::Binary); + + // After Process() completes, Companion cleans up Instance, AudioManager, and Decompressor cache. + // Find the output .o2r file + std::string o2rPath; + for (auto& entry : fs::recursive_directory_iterator(tmpDir)) { + if (entry.path().extension() == ".o2r") { + o2rPath = entry.path().string(); + break; + } + } + + std::map> assets; + if (!o2rPath.empty()) { + mz_zip_archive zip{}; + if (mz_zip_reader_init_file(&zip, o2rPath.c_str(), 0)) { + mz_uint numFiles = mz_zip_reader_get_num_files(&zip); + for (mz_uint i = 0; i < numFiles; i++) { + char filename[512]; + mz_zip_reader_get_filename(&zip, i, filename, sizeof(filename)); + + size_t size = 0; + void* data = mz_zip_reader_extract_to_heap(&zip, i, &size, 0); + if (data) { + auto* bytes = static_cast(data); + assets[filename] = std::vector(bytes, bytes + size); + mz_free(data); + } + } + mz_zip_reader_end(&zip); + } + } + + fs::remove_all(tmpDir); + return assets; +} diff --git a/tests/integration/IntegrationTestHelpers.h b/tests/integration/IntegrationTestHelpers.h new file mode 100644 index 00000000..15ce297c --- /dev/null +++ b/tests/integration/IntegrationTestHelpers.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ROM SHA-1 constants +constexpr const char* SM64_US_SHA1 = "9bef1128717f958171a4afac3ed78ee2bb4e86ce"; + +inline std::string GetRomDir() { + return std::string(INTEGRATION_ROM_DIR); +} + +inline std::string GetTestDir() { + return std::string(INTEGRATION_TEST_DIR); +} + +inline bool RomExists(const std::string& filename) { + return fs::exists(fs::path(GetRomDir()) / filename); +} + +// Binary header parsed from an exported asset +struct AssetHeader { + int8_t endianness; + uint32_t resourceType; + uint32_t version; + uint64_t deadbeef; +}; + +inline AssetHeader ParseHeader(const std::vector& data) { + AssetHeader header{}; + if (data.size() < 0x40) return header; + + std::memcpy(&header.endianness, data.data() + 0x00, 1); + std::memcpy(&header.resourceType, data.data() + 0x04, 4); + std::memcpy(&header.version, data.data() + 0x08, 4); + std::memcpy(&header.deadbeef, data.data() + 0x0C, 8); + return header; +} + +inline void ValidateHeader(const std::vector& data, uint32_t expectedResourceType) { + ASSERT_GE(data.size(), 0x40u) << "Asset too small to contain header"; + auto header = ParseHeader(data); + EXPECT_EQ(header.endianness, 0) << "Expected Native endianness"; + EXPECT_EQ(header.resourceType, expectedResourceType) << "Resource type mismatch"; + EXPECT_EQ(header.version, 0u) << "Expected version 0"; + EXPECT_EQ(header.deadbeef, 0xDEADBEEFDEADBEEFULL) << "Missing DEADBEEF marker"; +} + +// Run the full Companion pipeline for a given config directory and ROM path. +// Returns a map of asset name -> binary content extracted from the output .o2r. +// Defined in IntegrationTestHelpers.cpp to avoid miniz ODR violations. +std::map> RunPipeline(const std::string& configDir, const std::string& romPath); diff --git a/tests/integration/SM64IntegrationTest.cpp b/tests/integration/SM64IntegrationTest.cpp new file mode 100644 index 00000000..fc568595 --- /dev/null +++ b/tests/integration/SM64IntegrationTest.cpp @@ -0,0 +1,413 @@ +#include +#include "IntegrationTestHelpers.h" +#include "factories/ResourceType.h" + +static const std::string SM64_US_ROM = "sm64.us.z64"; +static const std::string SM64_US_CONFIG_DIR = GetTestDir() + "/sm64/us"; + +class SM64USIntegrationTest : public ::testing::Test { +protected: + static std::map> sAssets; + static bool sPipelineRan; + + static void SetUpTestSuite() { + if (!RomExists(SM64_US_ROM)) { + return; + } + auto romPath = GetRomDir() + "/" + SM64_US_ROM; + sAssets = RunPipeline(SM64_US_CONFIG_DIR, romPath); + sPipelineRan = true; + } + + void SetUp() override { + if (!RomExists(SM64_US_ROM)) { + GTEST_SKIP() << "SM64 US ROM not found at " << GetRomDir() << "/" << SM64_US_ROM; + } + ASSERT_TRUE(sPipelineRan) << "Pipeline did not run successfully"; + if (sAssets.empty()) { + GTEST_SKIP() << "Pipeline produced no assets (missing config.yml?)"; + } + } + + const std::vector& GetAsset(const std::string& name) { + const std::string suffix = "/" + name; + for (auto& [key, value] : sAssets) { + if (key == name || key.ends_with(suffix)) { + return value; + } + } + static std::vector empty; + return empty; + } +}; + +std::map> SM64USIntegrationTest::sAssets; +bool SM64USIntegrationTest::sPipelineRan = false; + +TEST_F(SM64USIntegrationTest, PipelineProducesAssets) { + EXPECT_FALSE(sAssets.empty()) << "Pipeline produced no assets"; +} + +TEST_F(SM64USIntegrationTest, TextureRGBA16) { + auto& data = GetAsset("test_texture"); + ASSERT_FALSE(data.empty()) << "Texture asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Texture)); + + // After 0x40 header: uint32 format, uint32 width, uint32 height, uint32 buf_size, then buffer + ASSERT_GE(data.size(), 0x50u) << "Texture too small to contain metadata"; + + uint32_t width, height, bufSize; + std::memcpy(&width, data.data() + 0x44, 4); + std::memcpy(&height, data.data() + 0x48, 4); + std::memcpy(&bufSize, data.data() + 0x4C, 4); + + EXPECT_EQ(width, 32u); + EXPECT_EQ(height, 32u); + EXPECT_EQ(bufSize, 2048u); + EXPECT_EQ(data.size(), 0x40u + 16u + 2048u) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, VtxBasic) { + auto& data = GetAsset("test_vtx"); + ASSERT_FALSE(data.empty()) << "VTX asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Vertex)); + + // After 0x40 header: uint32 count, then count * 16 bytes per vertex + ASSERT_GE(data.size(), 0x44u) << "VTX too small to contain count"; + + uint32_t count; + std::memcpy(&count, data.data() + 0x40, 4); + + EXPECT_EQ(count, 4u); + EXPECT_EQ(data.size(), 0x40u + 4u + count * 16u) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, BlobBasic) { + auto& data = GetAsset("test_blob"); + ASSERT_FALSE(data.empty()) << "Blob asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Blob)); + + // After 0x40 header: uint32 size, then raw data + ASSERT_GE(data.size(), 0x44u) << "Blob too small to contain size"; + + uint32_t blobSize; + std::memcpy(&blobSize, data.data() + 0x40, 4); + + EXPECT_EQ(blobSize, 64u); + EXPECT_EQ(data.size(), 0x40u + 4u + blobSize) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, CollisionBasic) { + auto& data = GetAsset("test_collision"); + ASSERT_FALSE(data.empty()) << "Collision asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Collision)); + + // After 0x40 header: uint32 command_count, then command_count * int16_t + ASSERT_GE(data.size(), 0x44u) << "Collision too small to contain count"; + + uint32_t cmdCount; + std::memcpy(&cmdCount, data.data() + 0x40, 4); + + EXPECT_GT(cmdCount, 0u) << "Expected at least one collision command"; + EXPECT_EQ(data.size(), 0x40u + 4u + cmdCount * 2u) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, GfxBasic) { + auto& data = GetAsset("test_gfx"); + ASSERT_FALSE(data.empty()) << "GFX asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::DisplayList)); + + // After 0x40 header: int8 GBI version, padding to 8-byte align, then marker + commands + ASSERT_GE(data.size(), 0x50u) << "GFX too small"; + + // GBI version byte at 0x40 + int8_t gbiVersion; + std::memcpy(&gbiVersion, data.data() + 0x40, 1); + EXPECT_GE(gbiVersion, 0) << "Invalid GBI version"; + + // BEEFBEEF marker at 0x4C (after 8-byte aligned padding + G_MARKER word) + uint32_t beefMarker; + std::memcpy(&beefMarker, data.data() + 0x4C, 4); + EXPECT_EQ(beefMarker, 0xBEEFBEEFu) << "Missing BEEFBEEF marker"; +} + +TEST_F(SM64USIntegrationTest, LightsBasic) { + auto& data = GetAsset("test_lights"); + ASSERT_FALSE(data.empty()) << "Lights asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Lights)); + + // After 0x40 header: raw Lights1Raw struct (24 bytes) + EXPECT_EQ(data.size(), 0x40u + 24u) << "Lights size mismatch"; +} + +TEST_F(SM64USIntegrationTest, AnimBasic) { + auto& data = GetAsset("test_anim"); + ASSERT_FALSE(data.empty()) << "Anim asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Anim)); + + // After 0x40 header: 6x int16 (12 bytes) + uint64 length (8 bytes) + // + uint32 indices count + indices + uint32 entries count + entries + ASSERT_GE(data.size(), 0x40u + 20u + 4u) << "Anim too small"; + + // Read indices count at offset 0x54 (0x40 + 12 + 8) + uint32_t indicesCount; + std::memcpy(&indicesCount, data.data() + 0x54, 4); + EXPECT_GT(indicesCount, 0u) << "Expected at least one animation index"; +} + +TEST_F(SM64USIntegrationTest, DialogBasic) { + auto& data = GetAsset("test_dialog"); + ASSERT_FALSE(data.empty()) << "Dialog asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::SDialog)); + + // After 0x40 header: uint32 unused + int8 linesPerBox + int16 leftOffset + int16 width + // + uint32 text size + text data + ASSERT_GE(data.size(), 0x40u + 4u + 1u + 2u + 2u + 4u) << "Dialog too small"; + + // Read text size at offset 0x49 (0x40 + 4 + 1 + 2 + 2) + uint32_t textSize; + std::memcpy(&textSize, data.data() + 0x49, 4); + EXPECT_GT(textSize, 0u) << "Expected non-empty dialog text"; + EXPECT_EQ(data.size(), 0x40u + 4u + 1u + 2u + 2u + 4u + textSize) << "Dialog size mismatch"; +} + +TEST_F(SM64USIntegrationTest, TextBasic) { + auto& data = GetAsset("test_text"); + ASSERT_FALSE(data.empty()) << "Text asset not found in output"; + + // SM64:TEXT exports as Blob type + ValidateHeader(data, static_cast(Torch::ResourceType::Blob)); + + ASSERT_GE(data.size(), 0x44u) << "Text too small to contain size"; + + uint32_t textSize; + std::memcpy(&textSize, data.data() + 0x40, 4); + EXPECT_GT(textSize, 0u) << "Expected non-empty text data"; + EXPECT_EQ(data.size(), 0x40u + 4u + textSize) << "Text size mismatch"; +} + +TEST_F(SM64USIntegrationTest, MacroBasic) { + auto& data = GetAsset("test_macro"); + ASSERT_FALSE(data.empty()) << "Macro asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::MacroObject)); + + // After 0x40 header: uint32 count, then count * int16 + ASSERT_GE(data.size(), 0x44u) << "Macro too small to contain count"; + + uint32_t count; + std::memcpy(&count, data.data() + 0x40, 4); + EXPECT_GT(count, 0u) << "Expected at least one macro entry"; + EXPECT_EQ(data.size(), 0x40u + 4u + count * 2u) << "Macro size mismatch"; +} + +TEST_F(SM64USIntegrationTest, MovtexBasic) { + auto& data = GetAsset("test_movtex"); + ASSERT_FALSE(data.empty()) << "Movtex asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Movtex)); + + // After 0x40 header: uint32 size, then int16 data + ASSERT_GE(data.size(), 0x44u) << "Movtex too small to contain size"; + + uint32_t bufSize; + std::memcpy(&bufSize, data.data() + 0x40, 4); + EXPECT_GT(bufSize, 0u) << "Expected non-empty movtex data"; + EXPECT_EQ(data.size(), 0x40u + 4u + bufSize * 2u) << "Movtex size mismatch"; +} + +TEST_F(SM64USIntegrationTest, MovtexQuadBasic) { + auto& data = GetAsset("test_movtex_quad"); + ASSERT_FALSE(data.empty()) << "MovtexQuad asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::MovtexQuad)); + + // After 0x40 header: uint32 count, then (int16 id + uint64 hash) per quad + ASSERT_GE(data.size(), 0x44u) << "MovtexQuad too small to contain count"; + + uint32_t count; + std::memcpy(&count, data.data() + 0x40, 4); + EXPECT_EQ(count, 2u) << "Expected 2 movtex quads"; +} + +TEST_F(SM64USIntegrationTest, TrajectoryBasic) { + auto& data = GetAsset("test_trajectory"); + ASSERT_FALSE(data.empty()) << "Trajectory asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Trajectory)); + + // After 0x40 header: uint32 count, then count * (4x int16 = 8 bytes) + ASSERT_GE(data.size(), 0x44u) << "Trajectory too small to contain count"; + + uint32_t count; + std::memcpy(&count, data.data() + 0x40, 4); + EXPECT_GT(count, 0u) << "Expected at least one trajectory point"; + EXPECT_EQ(data.size(), 0x40u + 4u + count * 8u) << "Trajectory size mismatch"; +} + +TEST_F(SM64USIntegrationTest, PaintingBasic) { + auto& data = GetAsset("test_painting"); + ASSERT_FALSE(data.empty()) << "Painting asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Painting)); + + // Painting is a large fixed struct - just verify it's bigger than the header + EXPECT_GT(data.size(), 0x40u + 20u) << "Painting too small"; +} + +TEST_F(SM64USIntegrationTest, PaintingMapBasic) { + auto& data = GetAsset("test_painting_map"); + ASSERT_FALSE(data.empty()) << "PaintingMap asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::PaintingData)); + + // After 0x40 header: uint32 total elements, then mappings and groups + ASSERT_GE(data.size(), 0x44u) << "PaintingMap too small to contain count"; + + uint32_t totalElements; + std::memcpy(&totalElements, data.data() + 0x40, 4); + EXPECT_GT(totalElements, 0u) << "Expected at least one painting map element"; +} + +TEST_F(SM64USIntegrationTest, DictionaryBasic) { + auto& data = GetAsset("test_dictionary"); + ASSERT_FALSE(data.empty()) << "Dictionary asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Dictionary)); + + // After 0x40 header: uint32 dict size, then key-value pairs + ASSERT_GE(data.size(), 0x44u) << "Dictionary too small to contain size"; + + uint32_t dictSize; + std::memcpy(&dictSize, data.data() + 0x40, 4); + EXPECT_EQ(dictSize, 3u) << "Expected 3 dictionary entries"; +} + +TEST_F(SM64USIntegrationTest, CollisionLevel) { + auto& data = GetAsset("test_collision_level"); + ASSERT_FALSE(data.empty()) << "Level collision asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Collision)); + + ASSERT_GE(data.size(), 0x44u) << "Collision too small to contain count"; + + uint32_t cmdCount; + std::memcpy(&cmdCount, data.data() + 0x40, 4); + + // Bob-omb Battlefield level collision is large — has vertices, surfaces, + // special objects, and environment boxes + EXPECT_GT(cmdCount, 100u) << "Expected a large collision command set"; + EXPECT_EQ(data.size(), 0x40u + 4u + cmdCount * 2u) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, MovtexNonQuad) { + auto& data = GetAsset("test_movtex_nonquad"); + ASSERT_FALSE(data.empty()) << "Non-quad movtex asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Movtex)); + + ASSERT_GE(data.size(), 0x44u) << "Movtex too small to contain size"; + + uint32_t bufSize; + std::memcpy(&bufSize, data.data() + 0x40, 4); + EXPECT_GT(bufSize, 0u) << "Expected non-empty movtex data"; + EXPECT_EQ(data.size(), 0x40u + 4u + bufSize * 2u) << "Movtex size mismatch"; +} + +TEST_F(SM64USIntegrationTest, MovtexNonQuadColor) { + auto& data = GetAsset("test_movtex_nonquad_color"); + ASSERT_FALSE(data.empty()) << "Non-quad color movtex asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Movtex)); + + ASSERT_GE(data.size(), 0x44u) << "Movtex too small to contain size"; + + uint32_t bufSize; + std::memcpy(&bufSize, data.data() + 0x40, 4); + EXPECT_GT(bufSize, 0u) << "Expected non-empty movtex data"; + EXPECT_EQ(data.size(), 0x40u + 4u + bufSize * 2u) << "Movtex size mismatch"; +} + +TEST_F(SM64USIntegrationTest, CollisionWater) { + auto& data = GetAsset("test_collision_water"); + ASSERT_FALSE(data.empty()) << "Water collision asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Collision)); + + ASSERT_GE(data.size(), 0x44u) << "Collision too small to contain count"; + + uint32_t cmdCount; + std::memcpy(&cmdCount, data.data() + 0x40, 4); + + // JRB area 1 collision should have water environment boxes + EXPECT_GT(cmdCount, 100u) << "Expected a large collision command set"; + EXPECT_EQ(data.size(), 0x40u + 4u + cmdCount * 2u) << "Total size mismatch"; +} + +TEST_F(SM64USIntegrationTest, GeoLayout) { + auto& data = GetAsset("test_geo_layout"); + ASSERT_FALSE(data.empty()) << "GeoLayout asset not found in output"; + + // GeoLayout exports as Blob type + ValidateHeader(data, static_cast(Torch::ResourceType::Blob)); + + ASSERT_GE(data.size(), 0x44u) << "GeoLayout too small to contain size"; + + uint32_t blobSize; + std::memcpy(&blobSize, data.data() + 0x40, 4); + EXPECT_GT(blobSize, 0u) << "Expected non-empty geo layout data"; +} + +TEST_F(SM64USIntegrationTest, GeoLayoutBomb) { + auto& data = GetAsset("test_geo_bomb"); + ASSERT_FALSE(data.empty()) << "Bomb GeoLayout asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Blob)); + + ASSERT_GE(data.size(), 0x44u) << "GeoLayout too small to contain size"; + + uint32_t blobSize; + std::memcpy(&blobSize, data.data() + 0x40, 4); + EXPECT_GT(blobSize, 0u) << "Expected non-empty geo layout data"; +} + +TEST_F(SM64USIntegrationTest, GeoLayoutGoomba) { + auto& data = GetAsset("test_geo_goomba"); + ASSERT_FALSE(data.empty()) << "Goomba GeoLayout asset not found in output"; + + ValidateHeader(data, static_cast(Torch::ResourceType::Blob)); + + ASSERT_GE(data.size(), 0x44u) << "GeoLayout too small to contain size"; + + uint32_t blobSize; + std::memcpy(&blobSize, data.data() + 0x40, 4); + EXPECT_GT(blobSize, 0u) << "Expected non-empty geo layout data"; +} + +// Error handling tests: verify the pipeline doesn't crash on bad input +static const std::string SM64_US_ERROR_CONFIG_DIR = GetTestDir() + "/sm64/us_error"; + +TEST(SM64USErrorTest, BadInputDoesNotCrash) { + if (!RomExists(SM64_US_ROM)) { + GTEST_SKIP() << "SM64 US ROM not found"; + } + auto romPath = GetRomDir() + "/" + SM64_US_ROM; + + // This runs the pipeline with YAMLs that have bad offsets, invalid formats, and missing fields. + // We just verify it doesn't crash (SEGV/abort). + EXPECT_NO_FATAL_FAILURE({ + try { + auto assets = RunPipeline(SM64_US_ERROR_CONFIG_DIR, romPath); + } catch (...) { + // Any exception is fine - we just don't want crashes + } + }); +} diff --git a/tests/integration/sm64/us/assets/test_anim.yml b/tests/integration/sm64/us/assets/test_anim.yml new file mode 100644 index 00000000..4892bd3b --- /dev/null +++ b/tests/integration/sm64/us/assets/test_anim.yml @@ -0,0 +1,7 @@ +:config: + force: true + +test_anim: + type: SM64:ANIM + offset: 0x4EC690 + symbol: test_anim_00 diff --git a/tests/integration/sm64/us/assets/test_blob_basic.yml b/tests/integration/sm64/us/assets/test_blob_basic.yml new file mode 100644 index 00000000..c8e199a3 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_blob_basic.yml @@ -0,0 +1,8 @@ +:config: + force: true + +test_blob: + type: BLOB + size: 64 + offset: 0x40 + symbol: test_blob_data diff --git a/tests/integration/sm64/us/assets/test_collision.yml b/tests/integration/sm64/us/assets/test_collision.yml new file mode 100644 index 00000000..fcc7305c --- /dev/null +++ b/tests/integration/sm64/us/assets/test_collision.yml @@ -0,0 +1,9 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_collision: + type: SM64:COLLISION + offset: 0x4950 + symbol: test_cannon_lid_collision diff --git a/tests/integration/sm64/us/assets/test_collision_level.yml b/tests/integration/sm64/us/assets/test_collision_level.yml new file mode 100644 index 00000000..3c4f508c --- /dev/null +++ b/tests/integration/sm64/us/assets/test_collision_level.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x3FC2B0 + segments: + - [ 0x7, 0x3FC2B0 ] + force: true + +test_collision_level: + type: SM64:COLLISION + offset: 0xE958 + symbol: test_bob_seg7_collision_level diff --git a/tests/integration/sm64/us/assets/test_collision_water.yml b/tests/integration/sm64/us/assets/test_collision_water.yml new file mode 100644 index 00000000..fbab5a5e --- /dev/null +++ b/tests/integration/sm64/us/assets/test_collision_water.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x41A760 + segments: + - [ 0x7, 0x41A760 ] + force: true + +test_collision_water: + type: SM64:COLLISION + offset: 0xB058 + symbol: test_jrb_area_1_collision diff --git a/tests/integration/sm64/us/assets/test_dialog.yml b/tests/integration/sm64/us/assets/test_dialog.yml new file mode 100644 index 00000000..9f1e39bc --- /dev/null +++ b/tests/integration/sm64/us/assets/test_dialog.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x108A40 + segments: + - [ 0x2, 0x108A40 ] + force: true + +test_dialog: + type: SM64:DIALOG + offset: 0xFFC8 + symbol: test_dialog_00 diff --git a/tests/integration/sm64/us/assets/test_dictionary.yml b/tests/integration/sm64/us/assets/test_dictionary.yml new file mode 100644 index 00000000..aa66c78d --- /dev/null +++ b/tests/integration/sm64/us/assets/test_dictionary.yml @@ -0,0 +1,9 @@ +:config: + force: true + +test_dictionary: + type: SM64:DICTIONARY + keys: + TEXT_ZERO: 8177343 + TEXT_COIN: 8155500 + TEXT_STAR: 8142922 diff --git a/tests/integration/sm64/us/assets/test_geo_bomb.yml b/tests/integration/sm64/us/assets/test_geo_bomb.yml new file mode 100644 index 00000000..afeed3bd --- /dev/null +++ b/tests/integration/sm64/us/assets/test_geo_bomb.yml @@ -0,0 +1,9 @@ +:config: + segments: + - [ 0xD, 0x1B9070 ] + force: true + +test_geo_bomb: + type: SM64:GEO_LAYOUT + offset: 0xD000BBC + symbol: test_bowser_bomb_geo diff --git a/tests/integration/sm64/us/assets/test_geo_goomba.yml b/tests/integration/sm64/us/assets/test_geo_goomba.yml new file mode 100644 index 00000000..35dc4d31 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_geo_goomba.yml @@ -0,0 +1,9 @@ +:config: + segments: + - [ 0xF, 0x2008D0 ] + force: true + +test_geo_goomba: + type: SM64:GEO_LAYOUT + offset: 0xF0006E4 + symbol: test_goomba_geo diff --git a/tests/integration/sm64/us/assets/test_geo_layout.yml b/tests/integration/sm64/us/assets/test_geo_layout.yml new file mode 100644 index 00000000..7af12e04 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_geo_layout.yml @@ -0,0 +1,11 @@ +:config: + external_files: + - "assets/test_geo_layout_data.yml" + segments: + - [ 0x16, 0x218DA0 ] + force: true + +test_geo_layout: + type: SM64:GEO_LAYOUT + offset: 0x16000000 + symbol: test_mist_geo diff --git a/tests/integration/sm64/us/assets/test_geo_layout_data.yml b/tests/integration/sm64/us/assets/test_geo_layout_data.yml new file mode 100644 index 00000000..8d47a329 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_geo_layout_data.yml @@ -0,0 +1,35 @@ +:config: + segments: + - [ 0x3, 0x201410 ] + force: true + +mist_vtx_0: + type: VTX + count: 4 + offset: 0x0 + symbol: test_mist_vtx_0 + +mist_vtx_40: + type: VTX + count: 4 + offset: 0x40 + symbol: test_mist_vtx_40 + +mist_texture: + type: TEXTURE + size: 2048 + width: 32 + height: 32 + offset: 0x80 + format: IA16 + symbol: test_mist_texture + +mist_dl_880: + type: GFX + offset: 0x880 + symbol: test_mist_dl_880 + +mist_dl_920: + type: GFX + offset: 0x920 + symbol: test_mist_dl_920 diff --git a/tests/integration/sm64/us/assets/test_gfx_basic.yml b/tests/integration/sm64/us/assets/test_gfx_basic.yml new file mode 100644 index 00000000..a71338ce --- /dev/null +++ b/tests/integration/sm64/us/assets/test_gfx_basic.yml @@ -0,0 +1,9 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_gfx: + type: GFX + offset: 0x2B68 + symbol: test_amp_electricity_sub_dl diff --git a/tests/integration/sm64/us/assets/test_lights_basic.yml b/tests/integration/sm64/us/assets/test_lights_basic.yml new file mode 100644 index 00000000..8a40c2bd --- /dev/null +++ b/tests/integration/sm64/us/assets/test_lights_basic.yml @@ -0,0 +1,10 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_lights: + type: LIGHTS + size: 24 + offset: 0x4040 + symbol: test_cannon_lid_lights diff --git a/tests/integration/sm64/us/assets/test_macro.yml b/tests/integration/sm64/us/assets/test_macro.yml new file mode 100644 index 00000000..7effe41f --- /dev/null +++ b/tests/integration/sm64/us/assets/test_macro.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_macro: + type: SM64:MACRO + offset: 0x77764 + symbol: test_castle_inside_macro_objs diff --git a/tests/integration/sm64/us/assets/test_movtex.yml b/tests/integration/sm64/us/assets/test_movtex.yml new file mode 100644 index 00000000..627df20f --- /dev/null +++ b/tests/integration/sm64/us/assets/test_movtex.yml @@ -0,0 +1,12 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_movtex: + type: SM64:MOVTEX + quad: true + offset: 0x79090 + symbol: test_castle_inside_movtex_data diff --git a/tests/integration/sm64/us/assets/test_movtex_nonquad.yml b/tests/integration/sm64/us/assets/test_movtex_nonquad.yml new file mode 100644 index 00000000..9d3d9b42 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_movtex_nonquad.yml @@ -0,0 +1,13 @@ +:config: + compression: + offset: 0x4614D0 + segments: + - [ 0x7, 0x4614D0 ] + force: true + +test_movtex_nonquad: + type: SM64:MOVTEX + count: 4 + has_color: false + offset: 0x15AF0 + symbol: test_bitfs_movtex_tris_lava_first_section diff --git a/tests/integration/sm64/us/assets/test_movtex_nonquad_color.yml b/tests/integration/sm64/us/assets/test_movtex_nonquad_color.yml new file mode 100644 index 00000000..e827d306 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_movtex_nonquad_color.yml @@ -0,0 +1,13 @@ +:config: + compression: + offset: 0x42CF20 + segments: + - [ 0x7, 0x42CF20 ] + force: true + +test_movtex_nonquad_color: + type: SM64:MOVTEX + count: 14 + has_color: true + offset: 0x16840 + symbol: test_ttc_movtex_tris_big_surface_treadmill diff --git a/tests/integration/sm64/us/assets/test_movtex_quad.yml b/tests/integration/sm64/us/assets/test_movtex_quad.yml new file mode 100644 index 00000000..e6d8d035 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_movtex_quad.yml @@ -0,0 +1,12 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_movtex_quad: + type: SM64:MOVTEX_QUAD + count: 2 + offset: 0x790F0 + symbol: test_castle_inside_movtex_quad diff --git a/tests/integration/sm64/us/assets/test_painting.yml b/tests/integration/sm64/us/assets/test_painting.yml new file mode 100644 index 00000000..2e6e5f53 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_painting.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_painting: + type: SM64:PAINTING + offset: 0x23620 + symbol: test_bob_painting diff --git a/tests/integration/sm64/us/assets/test_painting_map.yml b/tests/integration/sm64/us/assets/test_painting_map.yml new file mode 100644 index 00000000..3da9ef01 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_painting_map.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_painting_map: + type: SM64:PAINTING_MAP + offset: 0x21AE0 + symbol: test_castle_inside_painting_map diff --git a/tests/integration/sm64/us/assets/test_text.yml b/tests/integration/sm64/us/assets/test_text.yml new file mode 100644 index 00000000..83c835b3 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_text.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x108A40 + segments: + - [ 0x2, 0x108A40 ] + force: true + +test_text: + type: SM64:TEXT + offset: 0x10FD4 + symbol: test_act_00 diff --git a/tests/integration/sm64/us/assets/test_texture_rgba16.yml b/tests/integration/sm64/us/assets/test_texture_rgba16.yml new file mode 100644 index 00000000..4db63b41 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_texture_rgba16.yml @@ -0,0 +1,13 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_texture: + type: TEXTURE + size: 2048 + width: 32 + height: 32 + offset: 0x1B18 + format: RGBA16 + symbol: test_amp_body_texture diff --git a/tests/integration/sm64/us/assets/test_trajectory.yml b/tests/integration/sm64/us/assets/test_trajectory.yml new file mode 100644 index 00000000..acb875a4 --- /dev/null +++ b/tests/integration/sm64/us/assets/test_trajectory.yml @@ -0,0 +1,11 @@ +:config: + compression: + offset: 0x396340 + segments: + - [ 0x7, 0x396340 ] + force: true + +test_trajectory: + type: SM64:TRAJECTORY + offset: 0x78EF8 + symbol: test_castle_inside_trajectory_mips diff --git a/tests/integration/sm64/us/assets/test_vtx_basic.yml b/tests/integration/sm64/us/assets/test_vtx_basic.yml new file mode 100644 index 00000000..d984345b --- /dev/null +++ b/tests/integration/sm64/us/assets/test_vtx_basic.yml @@ -0,0 +1,10 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_vtx: + type: VTX + count: 4 + offset: 0x2DE0 + symbol: test_amp_body_vtx diff --git a/tests/integration/sm64/us/config.yml b/tests/integration/sm64/us/config.yml new file mode 100644 index 00000000..32641525 --- /dev/null +++ b/tests/integration/sm64/us/config.yml @@ -0,0 +1,33 @@ +9bef1128717f958171a4afac3ed78ee2bb4e86ce: + name: Super Mario 64 [US] + path: assets + config: + gbi: F3D + sort: OFFSET + output: + binary: test_output.o2r + segments: + - 0x000000 + - 0x000000 + - 0x108a40 + - 0x201410 + - 0x114750 + - 0x12a7e0 + - 0x188440 + - 0x26a3a0 + - 0x1f2200 + - 0x31e1d0 + - 0x2708c0 + - 0x36f530 + - 0x132850 + - 0x1b9070 + - 0x3828c0 + - 0x2008d0 + - 0x108a10 + - 0x000000 + - 0x000000 + - 0x219e00 + - 0x269ea0 + - 0x2abca0 + - 0x218da0 + - 0x1279b0 diff --git a/tests/integration/sm64/us_error/assets/test_invalid_format.yml b/tests/integration/sm64/us_error/assets/test_invalid_format.yml new file mode 100644 index 00000000..d061702a --- /dev/null +++ b/tests/integration/sm64/us_error/assets/test_invalid_format.yml @@ -0,0 +1,13 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_invalid_format: + type: TEXTURE + size: 2048 + width: 32 + height: 32 + offset: 0x1B18 + format: INVALID_FORMAT + symbol: test_invalid_format_texture diff --git a/tests/integration/sm64/us_error/assets/test_missing_count.yml b/tests/integration/sm64/us_error/assets/test_missing_count.yml new file mode 100644 index 00000000..2f532254 --- /dev/null +++ b/tests/integration/sm64/us_error/assets/test_missing_count.yml @@ -0,0 +1,9 @@ +:config: + segments: + - [ 0x8, 0x1F2200 ] + force: true + +test_missing_count: + type: VTX + offset: 0x2DE0 + symbol: test_missing_count_vtx diff --git a/tests/integration/sm64/us_error/config.yml b/tests/integration/sm64/us_error/config.yml new file mode 100644 index 00000000..32641525 --- /dev/null +++ b/tests/integration/sm64/us_error/config.yml @@ -0,0 +1,33 @@ +9bef1128717f958171a4afac3ed78ee2bb4e86ce: + name: Super Mario 64 [US] + path: assets + config: + gbi: F3D + sort: OFFSET + output: + binary: test_output.o2r + segments: + - 0x000000 + - 0x000000 + - 0x108a40 + - 0x201410 + - 0x114750 + - 0x12a7e0 + - 0x188440 + - 0x26a3a0 + - 0x1f2200 + - 0x31e1d0 + - 0x2708c0 + - 0x36f530 + - 0x132850 + - 0x1b9070 + - 0x3828c0 + - 0x2008d0 + - 0x108a10 + - 0x000000 + - 0x000000 + - 0x219e00 + - 0x269ea0 + - 0x2abca0 + - 0x218da0 + - 0x1279b0 diff --git a/tests/roms/.gitignore b/tests/roms/.gitignore new file mode 100644 index 00000000..a462b23d --- /dev/null +++ b/tests/roms/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file and the README +!.gitignore +!README.md diff --git a/tests/roms/README.md b/tests/roms/README.md new file mode 100644 index 00000000..b1eb5bb5 --- /dev/null +++ b/tests/roms/README.md @@ -0,0 +1,11 @@ +# Integration Test ROMs + +Place ROM files here for integration testing. This directory is gitignored. + +## Supported ROMs + +| Game | Version | Expected Filename | SHA-1 | +|------|---------|-------------------|-------| +| Super Mario 64 | US | `sm64.us.z64` | `9bef1128717f958171a4afac3ed78ee2bb4e86ce` | + +Tests that require a ROM will be skipped (not failed) when the ROM is not present.