diff --git a/include/gc/RvbLayout.hpp b/include/gc/RvbLayout.hpp index aa135b2..073bbbe 100644 --- a/include/gc/RvbLayout.hpp +++ b/include/gc/RvbLayout.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,12 @@ struct RvbImageDraw { // root timeline). Clips not mentioned here remain on their first FRAM. struct RvbSnapshotState { std::unordered_map frameByPath; + // Frame offset after a labeled play() entry point. The default resolves + // to the following stop(), preserving the static snapshot behavior. + uint32_t animationFrame = std::numeric_limits::max(); + // Optional per-clip overrides allow a transition to advance while child + // loops and unrelated state clips remain at their authored stop frames. + std::unordered_map animationFrameByPath; bool includeRootOther = false; }; @@ -46,6 +53,14 @@ bool BuildRvbSnapshot(const std::vector& bytes, std::vector* draws, std::string* error = nullptr); +// Returns the longest play()..stop() span selected by this state. The result +// includes the labeled entry frame and can be converted to seconds with the +// scene frame rate. +uint32_t MeasureRvbSnapshotAnimationFrames(const std::vector& bytes, + const RvbScene& scene, + const RvbSnapshotState& state, + 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). diff --git a/src/RvbLayout.cpp b/src/RvbLayout.cpp index 6c30eeb..6f40a44 100644 --- a/src/RvbLayout.cpp +++ b/src/RvbLayout.cpp @@ -162,6 +162,7 @@ struct Context { std::vector* draws = nullptr; std::string* error = nullptr; const RvbSnapshotState* state = nullptr; + uint32_t maxAnimationFrames = 1; bool renderDefinition(const std::string& name, const Matrix& matrix, const ColorTransform& color, uint32_t depth, unsigned recursion, @@ -229,11 +230,11 @@ struct Context { } } 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. + // A label whose script calls play() is an animation entry point. Walk + // toward the following stop() and select the requested relative frame. + // UINT32_MAX retains the previous static behavior by selecting the end. if (frameActionSource(bytes, *targetFrame).find("play();") != std::string::npos) { + std::vector animationFrames{targetFrame}; bool afterTarget = false; for (const RvbNode& child : timeline.children) { if (child.tag != "FRAM") continue; @@ -242,9 +243,23 @@ struct Context { continue; } if (!afterTarget) continue; - targetFrame = &child; - if (frameActionSource(bytes, child).find("stop();") != std::string::npos) break; + animationFrames.push_back(&child); + if (frameActionSource(bytes, child).find("stop();") != std::string::npos) { + break; + } } + maxAnimationFrames = std::max( + maxAnimationFrames, static_cast(animationFrames.size())); + uint32_t requested = state ? state->animationFrame + : std::numeric_limits::max(); + if (state) { + const auto pathFrame = state->animationFrameByPath.find(path); + if (pathFrame != state->animationFrameByPath.end()) { + requested = pathFrame->second; + } + } + const size_t frameIndex = std::min(requested, animationFrames.size() - 1); + targetFrame = animationFrames[frameIndex]; } struct Placement { uint32_t depth = 0; @@ -383,6 +398,24 @@ bool BuildRvbSnapshot(const std::vector& bytes, return context.renderTimeline(*rootTimeline, {}, {}, 0, "/"); } +uint32_t MeasureRvbSnapshotAnimationFrames(const std::vector& bytes, + const RvbScene& scene, + const RvbSnapshotState& state, + std::string* error) { + std::vector draws; + RvbSnapshotState terminalState = state; + terminalState.animationFrame = std::numeric_limits::max(); + Context context{bytes, {}, &draws, error, &terminalState}; + const RvbNode* rootTimeline = nullptr; + collectDefinitions(bytes, scene, &context, &rootTimeline); + if (!rootTimeline) { + if (error) *error = "RVB has no root TIME timeline"; + return 0; + } + if (!context.renderTimeline(*rootTimeline, {}, {}, 0, "/")) return 0; + return context.maxAnimationFrames; +} + bool BuildRvbSymbolSnapshot(const std::vector& bytes, const RvbScene& scene, const std::string& symbolName, diff --git a/tools/rvb_probe.cpp b/tools/rvb_probe.cpp index 0e94514..5b32122 100644 --- a/tools/rvb_probe.cpp +++ b/tools/rvb_probe.cpp @@ -42,11 +42,15 @@ bool probe(const std::string& path, bool tree, const std::string& symbol, std::cerr << path << ": snapshot failed: " << error << '\n'; return false; } + const uint32_t animationFrames = symbol.empty() + ? gc::MeasureRvbSnapshotAnimationFrames(bytes, scene, state, &error) + : 1; std::cout << path << "\n movie=" << scene.sourceWidth << 'x' << scene.sourceHeight << " @ " << static_cast(scene.framesPerSecond) << " fps" << " bindings=" << scene.bindings.size() << " images=" << scene.images.size() + << " animationFrames=" << animationFrames << " initialDraws=" << snapshot.size() << "\n chunks:"; for (const gc::RvbChunk& chunk : scene.chunks) { std::cout << ' ' << chunk.tag << "@0x" << std::hex << chunk.offset