diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c4e0cf..e9a8d77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,17 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) option(VECTORAIL_GC_BUILD_TOOLS "Build format inspection tools" ON) +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(VECTORAIL_GC_INSTALL_DEFAULT ON) +else() + set(VECTORAIL_GC_INSTALL_DEFAULT OFF) +endif() +option(VECTORAIL_GC_INSTALL "Generate VectorailGC install rules" + ${VECTORAIL_GC_INSTALL_DEFAULT}) -find_package(glm CONFIG REQUIRED) +if(NOT TARGET glm::glm) + find_package(glm CONFIG REQUIRED) +endif() add_library(vectorail-gc src/EventStream.cpp @@ -51,6 +60,9 @@ if(VECTORAIL_GC_BUILD_TOOLS) add_executable(vectorail-gc-stage-probe tools/stage_probe.cpp) target_link_libraries(vectorail-gc-stage-probe PRIVATE Vectorail::GC) + add_executable(vectorail-gc-stage-ext-probe tools/stage_ext_probe.cpp) + target_link_libraries(vectorail-gc-stage-ext-probe PRIVATE Vectorail::GC) + add_executable(vectorail-gc-rvb-probe tools/rvb_probe.cpp) target_link_libraries(vectorail-gc-rvb-probe PRIVATE Vectorail::GC) @@ -61,33 +73,34 @@ if(VECTORAIL_GC_BUILD_TOOLS) target_link_libraries(vectorail-gc-effect-probe PRIVATE Vectorail::GCEffects) endif() -install(TARGETS vectorail-gc vectorail-gc-effects - EXPORT VectorailGCTargets - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} -) -install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install(EXPORT VectorailGCTargets - FILE VectorailGCTargets.cmake - NAMESPACE Vectorail:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC -) - -configure_package_config_file( - cmake/VectorailGCConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC -) -write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) -install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC -) +if(VECTORAIL_GC_INSTALL) + install(TARGETS vectorail-gc vectorail-gc-effects + EXPORT VectorailGCTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ) + install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + install(EXPORT VectorailGCTargets + FILE VectorailGCTargets.cmake + NAMESPACE Vectorail:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC + ) + configure_package_config_file( + cmake/VectorailGCConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC + ) + write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC + ) +endif() diff --git a/include/gc/StagePattern.hpp b/include/gc/StagePattern.hpp index 3227716..6c8a474 100644 --- a/include/gc/StagePattern.hpp +++ b/include/gc/StagePattern.hpp @@ -138,6 +138,9 @@ struct CameraPoint { struct ParticlePoint { static constexpr size_t kRecordSize = 44; + // TuneBGEffectData in Android libtune.so. GameScene::ExecFlowItem uses + // these authored keys to spawn the stage's flowing background items. + uint32_t timeMs = 0; uint32_t enabled = 0; uint32_t shape = 0; @@ -157,6 +160,16 @@ struct VisualizerPoint { Color color; }; +struct BackgroundImagePoint { + static constexpr size_t kRecordSize = 20; + + uint32_t timeMs = 0; + uint32_t mode = 0; + uint32_t atlasIndex = 0; + int32_t textureIndex = -1; + Color color; +}; + struct BackgroundColorPoint { static constexpr size_t kRecordSize = 22; @@ -166,27 +179,27 @@ struct BackgroundColorPoint { Color bottomRight; Color bottomLeft; bool interpolateToNext = false; - bool audioReactive = false; + bool rhythmReactive = false; }; struct VisibilityPoint { uint32_t timeMs = 0; - bool fadeOut = false; - bool fadeIn = false; + bool repeat = false; // wire +4: member of a repeating key range + bool interpolate = false; // wire +5: interpolate/fade into this key bool visible = false; }; struct TransformPoint { uint32_t timeMs = 0; - bool tweenTowards = false; - bool tweenAway = false; + bool repeat = false; // wire +4: member of a repeating key range + bool interpolate = false; // wire +5: interpolate to the following key float value[3] = {0.0f, 0.0f, 0.0f}; }; struct ObjectColorPoint { uint32_t timeMs = 0; - bool tweenTowards = false; - bool tweenAway = false; + bool repeat = false; // wire +4: member of a repeating key range + bool interpolate = false; // wire +5: interpolate to the following key Color color; }; @@ -221,13 +234,24 @@ struct ParsedStagePattern { std::vector cameras; std::vector particles; std::vector visualizer; + std::vector backgroundTextureNames; + std::vector backgroundImages; std::vector backgroundColors; std::vector modelNames; std::vector fragmentShaderNames; std::vector objects; }; +struct StageExtension { + std::array, 4> noteSettings; + uint32_t unknown = 0; + std::vector notes; + size_t trailingBytes = 0; +}; + bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err); +bool ParseStageExtension(const std::vector& bytes, StageExtension* out, + std::string* err); } // namespace gc diff --git a/include/gc/TumoModel.hpp b/include/gc/TumoModel.hpp index 43c01ff..f1a149e 100644 --- a/include/gc/TumoModel.hpp +++ b/include/gc/TumoModel.hpp @@ -17,9 +17,11 @@ struct TumoGeometry { std::vector triangles; std::vector solidLines; std::vector wireframeLines; + std::uint32_t meshCount = 0; + std::uint32_t nodeCount = 0; }; -// Decodes the common one-mesh TUMO layout used by gameplay stage objects. +// Decodes and aggregates every static TUMO mesh used by gameplay stage objects. // Polygon fans are expanded on the host so the PSP only has to submit flat // GU_TRIANGLES/GU_LINES arrays. bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* error); diff --git a/src/StagePattern.cpp b/src/StagePattern.cpp index 253a87a..82f0503 100644 --- a/src/StagePattern.cpp +++ b/src/StagePattern.cpp @@ -231,6 +231,43 @@ bool parseTrack(const StageDat& dat, uint32_t start, uint32_t next, std::vector< return true; } +bool readStageNote(PatternReader& r, StageNote* out, std::string* err) { + if (!out) return false; + StageNote note; + uint8_t rawType = 0; + if (!r.u32(¬e.timeMs) || !r.u8(&rawType) || !r.u8(¬e.typeOverride)) { + return failAt("Note prefix", r.tell(), err); + } + note.type = static_cast(rawType); + for (int16_t& value : note.params16) { + uint16_t encoded = 0; + if (!r.u16(&encoded)) return failAt("Note s16 fields", r.tell(), err); + value = static_cast(encoded); + } + if (!r.u8(¬e.flag24)) return failAt("Note flag24", r.tell(), err); + for (float& value : note.params25) { + if (!r.f32(&value)) return failAt("Note float fields at +25", r.tell(), err); + } + if (!r.u8(¬e.flag37) || !r.u8(¬e.flag38)) { + return failAt("Note flags at +37", r.tell(), err); + } + for (float& value : note.params39) { + if (!r.f32(&value)) return failAt("Note float fields at +39", r.tell(), err); + } + for (uint32_t& value : note.params55) { + if (!r.u32(&value)) return failAt("Note u32 fields at +55", r.tell(), err); + } + if (!r.f32(¬e.param67) || !r.u32(¬e.param71)) { + return failAt("Note fields at +67", r.tell(), err); + } + for (float& value : note.params75) { + if (!r.f32(&value)) return failAt("Note float fields at +75", r.tell(), err); + } + if (!r.u32(¬e.param95)) return failAt("Note field at +95", r.tell(), err); + *out = note; + return true; +} + bool parseNotes( const StageDat& dat, uint32_t start, @@ -257,26 +294,7 @@ bool parseNotes( out->reserve(count); for (uint32_t i = 0; i < count; ++i) { StageNote note; - uint8_t rawType = 0; - if (!r.u32(¬e.timeMs) || - !r.u8(&rawType) || - !r.u8(¬e.typeOverride)) { - return failAt("Note prefix", r.tell(), err); - } - note.type = static_cast(rawType); - for (int16_t& value : note.params16) { - uint16_t encoded = 0; - if (!r.u16(&encoded)) return failAt("Note s16 fields", r.tell(), err); - value = static_cast(encoded); - } - if (!r.u8(¬e.flag24)) return failAt("Note flag24", r.tell(), err); - for (float& value : note.params25) if (!r.f32(&value)) return failAt("Note float fields at +25", r.tell(), err); - if (!r.u8(¬e.flag37) || !r.u8(¬e.flag38)) return failAt("Note flags at +37", r.tell(), err); - for (float& value : note.params39) if (!r.f32(&value)) return failAt("Note float fields at +39", r.tell(), err); - for (uint32_t& value : note.params55) if (!r.u32(&value)) return failAt("Note u32 fields at +55", r.tell(), err); - if (!r.f32(¬e.param67) || !r.u32(¬e.param71)) return failAt("Note fields at +67", r.tell(), err); - for (float& value : note.params75) if (!r.f32(&value)) return failAt("Note float fields at +75", r.tell(), err); - if (!r.u32(¬e.param95)) return failAt("Note field at +95", r.tell(), err); + if (!readStageNote(r, ¬e, err)) return false; out->push_back(note); } @@ -368,6 +386,43 @@ bool parseVisualizer(const StageDat& dat, uint32_t start, uint32_t next, return true; } +bool parseBackgroundImages(const StageDat& dat, uint32_t start, uint32_t next, + std::vector* textureNames, + std::vector* out, std::string* err) { + if (!textureNames || !out || !validOffset(dat, start)) return false; + PatternReader r(dat.bytes, start, next); + textureNames->clear(); + out->clear(); + + uint32_t count = 0; + if (!r.u32(&count)) return failAt("BackgroundImage texture count", r.tell(), err); + textureNames->reserve(count); + for (uint32_t i = 0; i < count; ++i) { + std::string name; + if (!r.string8(&name)) return failAt("BackgroundImage texture name", r.tell(), err); + textureNames->push_back(std::move(name)); + } + + if (!r.u32(&count)) return failAt("BackgroundImage key count", r.tell(), err); + if (!readCount(count, BackgroundImagePoint::kRecordSize, r.tell(), next, + "BackgroundImage", err)) { + return false; + } + out->reserve(count); + for (uint32_t i = 0; i < count; ++i) { + BackgroundImagePoint point; + uint32_t textureIndex = 0; + if (!r.u32(&point.timeMs) || !r.u32(&point.mode) || !r.u32(&point.atlasIndex) || + !r.u32(&textureIndex) || !r.color(&point.color)) { + return failAt("BackgroundImage", r.tell(), err); + } + point.textureIndex = static_cast(textureIndex); + out->push_back(point); + } + if (r.tell() != next) return failAt("BackgroundImage trailing bytes", r.tell(), err); + return true; +} + bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next, std::vector* out, std::string* err) { if (!out || !validOffset(dat, start)) return false; @@ -380,14 +435,14 @@ bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next, for (uint32_t i = 0; i < count; ++i) { BackgroundColorPoint c; uint8_t interpolateToNext = 0; - uint8_t audioReactive = 0; + uint8_t rhythmReactive = 0; if (!r.u32(&c.timeMs) || !r.color(&c.topRight) || !r.color(&c.topLeft) || !r.color(&c.bottomRight) || !r.color(&c.bottomLeft) || - !r.u8(&interpolateToNext) || !r.u8(&audioReactive)) { + !r.u8(&interpolateToNext) || !r.u8(&rhythmReactive)) { return failAt("ColorTable entry", r.tell(), err); } c.interpolateToNext = interpolateToNext != 0; - c.audioReactive = audioReactive != 0; + c.rhythmReactive = rhythmReactive != 0; out->push_back(c); } if (r.tell() != next) return failAt("ColorTable trailing bytes", r.tell(), err); @@ -402,14 +457,14 @@ bool parseTransformArray(PatternReader& r, std::vector* out, out->reserve(count); for (uint32_t i = 0; i < count; ++i) { TransformPoint p; - uint8_t towards = 0; - uint8_t away = 0; - if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) || + uint8_t repeat = 0; + uint8_t interpolate = 0; + if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) || !r.f32(&p.value[0]) || !r.f32(&p.value[1]) || !r.f32(&p.value[2])) { return failAt(what, r.tell(), err); } - p.tweenTowards = towards != 0; - p.tweenAway = away != 0; + p.repeat = repeat != 0; + p.interpolate = interpolate != 0; out->push_back(p); } return true; @@ -418,8 +473,12 @@ bool parseTransformArray(PatternReader& r, std::vector* out, bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next, std::vector* modelNames, std::vector* fragmentShaderNames, - std::vector* out, std::string* err) { - if (!modelNames || !fragmentShaderNames || !out || !validOffset(dat, start)) return false; + std::vector* out, size_t* extensionStart, + std::string* err) { + if (!modelNames || !fragmentShaderNames || !out || !extensionStart || + !validOffset(dat, start)) { + return false; + } PatternReader r(dat.bytes, start, next); modelNames->clear(); fragmentShaderNames->clear(); @@ -466,14 +525,14 @@ bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next, object.visibility.reserve(keyCount); for (uint32_t key = 0; key < keyCount; ++key) { VisibilityPoint p; - uint8_t fadeOut = 0; - uint8_t fadeIn = 0; + uint8_t repeat = 0; + uint8_t interpolate = 0; uint8_t visible = 0; - if (!r.u32(&p.timeMs) || !r.u8(&fadeOut) || !r.u8(&fadeIn) || !r.u8(&visible)) { + if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) || !r.u8(&visible)) { return failAt("Object visibility", r.tell(), err); } - p.fadeOut = fadeOut != 0; - p.fadeIn = fadeIn != 0; + p.repeat = repeat != 0; + p.interpolate = interpolate != 0; p.visible = visible != 0; object.visibility.push_back(p); } @@ -487,18 +546,21 @@ bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next, object.colorChanges.reserve(keyCount); for (uint32_t key = 0; key < keyCount; ++key) { ObjectColorPoint p; - uint8_t towards = 0; - uint8_t away = 0; - if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) || !r.color(&p.color)) { + uint8_t repeat = 0; + uint8_t interpolate = 0; + if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) || !r.color(&p.color)) { return failAt("Object color key", r.tell(), err); } - p.tweenTowards = towards != 0; - p.tweenAway = away != 0; + p.repeat = repeat != 0; + p.interpolate = interpolate != 0; object.colorChanges.push_back(p); } out->push_back(std::move(object)); } - if (r.tell() != next) return failAt("ObjectArray trailing bytes", r.tell(), err); + // LoadBGData records RomReadPosition() here and uses it as the base of the + // extension offset table. Header word 11 points into that table, so it is + // not a reliable object-stream end marker on older mobile charts. + *extensionStart = r.tell(); return true; } @@ -573,12 +635,17 @@ bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string if (err) *err = "invalid background section order"; return false; } + size_t objectExtensionStart = 0; if (!parseParticles(dat, tmp.header.particles, tmp.header.visualizer, &tmp.particles, err) || !parseVisualizer(dat, tmp.header.visualizer, tmp.header.unk1, &tmp.visualizer, err) || + !parseBackgroundImages(dat, tmp.header.unk1, tmp.header.colors, + &tmp.backgroundTextureNames, &tmp.backgroundImages, err) || !parseBackgroundColors(dat, tmp.header.colors, tmp.header.objects, &tmp.backgroundColors, err) || !parseObjects(dat, tmp.header.objects, tmp.header.colors2, - &tmp.modelNames, &tmp.fragmentShaderNames, &tmp.objects, err) || - !parseObjectParents(dat, tmp.header.colors2, tmp.header.unk3, &tmp.objects, err)) { + &tmp.modelNames, &tmp.fragmentShaderNames, &tmp.objects, + &objectExtensionStart, err) || + !parseObjectParents(dat, static_cast(objectExtensionStart), tmp.header.unk3, + &tmp.objects, err)) { return false; } @@ -586,4 +653,46 @@ bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string return true; } +bool ParseStageExtension(const std::vector& bytes, StageExtension* out, + std::string* err) { + if (!out || bytes.size() < 6) { + if (err) *err = "stage extension is smaller than its six-byte prefix"; + return false; + } + + StageExtension tmp; + PatternReader r(bytes, 6, bytes.size()); + for (std::vector& list : tmp.noteSettings) { + uint16_t count = 0; + if (!r.u16(&count) || + !readCount(count, 12, r.tell(), bytes.size(), "StageExtension timing", err)) { + return false; + } + list.reserve(count); + for (uint16_t i = 0; i < count; ++i) { + NoteSetting setting; + if (!r.u32(&setting.timeMs) || !r.u32(&setting.mode) || !r.f32(&setting.value)) { + return failAt("StageExtension timing", r.tell(), err); + } + list.push_back(setting); + } + } + + uint32_t noteCount = 0; + if (!r.u32(&tmp.unknown) || !r.u32(¬eCount) || + !readCount(noteCount, StageNote::kRecordSize, r.tell(), bytes.size(), + "StageExtension notes", err)) { + return false; + } + tmp.notes.reserve(noteCount); + for (uint32_t i = 0; i < noteCount; ++i) { + StageNote note; + if (!readStageNote(r, ¬e, err)) return false; + tmp.notes.push_back(note); + } + tmp.trailingBytes = bytes.size() - r.tell(); + *out = std::move(tmp); + return true; +} + } // namespace gc diff --git a/src/TumoModel.cpp b/src/TumoModel.cpp index 0f12158..303c572 100644 --- a/src/TumoModel.cpp +++ b/src/TumoModel.cpp @@ -72,87 +72,99 @@ bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* e Reader reader(std::move(bytes)); std::uint32_t meshCount = 0; - if (!reader.u32(&meshCount) || meshCount != 1) { - return malformed(error, "unsupported TUMO mesh layout"); - } - std::uint32_t nameCount = 0; - if (!reader.u32(&nameCount) || nameCount > 4096) return malformed(error, "invalid TUMO names"); - for (std::uint32_t i = 0; i < nameCount; ++i) { - if (!reader.string8()) return malformed(error, "truncated TUMO name table"); + if (!reader.u32(&meshCount) || meshCount == 0 || meshCount > 100000) { + return malformed(error, "invalid TUMO mesh count"); } + out->meshCount = meshCount; - std::uint32_t vertexCount = 0; - if (!reader.u32(&vertexCount) || vertexCount > 10000000) { - return malformed(error, "invalid TUMO vertex count"); - } - std::vector vertices(vertexCount); - for (TumoVertex& vertex : vertices) { - if (!reader.f32(&vertex.x) || !reader.f32(&vertex.y) || !reader.f32(&vertex.z)) { - return malformed(error, "truncated TUMO vertices"); + for (std::uint32_t mesh = 0; mesh < meshCount; ++mesh) { + std::uint32_t nameCount = 0; + if (!reader.u32(&nameCount) || nameCount > 4096) return malformed(error, "invalid TUMO names"); + for (std::uint32_t i = 0; i < nameCount; ++i) { + if (!reader.string8()) return malformed(error, "truncated TUMO name table"); } - } - float ignoredBound = 0.0f; - for (int i = 0; i < 8; ++i) { - if (!reader.f32(&ignoredBound)) return malformed(error, "truncated TUMO bounds"); - } - std::uint32_t partCount = 0; - if (!reader.u32(&partCount) || partCount > 100000) { - return malformed(error, "invalid TUMO part count"); - } - for (std::uint32_t part = 0; part < partCount; ++part) { - std::uint32_t primitiveType = 0; - std::uint32_t ignoredMaterial = 0; - std::uint32_t ignoredIndexCount = 0; - std::uint32_t polygonCount = 0; - if (!reader.u32(&primitiveType) || !reader.u32(&ignoredMaterial) || - !reader.u32(&ignoredIndexCount) || !reader.u32(&polygonCount) || - polygonCount > 1000000) { - return malformed(error, "invalid TUMO part"); + std::uint32_t vertexCount = 0; + if (!reader.u32(&vertexCount) || vertexCount > 10000000) { + return malformed(error, "invalid TUMO vertex count"); } - for (std::uint32_t polygon = 0; polygon < polygonCount; ++polygon) { - std::uint32_t cornerCount = 0; - if (!reader.u32(&cornerCount) || cornerCount > 1000000) { - return malformed(error, "invalid TUMO polygon"); + std::vector vertices(vertexCount); + for (TumoVertex& vertex : vertices) { + if (!reader.f32(&vertex.x) || !reader.f32(&vertex.y) || !reader.f32(&vertex.z)) { + return malformed(error, "truncated TUMO vertices"); } - std::vector corners; - corners.reserve(cornerCount); - for (std::uint32_t corner = 0; corner < cornerCount; ++corner) { - std::uint32_t index = 0; - float ignoredU = 0.0f; - float ignoredV = 0.0f; - if (!reader.u32(&index) || !reader.f32(&ignoredU) || !reader.f32(&ignoredV) || - index >= vertices.size()) { - return malformed(error, "invalid TUMO polygon corner"); - } - corners.push_back(index); + } + float ignoredBound = 0.0f; + for (int i = 0; i < 8; ++i) { + if (!reader.f32(&ignoredBound)) return malformed(error, "truncated TUMO bounds"); + } + + std::uint32_t partCount = 0; + if (!reader.u32(&partCount) || partCount > 100000) { + return malformed(error, "invalid TUMO part count"); + } + for (std::uint32_t part = 0; part < partCount; ++part) { + std::uint32_t primitiveType = 0; + std::uint32_t ignoredMaterial = 0; + std::uint32_t ignoredIndexCount = 0; + std::uint32_t polygonCount = 0; + if (!reader.u32(&primitiveType) || !reader.u32(&ignoredMaterial) || + !reader.u32(&ignoredIndexCount) || !reader.u32(&polygonCount) || + polygonCount > 1000000) { + return malformed(error, "invalid TUMO part"); } - if (primitiveType == 1) { - for (std::size_t corner = 0; corner + 1 < corners.size(); corner += 2) { - out->solidLines.push_back(vertices[corners[corner]]); - out->solidLines.push_back(vertices[corners[corner + 1]]); + for (std::uint32_t polygon = 0; polygon < polygonCount; ++polygon) { + std::uint32_t cornerCount = 0; + if (!reader.u32(&cornerCount) || cornerCount > 1000000) { + return malformed(error, "invalid TUMO polygon"); } - } else { - for (std::size_t corner = 1; corner + 1 < corners.size(); ++corner) { - out->triangles.push_back(vertices[corners[0]]); - out->triangles.push_back(vertices[corners[corner]]); - out->triangles.push_back(vertices[corners[corner + 1]]); + std::vector corners; + corners.reserve(cornerCount); + for (std::uint32_t corner = 0; corner < cornerCount; ++corner) { + std::uint32_t index = 0; + float ignoredU = 0.0f; + float ignoredV = 0.0f; + if (!reader.u32(&index) || !reader.f32(&ignoredU) || !reader.f32(&ignoredV) || + index >= vertices.size()) { + return malformed(error, "invalid TUMO polygon corner"); + } + corners.push_back(index); + } + if (primitiveType == 1) { + for (std::size_t corner = 0; corner + 1 < corners.size(); corner += 2) { + out->solidLines.push_back(vertices[corners[corner]]); + out->solidLines.push_back(vertices[corners[corner + 1]]); + } + } else { + for (std::size_t corner = 1; corner + 1 < corners.size(); ++corner) { + out->triangles.push_back(vertices[corners[0]]); + out->triangles.push_back(vertices[corners[corner]]); + out->triangles.push_back(vertices[corners[corner + 1]]); + } } } } - } - std::uint32_t lineCount = 0; - if (!reader.u32(&lineCount) || lineCount > 10000000) { - return malformed(error, "invalid TUMO edge count"); - } - out->wireframeLines.reserve(static_cast(lineCount) * 2); - for (std::uint64_t line = 0; line < static_cast(lineCount) * 2; ++line) { - std::uint32_t index = 0; - if (!reader.u32(&index) || index >= vertices.size()) { - return malformed(error, "invalid TUMO edge"); + std::uint32_t lineCount = 0; + if (!reader.u32(&lineCount) || lineCount > 10000000) { + return malformed(error, "invalid TUMO edge count"); } - out->wireframeLines.push_back(vertices[index]); + out->wireframeLines.reserve(out->wireframeLines.size() + static_cast(lineCount) * 2); + for (std::uint64_t line = 0; line < static_cast(lineCount) * 2; ++line) { + std::uint32_t index = 0; + if (!reader.u32(&index) || index >= vertices.size()) { + return malformed(error, "invalid TUMO edge"); + } + out->wireframeLines.push_back(vertices[index]); + } + + std::uint32_t nodeCount = 0; + if (!reader.u32(&nodeCount)) return malformed(error, "missing TUMO node count"); + out->nodeCount += nodeCount; + if (nodeCount != 0 && mesh + 1 != meshCount) { + return malformed(error, "unsupported TUMO nodes before the final mesh"); + } + if (nodeCount != 0) break; } if (out->triangles.empty() && out->solidLines.empty() && out->wireframeLines.empty()) { return malformed(error, "TUMO model contains no geometry"); diff --git a/tools/stage_ext_probe.cpp b/tools/stage_ext_probe.cpp new file mode 100644 index 0000000..c5fc060 --- /dev/null +++ b/tools/stage_ext_probe.cpp @@ -0,0 +1,47 @@ +#include "gc/StagePattern.hpp" + +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " [...]\n"; + return 2; + } + + bool okay = true; + for (int i = 1; i < argc; ++i) { + std::ifstream file(argv[i], std::ios::binary); + if (!file) { + std::cerr << argv[i] << ": could not open file\n"; + okay = false; + continue; + } + std::vector bytes((std::istreambuf_iterator(file)), {}); + gc::StageExtension extension; + std::string error; + if (!gc::ParseStageExtension(bytes, &extension, &error)) { + std::cerr << argv[i] << ": " << error << '\n'; + okay = false; + continue; + } + + std::map types; + for (const gc::StageNote& note : extension.notes) { + ++types[static_cast(note.type)]; + } + std::cout << argv[i] << ": notes=" << extension.notes.size() + << " trailing=" << extension.trailingBytes << " timing="; + for (const auto& list : extension.noteSettings) std::cout << list.size() << ','; + std::cout << " types="; + for (const auto& [type, count] : types) { + std::cout << static_cast(type) << ':' << count << ','; + } + std::cout << '\n'; + } + return okay ? 0 : 1; +} diff --git a/tools/stage_probe.cpp b/tools/stage_probe.cpp index 75fb7b0..a384abf 100644 --- a/tools/stage_probe.cpp +++ b/tools/stage_probe.cpp @@ -39,12 +39,29 @@ bool probe(const std::string& path) { size_t flashingObjects = 0; size_t noGlobalFadeObjects = 0; size_t translucentObjects = 0; + size_t objectAnimationKeys = 0; + size_t objectRepeatKeys = 0; + size_t objectInterpolateKeys = 0; + size_t objectDifferingFlagKeys = 0; + const auto countAnimationKeys = [&](const auto& keys) { + objectAnimationKeys += keys.size(); + for (const auto& key : keys) { + objectRepeatKeys += key.repeat ? 1 : 0; + objectInterpolateKeys += key.interpolate ? 1 : 0; + objectDifferingFlagKeys += key.repeat != key.interpolate ? 1 : 0; + } + }; for (const gc::StageObject& object : stage.objects) { if (object.parentIndex >= 0) ++parentedObjects; if (object.wireframe) ++wireframeObjects; if (object.flashing) ++flashingObjects; if (object.unknownFlag) ++noGlobalFadeObjects; if (object.color[3] < 0.999f) ++translucentObjects; + countAnimationKeys(object.visibility); + countAnimationKeys(object.movement); + countAnimationKeys(object.scaling); + countAnimationKeys(object.rotations); + countAnimationKeys(object.colorChanges); } std::cout << path @@ -67,6 +84,8 @@ bool probe(const std::string& path) { << " noteNames=" << stage.noteNames.size() << "\n background: particles=" << stage.particles.size() << " visualizerKeys=" << stage.visualizer.size() + << " imageTextures=" << stage.backgroundTextureNames.size() + << " imageKeys=" << stage.backgroundImages.size() << " colorKeys=" << stage.backgroundColors.size() << " models=" << stage.modelNames.size() << " objects=" << stage.objects.size() @@ -74,7 +93,11 @@ bool probe(const std::string& path) { << " wire=" << wireframeObjects << " flashing=" << flashingObjects << " noGlobalFade=" << noGlobalFadeObjects - << " translucent=" << translucentObjects; + << " translucent=" << translucentObjects + << " objectKeys=" << objectAnimationKeys + << " repeat=" << objectRepeatKeys + << " interpolate=" << objectInterpolateKeys + << " differingFlags=" << objectDifferingFlagKeys; if (!stage.notes.empty()) { const auto [lo, hi] = std::minmax_element( stage.notes.begin(), stage.notes.end(), @@ -96,6 +119,32 @@ bool probe(const std::string& path) { for (const auto& key : stage.visualizer) ++visualizerTypes[key.type]; std::cout << "\n visualizerTypes:"; for (const auto& [type, count] : visualizerTypes) std::cout << ' ' << type << '=' << count; + std::cout << "\n visualizerKeys:"; + for (const auto& key : stage.visualizer) { + std::cout << ' ' << key.timeMs << ":type=" << key.type + << ",rgba=" << static_cast(key.color.r) << ',' + << static_cast(key.color.g) << ',' + << static_cast(key.color.b) << ',' + << static_cast(key.color.a); + } + } + if (!stage.backgroundTextureNames.empty()) { + std::cout << "\n backgroundTextures:"; + for (size_t i = 0; i < stage.backgroundTextureNames.size(); ++i) { + std::cout << ' ' << i << '=' << stage.backgroundTextureNames[i]; + } + } + if (!stage.backgroundImages.empty()) { + std::cout << "\n backgroundImageKeys:"; + for (const auto& image : stage.backgroundImages) { + std::cout << ' ' << image.timeMs << ":mode=" << image.mode + << ",atlas=" << image.atlasIndex + << ",texture=" << image.textureIndex + << ",rgba=" << static_cast(image.color.r) << ',' + << static_cast(image.color.g) << ',' + << static_cast(image.color.b) << ',' + << static_cast(image.color.a); + } } if (!stage.drawDistances.empty()) { std::cout << "\n drawDistance=" << stage.drawDistances.front().timeMs << ':'