Add frame-based RVB animation snapshots

This commit is contained in:
2026-08-15 16:25:07 +02:00
parent c9575e1a9b
commit 777331d034
3 changed files with 58 additions and 6 deletions
+15
View File
@@ -4,6 +4,7 @@
#include <array>
#include <cstdint>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>
@@ -25,6 +26,12 @@ struct RvbImageDraw {
// root timeline). Clips not mentioned here remain on their first FRAM.
struct RvbSnapshotState {
std::unordered_map<std::string, std::string> 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<uint32_t>::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<std::string, uint32_t> animationFrameByPath;
bool includeRootOther = false;
};
@@ -46,6 +53,14 @@ bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
std::vector<RvbImageDraw>* 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<uint8_t>& 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).
+39 -6
View File
@@ -162,6 +162,7 @@ struct Context {
std::vector<RvbImageDraw>* 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<const RvbNode*> 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<uint32_t>(animationFrames.size()));
uint32_t requested = state ? state->animationFrame
: std::numeric_limits<uint32_t>::max();
if (state) {
const auto pathFrame = state->animationFrameByPath.find(path);
if (pathFrame != state->animationFrameByPath.end()) {
requested = pathFrame->second;
}
}
const size_t frameIndex = std::min<size_t>(requested, animationFrames.size() - 1);
targetFrame = animationFrames[frameIndex];
}
struct Placement {
uint32_t depth = 0;
@@ -383,6 +398,24 @@ bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
return context.renderTimeline(*rootTimeline, {}, {}, 0, "/");
}
uint32_t MeasureRvbSnapshotAnimationFrames(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
const RvbSnapshotState& state,
std::string* error) {
std::vector<RvbImageDraw> draws;
RvbSnapshotState terminalState = state;
terminalState.animationFrame = std::numeric_limits<uint32_t>::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<uint8_t>& bytes,
const RvbScene& scene,
const std::string& symbolName,
+4
View File
@@ -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<unsigned>(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