Initial Vectorail GC format library
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
/build/
|
||||
/cmake-build-*/
|
||||
/.cache/
|
||||
/.clangd/
|
||||
/compile_commands.json
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.dll
|
||||
*.dylib
|
||||
*.exe
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(vectorail-gc VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
option(VECTORAIL_GC_BUILD_TOOLS "Build format inspection tools" ON)
|
||||
|
||||
find_package(glm CONFIG REQUIRED)
|
||||
|
||||
add_library(vectorail-gc
|
||||
src/EventStream.cpp
|
||||
src/MtxArchive.cpp
|
||||
src/RvbLayout.cpp
|
||||
src/RvbScene.cpp
|
||||
src/StageCatalog.cpp
|
||||
src/StageDat.cpp
|
||||
src/StagePattern.cpp
|
||||
src/TumoModel.cpp
|
||||
)
|
||||
add_library(Vectorail::GC ALIAS vectorail-gc)
|
||||
target_compile_features(vectorail-gc PUBLIC cxx_std_17)
|
||||
target_include_directories(vectorail-gc
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
set_target_properties(vectorail-gc PROPERTIES
|
||||
EXPORT_NAME GC
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
|
||||
add_library(vectorail-gc-effects src/GcTargetEffect.cpp)
|
||||
add_library(Vectorail::GCEffects ALIAS vectorail-gc-effects)
|
||||
target_compile_features(vectorail-gc-effects PUBLIC cxx_std_17)
|
||||
target_include_directories(vectorail-gc-effects
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
target_link_libraries(vectorail-gc-effects PUBLIC glm::glm)
|
||||
set_target_properties(vectorail-gc-effects PROPERTIES
|
||||
EXPORT_NAME GCEffects
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
|
||||
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-rvb-probe tools/rvb_probe.cpp)
|
||||
target_link_libraries(vectorail-gc-rvb-probe PRIVATE Vectorail::GC)
|
||||
|
||||
add_executable(vectorail-gc-mtx-probe tools/mtx_probe.cpp)
|
||||
target_link_libraries(vectorail-gc-mtx-probe PRIVATE Vectorail::GC)
|
||||
|
||||
add_executable(vectorail-gc-effect-probe tools/effect_probe.cpp)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Kiyooru Takasaki
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Vectorail GC
|
||||
|
||||
Headless parsers and evaluators for Groove Coaster interoperability. The
|
||||
library has no windowing, rendering, audio, input, or gameplay dependencies.
|
||||
|
||||
## Formats
|
||||
|
||||
- arcade stage `.dat` streams and note opcodes;
|
||||
- stage catalog and pattern data;
|
||||
- `.tumo` model geometry;
|
||||
- MTX texture archives;
|
||||
- RVB scenes and layout snapshots;
|
||||
- effect and UV animation data.
|
||||
|
||||
No game files are distributed with this repository. Applications must point
|
||||
the library at data obtained from their own installation or hardware dump.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
cmake --install build --prefix /desired/prefix
|
||||
```
|
||||
|
||||
Consumers use `Vectorail::GC` for the base parsers and
|
||||
`Vectorail::GCEffects` for the GLM-based effect evaluator.
|
||||
|
||||
Vectorail GC is distributed under the MIT License.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
find_dependency(glm CONFIG)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/VectorailGCTargets.cmake")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef OPENROLLER_GC_EVENTSTREAM_HPP
|
||||
#define OPENROLLER_GC_EVENTSTREAM_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct GameEvent {
|
||||
uint16_t id = 0;
|
||||
float timestamp = 0.0f;
|
||||
uint16_t a = 0; // unknown (often 0)
|
||||
uint32_t type = 0;
|
||||
float value = 0.0f;
|
||||
uint16_t b = 0; // unknown (often 0)
|
||||
};
|
||||
|
||||
struct EventStreamDecodeResult {
|
||||
size_t recordSize = 16; // 16 or 12 (for now)
|
||||
int alignment = 0; // best alignment within [0,recordSize-1]
|
||||
size_t eventCount = 0; // decoded event count at best alignment (bounded)
|
||||
double padZeroRatio = 0.0; // fraction of padding fields that are zero (0..1)
|
||||
int score = 0; // heuristic score for ranking
|
||||
};
|
||||
|
||||
EventStreamDecodeResult TryDecodeEventStream(const std::vector<uint8_t>& bytes, size_t start, size_t end);
|
||||
|
||||
// Like TryDecodeEventStream, but forces recordSize=12 or 16.
|
||||
EventStreamDecodeResult TryDecodeEventStreamFixed(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t recordSize);
|
||||
|
||||
// Decodes using a fixed record size (big endian).
|
||||
// recordSize=16: u16 id, f32 timestamp, u16 a, u16 type, f32 value, u16 b
|
||||
// recordSize=12: f32 timestamp, u32 type, f32 value
|
||||
bool DecodeEventStream(
|
||||
const std::vector<uint8_t>& bytes,
|
||||
size_t start,
|
||||
size_t end,
|
||||
size_t recordSize,
|
||||
std::vector<GameEvent>* out,
|
||||
std::string* err);
|
||||
|
||||
} // namespace gc
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
struct GcUvCell {
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
};
|
||||
|
||||
struct GcEffectSprite {
|
||||
uint16_t uvRecord = 0;
|
||||
int frame = 0;
|
||||
glm::vec3 offsetPixels{0.0f};
|
||||
glm::vec4 color{1.0f};
|
||||
glm::vec2 scale{1.0f};
|
||||
float rotationDegrees = 0.0f;
|
||||
};
|
||||
|
||||
// Interpreter for the sprite subset of the original common effect format.
|
||||
// It follows game471 FUN_005f2030/FUN_005f2250/FUN_005f23f0 and is enough to
|
||||
// reproduce control-helper effects 61..69, including parent rotation nodes.
|
||||
class GcTargetEffectBank {
|
||||
public:
|
||||
bool load(const std::string& efcPath, const std::string& uvPath, std::string* error = nullptr);
|
||||
int lifetime(int effectId) const;
|
||||
std::vector<GcEffectSprite> evaluate(int effectId, float tick, int uvRecordBase = 0) const;
|
||||
const GcUvCell* uvCell(uint16_t record, int frame) const;
|
||||
int uvTexture(uint16_t record) const;
|
||||
|
||||
private:
|
||||
struct Key {
|
||||
uint16_t time = 0;
|
||||
uint8_t interpolation = 0;
|
||||
std::vector<float> values;
|
||||
};
|
||||
struct Track {
|
||||
uint16_t loopStart = 0;
|
||||
uint16_t loopEnd = 0;
|
||||
std::vector<Key> keys;
|
||||
};
|
||||
struct Child {
|
||||
uint8_t type = 0;
|
||||
uint16_t reference = 0xffff;
|
||||
bool inheritParent = false;
|
||||
std::array<Track, 5> tracks;
|
||||
};
|
||||
struct Effect {
|
||||
uint16_t lifetime = 0;
|
||||
std::vector<Child> children;
|
||||
};
|
||||
struct UvRecord {
|
||||
int16_t textureIndex = -1;
|
||||
std::vector<GcUvCell> cells;
|
||||
};
|
||||
|
||||
static std::vector<float> sampleTrack(const Track& track, float tick, bool* started);
|
||||
|
||||
std::vector<Effect> effects_;
|
||||
std::vector<UvRecord> uvRecords_;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct MtxTexture {
|
||||
uint32_t offset = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t bitsPerPixel = 0;
|
||||
uint32_t fourCC = 0;
|
||||
};
|
||||
|
||||
struct MtxArchive {
|
||||
std::vector<MtxTexture> textures;
|
||||
};
|
||||
|
||||
bool ParseMtxArchive(const std::vector<uint8_t>& bytes,
|
||||
MtxArchive* archive,
|
||||
std::string* error = nullptr);
|
||||
|
||||
// MTX stores a normal DDS surface with its four-byte magic replaced by an
|
||||
// internal field. This returns the exact embedded surface with "DDS " put
|
||||
// back, suitable for the existing DDS loader or external inspection tools.
|
||||
bool ExtractMtxTextureDds(const std::vector<uint8_t>& bytes,
|
||||
const MtxTexture& texture,
|
||||
std::vector<uint8_t>* dds,
|
||||
std::string* error = nullptr);
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace gc {
|
||||
|
||||
// NOTE: these IDs are based on static analysis of a limited sample set and may
|
||||
// need adjustments as we decode more charts.
|
||||
enum class NoteType : uint32_t {
|
||||
// --- System / Timing ---
|
||||
BpmChange = 0x00000000, // Value = BPM? Or a timing/speed scalar.
|
||||
Camera = 0x00000002, // Camera control (hypothesis)
|
||||
MeasureLine = 0x00000004, // Measure start / marker (hypothesis)
|
||||
BeatLine = 0x0000003C, // (60) Visual beat grid line (hypothesis)
|
||||
|
||||
// --- Basic Notes ---
|
||||
Tap = 0x00000029, // (41) HIT: regular tap (hypothesis)
|
||||
Critical = 0x0000002A, // (42) CRITICAL: double tap / star (hypothesis)
|
||||
|
||||
// --- Holds ---
|
||||
HoldStart = 0x0000002B, // (43) HOLD start (hypothesis)
|
||||
HoldEnd = 0x0000002C, // (44) HOLD end (hypothesis)
|
||||
DualHold = 0x0000002F, // (47) Dual hold (hypothesis)
|
||||
|
||||
// --- Slides (Value = Angle in Radians) ---
|
||||
Slide = 0x0000002D, // (45) Slide (hypothesis)
|
||||
DualSlide = 0x0000002E, // (46) Dual slide (hypothesis)
|
||||
SlideHold = 0x00000030, // (48) Slide + hold? (hypothesis)
|
||||
|
||||
// --- Special / Legacy (seen in 10pt8tion) ---
|
||||
Legacy_Tap = 0x00000100,
|
||||
Legacy_Slide = 0x00000A00,
|
||||
|
||||
// --- Unknown ---
|
||||
Unknown = 0xFFFFFFFFu,
|
||||
};
|
||||
|
||||
inline bool IsNote(uint32_t type) {
|
||||
return (type >= 0x29u && type <= 0x30u) || type == 0x100u || type == 0xA00u;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "gc/RvbScene.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct RvbImageDraw {
|
||||
std::string imageSymbol;
|
||||
std::string instancePath;
|
||||
// top-left, top-right, bottom-left, bottom-right in the RVB canvas
|
||||
std::array<std::array<float, 2>, 4> corners{};
|
||||
std::array<float, 3> color{1.0f, 1.0f, 1.0f};
|
||||
float alpha = 1.0f;
|
||||
uint32_t depth = 0;
|
||||
};
|
||||
|
||||
// A Flash-style MovieClip path mapped to the labeled frame that should be
|
||||
// visible. Paths are the same absolute paths exported in PREP ("/" is the
|
||||
// root timeline). Clips not mentioned here remain on their first FRAM.
|
||||
struct RvbSnapshotState {
|
||||
std::unordered_map<std::string, std::string> frameByPath;
|
||||
bool includeRootOther = false;
|
||||
};
|
||||
|
||||
// Builds the display list produced by the first frame of the root timeline
|
||||
// and the first frame of every placed MovieClip. This is the exact static
|
||||
// base of the original scene; later timeline frames and ActionScript state
|
||||
// changes can be layered on top as their opcodes are recovered.
|
||||
bool BuildRvbInitialSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error = nullptr);
|
||||
|
||||
// Builds the display list at selected labeled frames. RVB FRAM records are
|
||||
// deltas, so this applies PLC3/RMOV commands in order through each requested
|
||||
// frame instead of treating a frame as a self-contained draw list.
|
||||
bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
const RvbSnapshotState& state,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error = nullptr);
|
||||
|
||||
// Builds an exported/linkage MovieClip without requiring it to be placed on
|
||||
// the root timeline. game471.exe uses this path for the twelve dynamically
|
||||
// instantiated select-music rows (mc_music_link / mc_index_link).
|
||||
bool BuildRvbSymbolSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
const std::string& symbolName,
|
||||
const RvbSnapshotState& state,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error = nullptr);
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct RvbChunk {
|
||||
std::string tag;
|
||||
uint32_t offset = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
// A PREP entry binds an animation/action name to an instance path in the
|
||||
// exported menu scene. These names are what game471.exe uses to drive the
|
||||
// select-music and difficulty state machines.
|
||||
struct RvbBinding {
|
||||
std::string action;
|
||||
std::string instancePath;
|
||||
};
|
||||
|
||||
struct RvbImageResource {
|
||||
std::string fileName;
|
||||
std::string symbolName;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
};
|
||||
|
||||
struct RvbExport {
|
||||
std::string definitionName;
|
||||
std::string linkageName;
|
||||
};
|
||||
|
||||
struct RvbNode {
|
||||
std::string tag;
|
||||
uint32_t offset = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t localDataOffset = 0;
|
||||
uint32_t localDataSize = 0;
|
||||
std::vector<RvbNode> children;
|
||||
};
|
||||
|
||||
struct RvbScene {
|
||||
uint8_t framesPerSecond = 0;
|
||||
uint16_t sourceWidth = 0;
|
||||
uint16_t sourceHeight = 0;
|
||||
std::vector<RvbChunk> chunks;
|
||||
std::vector<RvbBinding> bindings;
|
||||
std::vector<RvbImageResource> images;
|
||||
std::vector<RvbExport> exports;
|
||||
std::vector<RvbNode> roots;
|
||||
};
|
||||
|
||||
bool ParseRvbScene(const std::vector<uint8_t>& bytes,
|
||||
RvbScene* scene,
|
||||
std::string* error = nullptr);
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
// One 0xb4-byte runtime entry created by game471.exe's stage_param.dat loader.
|
||||
// Strings in the file are byte-length-prefixed and retain the game's source
|
||||
// encoding (mostly CP932 for localized text).
|
||||
struct StageCatalogEntry {
|
||||
uint32_t id = 0;
|
||||
std::string title;
|
||||
std::string imageKey;
|
||||
std::string artist;
|
||||
std::string source;
|
||||
std::string sortKey;
|
||||
uint8_t genre = 0;
|
||||
std::string duration;
|
||||
std::array<uint8_t, 4> difficultyRatings{};
|
||||
std::string bpm;
|
||||
// Per-difficulty percentage levels passed to LoadStageBGM for the two
|
||||
// continuously synchronized stage stems.
|
||||
std::array<uint8_t, 4> bgmVolumes{};
|
||||
std::array<uint8_t, 4> shotVolumes{};
|
||||
std::array<uint32_t, 3> timingValues{};
|
||||
std::array<uint8_t, 2> unknown60{};
|
||||
std::string bgmBase;
|
||||
std::array<std::string, 4> chartGroup0{};
|
||||
std::array<std::string, 4> chartSuffixes{};
|
||||
std::array<std::string, 4> chartIds{};
|
||||
std::string unknown9c;
|
||||
uint32_t unknownA0 = 0;
|
||||
std::array<uint8_t, 2> unknownA4{};
|
||||
std::string unknownAc;
|
||||
uint8_t unknownB0 = 0;
|
||||
};
|
||||
|
||||
bool ParseStageCatalog(const std::vector<uint8_t>& bytes,
|
||||
std::vector<StageCatalogEntry>* entries,
|
||||
std::string* error = nullptr);
|
||||
|
||||
const StageCatalogEntry* FindStageCatalogEntryByChart(
|
||||
const std::vector<StageCatalogEntry>& entries,
|
||||
const std::string& chartId);
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef OPENROLLER_GC_STAGEDAT_HPP
|
||||
#define OPENROLLER_GC_STAGEDAT_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct Section {
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
};
|
||||
|
||||
struct StageDat {
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
uint32_t headerSize = 0;
|
||||
std::vector<uint32_t> headerWords;
|
||||
std::vector<size_t> offsets; // sorted unique offsets within file
|
||||
std::vector<Section> sections; // derived from offsets
|
||||
|
||||
static bool LoadFromFile(const std::string& path, StageDat& out, std::string* err);
|
||||
};
|
||||
|
||||
} // namespace gc
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#ifndef OPENROLLER_GC_STAGEPATTERN_HPP
|
||||
#define OPENROLLER_GC_STAGEPATTERN_HPP
|
||||
|
||||
#include "gc/StageDat.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct Color {
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 0;
|
||||
};
|
||||
|
||||
struct StageHeader {
|
||||
uint32_t stageCfg = 0;
|
||||
uint32_t trackDrawDist = 0;
|
||||
uint32_t track = 0;
|
||||
uint32_t notes = 0;
|
||||
uint32_t camera = 0;
|
||||
uint32_t particles = 0;
|
||||
uint32_t visualizer = 0;
|
||||
uint32_t unk1 = 0;
|
||||
uint32_t colors = 0;
|
||||
uint32_t objects = 0;
|
||||
uint32_t unk2 = 0;
|
||||
uint32_t colors2 = 0;
|
||||
uint32_t unk3 = 0;
|
||||
};
|
||||
|
||||
struct BpmChange {
|
||||
uint32_t timeMs = 0;
|
||||
uint32_t bpm = 0;
|
||||
};
|
||||
|
||||
struct NoteSetting {
|
||||
uint32_t timeMs = 0;
|
||||
// TuneTimingData::GetTime: 1 is an absolute millisecond value, 3 uses
|
||||
// the spacing to the next note, and every other mode scales by beat_ms.
|
||||
uint32_t mode = 0;
|
||||
float value = 0.0f;
|
||||
};
|
||||
|
||||
struct StageConfig {
|
||||
float endTime1 = 0.0f;
|
||||
float endTime2 = 0.0f;
|
||||
float outroTime = 0.0f;
|
||||
std::vector<BpmChange> bpmChanges;
|
||||
std::array<std::vector<NoteSetting>, 4> noteSettings;
|
||||
std::string chartName;
|
||||
std::string chartName2;
|
||||
std::string bgmName;
|
||||
std::string shotName;
|
||||
float backwardsDrawDist = 0.0f;
|
||||
float forwardDrawDist = 0.0f;
|
||||
Color trackAheadColor;
|
||||
Color trackBehindColor;
|
||||
uint8_t audioOffset = 0;
|
||||
float visualOffset = 0.0f;
|
||||
Color unkColor;
|
||||
};
|
||||
|
||||
struct DrawDistancePoint {
|
||||
uint32_t timeMs = 0;
|
||||
float distance = 0.0f;
|
||||
};
|
||||
|
||||
struct TrackPiece {
|
||||
uint32_t timeMs = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
};
|
||||
|
||||
// Type byte used by the original arcade stage-note record. Keep this
|
||||
// distinct from the legacy editor-side NoteType in include/gc/NoteTypes.hpp,
|
||||
// whose numeric IDs describe another interchange format.
|
||||
enum class StageNoteType : uint8_t {
|
||||
None = 0,
|
||||
Normal = 1,
|
||||
Flick = 2,
|
||||
Hold = 3,
|
||||
Scratch = 4,
|
||||
Beat = 5,
|
||||
MerryGoRound = 6,
|
||||
Hidden = 7,
|
||||
Hidden2 = 8,
|
||||
Critical = 9,
|
||||
SlideHold = 10,
|
||||
SlideCounter = 11,
|
||||
Turn = 12,
|
||||
Spin = 13,
|
||||
Finish = 14,
|
||||
DualHold = 15,
|
||||
};
|
||||
|
||||
const char* NoteTypeName(uint8_t rawType);
|
||||
|
||||
// Wire layout recovered from game471.exe: FUN_005ea800 reads exactly 99 bytes
|
||||
// per note. Field semantics remain neutral until their consumers are mapped.
|
||||
struct StageNote {
|
||||
static constexpr size_t kRecordSize = 99;
|
||||
|
||||
uint32_t timeMs = 0;
|
||||
StageNoteType type = StageNoteType::None;
|
||||
uint8_t typeOverride = 0; // original runtime forces type=1 when non-zero
|
||||
std::array<int16_t, 9> params16{};
|
||||
uint8_t flag24 = 0;
|
||||
std::array<float, 3> params25{};
|
||||
uint8_t flag37 = 0;
|
||||
uint8_t flag38 = 0;
|
||||
std::array<float, 4> params39{};
|
||||
std::array<uint32_t, 3> params55{};
|
||||
float param67 = 0.0f;
|
||||
uint32_t param71 = 0;
|
||||
std::array<float, 5> params75{};
|
||||
uint32_t param95 = 0;
|
||||
};
|
||||
|
||||
struct CameraPoint {
|
||||
uint32_t timeMs = 0;
|
||||
uint8_t aMode = 0;
|
||||
uint8_t fMode = 0;
|
||||
float dist = 0.0f;
|
||||
float rotationA[2] = {0.0f, 0.0f};
|
||||
float originOff[3] = {0.0f, 0.0f, 0.0f};
|
||||
uint8_t projType = 0;
|
||||
float fieldFar[3] = {0.0f, 0.0f, 0.0f};
|
||||
float fieldNear[3] = {0.0f, 0.0f, 0.0f};
|
||||
float rotationB = 0.0f;
|
||||
};
|
||||
|
||||
struct ParticlePoint {
|
||||
static constexpr size_t kRecordSize = 44;
|
||||
|
||||
uint32_t timeMs = 0;
|
||||
uint32_t enabled = 0;
|
||||
uint32_t shape = 0;
|
||||
uint32_t texture = 0;
|
||||
Color color;
|
||||
float velocity[3] = {0.0f, 0.0f, 0.0f};
|
||||
float repeatMeasure = 0.0f;
|
||||
float lifespanMeasure = 0.0f;
|
||||
uint32_t groupShapeSize = 0;
|
||||
};
|
||||
|
||||
struct VisualizerPoint {
|
||||
static constexpr size_t kRecordSize = 12;
|
||||
|
||||
uint32_t timeMs = 0;
|
||||
uint32_t type = 0;
|
||||
Color color;
|
||||
};
|
||||
|
||||
struct BackgroundColorPoint {
|
||||
static constexpr size_t kRecordSize = 22;
|
||||
|
||||
uint32_t timeMs = 0;
|
||||
Color topRight;
|
||||
Color topLeft;
|
||||
Color bottomRight;
|
||||
Color bottomLeft;
|
||||
bool interpolateToNext = false;
|
||||
bool audioReactive = false;
|
||||
};
|
||||
|
||||
struct VisibilityPoint {
|
||||
uint32_t timeMs = 0;
|
||||
bool fadeOut = false;
|
||||
bool fadeIn = false;
|
||||
bool visible = false;
|
||||
};
|
||||
|
||||
struct TransformPoint {
|
||||
uint32_t timeMs = 0;
|
||||
bool tweenTowards = false;
|
||||
bool tweenAway = false;
|
||||
float value[3] = {0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
struct ObjectColorPoint {
|
||||
uint32_t timeMs = 0;
|
||||
bool tweenTowards = false;
|
||||
bool tweenAway = false;
|
||||
Color color;
|
||||
};
|
||||
|
||||
struct StageObject {
|
||||
uint32_t model = 0;
|
||||
uint32_t fragmentShader = 0;
|
||||
bool wireframe = false;
|
||||
bool flashing = false;
|
||||
bool unknownFlag = false;
|
||||
float position[3] = {0.0f, 0.0f, 0.0f};
|
||||
float scale[3] = {1.0f, 1.0f, 1.0f};
|
||||
float rotation[3] = {0.0f, 0.0f, 0.0f};
|
||||
float color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
float unknownVector[3] = {0.0f, 0.0f, 0.0f};
|
||||
// game471 supplemental object table (stage format > 0x29ce). A child is
|
||||
// rendered with parent * child transform and component-wise parent color.
|
||||
int32_t parentIndex = -1;
|
||||
std::vector<VisibilityPoint> visibility;
|
||||
std::vector<TransformPoint> movement;
|
||||
std::vector<TransformPoint> scaling;
|
||||
std::vector<TransformPoint> rotations;
|
||||
std::vector<ObjectColorPoint> colorChanges;
|
||||
};
|
||||
|
||||
struct ParsedStagePattern {
|
||||
StageHeader header;
|
||||
StageConfig config;
|
||||
std::vector<DrawDistancePoint> drawDistances;
|
||||
std::vector<TrackPiece> track;
|
||||
std::vector<std::string> noteNames;
|
||||
std::vector<StageNote> notes;
|
||||
std::vector<CameraPoint> cameras;
|
||||
std::vector<ParticlePoint> particles;
|
||||
std::vector<VisualizerPoint> visualizer;
|
||||
std::vector<BackgroundColorPoint> backgroundColors;
|
||||
std::vector<std::string> modelNames;
|
||||
std::vector<std::string> fragmentShaderNames;
|
||||
std::vector<StageObject> objects;
|
||||
};
|
||||
|
||||
bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err);
|
||||
|
||||
} // namespace gc
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef OPENROLLER_GC_TUMOMODEL_HPP
|
||||
#define OPENROLLER_GC_TUMOMODEL_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
|
||||
struct TumoVertex {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
};
|
||||
|
||||
struct TumoGeometry {
|
||||
std::vector<TumoVertex> triangles;
|
||||
std::vector<TumoVertex> solidLines;
|
||||
std::vector<TumoVertex> wireframeLines;
|
||||
};
|
||||
|
||||
// Decodes the common one-mesh TUMO layout 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);
|
||||
|
||||
} // namespace gc
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "gc/EventStream.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace gc {
|
||||
|
||||
static uint16_t u16be(const std::vector<uint8_t>& b, size_t off) {
|
||||
return static_cast<uint16_t>((static_cast<uint16_t>(b[off + 0]) << 8) |
|
||||
(static_cast<uint16_t>(b[off + 1]) << 0));
|
||||
}
|
||||
|
||||
static uint32_t u32be(const std::vector<uint8_t>& b, size_t off) {
|
||||
return (static_cast<uint32_t>(b[off + 0]) << 24) |
|
||||
(static_cast<uint32_t>(b[off + 1]) << 16) |
|
||||
(static_cast<uint32_t>(b[off + 2]) << 8) |
|
||||
(static_cast<uint32_t>(b[off + 3]) << 0);
|
||||
}
|
||||
|
||||
static float f32be(const std::vector<uint8_t>& b, size_t off) {
|
||||
const uint32_t u = u32be(b, off);
|
||||
float f = 0.0f;
|
||||
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
|
||||
std::memcpy(&f, &u, sizeof(float));
|
||||
return f;
|
||||
}
|
||||
|
||||
static bool isPlausibleTs(float ts) {
|
||||
// Timestamps appear to be a monotonic progress/beat count float.
|
||||
return std::isfinite(ts) && ts >= -1.0f && ts <= 1.0e6f;
|
||||
}
|
||||
|
||||
static bool isPlausibleVal(float v) {
|
||||
return std::isfinite(v) && std::fabs(v) <= 1.0e7f;
|
||||
}
|
||||
|
||||
static int scoreStream16(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t* outEventCount, double* outPadZeroRatio) {
|
||||
if (end <= start) return 0;
|
||||
size_t len = end - start;
|
||||
len -= (len % 16);
|
||||
if (len < 16 * 10) return 0;
|
||||
|
||||
// Score only first N events for speed and to avoid false confidence on huge blocks.
|
||||
const size_t n = std::min<size_t>(len / 16, 200);
|
||||
int score = 0;
|
||||
size_t padFields = 0;
|
||||
size_t padZeros = 0;
|
||||
size_t plausible = 0;
|
||||
size_t typeNonZero = 0;
|
||||
size_t tsNonZero = 0;
|
||||
size_t tsChanges = 0;
|
||||
size_t tsBackwards = 0;
|
||||
float prevTs = 0.0f;
|
||||
bool havePrevTs = false;
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const size_t off = start + i * 16;
|
||||
const uint16_t id = u16be(bytes, off + 0);
|
||||
const float ts = f32be(bytes, off + 2);
|
||||
const uint16_t a = u16be(bytes, off + 6);
|
||||
const uint16_t type = u16be(bytes, off + 8);
|
||||
const float val = f32be(bytes, off + 10);
|
||||
const uint16_t b = u16be(bytes, off + 14);
|
||||
|
||||
padFields += 2;
|
||||
if (a == 0) padZeros++;
|
||||
if (b == 0) padZeros++;
|
||||
|
||||
if (id == 0) score += 1;
|
||||
if (a == 0) score += 2;
|
||||
if (b == 0) score += 2;
|
||||
|
||||
if (type != 0 && type != 0xFFFF) score += 1;
|
||||
if (type > 0 && type < 0x4000) score += 1;
|
||||
if (type != 0 && type != 0xFFFF) typeNonZero++;
|
||||
|
||||
if (isPlausibleTs(ts)) {
|
||||
score += 2;
|
||||
plausible++;
|
||||
}
|
||||
if (isPlausibleVal(val)) {
|
||||
score += 1;
|
||||
}
|
||||
|
||||
if (ts != 0.0f) tsNonZero++;
|
||||
if (havePrevTs) {
|
||||
if (ts < prevTs) tsBackwards++;
|
||||
if (std::fabs(ts - prevTs) > 1.0e-6f) tsChanges++;
|
||||
} else {
|
||||
havePrevTs = true;
|
||||
}
|
||||
prevTs = ts;
|
||||
}
|
||||
|
||||
if (outEventCount) *outEventCount = n;
|
||||
if (outPadZeroRatio) *outPadZeroRatio = padFields ? (static_cast<double>(padZeros) / static_cast<double>(padFields)) : 0.0;
|
||||
|
||||
// Penalize if almost nothing looks plausible.
|
||||
if (plausible < (n / 4)) score /= 2;
|
||||
|
||||
// Prefer streams that look like a real timeline (changing, mostly monotonic),
|
||||
// and avoid giant zero-filled blocks that otherwise look "plausible".
|
||||
if (typeNonZero < (n / 4)) score /= 2;
|
||||
if (tsNonZero < (n / 4)) score /= 2;
|
||||
if (tsChanges < (n / 8)) score /= 2;
|
||||
if (tsBackwards > (n / 20)) score /= 2;
|
||||
return score;
|
||||
}
|
||||
|
||||
static int scoreStream12(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t* outEventCount) {
|
||||
if (end <= start) return 0;
|
||||
size_t len = end - start;
|
||||
len -= (len % 12);
|
||||
if (len < 12 * 10) return 0;
|
||||
|
||||
const size_t n = std::min<size_t>(len / 12, 200);
|
||||
int score = 0;
|
||||
size_t plausible = 0;
|
||||
size_t typeNonZero = 0;
|
||||
size_t tsNonZero = 0;
|
||||
size_t tsChanges = 0;
|
||||
size_t tsBackwards = 0;
|
||||
float prevTs = 0.0f;
|
||||
bool havePrevTs = false;
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const size_t off = start + i * 12;
|
||||
const float ts = f32be(bytes, off + 0);
|
||||
const uint32_t type = u32be(bytes, off + 4);
|
||||
const float val = f32be(bytes, off + 8);
|
||||
|
||||
if (type != 0 && type != 0xFFFFFFFFu) score += 3;
|
||||
if (type != 0 && type != 0xFFFFFFFFu) typeNonZero++;
|
||||
|
||||
if (isPlausibleTs(ts)) {
|
||||
score += 2;
|
||||
plausible++;
|
||||
}
|
||||
if (isPlausibleVal(val)) score += 1;
|
||||
|
||||
if (ts != 0.0f) tsNonZero++;
|
||||
if (havePrevTs) {
|
||||
if (ts < prevTs) tsBackwards++;
|
||||
if (std::fabs(ts - prevTs) > 1.0e-6f) tsChanges++;
|
||||
} else {
|
||||
havePrevTs = true;
|
||||
}
|
||||
prevTs = ts;
|
||||
}
|
||||
|
||||
if (outEventCount) *outEventCount = n;
|
||||
|
||||
if (plausible < (n / 4)) score /= 2;
|
||||
if (typeNonZero < (n / 4)) score /= 2;
|
||||
if (tsNonZero < (n / 4)) score /= 2;
|
||||
if (tsChanges < (n / 8)) score /= 2;
|
||||
if (tsBackwards > (n / 20)) score /= 2;
|
||||
return score;
|
||||
}
|
||||
|
||||
EventStreamDecodeResult TryDecodeEventStreamFixed(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t recordSize) {
|
||||
EventStreamDecodeResult best;
|
||||
best.recordSize = recordSize;
|
||||
best.alignment = 0;
|
||||
best.score = 0;
|
||||
best.eventCount = 0;
|
||||
best.padZeroRatio = 0.0;
|
||||
|
||||
if (end <= start) return best;
|
||||
if (recordSize != 12 && recordSize != 16) return best;
|
||||
|
||||
const size_t maxAlign = recordSize;
|
||||
for (size_t align = 0; align < maxAlign; align++) {
|
||||
if (start + align >= end) break;
|
||||
if (recordSize == 16) {
|
||||
size_t eventCount = 0;
|
||||
double padZeroRatio = 0.0;
|
||||
const int s = scoreStream16(bytes, start + align, end, &eventCount, &padZeroRatio);
|
||||
if (s > best.score) {
|
||||
best.recordSize = 16;
|
||||
best.score = s;
|
||||
best.alignment = static_cast<int>(align);
|
||||
best.eventCount = eventCount;
|
||||
best.padZeroRatio = padZeroRatio;
|
||||
}
|
||||
} else { // 12
|
||||
size_t eventCount = 0;
|
||||
const int s = scoreStream12(bytes, start + align, end, &eventCount);
|
||||
if (s > best.score) {
|
||||
best.recordSize = 12;
|
||||
best.score = s;
|
||||
best.alignment = static_cast<int>(align);
|
||||
best.eventCount = eventCount;
|
||||
best.padZeroRatio = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
EventStreamDecodeResult TryDecodeEventStream(const std::vector<uint8_t>& bytes, size_t start, size_t end) {
|
||||
EventStreamDecodeResult best16 = TryDecodeEventStreamFixed(bytes, start, end, 16);
|
||||
EventStreamDecodeResult best12 = TryDecodeEventStreamFixed(bytes, start, end, 12);
|
||||
return (best12.score > best16.score) ? best12 : best16;
|
||||
}
|
||||
|
||||
bool DecodeEventStream(
|
||||
const std::vector<uint8_t>& bytes,
|
||||
size_t start,
|
||||
size_t end,
|
||||
size_t recordSize,
|
||||
std::vector<GameEvent>* out,
|
||||
std::string* err) {
|
||||
if (!out) return false;
|
||||
out->clear();
|
||||
|
||||
if (end < start) {
|
||||
if (err) *err = "end < start";
|
||||
return false;
|
||||
}
|
||||
if (recordSize != 12 && recordSize != 16) {
|
||||
if (err) *err = "unsupported recordSize";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = end - start;
|
||||
len -= (len % recordSize);
|
||||
if (len == 0) return true;
|
||||
if (start + len > bytes.size()) {
|
||||
if (err) *err = "range out of bounds";
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t n = len / recordSize;
|
||||
out->reserve(n);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const size_t off = start + i * recordSize;
|
||||
GameEvent e;
|
||||
if (recordSize == 16) {
|
||||
e.id = u16be(bytes, off + 0);
|
||||
e.timestamp = f32be(bytes, off + 2);
|
||||
e.a = u16be(bytes, off + 6);
|
||||
e.type = static_cast<uint32_t>(u16be(bytes, off + 8));
|
||||
e.value = f32be(bytes, off + 10);
|
||||
e.b = u16be(bytes, off + 14);
|
||||
} else { // 12
|
||||
e.id = 0;
|
||||
e.timestamp = f32be(bytes, off + 0);
|
||||
e.a = 0;
|
||||
e.type = u32be(bytes, off + 4);
|
||||
e.value = f32be(bytes, off + 8);
|
||||
e.b = 0;
|
||||
}
|
||||
out->push_back(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "gc/GcTargetEffect.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
|
||||
bool readFile(const std::string& path, std::vector<uint8_t>* out) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return false;
|
||||
file.seekg(0, std::ios::end);
|
||||
const std::streamoff length = file.tellg();
|
||||
if (length < 0) return false;
|
||||
file.seekg(0, std::ios::beg);
|
||||
out->assign(static_cast<size_t>(length), 0);
|
||||
if (!out->empty()) file.read(reinterpret_cast<char*>(out->data()), length);
|
||||
return static_cast<bool>(file) || file.eof();
|
||||
}
|
||||
|
||||
uint16_t u16be(const std::vector<uint8_t>& data, size_t offset) {
|
||||
return static_cast<uint16_t>((static_cast<uint16_t>(data[offset]) << 8) | data[offset + 1]);
|
||||
}
|
||||
|
||||
uint32_t u32be(const std::vector<uint8_t>& data, size_t offset) {
|
||||
return (static_cast<uint32_t>(data[offset]) << 24) |
|
||||
(static_cast<uint32_t>(data[offset + 1]) << 16) |
|
||||
(static_cast<uint32_t>(data[offset + 2]) << 8) |
|
||||
static_cast<uint32_t>(data[offset + 3]);
|
||||
}
|
||||
|
||||
float f32be(const std::vector<uint8_t>& data, size_t offset) {
|
||||
const uint32_t bits = u32be(data, offset);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<float> GcTargetEffectBank::sampleTrack(const Track& track, float tick, bool* started) {
|
||||
if (started) *started = false;
|
||||
if (track.keys.empty()) return {};
|
||||
|
||||
float sample = tick;
|
||||
const int loopLength = static_cast<int>(track.loopEnd) - static_cast<int>(track.loopStart);
|
||||
if (loopLength > 0 && sample >= track.loopStart) {
|
||||
sample = std::fmod(sample - track.loopStart, static_cast<float>(loopLength)) + track.loopStart;
|
||||
}
|
||||
if (sample < track.keys.front().time) return track.keys.front().values;
|
||||
if (started) *started = true;
|
||||
|
||||
size_t index = 0;
|
||||
while (index + 1 < track.keys.size() && sample >= track.keys[index + 1].time) ++index;
|
||||
const auto& current = track.keys[index];
|
||||
if (index + 1 >= track.keys.size() || current.interpolation == 0) return current.values;
|
||||
|
||||
const auto& next = track.keys[index + 1];
|
||||
const float span = static_cast<float>(next.time - current.time);
|
||||
const float amount = span > 0.0f ? std::clamp((sample - current.time) / span, 0.0f, 1.0f) : 0.0f;
|
||||
std::vector<float> result = current.values;
|
||||
for (size_t i = 0; i < result.size() && i < next.values.size(); ++i) {
|
||||
result[i] = current.values[i] + (next.values[i] - current.values[i]) * amount;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool GcTargetEffectBank::load(const std::string& efcPath, const std::string& uvPath, std::string* error) {
|
||||
std::vector<uint8_t> uv;
|
||||
std::vector<uint8_t> efc;
|
||||
if (!readFile(uvPath, &uv) || uv.size() < 6) {
|
||||
if (error) *error = "could not read uvdata.dat";
|
||||
return false;
|
||||
}
|
||||
if (!readFile(efcPath, &efc) || efc.size() < 6) {
|
||||
if (error) *error = "could not read efcdata.dat";
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t uvCount = u16be(uv, 4);
|
||||
if (6u + static_cast<size_t>(uvCount) * 4u > uv.size()) {
|
||||
if (error) *error = "invalid uvdata.dat offset table";
|
||||
return false;
|
||||
}
|
||||
uvRecords_.assign(uvCount, {});
|
||||
for (uint16_t record = 0; record < uvCount; ++record) {
|
||||
const size_t begin = u32be(uv, 6 + record * 4);
|
||||
const size_t end = record + 1 < uvCount ? u32be(uv, 6 + (record + 1) * 4) : uv.size();
|
||||
if (begin + 4 > end || end > uv.size()) continue;
|
||||
const uint16_t cellCount = u16be(uv, begin + 2);
|
||||
if (begin + 4u + static_cast<size_t>(cellCount) * 8u > end) continue;
|
||||
UvRecord& decoded = uvRecords_[record];
|
||||
decoded.textureIndex = static_cast<int16_t>(u16be(uv, begin));
|
||||
auto& cells = decoded.cells;
|
||||
cells.reserve(cellCount);
|
||||
for (uint16_t cell = 0; cell < cellCount; ++cell) {
|
||||
const size_t at = begin + 4 + cell * 8;
|
||||
cells.push_back({u16be(uv, at), u16be(uv, at + 2), u16be(uv, at + 4), u16be(uv, at + 6)});
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr int firstTrackDimensions[] = {1, 0, 7, 4, 13, 3, 2};
|
||||
static constexpr int otherTrackDimensions[] = {0, 3, 4, 2, 1};
|
||||
const uint16_t effectCount = u16be(efc, 4);
|
||||
if (6u + static_cast<size_t>(effectCount) * 4u > efc.size()) {
|
||||
if (error) *error = "invalid efcdata.dat offset table";
|
||||
return false;
|
||||
}
|
||||
effects_.assign(effectCount, {});
|
||||
for (uint16_t effectIndex = 0; effectIndex < effectCount; ++effectIndex) {
|
||||
const size_t begin = u32be(efc, 6 + effectIndex * 4);
|
||||
const size_t end = effectIndex + 1 < effectCount ? u32be(efc, 6 + (effectIndex + 1) * 4) : efc.size();
|
||||
if (begin + 3 > end || end > efc.size()) continue;
|
||||
Effect& effect = effects_[effectIndex];
|
||||
effect.lifetime = u16be(efc, begin);
|
||||
const uint8_t childCount = efc[begin + 2];
|
||||
if (begin + 3u + static_cast<size_t>(childCount) * 2u > end) continue;
|
||||
effect.children.reserve(childCount);
|
||||
for (uint8_t childIndex = 0; childIndex < childCount; ++childIndex) {
|
||||
const size_t child = begin + u16be(efc, begin + 3 + childIndex * 2);
|
||||
if (child + 15 > end) continue;
|
||||
Child decoded;
|
||||
decoded.type = efc[child];
|
||||
decoded.reference = u16be(efc, child + 1);
|
||||
decoded.inheritParent = efc[child + 4] != 0;
|
||||
for (int trackIndex = 0; trackIndex < 5; ++trackIndex) {
|
||||
const size_t trackAt = child + u16be(efc, child + 5 + trackIndex * 2);
|
||||
if (trackAt + 6 > end) continue;
|
||||
Track& track = decoded.tracks[trackIndex];
|
||||
const uint16_t keyCount = u16be(efc, trackAt);
|
||||
track.loopStart = u16be(efc, trackAt + 2);
|
||||
track.loopEnd = u16be(efc, trackAt + 4);
|
||||
const int dimensions = trackIndex == 0
|
||||
? (decoded.type < sizeof(firstTrackDimensions) / sizeof(firstTrackDimensions[0])
|
||||
? firstTrackDimensions[decoded.type] : 0)
|
||||
: otherTrackDimensions[trackIndex];
|
||||
const size_t stride = 3u + static_cast<size_t>(dimensions) * 4u;
|
||||
if (dimensions <= 0 || trackAt + 6u + static_cast<size_t>(keyCount) * stride > end) continue;
|
||||
track.keys.reserve(keyCount);
|
||||
for (uint16_t keyIndex = 0; keyIndex < keyCount; ++keyIndex) {
|
||||
const size_t keyAt = trackAt + 6 + keyIndex * stride;
|
||||
Key key;
|
||||
key.time = u16be(efc, keyAt);
|
||||
key.interpolation = efc[keyAt + 2];
|
||||
key.values.reserve(dimensions);
|
||||
for (int value = 0; value < dimensions; ++value) {
|
||||
key.values.push_back(f32be(efc, keyAt + 3 + value * 4));
|
||||
}
|
||||
track.keys.push_back(std::move(key));
|
||||
}
|
||||
}
|
||||
effect.children.push_back(std::move(decoded));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int GcTargetEffectBank::lifetime(int effectId) const {
|
||||
return effectId >= 0 && static_cast<size_t>(effectId) < effects_.size() ? effects_[effectId].lifetime : 0;
|
||||
}
|
||||
|
||||
std::vector<GcEffectSprite> GcTargetEffectBank::evaluate(int effectId, float tick, int uvRecordBase) const {
|
||||
std::vector<GcEffectSprite> result;
|
||||
if (effectId < 0 || static_cast<size_t>(effectId) >= effects_.size()) return result;
|
||||
|
||||
float parentRadians = 0.0f;
|
||||
bool haveParent = false;
|
||||
for (const Child& child : effects_[effectId].children) {
|
||||
bool firstTrackStarted = false;
|
||||
const std::vector<float> first = sampleTrack(child.tracks[0], tick, &firstTrackStarted);
|
||||
if (child.type == 0) {
|
||||
parentRadians = !first.empty() ? first[0] : 0.0f;
|
||||
haveParent = firstTrackStarted;
|
||||
continue;
|
||||
}
|
||||
if (child.type != 2 || child.reference == 0xffff || first.size() < 1 || !firstTrackStarted) continue;
|
||||
|
||||
GcEffectSprite sprite;
|
||||
const int resolvedRecord = static_cast<int>(child.reference) + uvRecordBase;
|
||||
if (resolvedRecord < 0 || resolvedRecord > 0xffff) continue;
|
||||
sprite.uvRecord = static_cast<uint16_t>(resolvedRecord);
|
||||
sprite.frame = static_cast<int>(std::lround(first[0]));
|
||||
const std::vector<float> position = sampleTrack(child.tracks[1], tick, nullptr);
|
||||
const std::vector<float> color = sampleTrack(child.tracks[2], tick, nullptr);
|
||||
const std::vector<float> scale = sampleTrack(child.tracks[3], tick, nullptr);
|
||||
const std::vector<float> rotation = sampleTrack(child.tracks[4], tick, nullptr);
|
||||
if (position.size() >= 3) sprite.offsetPixels = {position[0], position[1], position[2]};
|
||||
if (color.size() >= 4) sprite.color = {color[1], color[2], color[3], color[0]};
|
||||
if (scale.size() >= 2) sprite.scale = {scale[0], scale[1]};
|
||||
if (!rotation.empty()) sprite.rotationDegrees = rotation[0];
|
||||
|
||||
if (child.inheritParent && haveParent) {
|
||||
const float c = std::cos(parentRadians);
|
||||
const float s = std::sin(parentRadians);
|
||||
const glm::vec2 p(sprite.offsetPixels.x, sprite.offsetPixels.y);
|
||||
sprite.offsetPixels.x = c * p.x - s * p.y;
|
||||
sprite.offsetPixels.y = s * p.x + c * p.y;
|
||||
sprite.rotationDegrees += glm::degrees(parentRadians);
|
||||
}
|
||||
result.push_back(sprite);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const GcUvCell* GcTargetEffectBank::uvCell(uint16_t record, int frame) const {
|
||||
if (record >= uvRecords_.size() || frame < 0 ||
|
||||
static_cast<size_t>(frame) >= uvRecords_[record].cells.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &uvRecords_[record].cells[frame];
|
||||
}
|
||||
|
||||
int GcTargetEffectBank::uvTexture(uint16_t record) const {
|
||||
return record < uvRecords_.size() ? uvRecords_[record].textureIndex : -1;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "gc/MtxArchive.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace gc {
|
||||
namespace {
|
||||
|
||||
uint16_t readU16Le(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
return static_cast<uint16_t>(bytes[offset]) |
|
||||
static_cast<uint16_t>(bytes[offset + 1] << 8);
|
||||
}
|
||||
|
||||
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
return static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
bool fail(std::string* error, const std::string& message) {
|
||||
if (error) *error = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ParseMtxArchive(const std::vector<uint8_t>& bytes,
|
||||
MtxArchive* archive,
|
||||
std::string* error) {
|
||||
if (!archive) return fail(error, "null MTX archive output");
|
||||
*archive = {};
|
||||
if (bytes.size() < 32 || std::memcmp(bytes.data(), "MTX\0", 4) != 0) {
|
||||
return fail(error, "missing MTX header");
|
||||
}
|
||||
const uint32_t headerOffset = readU32Le(bytes, 8);
|
||||
if (headerOffset > bytes.size() - 16) return fail(error, "invalid MTX header offset");
|
||||
const uint32_t count = readU32Le(bytes, headerOffset);
|
||||
const uint32_t tableOffset = readU32Le(bytes, headerOffset + 4);
|
||||
const uint32_t stride = readU32Le(bytes, headerOffset + 8);
|
||||
const uint16_t nameBytes = readU16Le(bytes, headerOffset + 12);
|
||||
if (stride < 16 || count > std::numeric_limits<uint32_t>::max() / stride) {
|
||||
return fail(error, "invalid MTX entry table shape");
|
||||
}
|
||||
const uint64_t tableStart = static_cast<uint64_t>(headerOffset) + tableOffset;
|
||||
const uint64_t tableBytes = static_cast<uint64_t>(count) * stride;
|
||||
const uint64_t dataStart64 = tableStart + tableBytes + nameBytes;
|
||||
if (tableStart > bytes.size() || dataStart64 > bytes.size()) {
|
||||
return fail(error, "MTX entry table exceeds the file");
|
||||
}
|
||||
|
||||
size_t dataCursor = static_cast<size_t>(dataStart64);
|
||||
archive->textures.reserve(count);
|
||||
for (uint32_t index = 0; index < count; ++index) {
|
||||
const size_t entry = static_cast<size_t>(tableStart) + static_cast<size_t>(index) * stride;
|
||||
const uint32_t size = readU32Le(bytes, entry);
|
||||
if (size < 128 || size > bytes.size() - dataCursor) {
|
||||
return fail(error, "MTX texture payload exceeds the file");
|
||||
}
|
||||
// Apart from the first dword, each embedded payload is a standard
|
||||
// little-endian DDS_HEADER. game471.exe passes it directly to its DDS
|
||||
// reader after locating it with this same cumulative-size algorithm.
|
||||
if (readU32Le(bytes, dataCursor + 4) != 124 ||
|
||||
readU32Le(bytes, dataCursor + 76) != 32) {
|
||||
return fail(error, "MTX texture does not contain a DDS header");
|
||||
}
|
||||
MtxTexture texture;
|
||||
texture.offset = static_cast<uint32_t>(dataCursor);
|
||||
texture.size = size;
|
||||
texture.height = readU32Le(bytes, dataCursor + 12);
|
||||
texture.width = readU32Le(bytes, dataCursor + 16);
|
||||
texture.fourCC = readU32Le(bytes, dataCursor + 84);
|
||||
texture.bitsPerPixel = readU32Le(bytes, dataCursor + 88);
|
||||
archive->textures.push_back(texture);
|
||||
dataCursor += size;
|
||||
}
|
||||
if (dataCursor != bytes.size()) return fail(error, "MTX has trailing texture data");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExtractMtxTextureDds(const std::vector<uint8_t>& bytes,
|
||||
const MtxTexture& texture,
|
||||
std::vector<uint8_t>* dds,
|
||||
std::string* error) {
|
||||
if (!dds) return fail(error, "null DDS output");
|
||||
dds->clear();
|
||||
if (texture.size < 128 || texture.offset > bytes.size() ||
|
||||
texture.size > bytes.size() - texture.offset) {
|
||||
return fail(error, "MTX texture range is invalid");
|
||||
}
|
||||
dds->assign(bytes.begin() + texture.offset,
|
||||
bytes.begin() + texture.offset + texture.size);
|
||||
(*dds)[0] = 'D';
|
||||
(*dds)[1] = 'D';
|
||||
(*dds)[2] = 'S';
|
||||
(*dds)[3] = ' ';
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,413 @@
|
||||
#include "gc/RvbLayout.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace gc {
|
||||
namespace {
|
||||
|
||||
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
return static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
float readFloatLe(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
const uint32_t value = readU32Le(bytes, offset);
|
||||
float result = 0.0f;
|
||||
std::memcpy(&result, &value, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool readCString(const std::vector<uint8_t>& bytes, size_t* cursor,
|
||||
size_t end, std::string* value) {
|
||||
if (!cursor || !value || *cursor >= end || end > bytes.size()) return false;
|
||||
const auto begin = bytes.begin() + static_cast<std::ptrdiff_t>(*cursor);
|
||||
const auto last = bytes.begin() + static_cast<std::ptrdiff_t>(end);
|
||||
const auto zero = std::find(begin, last, uint8_t{0});
|
||||
if (zero == last) return false;
|
||||
value->assign(reinterpret_cast<const char*>(&*begin),
|
||||
static_cast<size_t>(zero - begin));
|
||||
*cursor += value->size() + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Matrix {
|
||||
float xx = 1.0f;
|
||||
float yy = 1.0f;
|
||||
float xy = 0.0f;
|
||||
float yx = 0.0f;
|
||||
float tx = 0.0f;
|
||||
float ty = 0.0f;
|
||||
};
|
||||
|
||||
Matrix multiply(const Matrix& a, const Matrix& b) {
|
||||
Matrix out;
|
||||
out.xx = a.xx * b.xx + a.xy * b.yx;
|
||||
out.xy = a.xx * b.xy + a.xy * b.yy;
|
||||
out.tx = a.xx * b.tx + a.xy * b.ty + a.tx;
|
||||
out.yx = a.yx * b.xx + a.yy * b.yx;
|
||||
out.yy = a.yx * b.xy + a.yy * b.yy;
|
||||
out.ty = a.yx * b.tx + a.yy * b.ty + a.ty;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<float, 2> transform(const Matrix& m, float x, float y) {
|
||||
return {m.xx * x + m.xy * y + m.tx, m.yx * x + m.yy * y + m.ty};
|
||||
}
|
||||
|
||||
Matrix nodeTransform(const std::vector<uint8_t>& bytes, const RvbNode& node) {
|
||||
for (const RvbNode& child : node.children) {
|
||||
if (child.tag != "TRN2" && child.tag != "TRAN") continue;
|
||||
if (child.localDataSize < 24) continue;
|
||||
const size_t p = child.localDataOffset;
|
||||
Matrix m;
|
||||
m.xx = readFloatLe(bytes, p + 0);
|
||||
m.yy = readFloatLe(bytes, p + 4);
|
||||
m.xy = readFloatLe(bytes, p + 8);
|
||||
m.yx = readFloatLe(bytes, p + 12);
|
||||
m.tx = readFloatLe(bytes, p + 16);
|
||||
m.ty = readFloatLe(bytes, p + 20);
|
||||
return m;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool hasNodeTransform(const RvbNode& node) {
|
||||
for (const RvbNode& child : node.children) {
|
||||
if ((child.tag == "TRN2" || child.tag == "TRAN") && child.localDataSize >= 24) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ColorTransform {
|
||||
float red = 1.0f;
|
||||
float green = 1.0f;
|
||||
float blue = 1.0f;
|
||||
float alpha = 1.0f;
|
||||
};
|
||||
|
||||
ColorTransform multiply(const ColorTransform& a, const ColorTransform& b) {
|
||||
return {a.red * b.red, a.green * b.green, a.blue * b.blue, a.alpha * b.alpha};
|
||||
}
|
||||
|
||||
bool nodeColor(const std::vector<uint8_t>& bytes, const RvbNode& node,
|
||||
ColorTransform* color) {
|
||||
if (!color) return false;
|
||||
for (const RvbNode& child : node.children) {
|
||||
if (child.tag != "COLT" || child.localDataSize < 20) continue;
|
||||
// RGBAColorTransform::read (game471.exe FUN_004d64b0) reads four
|
||||
// floats followed by four bytes. The animation node adds a leading
|
||||
// four-byte field, so alpha multiplication is the fourth float at
|
||||
// local + 16. Alpha zero is how authored clips are made invisible.
|
||||
color->red = readFloatLe(bytes, child.localDataOffset + 4);
|
||||
color->green = readFloatLe(bytes, child.localDataOffset + 8);
|
||||
color->blue = readFloatLe(bytes, child.localDataOffset + 12);
|
||||
color->alpha = readFloatLe(bytes, child.localDataOffset + 16);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const RvbNode* firstChild(const RvbNode& node, const char* tag) {
|
||||
for (const RvbNode& child : node.children) {
|
||||
if (child.tag == tag) return &child;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string definitionName(const std::vector<uint8_t>& bytes, const RvbNode& node) {
|
||||
size_t cursor = node.localDataOffset;
|
||||
std::string name;
|
||||
readCString(bytes, &cursor, node.localDataOffset + node.localDataSize, &name);
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string frameActionSource(const std::vector<uint8_t>& bytes, const RvbNode& frame) {
|
||||
for (const RvbNode& child : frame.children) {
|
||||
if (child.tag != "ASRC") continue;
|
||||
size_t cursor = child.localDataOffset;
|
||||
std::string source;
|
||||
if (readCString(bytes, &cursor, child.localDataOffset + child.localDataSize, &source)) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string findImageSymbol(const std::vector<uint8_t>& bytes, const RvbNode& node) {
|
||||
if (node.tag == "IMGF" && node.localDataSize >= 2) {
|
||||
size_t cursor = node.localDataOffset + 1;
|
||||
std::string symbol;
|
||||
if (readCString(bytes, &cursor, node.localDataOffset + node.localDataSize, &symbol) &&
|
||||
!symbol.empty()) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
for (const RvbNode& child : node.children) {
|
||||
std::string symbol = findImageSymbol(bytes, child);
|
||||
if (!symbol.empty()) return symbol;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct Context {
|
||||
const std::vector<uint8_t>& bytes;
|
||||
std::unordered_map<std::string, const RvbNode*> definitions;
|
||||
std::vector<RvbImageDraw>* draws = nullptr;
|
||||
std::string* error = nullptr;
|
||||
const RvbSnapshotState* state = nullptr;
|
||||
|
||||
bool renderDefinition(const std::string& name, const Matrix& matrix,
|
||||
const ColorTransform& color, uint32_t depth, unsigned recursion,
|
||||
const std::string& path) {
|
||||
if (recursion > 64) {
|
||||
if (error) *error = "RVB MovieClip recursion limit exceeded";
|
||||
return false;
|
||||
}
|
||||
const auto found = definitions.find(name);
|
||||
if (found == definitions.end()) return true;
|
||||
const RvbNode& definition = *found->second;
|
||||
if (definition.tag == "SHAP") {
|
||||
size_t cursor = definition.localDataOffset;
|
||||
std::string ignored;
|
||||
if (!readCString(bytes, &cursor,
|
||||
definition.localDataOffset + definition.localDataSize, &ignored) ||
|
||||
cursor + 16 > definition.localDataOffset + definition.localDataSize) {
|
||||
return true;
|
||||
}
|
||||
const std::string image = findImageSymbol(bytes, definition);
|
||||
if (image.empty()) return true;
|
||||
const float left = readFloatLe(bytes, cursor + 0);
|
||||
const float top = readFloatLe(bytes, cursor + 4);
|
||||
const float right = readFloatLe(bytes, cursor + 8);
|
||||
const float bottom = readFloatLe(bytes, cursor + 12);
|
||||
RvbImageDraw draw;
|
||||
draw.imageSymbol = image;
|
||||
draw.instancePath = path;
|
||||
draw.corners[0] = transform(matrix, left, top);
|
||||
draw.corners[1] = transform(matrix, right, top);
|
||||
draw.corners[2] = transform(matrix, left, bottom);
|
||||
draw.corners[3] = transform(matrix, right, bottom);
|
||||
draw.depth = depth;
|
||||
draw.color = {color.red, color.green, color.blue};
|
||||
draw.alpha = color.alpha;
|
||||
draws->push_back(std::move(draw));
|
||||
return true;
|
||||
}
|
||||
if (definition.tag == "MOVC") {
|
||||
const RvbNode* timeline = firstChild(definition, "TIME");
|
||||
return !timeline || renderTimeline(*timeline, matrix, color, recursion + 1, path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool renderTimeline(const RvbNode& timeline, const Matrix& parent,
|
||||
const ColorTransform& parentColor,
|
||||
unsigned recursion, const std::string& path) {
|
||||
const RvbNode* targetFrame = nullptr;
|
||||
std::string wantedLabel;
|
||||
if (state) {
|
||||
const auto found = state->frameByPath.find(path);
|
||||
if (found != state->frameByPath.end()) wantedLabel = found->second;
|
||||
}
|
||||
for (const RvbNode& child : timeline.children) {
|
||||
if (child.tag != "FRAM") continue;
|
||||
if (!targetFrame) targetFrame = &child;
|
||||
if (wantedLabel.empty()) break;
|
||||
size_t cursor = child.localDataOffset;
|
||||
std::string label;
|
||||
if (readCString(bytes, &cursor, child.localDataOffset + child.localDataSize,
|
||||
&label) && label == wantedLabel) {
|
||||
targetFrame = &child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetFrame) return true;
|
||||
// A label whose script calls play() is an animation entry point, not
|
||||
// the visible steady state. The original player advances until the
|
||||
// following stop(); doing the same resolves authored fades such as
|
||||
// lf_title_selectmusic_start and jf_focusds_start.
|
||||
if (frameActionSource(bytes, *targetFrame).find("play();") != std::string::npos) {
|
||||
bool afterTarget = false;
|
||||
for (const RvbNode& child : timeline.children) {
|
||||
if (child.tag != "FRAM") continue;
|
||||
if (&child == targetFrame) {
|
||||
afterTarget = true;
|
||||
continue;
|
||||
}
|
||||
if (!afterTarget) continue;
|
||||
targetFrame = &child;
|
||||
if (frameActionSource(bytes, child).find("stop();") != std::string::npos) break;
|
||||
}
|
||||
}
|
||||
struct Placement {
|
||||
uint32_t depth = 0;
|
||||
std::string definition;
|
||||
std::string instance;
|
||||
Matrix matrix;
|
||||
ColorTransform color;
|
||||
};
|
||||
std::unordered_map<uint32_t, Placement> displayList;
|
||||
bool reachedTarget = false;
|
||||
for (const RvbNode& frame : timeline.children) {
|
||||
if (frame.tag != "FRAM") continue;
|
||||
for (const RvbNode& command : frame.children) {
|
||||
if (command.tag == "RMOV") {
|
||||
if (command.localDataSize >= 5) {
|
||||
displayList.erase(readU32Le(bytes, command.localDataOffset + 1));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (command.tag != "PLC3") continue;
|
||||
size_t cursor = command.localDataOffset;
|
||||
const size_t end = command.localDataOffset + command.localDataSize;
|
||||
std::string definition;
|
||||
std::string instance;
|
||||
if (!readCString(bytes, &cursor, end, &definition) ||
|
||||
!readCString(bytes, &cursor, end, &instance) || cursor + 8 > end) {
|
||||
continue;
|
||||
}
|
||||
cursor += 4;
|
||||
const uint32_t depth = readU32Le(bytes, cursor);
|
||||
const bool hasTransform = hasNodeTransform(command);
|
||||
ColorTransform color;
|
||||
const bool hasColor = nodeColor(bytes, command, &color);
|
||||
if (!definition.empty()) {
|
||||
Placement placement;
|
||||
placement.depth = depth;
|
||||
placement.definition = std::move(definition);
|
||||
placement.instance = std::move(instance);
|
||||
if (hasTransform) placement.matrix = nodeTransform(bytes, command);
|
||||
if (hasColor) placement.color = color;
|
||||
displayList[depth] = std::move(placement);
|
||||
} else {
|
||||
const auto found = displayList.find(depth);
|
||||
if (found == displayList.end()) continue;
|
||||
if (!instance.empty()) found->second.instance = std::move(instance);
|
||||
if (hasTransform) found->second.matrix = nodeTransform(bytes, command);
|
||||
if (hasColor) found->second.color = color;
|
||||
}
|
||||
}
|
||||
if (&frame == targetFrame) {
|
||||
reachedTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!reachedTarget) return true;
|
||||
std::vector<Placement> placements;
|
||||
placements.reserve(displayList.size());
|
||||
for (auto& [_, placement] : displayList) placements.push_back(std::move(placement));
|
||||
std::stable_sort(placements.begin(), placements.end(),
|
||||
[](const Placement& a, const Placement& b) { return a.depth < b.depth; });
|
||||
for (const Placement& placement : placements) {
|
||||
// imc_other is the game's reusable off-focus song template. The
|
||||
// CSelectMusicTask renderer moves and draws it once per carousel
|
||||
// entry; leaving its authored root placement visible produces a
|
||||
// spurious duplicate focus panel in a static snapshot.
|
||||
if (recursion == 0 && placement.instance == "imc_other" &&
|
||||
(!state || !state->includeRootOther)) {
|
||||
continue;
|
||||
}
|
||||
const Matrix placed = multiply(parent, placement.matrix);
|
||||
const ColorTransform color = multiply(parentColor, placement.color);
|
||||
if (color.alpha <= 0.0001f) continue;
|
||||
const std::string childName = placement.instance.empty()
|
||||
? placement.definition
|
||||
: placement.instance;
|
||||
const std::string childPath = path == "/" ? "/" + childName
|
||||
: path + "/" + childName;
|
||||
if (!renderDefinition(placement.definition, placed, color, placement.depth,
|
||||
recursion, childPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
void collectDefinitions(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
Context* context,
|
||||
const RvbNode** rootTimeline = nullptr) {
|
||||
if (rootTimeline) *rootTimeline = nullptr;
|
||||
for (const RvbNode& root : scene.roots) {
|
||||
if (root.tag == "DEFN") {
|
||||
for (const RvbNode& definition : root.children) {
|
||||
const std::string name = definitionName(bytes, definition);
|
||||
if (!name.empty()) context->definitions.emplace(name, &definition);
|
||||
}
|
||||
} else if (root.tag == "TIME" && rootTimeline) {
|
||||
*rootTimeline = &root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool BuildRvbInitialSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error) {
|
||||
if (!draws) {
|
||||
if (error) *error = "null RVB snapshot output";
|
||||
return false;
|
||||
}
|
||||
draws->clear();
|
||||
const RvbSnapshotState state;
|
||||
return BuildRvbSnapshot(bytes, scene, state, draws, error);
|
||||
}
|
||||
|
||||
bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
const RvbSnapshotState& state,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error) {
|
||||
if (!draws) {
|
||||
if (error) *error = "null RVB snapshot output";
|
||||
return false;
|
||||
}
|
||||
draws->clear();
|
||||
Context context{bytes, {}, draws, error, &state};
|
||||
const RvbNode* rootTimeline = nullptr;
|
||||
collectDefinitions(bytes, scene, &context, &rootTimeline);
|
||||
if (!rootTimeline) {
|
||||
if (error) *error = "RVB has no root TIME timeline";
|
||||
return false;
|
||||
}
|
||||
return context.renderTimeline(*rootTimeline, {}, {}, 0, "/");
|
||||
}
|
||||
|
||||
bool BuildRvbSymbolSnapshot(const std::vector<uint8_t>& bytes,
|
||||
const RvbScene& scene,
|
||||
const std::string& symbolName,
|
||||
const RvbSnapshotState& state,
|
||||
std::vector<RvbImageDraw>* draws,
|
||||
std::string* error) {
|
||||
if (!draws) {
|
||||
if (error) *error = "null RVB symbol snapshot output";
|
||||
return false;
|
||||
}
|
||||
draws->clear();
|
||||
Context context{bytes, {}, draws, error, &state};
|
||||
collectDefinitions(bytes, scene, &context);
|
||||
std::string definitionName = symbolName;
|
||||
for (const RvbExport& exported : scene.exports) {
|
||||
if (exported.linkageName == symbolName) {
|
||||
definitionName = exported.definitionName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (context.definitions.find(definitionName) == context.definitions.end()) {
|
||||
if (error) *error = "RVB symbol is not present in DEFN: " + symbolName;
|
||||
return false;
|
||||
}
|
||||
return context.renderDefinition(definitionName, {}, {}, 0, 0, "/");
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "gc/RvbScene.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
namespace gc {
|
||||
namespace {
|
||||
|
||||
uint16_t readU16Le(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
return static_cast<uint16_t>(bytes[offset]) |
|
||||
static_cast<uint16_t>(bytes[offset + 1] << 8);
|
||||
}
|
||||
|
||||
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
|
||||
return static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
bool hasTag(const std::vector<uint8_t>& bytes, size_t offset, const char tag[5]) {
|
||||
return offset + 4 <= bytes.size() &&
|
||||
std::memcmp(bytes.data() + offset, tag, 4) == 0;
|
||||
}
|
||||
|
||||
bool readCString(const std::vector<uint8_t>& bytes, size_t* cursor,
|
||||
size_t end, std::string* value) {
|
||||
if (!cursor || !value || *cursor >= end || end > bytes.size()) return false;
|
||||
const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(*cursor);
|
||||
const auto last = bytes.begin() + static_cast<std::ptrdiff_t>(end);
|
||||
const auto zero = std::find(first, last, uint8_t{0});
|
||||
if (zero == last) return false;
|
||||
value->assign(reinterpret_cast<const char*>(&*first),
|
||||
static_cast<size_t>(zero - first));
|
||||
*cursor += value->size() + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fail(std::string* error, const std::string& message) {
|
||||
if (error) *error = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseNode(const std::vector<uint8_t>& bytes, size_t offset, size_t limit,
|
||||
RvbNode* node, std::string* error) {
|
||||
if (!node || offset + 12 > limit || limit > bytes.size()) {
|
||||
return fail(error, "truncated RVB animation node");
|
||||
}
|
||||
const uint32_t size = readU32Le(bytes, offset + 4);
|
||||
const uint32_t localSize = readU32Le(bytes, offset + 8);
|
||||
if (size < 12 || size > limit - offset || localSize > size - 12) {
|
||||
std::ostringstream message;
|
||||
message << "invalid RVB animation node at 0x" << std::hex << offset;
|
||||
return fail(error, message.str());
|
||||
}
|
||||
node->tag.assign(reinterpret_cast<const char*>(bytes.data() + offset), 4);
|
||||
node->offset = static_cast<uint32_t>(offset);
|
||||
node->size = size;
|
||||
node->localDataOffset = static_cast<uint32_t>(offset + 12);
|
||||
node->localDataSize = localSize;
|
||||
size_t childCursor = offset + 12 + localSize;
|
||||
const size_t nodeEnd = offset + size;
|
||||
while (childCursor < nodeEnd) {
|
||||
RvbNode child;
|
||||
if (!parseNode(bytes, childCursor, nodeEnd, &child, error)) return false;
|
||||
childCursor += child.size;
|
||||
node->children.push_back(std::move(child));
|
||||
}
|
||||
return childCursor == nodeEnd;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ParseRvbScene(const std::vector<uint8_t>& bytes,
|
||||
RvbScene* scene,
|
||||
std::string* error) {
|
||||
if (!scene) return fail(error, "null RVB scene output");
|
||||
*scene = {};
|
||||
if (bytes.size() < 0x43) return fail(error, "RVB file is shorter than its MOVI header");
|
||||
if (!hasTag(bytes, 0, "RVB_")) return fail(error, "missing RVB_ root tag");
|
||||
if (readU32Le(bytes, 4) != bytes.size()) {
|
||||
return fail(error, "RVB_ root size does not match the file size");
|
||||
}
|
||||
if (!hasTag(bytes, 0x10, "MOVI")) return fail(error, "missing MOVI chunk at 0x10");
|
||||
const uint32_t movieSize = readU32Le(bytes, 0x14);
|
||||
if (movieSize < 0x23 || static_cast<uint64_t>(0x10) + movieSize != bytes.size()) {
|
||||
return fail(error, "invalid MOVI chunk size");
|
||||
}
|
||||
|
||||
// Recovered from the MOVI reader layout used by game471.exe. The file
|
||||
// stores height before width; root placements such as imc_title=(360,112)
|
||||
// confirm the authored scene is the cabinet's native portrait canvas.
|
||||
scene->framesPerSecond = bytes[0x1d];
|
||||
scene->sourceHeight = readU16Le(bytes, 0x2a);
|
||||
scene->sourceWidth = readU16Le(bytes, 0x2e);
|
||||
|
||||
// The first child begins at the unaligned 0x33 offset. Each child size
|
||||
// includes its four-byte tag and size field; walking the sizes lands
|
||||
// exactly on the end of MOVI.
|
||||
size_t cursor = 0x33;
|
||||
while (cursor < bytes.size()) {
|
||||
if (cursor + 8 > bytes.size()) return fail(error, "truncated RVB child chunk header");
|
||||
const uint32_t chunkSize = readU32Le(bytes, cursor + 4);
|
||||
if (chunkSize < 8 || chunkSize > bytes.size() - cursor) {
|
||||
std::ostringstream message;
|
||||
message << "invalid RVB child chunk size at 0x" << std::hex << cursor;
|
||||
return fail(error, message.str());
|
||||
}
|
||||
RvbChunk chunk;
|
||||
chunk.tag.assign(reinterpret_cast<const char*>(bytes.data() + cursor), 4);
|
||||
chunk.offset = static_cast<uint32_t>(cursor);
|
||||
chunk.size = chunkSize;
|
||||
scene->chunks.push_back(chunk);
|
||||
|
||||
if (chunk.tag == "PREP") {
|
||||
if (chunkSize < 16) return fail(error, "PREP chunk is too short");
|
||||
const uint32_t payloadSize = readU32Le(bytes, cursor + 8);
|
||||
const uint32_t count = readU32Le(bytes, cursor + 12);
|
||||
if (payloadSize != chunkSize - 12) {
|
||||
return fail(error, "PREP payload size does not match its chunk size");
|
||||
}
|
||||
size_t stringCursor = cursor + 16;
|
||||
const size_t chunkEnd = cursor + chunkSize;
|
||||
scene->bindings.reserve(count);
|
||||
for (uint32_t index = 0; index < count; ++index) {
|
||||
RvbBinding binding;
|
||||
if (!readCString(bytes, &stringCursor, chunkEnd, &binding.action) ||
|
||||
!readCString(bytes, &stringCursor, chunkEnd, &binding.instancePath)) {
|
||||
std::ostringstream message;
|
||||
message << "truncated PREP binding " << index;
|
||||
return fail(error, message.str());
|
||||
}
|
||||
scene->bindings.push_back(std::move(binding));
|
||||
}
|
||||
if (stringCursor != chunkEnd) {
|
||||
return fail(error, "PREP bindings do not consume the complete chunk");
|
||||
}
|
||||
} else if (chunk.tag == "REPO") {
|
||||
if (chunkSize < 12) return fail(error, "REPO chunk is too short");
|
||||
const size_t chunkEnd = cursor + chunkSize;
|
||||
const uint32_t headerSize = readU32Le(bytes, cursor + 8);
|
||||
size_t resourceCursor = cursor + 12 + headerSize;
|
||||
if (resourceCursor > chunkEnd) return fail(error, "REPO header exceeds its chunk");
|
||||
while (resourceCursor < chunkEnd) {
|
||||
if (resourceCursor + 12 > chunkEnd) {
|
||||
return fail(error, "truncated REPO resource header");
|
||||
}
|
||||
const std::string resourceTag(
|
||||
reinterpret_cast<const char*>(bytes.data() + resourceCursor), 4);
|
||||
const uint32_t resourceSize = readU32Le(bytes, resourceCursor + 4);
|
||||
if (resourceSize < 12 || resourceSize > chunkEnd - resourceCursor) {
|
||||
return fail(error, "invalid REPO resource size");
|
||||
}
|
||||
if (resourceTag == "IMAG") {
|
||||
const size_t resourceEnd = resourceCursor + resourceSize;
|
||||
size_t fieldCursor = resourceCursor + 16;
|
||||
RvbImageResource image;
|
||||
if (!readCString(bytes, &fieldCursor, resourceEnd, &image.fileName) ||
|
||||
!readCString(bytes, &fieldCursor, resourceEnd, &image.symbolName) ||
|
||||
fieldCursor + 13 != resourceEnd) {
|
||||
return fail(error, "malformed IMAG repository resource");
|
||||
}
|
||||
fieldCursor += 4; // resource flags, zero in the dumped menu assets
|
||||
image.height = readU32Le(bytes, fieldCursor);
|
||||
image.width = readU32Le(bytes, fieldCursor + 4);
|
||||
scene->images.push_back(std::move(image));
|
||||
}
|
||||
resourceCursor += resourceSize;
|
||||
}
|
||||
} else if (chunk.tag == "EXPG") {
|
||||
RvbNode exports;
|
||||
if (!parseNode(bytes, cursor, cursor + chunkSize, &exports, error)) return false;
|
||||
for (const RvbNode& child : exports.children) {
|
||||
if (child.tag != "EXPS") continue;
|
||||
size_t fieldCursor = child.localDataOffset;
|
||||
const size_t fieldEnd = child.localDataOffset + child.localDataSize;
|
||||
RvbExport exported;
|
||||
if (!readCString(bytes, &fieldCursor, fieldEnd, &exported.definitionName) ||
|
||||
!readCString(bytes, &fieldCursor, fieldEnd, &exported.linkageName) ||
|
||||
fieldCursor != fieldEnd) {
|
||||
return fail(error, "malformed EXPS linkage record");
|
||||
}
|
||||
scene->exports.push_back(std::move(exported));
|
||||
}
|
||||
}
|
||||
if (chunk.tag != "PREP") {
|
||||
RvbNode root;
|
||||
if (!parseNode(bytes, cursor, cursor + chunkSize, &root, error)) return false;
|
||||
scene->roots.push_back(std::move(root));
|
||||
}
|
||||
cursor += chunkSize;
|
||||
}
|
||||
if (scene->chunks.empty() || scene->chunks.front().tag != "PREP") {
|
||||
return fail(error, "MOVI has no leading PREP binding chunk");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "gc/StageCatalog.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <sstream>
|
||||
|
||||
namespace gc {
|
||||
namespace {
|
||||
|
||||
class CatalogReader {
|
||||
public:
|
||||
explicit CatalogReader(const std::vector<uint8_t>& bytes) : bytes_(bytes) {}
|
||||
|
||||
size_t tell() const { return pos_; }
|
||||
bool ok() const { return ok_; }
|
||||
|
||||
uint8_t u8() {
|
||||
if (pos_ >= bytes_.size()) {
|
||||
ok_ = false;
|
||||
return 0;
|
||||
}
|
||||
return bytes_[pos_++];
|
||||
}
|
||||
|
||||
uint16_t u16be() {
|
||||
const uint16_t a = u8();
|
||||
const uint16_t b = u8();
|
||||
return static_cast<uint16_t>((a << 8) | b);
|
||||
}
|
||||
|
||||
uint32_t u32be() {
|
||||
const uint32_t a = u8();
|
||||
const uint32_t b = u8();
|
||||
const uint32_t c = u8();
|
||||
const uint32_t d = u8();
|
||||
return (a << 24) | (b << 16) | (c << 8) | d;
|
||||
}
|
||||
|
||||
std::string string8() {
|
||||
const size_t size = u8();
|
||||
if (!ok_ || size > bytes_.size() - pos_) {
|
||||
ok_ = false;
|
||||
return {};
|
||||
}
|
||||
const char* begin = reinterpret_cast<const char*>(bytes_.data() + pos_);
|
||||
pos_ += size;
|
||||
return std::string(begin, size);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::vector<uint8_t>& bytes_;
|
||||
size_t pos_ = 0;
|
||||
bool ok_ = true;
|
||||
};
|
||||
|
||||
template <typename T, size_t N, typename Read>
|
||||
void readArray(std::array<T, N>& values, Read&& read) {
|
||||
for (T& value : values) value = read();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ParseStageCatalog(const std::vector<uint8_t>& bytes,
|
||||
std::vector<StageCatalogEntry>* entries,
|
||||
std::string* error) {
|
||||
if (!entries) {
|
||||
if (error) *error = "null stage catalog output";
|
||||
return false;
|
||||
}
|
||||
entries->clear();
|
||||
if (bytes.size() < 2) {
|
||||
if (error) *error = "stage_param.dat is shorter than its u16 count";
|
||||
return false;
|
||||
}
|
||||
|
||||
CatalogReader r(bytes);
|
||||
const uint16_t count = r.u16be();
|
||||
entries->reserve(count);
|
||||
for (uint16_t index = 0; index < count; ++index) {
|
||||
const size_t recordOffset = r.tell();
|
||||
StageCatalogEntry entry;
|
||||
entry.id = r.u32be();
|
||||
entry.title = r.string8();
|
||||
entry.imageKey = r.string8();
|
||||
entry.artist = r.string8();
|
||||
entry.source = r.string8();
|
||||
entry.sortKey = r.string8();
|
||||
entry.genre = r.u8();
|
||||
entry.duration = r.string8();
|
||||
readArray(entry.difficultyRatings, [&] { return r.u8(); });
|
||||
entry.bpm = r.string8();
|
||||
readArray(entry.bgmVolumes, [&] { return r.u8(); });
|
||||
readArray(entry.shotVolumes, [&] { return r.u8(); });
|
||||
readArray(entry.timingValues, [&] { return r.u32be(); });
|
||||
readArray(entry.unknown60, [&] { return r.u8(); });
|
||||
entry.bgmBase = r.string8();
|
||||
readArray(entry.chartGroup0, [&] { return r.string8(); });
|
||||
readArray(entry.chartSuffixes, [&] { return r.string8(); });
|
||||
readArray(entry.chartIds, [&] { return r.string8(); });
|
||||
entry.unknown9c = r.string8();
|
||||
entry.unknownA0 = r.u32be();
|
||||
readArray(entry.unknownA4, [&] { return r.u8(); });
|
||||
entry.unknownAc = r.string8();
|
||||
entry.unknownB0 = r.u8();
|
||||
|
||||
if (!r.ok()) {
|
||||
if (error) {
|
||||
std::ostringstream message;
|
||||
message << "truncated stage_param.dat record " << index
|
||||
<< " at file offset 0x" << std::hex << recordOffset;
|
||||
*error = message.str();
|
||||
}
|
||||
entries->clear();
|
||||
return false;
|
||||
}
|
||||
entries->push_back(std::move(entry));
|
||||
}
|
||||
|
||||
if (r.tell() != bytes.size()) {
|
||||
if (error) {
|
||||
std::ostringstream message;
|
||||
message << "stage_param.dat has " << (bytes.size() - r.tell())
|
||||
<< " trailing bytes after " << count << " records";
|
||||
*error = message.str();
|
||||
}
|
||||
entries->clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const StageCatalogEntry* FindStageCatalogEntryByChart(
|
||||
const std::vector<StageCatalogEntry>& entries,
|
||||
const std::string& chartId) {
|
||||
for (const StageCatalogEntry& entry : entries) {
|
||||
for (const std::string& candidate : entry.chartIds) {
|
||||
if (candidate == chartId) return &entry;
|
||||
}
|
||||
// Older/alternate data can use either of the two preceding chart groups.
|
||||
for (const std::string& candidate : entry.chartGroup0) {
|
||||
if (candidate == chartId) return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "gc/StageDat.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace gc {
|
||||
|
||||
static bool readFile(const std::string& path, std::vector<uint8_t>* out, std::string* err) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) {
|
||||
if (err) *err = "could not open file";
|
||||
return false;
|
||||
}
|
||||
f.seekg(0, std::ios::end);
|
||||
std::streampos end = f.tellg();
|
||||
if (end < 0) {
|
||||
if (err) *err = "tellg failed";
|
||||
return false;
|
||||
}
|
||||
const size_t size = static_cast<size_t>(end);
|
||||
f.seekg(0, std::ios::beg);
|
||||
|
||||
out->assign(size, 0);
|
||||
if (size > 0) f.read(reinterpret_cast<char*>(out->data()), static_cast<std::streamsize>(size));
|
||||
if (!f) {
|
||||
if (err) *err = "read failed";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t u32be(const std::vector<uint8_t>& b, size_t off) {
|
||||
return (static_cast<uint32_t>(b[off + 0]) << 24) |
|
||||
(static_cast<uint32_t>(b[off + 1]) << 16) |
|
||||
(static_cast<uint32_t>(b[off + 2]) << 8) |
|
||||
(static_cast<uint32_t>(b[off + 3]) << 0);
|
||||
}
|
||||
|
||||
bool StageDat::LoadFromFile(const std::string& path, StageDat& out, std::string* err) {
|
||||
StageDat tmp;
|
||||
if (!readFile(path, &tmp.bytes, err)) return false;
|
||||
if (tmp.bytes.size() < 4) {
|
||||
if (err) *err = "file too small";
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t size = tmp.bytes.size();
|
||||
tmp.headerSize = u32be(tmp.bytes, 0);
|
||||
if (tmp.headerSize < 4 || tmp.headerSize > size) {
|
||||
if (err) {
|
||||
std::ostringstream oss;
|
||||
oss << "invalid headerSize=" << tmp.headerSize << " for file size=" << size;
|
||||
*err = oss.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ((tmp.headerSize % 4) != 0) {
|
||||
if (err) *err = "headerSize is not multiple of 4";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The first headerSize bytes are typically a u32 table (big endian) containing section offsets.
|
||||
tmp.headerWords.reserve(tmp.headerSize / 4);
|
||||
for (size_t off = 0; off < tmp.headerSize; off += 4) {
|
||||
tmp.headerWords.push_back(u32be(tmp.bytes, off));
|
||||
}
|
||||
|
||||
// Collect plausible offsets from the header table.
|
||||
// We keep only offsets that point past the header and within the file.
|
||||
tmp.offsets.clear();
|
||||
tmp.offsets.reserve(tmp.headerWords.size());
|
||||
for (uint32_t w : tmp.headerWords) {
|
||||
if (w >= tmp.headerSize && w < size) tmp.offsets.push_back(static_cast<size_t>(w));
|
||||
}
|
||||
tmp.offsets.push_back(static_cast<size_t>(tmp.headerSize));
|
||||
tmp.offsets.push_back(size);
|
||||
std::sort(tmp.offsets.begin(), tmp.offsets.end());
|
||||
tmp.offsets.erase(std::unique(tmp.offsets.begin(), tmp.offsets.end()), tmp.offsets.end());
|
||||
|
||||
// Sections are just adjacent offset pairs.
|
||||
tmp.sections.clear();
|
||||
for (size_t i = 0; i + 1 < tmp.offsets.size(); i++) {
|
||||
const size_t a = tmp.offsets[i];
|
||||
const size_t b = tmp.offsets[i + 1];
|
||||
if (a >= b) continue;
|
||||
tmp.sections.push_back(Section{a, b});
|
||||
}
|
||||
|
||||
out = std::move(tmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
#include "gc/StagePattern.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
namespace gc {
|
||||
|
||||
namespace {
|
||||
|
||||
class PatternReader {
|
||||
public:
|
||||
PatternReader(const std::vector<uint8_t>& bytes, size_t pos, size_t end)
|
||||
: bytes_(bytes), pos_(pos), end_(end) {}
|
||||
|
||||
size_t tell() const { return pos_; }
|
||||
|
||||
bool u8(uint8_t* out) {
|
||||
if (!out || !need(1)) return false;
|
||||
*out = bytes_[pos_++];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool u16(uint16_t* out) {
|
||||
if (!out || !need(2)) return false;
|
||||
*out = static_cast<uint16_t>((static_cast<uint16_t>(bytes_[pos_]) << 8) |
|
||||
static_cast<uint16_t>(bytes_[pos_ + 1]));
|
||||
pos_ += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool u32(uint32_t* out) {
|
||||
if (!out || !need(4)) return false;
|
||||
*out = (static_cast<uint32_t>(bytes_[pos_ + 0]) << 24) |
|
||||
(static_cast<uint32_t>(bytes_[pos_ + 1]) << 16) |
|
||||
(static_cast<uint32_t>(bytes_[pos_ + 2]) << 8) |
|
||||
(static_cast<uint32_t>(bytes_[pos_ + 3]));
|
||||
pos_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool f32(float* out) {
|
||||
uint32_t u = 0;
|
||||
if (!u32(&u) || !out) return false;
|
||||
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
|
||||
std::memcpy(out, &u, sizeof(float));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool color(Color* out) {
|
||||
if (!out || !need(4)) return false;
|
||||
out->r = bytes_[pos_ + 0];
|
||||
out->g = bytes_[pos_ + 1];
|
||||
out->b = bytes_[pos_ + 2];
|
||||
out->a = bytes_[pos_ + 3];
|
||||
pos_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool string8(std::string* out) {
|
||||
uint8_t len = 0;
|
||||
if (!u8(&len)) return false;
|
||||
return sizedString(len, out);
|
||||
}
|
||||
|
||||
bool string16(std::string* out) {
|
||||
uint16_t len = 0;
|
||||
if (!u16(&len)) return false;
|
||||
return sizedString(len, out);
|
||||
}
|
||||
|
||||
private:
|
||||
bool need(size_t n) const {
|
||||
return pos_ <= end_ && n <= end_ - pos_ && pos_ + n <= bytes_.size();
|
||||
}
|
||||
|
||||
bool sizedString(size_t len, std::string* out) {
|
||||
if (!out || !need(len)) return false;
|
||||
out->assign(reinterpret_cast<const char*>(bytes_.data() + pos_), len);
|
||||
while (!out->empty() && out->back() == '\0') out->pop_back();
|
||||
pos_ += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>& bytes_;
|
||||
size_t pos_ = 0;
|
||||
size_t end_ = 0;
|
||||
};
|
||||
|
||||
bool failAt(const char* what, size_t off, std::string* err) {
|
||||
if (err) {
|
||||
std::ostringstream oss;
|
||||
oss << "failed to parse " << what << " at 0x" << std::hex << off;
|
||||
*err = oss.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validOffset(const StageDat& dat, uint32_t off) {
|
||||
return off < dat.bytes.size();
|
||||
}
|
||||
|
||||
bool validEndOffset(const StageDat& dat, uint32_t off) {
|
||||
return off <= dat.bytes.size();
|
||||
}
|
||||
|
||||
bool parseHeader(const StageDat& dat, StageHeader* out, std::string* err) {
|
||||
if (!out) return false;
|
||||
if (dat.headerWords.size() < 10) {
|
||||
if (err) *err = "file too small for stage header";
|
||||
return false;
|
||||
}
|
||||
out->stageCfg = dat.headerWords[0];
|
||||
out->trackDrawDist = dat.headerWords[1];
|
||||
out->track = dat.headerWords[2];
|
||||
out->notes = dat.headerWords[3];
|
||||
out->camera = dat.headerWords[4];
|
||||
out->particles = dat.headerWords[5];
|
||||
out->visualizer = dat.headerWords[6];
|
||||
out->unk1 = dat.headerWords[7];
|
||||
out->colors = dat.headerWords[8];
|
||||
out->objects = dat.headerWords[9];
|
||||
// Older stage revisions have an 11-word header and place ColorTable2 in
|
||||
// slot 10. The 13-word revision used by game471 inserts a scalar at 10,
|
||||
// moves ColorTable2 to 11, then appends another scalar.
|
||||
if (dat.headerWords.size() == 11) {
|
||||
out->colors2 = dat.headerWords[10];
|
||||
} else if (dat.headerWords.size() >= 13) {
|
||||
out->unk2 = dat.headerWords[10];
|
||||
out->colors2 = dat.headerWords[11];
|
||||
out->unk3 = dat.headerWords[12];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseStageConfig(const StageDat& dat, uint32_t start, uint32_t next, StageConfig* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint16_t bpmCount = 0;
|
||||
|
||||
if (!r.f32(&out->endTime1) ||
|
||||
!r.f32(&out->endTime2) ||
|
||||
!r.f32(&out->outroTime) ||
|
||||
!r.u16(&bpmCount)) {
|
||||
return failAt("StageConfig header", r.tell(), err);
|
||||
}
|
||||
|
||||
out->bpmChanges.clear();
|
||||
out->bpmChanges.reserve(bpmCount);
|
||||
for (uint16_t i = 0; i < bpmCount; i++) {
|
||||
BpmChange bp;
|
||||
if (!r.u32(&bp.timeMs) || !r.u32(&bp.bpm)) return failAt("BpmChange", r.tell(), err);
|
||||
out->bpmChanges.push_back(bp);
|
||||
}
|
||||
|
||||
for (std::vector<NoteSetting>& list : out->noteSettings) {
|
||||
uint16_t count = 0;
|
||||
if (!r.u16(&count)) return failAt("NoteSetting count", r.tell(), err);
|
||||
list.clear();
|
||||
list.reserve(count);
|
||||
for (uint16_t i = 0; i < count; ++i) {
|
||||
NoteSetting ns;
|
||||
if (!r.u32(&ns.timeMs) || !r.u32(&ns.mode) || !r.f32(&ns.value)) {
|
||||
return failAt("NoteSetting", r.tell(), err);
|
||||
}
|
||||
list.push_back(ns);
|
||||
}
|
||||
}
|
||||
|
||||
if (!r.string16(&out->chartName) ||
|
||||
!r.string16(&out->chartName2) ||
|
||||
!r.string16(&out->bgmName) ||
|
||||
!r.string16(&out->shotName) ||
|
||||
!r.f32(&out->backwardsDrawDist) ||
|
||||
!r.f32(&out->forwardDrawDist) ||
|
||||
!r.color(&out->trackAheadColor) ||
|
||||
!r.color(&out->trackBehindColor) ||
|
||||
!r.u8(&out->audioOffset) ||
|
||||
!r.f32(&out->visualOffset) ||
|
||||
!r.color(&out->unkColor)) {
|
||||
return failAt("StageConfig tail", r.tell(), err);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readCount(uint32_t count, size_t recordSize, size_t start, size_t end, const char* what, std::string* err) {
|
||||
const size_t maxCount = (end > start) ? ((end - start) / recordSize) : 0;
|
||||
if (count > maxCount) {
|
||||
if (err) {
|
||||
std::ostringstream oss;
|
||||
oss << what << " count " << count << " exceeds section capacity " << maxCount;
|
||||
*err = oss.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseDrawDistances(const StageDat& dat, uint32_t start, uint32_t next, std::vector<DrawDistancePoint>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("TrackDrawDist count", r.tell(), err);
|
||||
if (!readCount(count, 8, r.tell(), next, "TrackDrawDist", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
DrawDistancePoint p;
|
||||
if (!r.u32(&p.timeMs) || !r.f32(&p.distance)) return failAt("DrawDistance", r.tell(), err);
|
||||
out->push_back(p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseTrack(const StageDat& dat, uint32_t start, uint32_t next, std::vector<TrackPiece>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("TrackPieceArray count", r.tell(), err);
|
||||
if (!readCount(count, 16, r.tell(), next, "TrackPieceArray", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
TrackPiece p;
|
||||
if (!r.u32(&p.timeMs) || !r.f32(&p.x) || !r.f32(&p.y) || !r.f32(&p.z)) {
|
||||
return failAt("TrackPiece", r.tell(), err);
|
||||
}
|
||||
out->push_back(p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseNotes(
|
||||
const StageDat& dat,
|
||||
uint32_t start,
|
||||
uint32_t next,
|
||||
std::vector<std::string>* names,
|
||||
std::vector<StageNote>* out,
|
||||
std::string* err) {
|
||||
if (!names || !out || !validOffset(dat, start)) return false;
|
||||
names->clear();
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t nameCount = 0;
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&nameCount)) return failAt("NoteArray name count", r.tell(), err);
|
||||
names->reserve(nameCount);
|
||||
for (uint32_t i = 0; i < nameCount; ++i) {
|
||||
std::string name;
|
||||
if (!r.string8(&name)) return failAt("NoteArray name", r.tell(), err);
|
||||
names->push_back(std::move(name));
|
||||
}
|
||||
if (!r.u32(&count)) return failAt("NoteArray note count", r.tell(), err);
|
||||
if (!readCount(count, StageNote::kRecordSize, r.tell(), next, "NoteArray", err)) return false;
|
||||
|
||||
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<StageNoteType>(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<int16_t>(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->push_back(note);
|
||||
}
|
||||
|
||||
if (r.tell() != next) {
|
||||
if (err) {
|
||||
std::ostringstream oss;
|
||||
oss << "NoteArray leaves " << (next - r.tell()) << " trailing bytes";
|
||||
*err = oss.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseCameras(const StageDat& dat, uint32_t start, uint32_t next, std::vector<CameraPoint>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("CameraArray count", r.tell(), err);
|
||||
if (!readCount(count, 59, r.tell(), next, "CameraArray", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
CameraPoint c;
|
||||
if (!r.u32(&c.timeMs) ||
|
||||
!r.u8(&c.aMode) ||
|
||||
!r.u8(&c.fMode) ||
|
||||
!r.f32(&c.dist) ||
|
||||
!r.f32(&c.rotationA[0]) ||
|
||||
!r.f32(&c.rotationA[1]) ||
|
||||
!r.f32(&c.originOff[0]) ||
|
||||
!r.f32(&c.originOff[1]) ||
|
||||
!r.f32(&c.originOff[2]) ||
|
||||
!r.u8(&c.projType) ||
|
||||
!r.f32(&c.fieldFar[0]) ||
|
||||
!r.f32(&c.fieldFar[1]) ||
|
||||
!r.f32(&c.fieldFar[2]) ||
|
||||
!r.f32(&c.fieldNear[0]) ||
|
||||
!r.f32(&c.fieldNear[1]) ||
|
||||
!r.f32(&c.fieldNear[2]) ||
|
||||
!r.f32(&c.rotationB)) {
|
||||
return failAt("Camera", r.tell(), err);
|
||||
}
|
||||
out->push_back(c);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseParticles(const StageDat& dat, uint32_t start, uint32_t next,
|
||||
std::vector<ParticlePoint>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("ParticleArray count", r.tell(), err);
|
||||
if (!readCount(count, ParticlePoint::kRecordSize, r.tell(), next, "ParticleArray", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
ParticlePoint p;
|
||||
if (!r.u32(&p.timeMs) || !r.u32(&p.enabled) || !r.u32(&p.shape) || !r.u32(&p.texture) ||
|
||||
!r.color(&p.color) || !r.f32(&p.velocity[0]) || !r.f32(&p.velocity[1]) ||
|
||||
!r.f32(&p.velocity[2]) || !r.f32(&p.repeatMeasure) || !r.f32(&p.lifespanMeasure) ||
|
||||
!r.u32(&p.groupShapeSize)) {
|
||||
return failAt("Particle", r.tell(), err);
|
||||
}
|
||||
out->push_back(p);
|
||||
}
|
||||
if (r.tell() != next) return failAt("ParticleArray trailing bytes", r.tell(), err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseVisualizer(const StageDat& dat, uint32_t start, uint32_t next,
|
||||
std::vector<VisualizerPoint>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("VisualizerArray count", r.tell(), err);
|
||||
if (!readCount(count, VisualizerPoint::kRecordSize, r.tell(), next, "VisualizerArray", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
VisualizerPoint v;
|
||||
if (!r.u32(&v.timeMs) || !r.u32(&v.type) || !r.color(&v.color)) {
|
||||
return failAt("Visualizer", r.tell(), err);
|
||||
}
|
||||
out->push_back(v);
|
||||
}
|
||||
if (r.tell() != next) return failAt("VisualizerArray trailing bytes", r.tell(), err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next,
|
||||
std::vector<BackgroundColorPoint>* out, std::string* err) {
|
||||
if (!out || !validOffset(dat, start)) return false;
|
||||
out->clear();
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("ColorTable count", r.tell(), err);
|
||||
if (!readCount(count, BackgroundColorPoint::kRecordSize, r.tell(), next, "ColorTable", err)) return false;
|
||||
out->reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
BackgroundColorPoint c;
|
||||
uint8_t interpolateToNext = 0;
|
||||
uint8_t audioReactive = 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)) {
|
||||
return failAt("ColorTable entry", r.tell(), err);
|
||||
}
|
||||
c.interpolateToNext = interpolateToNext != 0;
|
||||
c.audioReactive = audioReactive != 0;
|
||||
out->push_back(c);
|
||||
}
|
||||
if (r.tell() != next) return failAt("ColorTable trailing bytes", r.tell(), err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseTransformArray(PatternReader& r, std::vector<TransformPoint>* out,
|
||||
const char* what, std::string* err) {
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt(what, r.tell(), err);
|
||||
out->clear();
|
||||
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) ||
|
||||
!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;
|
||||
out->push_back(p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next,
|
||||
std::vector<std::string>* modelNames,
|
||||
std::vector<std::string>* fragmentShaderNames,
|
||||
std::vector<StageObject>* out, std::string* err) {
|
||||
if (!modelNames || !fragmentShaderNames || !out || !validOffset(dat, start)) return false;
|
||||
PatternReader r(dat.bytes, start, next);
|
||||
modelNames->clear();
|
||||
fragmentShaderNames->clear();
|
||||
out->clear();
|
||||
|
||||
uint32_t count = 0;
|
||||
if (!r.u32(&count)) return failAt("ObjectArray model name count", r.tell(), err);
|
||||
modelNames->reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
std::string name;
|
||||
if (!r.string8(&name)) return failAt("ObjectArray model name", r.tell(), err);
|
||||
modelNames->push_back(std::move(name));
|
||||
}
|
||||
if (!r.u32(&count)) return failAt("ObjectArray shader name count", r.tell(), err);
|
||||
fragmentShaderNames->reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
std::string name;
|
||||
if (!r.string8(&name)) return failAt("ObjectArray shader name", r.tell(), err);
|
||||
fragmentShaderNames->push_back(std::move(name));
|
||||
}
|
||||
if (!r.u32(&count)) return failAt("ObjectArray count", r.tell(), err);
|
||||
out->reserve(count);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
StageObject object;
|
||||
uint8_t wireframe = 0;
|
||||
uint8_t flashing = 0;
|
||||
uint8_t unknown = 0;
|
||||
if (!r.u32(&object.model) || !r.u32(&object.fragmentShader) ||
|
||||
!r.u8(&wireframe) || !r.u8(&flashing) || !r.u8(&unknown)) {
|
||||
return failAt("Object prefix", r.tell(), err);
|
||||
}
|
||||
object.wireframe = wireframe != 0;
|
||||
object.flashing = flashing != 0;
|
||||
object.unknownFlag = unknown != 0;
|
||||
for (float& value : object.position) if (!r.f32(&value)) return failAt("Object position", r.tell(), err);
|
||||
for (float& value : object.scale) if (!r.f32(&value)) return failAt("Object scale", r.tell(), err);
|
||||
for (float& value : object.rotation) if (!r.f32(&value)) return failAt("Object rotation", r.tell(), err);
|
||||
for (float& value : object.color) if (!r.f32(&value)) return failAt("Object color", r.tell(), err);
|
||||
for (float& value : object.unknownVector) if (!r.f32(&value)) return failAt("Object vector", r.tell(), err);
|
||||
|
||||
uint32_t keyCount = 0;
|
||||
if (!r.u32(&keyCount)) return failAt("Object visibility count", r.tell(), err);
|
||||
object.visibility.reserve(keyCount);
|
||||
for (uint32_t key = 0; key < keyCount; ++key) {
|
||||
VisibilityPoint p;
|
||||
uint8_t fadeOut = 0;
|
||||
uint8_t fadeIn = 0;
|
||||
uint8_t visible = 0;
|
||||
if (!r.u32(&p.timeMs) || !r.u8(&fadeOut) || !r.u8(&fadeIn) || !r.u8(&visible)) {
|
||||
return failAt("Object visibility", r.tell(), err);
|
||||
}
|
||||
p.fadeOut = fadeOut != 0;
|
||||
p.fadeIn = fadeIn != 0;
|
||||
p.visible = visible != 0;
|
||||
object.visibility.push_back(p);
|
||||
}
|
||||
if (!parseTransformArray(r, &object.movement, "Object movement", err) ||
|
||||
!parseTransformArray(r, &object.scaling, "Object scaling", err) ||
|
||||
!parseTransformArray(r, &object.rotations, "Object rotation keys", err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!r.u32(&keyCount)) return failAt("Object color count", r.tell(), err);
|
||||
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)) {
|
||||
return failAt("Object color key", r.tell(), err);
|
||||
}
|
||||
p.tweenTowards = towards != 0;
|
||||
p.tweenAway = away != 0;
|
||||
object.colorChanges.push_back(p);
|
||||
}
|
||||
out->push_back(std::move(object));
|
||||
}
|
||||
if (r.tell() != next) return failAt("ObjectArray trailing bytes", r.tell(), err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseObjectParents(const StageDat& dat, uint32_t extensionBase, uint32_t version,
|
||||
std::vector<StageObject>* objects, std::string* err) {
|
||||
if (!objects || objects->empty() || version <= 0x29ce) return true;
|
||||
if (extensionBase > dat.bytes.size() || dat.bytes.size() - extensionBase < 12) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The extension begins with relative offsets. game471 seeks base+8 for
|
||||
// the parent stream, then reads objectCount followed by signed BE int16s.
|
||||
PatternReader table(dat.bytes, extensionBase + 8, dat.bytes.size());
|
||||
uint32_t relative = 0;
|
||||
if (!table.u32(&relative) || relative > dat.bytes.size() - extensionBase) {
|
||||
return true;
|
||||
}
|
||||
const size_t parentStart = static_cast<size_t>(extensionBase) + relative;
|
||||
PatternReader parents(dat.bytes, parentStart, dat.bytes.size());
|
||||
uint32_t count = 0;
|
||||
if (!parents.u32(&count) || count != objects->size()) return true;
|
||||
for (size_t i = 0; i < objects->size(); ++i) {
|
||||
uint16_t encoded = 0;
|
||||
if (!parents.u16(&encoded)) return failAt("ObjectArray parent index", parents.tell(), err);
|
||||
const int32_t parent = static_cast<int16_t>(encoded);
|
||||
if (parent >= 0 && static_cast<size_t>(parent) >= objects->size()) {
|
||||
continue;
|
||||
}
|
||||
(*objects)[i].parentIndex = parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* NoteTypeName(uint8_t rawType) {
|
||||
static constexpr const char* names[] = {
|
||||
"NONE", "NORMAL", "FLICK", "HOLD", "SCRATCH", "BEAT", "MERRY GO ROUND", "HIDDEN",
|
||||
"HIDDEN2", "CRITICAL", "SLIDE HOLD", "SLIDE COUNTER", "TURN", "SPIN", "FINISH", "DUAL HOLD",
|
||||
};
|
||||
return rawType < (sizeof(names) / sizeof(names[0])) ? names[rawType] : "UNKNOWN";
|
||||
}
|
||||
|
||||
bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err) {
|
||||
if (!out) return false;
|
||||
ParsedStagePattern tmp;
|
||||
if (!parseHeader(dat, &tmp.header, err)) return false;
|
||||
|
||||
if (!validOffset(dat, tmp.header.stageCfg) ||
|
||||
!validOffset(dat, tmp.header.trackDrawDist) ||
|
||||
!validOffset(dat, tmp.header.track) ||
|
||||
!validOffset(dat, tmp.header.camera)) {
|
||||
if (err) *err = "stage header contains out-of-file offsets";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parseStageConfig(dat, tmp.header.stageCfg, tmp.header.trackDrawDist, &tmp.config, err)) return false;
|
||||
if (!parseDrawDistances(dat, tmp.header.trackDrawDist, tmp.header.track, &tmp.drawDistances, err)) return false;
|
||||
if (!parseTrack(dat, tmp.header.track, tmp.header.notes, &tmp.track, err)) return false;
|
||||
if (!parseNotes(dat, tmp.header.notes, tmp.header.camera, &tmp.noteNames, &tmp.notes, err)) return false;
|
||||
|
||||
if (!validOffset(dat, tmp.header.particles) || tmp.header.particles < tmp.header.camera) {
|
||||
if (err) *err = "invalid camera/particle section order";
|
||||
return false;
|
||||
}
|
||||
if (!parseCameras(dat, tmp.header.camera, tmp.header.particles, &tmp.cameras, err)) return false;
|
||||
if (!validOffset(dat, tmp.header.visualizer) || tmp.header.visualizer < tmp.header.particles ||
|
||||
!validOffset(dat, tmp.header.unk1) || tmp.header.unk1 < tmp.header.visualizer ||
|
||||
!validOffset(dat, tmp.header.colors) || tmp.header.colors < tmp.header.unk1 ||
|
||||
!validOffset(dat, tmp.header.objects) || tmp.header.objects < tmp.header.colors ||
|
||||
!validEndOffset(dat, tmp.header.colors2) || tmp.header.colors2 < tmp.header.objects) {
|
||||
if (err) *err = "invalid background section order";
|
||||
return false;
|
||||
}
|
||||
if (!parseParticles(dat, tmp.header.particles, tmp.header.visualizer, &tmp.particles, err) ||
|
||||
!parseVisualizer(dat, tmp.header.visualizer, tmp.header.unk1, &tmp.visualizer, 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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = std::move(tmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "gc/TumoModel.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace gc {
|
||||
namespace {
|
||||
|
||||
class Reader {
|
||||
public:
|
||||
explicit Reader(std::vector<std::uint8_t> bytes) : bytes_(std::move(bytes)) {}
|
||||
|
||||
bool u32(std::uint32_t* value) {
|
||||
if (!value || offset_ + 4 > bytes_.size()) return false;
|
||||
*value = (static_cast<std::uint32_t>(bytes_[offset_]) << 24) |
|
||||
(static_cast<std::uint32_t>(bytes_[offset_ + 1]) << 16) |
|
||||
(static_cast<std::uint32_t>(bytes_[offset_ + 2]) << 8) |
|
||||
static_cast<std::uint32_t>(bytes_[offset_ + 3]);
|
||||
offset_ += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool f32(float* value) {
|
||||
std::uint32_t encoded = 0;
|
||||
if (!u32(&encoded) || !value) return false;
|
||||
std::memcpy(value, &encoded, sizeof(encoded));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool string8() {
|
||||
if (offset_ >= bytes_.size()) return false;
|
||||
const std::size_t length = bytes_[offset_++];
|
||||
if (length > bytes_.size() - offset_) return false;
|
||||
offset_ += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> bytes_;
|
||||
std::size_t offset_ = 0;
|
||||
};
|
||||
|
||||
bool readFile(const std::string& path, std::vector<std::uint8_t>* bytes) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
if (!stream || !bytes) return false;
|
||||
stream.seekg(0, std::ios::end);
|
||||
const std::streamoff size = stream.tellg();
|
||||
if (size < 0) return false;
|
||||
stream.seekg(0, std::ios::beg);
|
||||
bytes->resize(static_cast<std::size_t>(size));
|
||||
if (!bytes->empty()) {
|
||||
stream.read(reinterpret_cast<char*>(bytes->data()), size);
|
||||
}
|
||||
return static_cast<bool>(stream);
|
||||
}
|
||||
|
||||
bool malformed(std::string* error, const char* message) {
|
||||
if (error) *error = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* error) {
|
||||
if (!out) return malformed(error, "missing TUMO output");
|
||||
*out = {};
|
||||
std::vector<std::uint8_t> bytes;
|
||||
if (!readFile(path, &bytes)) return malformed(error, "could not read TUMO file");
|
||||
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");
|
||||
}
|
||||
|
||||
std::uint32_t vertexCount = 0;
|
||||
if (!reader.u32(&vertexCount) || vertexCount > 10000000) {
|
||||
return malformed(error, "invalid TUMO vertex count");
|
||||
}
|
||||
std::vector<TumoVertex> 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");
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
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<std::uint32_t> 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<std::size_t>(lineCount) * 2);
|
||||
for (std::uint64_t line = 0; line < static_cast<std::uint64_t>(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]);
|
||||
}
|
||||
if (out->triangles.empty() && out->solidLines.empty() && out->wireframeLines.empty()) {
|
||||
return malformed(error, "TUMO model contains no geometry");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "gc/GcTargetEffect.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "usage: opencoaster-effect-probe EFC_DATA UV_DATA [first [last]]\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
GcTargetEffectBank bank;
|
||||
std::string error;
|
||||
if (!bank.load(argv[1], argv[2], &error)) {
|
||||
std::cerr << error << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int first = argc > 3 ? std::atoi(argv[3]) : 0;
|
||||
const int last = argc > 4 ? std::atoi(argv[4]) : first;
|
||||
const int uvBase = argc > 5 ? std::atoi(argv[5]) : 0;
|
||||
for (int effect = first; effect <= last; ++effect) {
|
||||
std::cout << "effect " << effect << " lifetime=" << bank.lifetime(effect) << '\n';
|
||||
const int lifetime = bank.lifetime(effect);
|
||||
const int sampleTicks[] = {0, 1, lifetime / 2, lifetime > 0 ? lifetime - 1 : 0};
|
||||
std::set<int> seenTicks;
|
||||
for (const int tick : sampleTicks) {
|
||||
if (!seenTicks.insert(tick).second) continue;
|
||||
const auto sprites = bank.evaluate(effect, static_cast<float>(tick), uvBase);
|
||||
std::cout << " tick " << tick << " sprites=" << sprites.size() << '\n';
|
||||
for (const auto& sprite : sprites) {
|
||||
const GcUvCell* cell = bank.uvCell(sprite.uvRecord, sprite.frame);
|
||||
std::cout << " uv=" << sprite.uvRecord << " tex="
|
||||
<< bank.uvTexture(sprite.uvRecord) << " frame=" << sprite.frame;
|
||||
if (cell) {
|
||||
std::cout << " cell=" << cell->x << ',' << cell->y << ' '
|
||||
<< cell->width << 'x' << cell->height;
|
||||
}
|
||||
std::cout << " pos=" << sprite.offsetPixels.x << ',' << sprite.offsetPixels.y
|
||||
<< " scale=" << sprite.scale.x << ',' << sprite.scale.y
|
||||
<< " rot=" << sprite.rotationDegrees
|
||||
<< " rgba=" << sprite.color.r << ',' << sprite.color.g << ','
|
||||
<< sprite.color.b << ',' << sprite.color.a << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "gc/MtxArchive.hpp"
|
||||
#include "gc/RvbScene.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
bool readFile(const fs::path& path, std::vector<uint8_t>* bytes) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return false;
|
||||
bytes->assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t textureIndex(const gc::RvbImageResource& image) {
|
||||
static constexpr char prefix[] = "Image";
|
||||
if (image.symbolName.rfind(prefix, 0) != 0 || image.symbolName.size() <= 5) {
|
||||
return std::numeric_limits<size_t>::max();
|
||||
}
|
||||
size_t number = 0;
|
||||
for (size_t i = 5; i < image.symbolName.size(); ++i) {
|
||||
const char c = image.symbolName[i];
|
||||
if (c < '0' || c > '9') return std::numeric_limits<size_t>::max();
|
||||
number = number * 10 + static_cast<size_t>(c - '0');
|
||||
}
|
||||
return number == 0 ? std::numeric_limits<size_t>::max() : number - 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3 || argc > 4) {
|
||||
std::cerr << "Usage: " << argv[0] << " <scene.rvb> <textures.mtx> [output-dir]\n";
|
||||
return 2;
|
||||
}
|
||||
std::vector<uint8_t> rvbBytes;
|
||||
std::vector<uint8_t> mtxBytes;
|
||||
if (!readFile(argv[1], &rvbBytes) || !readFile(argv[2], &mtxBytes)) {
|
||||
std::cerr << "could not read RVB/MTX input\n";
|
||||
return 1;
|
||||
}
|
||||
gc::RvbScene scene;
|
||||
gc::MtxArchive archive;
|
||||
std::string error;
|
||||
if (!gc::ParseRvbScene(rvbBytes, &scene, &error) ||
|
||||
!gc::ParseMtxArchive(mtxBytes, &archive, &error)) {
|
||||
std::cerr << "parse failed: " << error << '\n';
|
||||
return 1;
|
||||
}
|
||||
if (scene.images.size() != archive.textures.size()) {
|
||||
std::cerr << "resource mismatch: RVB images=" << scene.images.size()
|
||||
<< " MTX textures=" << archive.textures.size() << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool extract = argc == 4;
|
||||
const fs::path output = extract ? fs::path(argv[3]) : fs::path();
|
||||
if (extract) fs::create_directories(output);
|
||||
for (const gc::RvbImageResource& image : scene.images) {
|
||||
const size_t index = textureIndex(image);
|
||||
if (index >= archive.textures.size()) {
|
||||
std::cerr << "invalid MTX symbol index: " << image.symbolName << '\n';
|
||||
return 1;
|
||||
}
|
||||
const gc::MtxTexture& texture = archive.textures[index];
|
||||
if (image.width != texture.width || image.height != texture.height) {
|
||||
std::cerr << "dimension mismatch at " << index << ": " << image.symbolName
|
||||
<< " RVB=" << image.width << 'x' << image.height
|
||||
<< " MTX=" << texture.width << 'x' << texture.height << '\n';
|
||||
return 1;
|
||||
}
|
||||
std::cout << index << ' ' << image.symbolName << ' ' << texture.width << 'x'
|
||||
<< texture.height << " bytes=" << texture.size << '\n';
|
||||
if (extract) {
|
||||
std::vector<uint8_t> dds;
|
||||
if (!gc::ExtractMtxTextureDds(mtxBytes, texture, &dds, &error)) {
|
||||
std::cerr << "extract failed: " << error << '\n';
|
||||
return 1;
|
||||
}
|
||||
std::ofstream file(output / (image.symbolName + ".dds"), std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(dds.data()),
|
||||
static_cast<std::streamsize>(dds.size()));
|
||||
if (!file) {
|
||||
std::cerr << "could not write extracted DDS\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "gc/RvbScene.hpp"
|
||||
#include "gc/RvbLayout.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void printNode(const gc::RvbNode& node, unsigned depth) {
|
||||
std::cout << " " << std::string(depth * 2, ' ') << node.tag
|
||||
<< " @0x" << std::hex << node.offset << "/0x" << node.size
|
||||
<< std::dec << " local=" << node.localDataSize << '\n';
|
||||
for (const gc::RvbNode& child : node.children) printNode(child, depth + 1);
|
||||
}
|
||||
|
||||
bool probe(const std::string& path, bool tree, const std::string& symbol,
|
||||
const gc::RvbSnapshotState& state) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << path << ": could not open\n";
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
gc::RvbScene scene;
|
||||
std::string error;
|
||||
if (!gc::ParseRvbScene(bytes, &scene, &error)) {
|
||||
std::cerr << path << ": parse failed: " << error << '\n';
|
||||
return false;
|
||||
}
|
||||
std::vector<gc::RvbImageDraw> snapshot;
|
||||
const bool built = symbol.empty()
|
||||
? gc::BuildRvbSnapshot(bytes, scene, state, &snapshot, &error)
|
||||
: gc::BuildRvbSymbolSnapshot(bytes, scene, symbol, state, &snapshot, &error);
|
||||
if (!built) {
|
||||
std::cerr << path << ": snapshot failed: " << error << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << path << "\n movie=" << scene.sourceWidth << 'x' << scene.sourceHeight
|
||||
<< " @ " << static_cast<unsigned>(scene.framesPerSecond) << " fps"
|
||||
<< " bindings=" << scene.bindings.size()
|
||||
<< " images=" << scene.images.size()
|
||||
<< " initialDraws=" << snapshot.size() << "\n chunks:";
|
||||
for (const gc::RvbChunk& chunk : scene.chunks) {
|
||||
std::cout << ' ' << chunk.tag << "@0x" << std::hex << chunk.offset
|
||||
<< "/0x" << chunk.size << std::dec;
|
||||
}
|
||||
std::cout << "\n PREP bindings:\n";
|
||||
for (const gc::RvbBinding& binding : scene.bindings) {
|
||||
std::cout << " " << std::left << std::setw(28) << binding.action
|
||||
<< ' ' << binding.instancePath << '\n';
|
||||
}
|
||||
if (tree) {
|
||||
std::cout << " animation tree:\n";
|
||||
for (const gc::RvbNode& root : scene.roots) printNode(root, 0);
|
||||
std::cout << " initial draws:\n";
|
||||
for (const gc::RvbImageDraw& draw : snapshot) {
|
||||
std::cout << " " << draw.imageSymbol << " depth=" << draw.depth
|
||||
<< " path=" << draw.instancePath
|
||||
<< " rgba=(" << draw.color[0] << ',' << draw.color[1] << ','
|
||||
<< draw.color[2] << ',' << draw.alpha << ')'
|
||||
<< " tl=(" << draw.corners[0][0] << ',' << draw.corners[0][1]
|
||||
<< ") br=(" << draw.corners[3][0] << ',' << draw.corners[3][1]
|
||||
<< ")\n";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
bool tree = false;
|
||||
std::string symbol;
|
||||
gc::RvbSnapshotState state;
|
||||
int firstPath = 1;
|
||||
while (firstPath < argc) {
|
||||
const std::string option = argv[firstPath];
|
||||
if (option == "--tree") {
|
||||
tree = true;
|
||||
++firstPath;
|
||||
} else if (option == "--include-other") {
|
||||
state.includeRootOther = true;
|
||||
++firstPath;
|
||||
} else if (option == "--symbol" && firstPath + 1 < argc) {
|
||||
symbol = argv[firstPath + 1];
|
||||
firstPath += 2;
|
||||
} else if (option == "--state" && firstPath + 1 < argc) {
|
||||
const std::string value = argv[firstPath + 1];
|
||||
const size_t equals = value.find('=');
|
||||
if (equals == std::string::npos) {
|
||||
std::cerr << "--state expects /path=frame_label\n";
|
||||
return 2;
|
||||
}
|
||||
state.frameByPath[value.substr(0, equals)] = value.substr(equals + 1);
|
||||
firstPath += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (argc <= firstPath) {
|
||||
std::cerr << "Usage: " << argv[0]
|
||||
<< " [--tree] [--include-other] [--symbol name]"
|
||||
" [--state /path=frame] <scene.rvb> [scene.rvb ...]\n";
|
||||
return 2;
|
||||
}
|
||||
bool ok = true;
|
||||
for (int i = firstPath; i < argc; ++i) ok = probe(argv[i], tree, symbol, state) && ok;
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "gc/StageDat.hpp"
|
||||
#include "gc/StagePattern.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
bool probe(const std::string& path) {
|
||||
gc::StageDat dat;
|
||||
std::string err;
|
||||
if (!gc::StageDat::LoadFromFile(path, dat, &err)) {
|
||||
std::cerr << path << ": load failed: " << err << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
gc::ParsedStagePattern stage;
|
||||
if (!gc::ParseStagePattern(dat, &stage, &err)) {
|
||||
std::cerr << path << ": parse failed: " << err << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<uint8_t, size_t> types;
|
||||
for (const gc::StageNote& note : stage.notes) ++types[static_cast<uint8_t>(note.type)];
|
||||
std::map<uint8_t, size_t> cameraAnchorModes;
|
||||
std::map<uint8_t, size_t> cameraFadeModes;
|
||||
std::map<uint8_t, size_t> cameraProjectionTypes;
|
||||
for (const gc::CameraPoint& camera : stage.cameras) {
|
||||
++cameraAnchorModes[camera.aMode];
|
||||
++cameraFadeModes[camera.fMode];
|
||||
++cameraProjectionTypes[camera.projType];
|
||||
}
|
||||
size_t parentedObjects = 0;
|
||||
size_t wireframeObjects = 0;
|
||||
size_t flashingObjects = 0;
|
||||
size_t noGlobalFadeObjects = 0;
|
||||
size_t translucentObjects = 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;
|
||||
}
|
||||
|
||||
std::cout << path
|
||||
<< "\n chart=" << stage.config.chartName
|
||||
<< " bgm=" << stage.config.bgmName
|
||||
<< "\n track=" << stage.track.size()
|
||||
<< " drawDistanceKeys=" << stage.drawDistances.size()
|
||||
<< " drawBehind=" << stage.config.backwardsDrawDist
|
||||
<< " drawAhead=" << stage.config.forwardDrawDist
|
||||
<< " audioOffset=" << static_cast<unsigned>(stage.config.audioOffset)
|
||||
<< " visualOffset=" << stage.config.visualOffset
|
||||
<< " trackColors=(" << static_cast<unsigned>(stage.config.trackAheadColor.r) << ','
|
||||
<< static_cast<unsigned>(stage.config.trackAheadColor.g) << ','
|
||||
<< static_cast<unsigned>(stage.config.trackAheadColor.b) << ")->("
|
||||
<< static_cast<unsigned>(stage.config.trackBehindColor.r) << ','
|
||||
<< static_cast<unsigned>(stage.config.trackBehindColor.g) << ','
|
||||
<< static_cast<unsigned>(stage.config.trackBehindColor.b) << ')'
|
||||
<< " notes=" << stage.notes.size()
|
||||
<< " cameras=" << stage.cameras.size()
|
||||
<< " noteNames=" << stage.noteNames.size()
|
||||
<< "\n background: particles=" << stage.particles.size()
|
||||
<< " visualizerKeys=" << stage.visualizer.size()
|
||||
<< " colorKeys=" << stage.backgroundColors.size()
|
||||
<< " models=" << stage.modelNames.size()
|
||||
<< " objects=" << stage.objects.size()
|
||||
<< " parented=" << parentedObjects
|
||||
<< " wire=" << wireframeObjects
|
||||
<< " flashing=" << flashingObjects
|
||||
<< " noGlobalFade=" << noGlobalFadeObjects
|
||||
<< " translucent=" << translucentObjects;
|
||||
if (!stage.notes.empty()) {
|
||||
const auto [lo, hi] = std::minmax_element(
|
||||
stage.notes.begin(), stage.notes.end(),
|
||||
[](const gc::StageNote& a, const gc::StageNote& b) { return a.timeMs < b.timeMs; });
|
||||
std::cout << " noteTimeMs=" << lo->timeMs << ".." << hi->timeMs;
|
||||
}
|
||||
if (!stage.particles.empty()) {
|
||||
std::cout << "\n particleKeys:";
|
||||
for (const auto& particle : stage.particles) {
|
||||
std::cout << ' ' << particle.timeMs << ":on=" << particle.enabled
|
||||
<< ",shape=" << particle.shape << ",tex=" << particle.texture
|
||||
<< ",repeat=" << particle.repeatMeasure
|
||||
<< ",life=" << particle.lifespanMeasure
|
||||
<< ",group=" << particle.groupShapeSize;
|
||||
}
|
||||
}
|
||||
if (!stage.visualizer.empty()) {
|
||||
std::map<std::uint32_t, std::size_t> visualizerTypes;
|
||||
for (const auto& key : stage.visualizer) ++visualizerTypes[key.type];
|
||||
std::cout << "\n visualizerTypes:";
|
||||
for (const auto& [type, count] : visualizerTypes) std::cout << ' ' << type << '=' << count;
|
||||
}
|
||||
if (!stage.drawDistances.empty()) {
|
||||
std::cout << "\n drawDistance=" << stage.drawDistances.front().timeMs << ':'
|
||||
<< stage.drawDistances.front().distance << ".."
|
||||
<< stage.drawDistances.back().timeMs << ':'
|
||||
<< stage.drawDistances.back().distance;
|
||||
}
|
||||
if (!stage.track.empty()) {
|
||||
const auto& p = stage.track.front();
|
||||
std::cout << "\n firstTrack=" << p.timeMs << ":(" << p.x << ',' << p.y << ',' << p.z << ')';
|
||||
if (stage.track.size() > 1) {
|
||||
const auto& q = stage.track[1];
|
||||
std::cout << " nextTrack=" << q.timeMs << ":(" << q.x << ',' << q.y << ',' << q.z << ')';
|
||||
}
|
||||
}
|
||||
if (!stage.cameras.empty()) {
|
||||
const auto& c = stage.cameras.front();
|
||||
std::cout << " firstCamera=" << c.timeMs << " dist=" << c.dist
|
||||
<< " rot=(" << c.rotationA[0] << ',' << c.rotationA[1] << ',' << c.rotationB << ')'
|
||||
<< " off=(" << c.originOff[0] << ',' << c.originOff[1] << ',' << c.originOff[2] << ')';
|
||||
std::cout << "\n cameraModes: aMode";
|
||||
for (const auto& [mode, count] : cameraAnchorModes) {
|
||||
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
|
||||
}
|
||||
std::cout << " fMode";
|
||||
for (const auto& [mode, count] : cameraFadeModes) {
|
||||
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
|
||||
}
|
||||
std::cout << " projType";
|
||||
for (const auto& [mode, count] : cameraProjectionTypes) {
|
||||
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
|
||||
}
|
||||
}
|
||||
std::cout << "\n noteTypes:";
|
||||
for (const auto& [type, count] : types) {
|
||||
std::cout << " 0x" << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<unsigned>(type) << std::dec << '/' << gc::NoteTypeName(type) << '=' << count;
|
||||
}
|
||||
if (!stage.fragmentShaderNames.empty()) {
|
||||
std::cout << "\n fragmentShaders:";
|
||||
for (size_t i = 0; i < stage.fragmentShaderNames.size(); ++i) {
|
||||
std::cout << ' ' << i << '=' << stage.fragmentShaderNames[i];
|
||||
}
|
||||
}
|
||||
if (!stage.modelNames.empty()) {
|
||||
std::cout << "\n modelNames:";
|
||||
for (size_t i = 0; i < stage.modelNames.size(); ++i) {
|
||||
std::cout << ' ' << i << '=' << stage.modelNames[i];
|
||||
}
|
||||
}
|
||||
std::cout << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: " << argv[0] << " <stage.dat> [stage.dat ...]\n";
|
||||
return 2;
|
||||
}
|
||||
bool ok = true;
|
||||
for (int i = 1; i < argc; ++i) ok = probe(argv[i]) && ok;
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user