Extend GC stage and TUMO parsing

This commit is contained in:
2026-08-15 13:01:11 +02:00
parent 8c4e1bcacb
commit c9575e1a9b
7 changed files with 406 additions and 150 deletions
+14 -1
View File
@@ -6,8 +6,17 @@ include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
option(VECTORAIL_GC_BUILD_TOOLS "Build format inspection tools" ON)
if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(VECTORAIL_GC_INSTALL_DEFAULT ON)
else()
set(VECTORAIL_GC_INSTALL_DEFAULT OFF)
endif()
option(VECTORAIL_GC_INSTALL "Generate VectorailGC install rules"
${VECTORAIL_GC_INSTALL_DEFAULT})
if(NOT TARGET glm::glm)
find_package(glm CONFIG REQUIRED)
endif()
add_library(vectorail-gc
src/EventStream.cpp
@@ -51,6 +60,9 @@ if(VECTORAIL_GC_BUILD_TOOLS)
add_executable(vectorail-gc-stage-probe tools/stage_probe.cpp)
target_link_libraries(vectorail-gc-stage-probe PRIVATE Vectorail::GC)
add_executable(vectorail-gc-stage-ext-probe tools/stage_ext_probe.cpp)
target_link_libraries(vectorail-gc-stage-ext-probe PRIVATE Vectorail::GC)
add_executable(vectorail-gc-rvb-probe tools/rvb_probe.cpp)
target_link_libraries(vectorail-gc-rvb-probe PRIVATE Vectorail::GC)
@@ -61,6 +73,7 @@ if(VECTORAIL_GC_BUILD_TOOLS)
target_link_libraries(vectorail-gc-effect-probe PRIVATE Vectorail::GCEffects)
endif()
if(VECTORAIL_GC_INSTALL)
install(TARGETS vectorail-gc vectorail-gc-effects
EXPORT VectorailGCTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
@@ -90,4 +103,4 @@ install(FILES
${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC
)
endif()
+31 -7
View File
@@ -138,6 +138,9 @@ struct CameraPoint {
struct ParticlePoint {
static constexpr size_t kRecordSize = 44;
// TuneBGEffectData in Android libtune.so. GameScene::ExecFlowItem uses
// these authored keys to spawn the stage's flowing background items.
uint32_t timeMs = 0;
uint32_t enabled = 0;
uint32_t shape = 0;
@@ -157,6 +160,16 @@ struct VisualizerPoint {
Color color;
};
struct BackgroundImagePoint {
static constexpr size_t kRecordSize = 20;
uint32_t timeMs = 0;
uint32_t mode = 0;
uint32_t atlasIndex = 0;
int32_t textureIndex = -1;
Color color;
};
struct BackgroundColorPoint {
static constexpr size_t kRecordSize = 22;
@@ -166,27 +179,27 @@ struct BackgroundColorPoint {
Color bottomRight;
Color bottomLeft;
bool interpolateToNext = false;
bool audioReactive = false;
bool rhythmReactive = false;
};
struct VisibilityPoint {
uint32_t timeMs = 0;
bool fadeOut = false;
bool fadeIn = false;
bool repeat = false; // wire +4: member of a repeating key range
bool interpolate = false; // wire +5: interpolate/fade into this key
bool visible = false;
};
struct TransformPoint {
uint32_t timeMs = 0;
bool tweenTowards = false;
bool tweenAway = false;
bool repeat = false; // wire +4: member of a repeating key range
bool interpolate = false; // wire +5: interpolate to the following key
float value[3] = {0.0f, 0.0f, 0.0f};
};
struct ObjectColorPoint {
uint32_t timeMs = 0;
bool tweenTowards = false;
bool tweenAway = false;
bool repeat = false; // wire +4: member of a repeating key range
bool interpolate = false; // wire +5: interpolate to the following key
Color color;
};
@@ -221,13 +234,24 @@ struct ParsedStagePattern {
std::vector<CameraPoint> cameras;
std::vector<ParticlePoint> particles;
std::vector<VisualizerPoint> visualizer;
std::vector<std::string> backgroundTextureNames;
std::vector<BackgroundImagePoint> backgroundImages;
std::vector<BackgroundColorPoint> backgroundColors;
std::vector<std::string> modelNames;
std::vector<std::string> fragmentShaderNames;
std::vector<StageObject> objects;
};
struct StageExtension {
std::array<std::vector<NoteSetting>, 4> noteSettings;
uint32_t unknown = 0;
std::vector<StageNote> notes;
size_t trailingBytes = 0;
};
bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err);
bool ParseStageExtension(const std::vector<uint8_t>& bytes, StageExtension* out,
std::string* err);
} // namespace gc
+3 -1
View File
@@ -17,9 +17,11 @@ struct TumoGeometry {
std::vector<TumoVertex> triangles;
std::vector<TumoVertex> solidLines;
std::vector<TumoVertex> wireframeLines;
std::uint32_t meshCount = 0;
std::uint32_t nodeCount = 0;
};
// Decodes the common one-mesh TUMO layout used by gameplay stage objects.
// Decodes and aggregates every static TUMO mesh used by gameplay stage objects.
// Polygon fans are expanded on the host so the PSP only has to submit flat
// GU_TRIANGLES/GU_LINES arrays.
bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* error);
+152 -43
View File
@@ -231,6 +231,43 @@ bool parseTrack(const StageDat& dat, uint32_t start, uint32_t next, std::vector<
return true;
}
bool readStageNote(PatternReader& r, StageNote* out, std::string* err) {
if (!out) return false;
StageNote note;
uint8_t rawType = 0;
if (!r.u32(&note.timeMs) || !r.u8(&rawType) || !r.u8(&note.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(&note.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(&note.flag37) || !r.u8(&note.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(&note.param67) || !r.u32(&note.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(&note.param95)) return failAt("Note field at +95", r.tell(), err);
*out = note;
return true;
}
bool parseNotes(
const StageDat& dat,
uint32_t start,
@@ -257,26 +294,7 @@ bool parseNotes(
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
StageNote note;
uint8_t rawType = 0;
if (!r.u32(&note.timeMs) ||
!r.u8(&rawType) ||
!r.u8(&note.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(&note.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(&note.flag37) || !r.u8(&note.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(&note.param67) || !r.u32(&note.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(&note.param95)) return failAt("Note field at +95", r.tell(), err);
if (!readStageNote(r, &note, err)) return false;
out->push_back(note);
}
@@ -368,6 +386,43 @@ bool parseVisualizer(const StageDat& dat, uint32_t start, uint32_t next,
return true;
}
bool parseBackgroundImages(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<std::string>* textureNames,
std::vector<BackgroundImagePoint>* out, std::string* err) {
if (!textureNames || !out || !validOffset(dat, start)) return false;
PatternReader r(dat.bytes, start, next);
textureNames->clear();
out->clear();
uint32_t count = 0;
if (!r.u32(&count)) return failAt("BackgroundImage texture count", r.tell(), err);
textureNames->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
std::string name;
if (!r.string8(&name)) return failAt("BackgroundImage texture name", r.tell(), err);
textureNames->push_back(std::move(name));
}
if (!r.u32(&count)) return failAt("BackgroundImage key count", r.tell(), err);
if (!readCount(count, BackgroundImagePoint::kRecordSize, r.tell(), next,
"BackgroundImage", err)) {
return false;
}
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
BackgroundImagePoint point;
uint32_t textureIndex = 0;
if (!r.u32(&point.timeMs) || !r.u32(&point.mode) || !r.u32(&point.atlasIndex) ||
!r.u32(&textureIndex) || !r.color(&point.color)) {
return failAt("BackgroundImage", r.tell(), err);
}
point.textureIndex = static_cast<int32_t>(textureIndex);
out->push_back(point);
}
if (r.tell() != next) return failAt("BackgroundImage trailing bytes", r.tell(), err);
return true;
}
bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<BackgroundColorPoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
@@ -380,14 +435,14 @@ bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next,
for (uint32_t i = 0; i < count; ++i) {
BackgroundColorPoint c;
uint8_t interpolateToNext = 0;
uint8_t audioReactive = 0;
uint8_t rhythmReactive = 0;
if (!r.u32(&c.timeMs) || !r.color(&c.topRight) || !r.color(&c.topLeft) ||
!r.color(&c.bottomRight) || !r.color(&c.bottomLeft) ||
!r.u8(&interpolateToNext) || !r.u8(&audioReactive)) {
!r.u8(&interpolateToNext) || !r.u8(&rhythmReactive)) {
return failAt("ColorTable entry", r.tell(), err);
}
c.interpolateToNext = interpolateToNext != 0;
c.audioReactive = audioReactive != 0;
c.rhythmReactive = rhythmReactive != 0;
out->push_back(c);
}
if (r.tell() != next) return failAt("ColorTable trailing bytes", r.tell(), err);
@@ -402,14 +457,14 @@ bool parseTransformArray(PatternReader& r, std::vector<TransformPoint>* out,
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
TransformPoint p;
uint8_t towards = 0;
uint8_t away = 0;
if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) ||
uint8_t repeat = 0;
uint8_t interpolate = 0;
if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) ||
!r.f32(&p.value[0]) || !r.f32(&p.value[1]) || !r.f32(&p.value[2])) {
return failAt(what, r.tell(), err);
}
p.tweenTowards = towards != 0;
p.tweenAway = away != 0;
p.repeat = repeat != 0;
p.interpolate = interpolate != 0;
out->push_back(p);
}
return true;
@@ -418,8 +473,12 @@ bool parseTransformArray(PatternReader& r, std::vector<TransformPoint>* out,
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;
std::vector<StageObject>* out, size_t* extensionStart,
std::string* err) {
if (!modelNames || !fragmentShaderNames || !out || !extensionStart ||
!validOffset(dat, start)) {
return false;
}
PatternReader r(dat.bytes, start, next);
modelNames->clear();
fragmentShaderNames->clear();
@@ -466,14 +525,14 @@ bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next,
object.visibility.reserve(keyCount);
for (uint32_t key = 0; key < keyCount; ++key) {
VisibilityPoint p;
uint8_t fadeOut = 0;
uint8_t fadeIn = 0;
uint8_t repeat = 0;
uint8_t interpolate = 0;
uint8_t visible = 0;
if (!r.u32(&p.timeMs) || !r.u8(&fadeOut) || !r.u8(&fadeIn) || !r.u8(&visible)) {
if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) || !r.u8(&visible)) {
return failAt("Object visibility", r.tell(), err);
}
p.fadeOut = fadeOut != 0;
p.fadeIn = fadeIn != 0;
p.repeat = repeat != 0;
p.interpolate = interpolate != 0;
p.visible = visible != 0;
object.visibility.push_back(p);
}
@@ -487,18 +546,21 @@ bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next,
object.colorChanges.reserve(keyCount);
for (uint32_t key = 0; key < keyCount; ++key) {
ObjectColorPoint p;
uint8_t towards = 0;
uint8_t away = 0;
if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) || !r.color(&p.color)) {
uint8_t repeat = 0;
uint8_t interpolate = 0;
if (!r.u32(&p.timeMs) || !r.u8(&repeat) || !r.u8(&interpolate) || !r.color(&p.color)) {
return failAt("Object color key", r.tell(), err);
}
p.tweenTowards = towards != 0;
p.tweenAway = away != 0;
p.repeat = repeat != 0;
p.interpolate = interpolate != 0;
object.colorChanges.push_back(p);
}
out->push_back(std::move(object));
}
if (r.tell() != next) return failAt("ObjectArray trailing bytes", r.tell(), err);
// LoadBGData records RomReadPosition() here and uses it as the base of the
// extension offset table. Header word 11 points into that table, so it is
// not a reliable object-stream end marker on older mobile charts.
*extensionStart = r.tell();
return true;
}
@@ -573,12 +635,17 @@ bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string
if (err) *err = "invalid background section order";
return false;
}
size_t objectExtensionStart = 0;
if (!parseParticles(dat, tmp.header.particles, tmp.header.visualizer, &tmp.particles, err) ||
!parseVisualizer(dat, tmp.header.visualizer, tmp.header.unk1, &tmp.visualizer, err) ||
!parseBackgroundImages(dat, tmp.header.unk1, tmp.header.colors,
&tmp.backgroundTextureNames, &tmp.backgroundImages, err) ||
!parseBackgroundColors(dat, tmp.header.colors, tmp.header.objects, &tmp.backgroundColors, err) ||
!parseObjects(dat, tmp.header.objects, tmp.header.colors2,
&tmp.modelNames, &tmp.fragmentShaderNames, &tmp.objects, err) ||
!parseObjectParents(dat, tmp.header.colors2, tmp.header.unk3, &tmp.objects, err)) {
&tmp.modelNames, &tmp.fragmentShaderNames, &tmp.objects,
&objectExtensionStart, err) ||
!parseObjectParents(dat, static_cast<uint32_t>(objectExtensionStart), tmp.header.unk3,
&tmp.objects, err)) {
return false;
}
@@ -586,4 +653,46 @@ bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string
return true;
}
bool ParseStageExtension(const std::vector<uint8_t>& bytes, StageExtension* out,
std::string* err) {
if (!out || bytes.size() < 6) {
if (err) *err = "stage extension is smaller than its six-byte prefix";
return false;
}
StageExtension tmp;
PatternReader r(bytes, 6, bytes.size());
for (std::vector<NoteSetting>& list : tmp.noteSettings) {
uint16_t count = 0;
if (!r.u16(&count) ||
!readCount(count, 12, r.tell(), bytes.size(), "StageExtension timing", err)) {
return false;
}
list.reserve(count);
for (uint16_t i = 0; i < count; ++i) {
NoteSetting setting;
if (!r.u32(&setting.timeMs) || !r.u32(&setting.mode) || !r.f32(&setting.value)) {
return failAt("StageExtension timing", r.tell(), err);
}
list.push_back(setting);
}
}
uint32_t noteCount = 0;
if (!r.u32(&tmp.unknown) || !r.u32(&noteCount) ||
!readCount(noteCount, StageNote::kRecordSize, r.tell(), bytes.size(),
"StageExtension notes", err)) {
return false;
}
tmp.notes.reserve(noteCount);
for (uint32_t i = 0; i < noteCount; ++i) {
StageNote note;
if (!readStageNote(r, &note, err)) return false;
tmp.notes.push_back(note);
}
tmp.trailingBytes = bytes.size() - r.tell();
*out = std::move(tmp);
return true;
}
} // namespace gc
+15 -3
View File
@@ -72,9 +72,12 @@ bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* e
Reader reader(std::move(bytes));
std::uint32_t meshCount = 0;
if (!reader.u32(&meshCount) || meshCount != 1) {
return malformed(error, "unsupported TUMO mesh layout");
if (!reader.u32(&meshCount) || meshCount == 0 || meshCount > 100000) {
return malformed(error, "invalid TUMO mesh count");
}
out->meshCount = meshCount;
for (std::uint32_t mesh = 0; mesh < meshCount; ++mesh) {
std::uint32_t nameCount = 0;
if (!reader.u32(&nameCount) || nameCount > 4096) return malformed(error, "invalid TUMO names");
for (std::uint32_t i = 0; i < nameCount; ++i) {
@@ -146,7 +149,7 @@ bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* e
if (!reader.u32(&lineCount) || lineCount > 10000000) {
return malformed(error, "invalid TUMO edge count");
}
out->wireframeLines.reserve(static_cast<std::size_t>(lineCount) * 2);
out->wireframeLines.reserve(out->wireframeLines.size() + 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()) {
@@ -154,6 +157,15 @@ bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* e
}
out->wireframeLines.push_back(vertices[index]);
}
std::uint32_t nodeCount = 0;
if (!reader.u32(&nodeCount)) return malformed(error, "missing TUMO node count");
out->nodeCount += nodeCount;
if (nodeCount != 0 && mesh + 1 != meshCount) {
return malformed(error, "unsupported TUMO nodes before the final mesh");
}
if (nodeCount != 0) break;
}
if (out->triangles.empty() && out->solidLines.empty() && out->wireframeLines.empty()) {
return malformed(error, "TUMO model contains no geometry");
}
+47
View File
@@ -0,0 +1,47 @@
#include "gc/StagePattern.hpp"
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
#include <vector>
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <stage_ext.dat> [...]\n";
return 2;
}
bool okay = true;
for (int i = 1; i < argc; ++i) {
std::ifstream file(argv[i], std::ios::binary);
if (!file) {
std::cerr << argv[i] << ": could not open file\n";
okay = false;
continue;
}
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)), {});
gc::StageExtension extension;
std::string error;
if (!gc::ParseStageExtension(bytes, &extension, &error)) {
std::cerr << argv[i] << ": " << error << '\n';
okay = false;
continue;
}
std::map<uint8_t, size_t> types;
for (const gc::StageNote& note : extension.notes) {
++types[static_cast<uint8_t>(note.type)];
}
std::cout << argv[i] << ": notes=" << extension.notes.size()
<< " trailing=" << extension.trailingBytes << " timing=";
for (const auto& list : extension.noteSettings) std::cout << list.size() << ',';
std::cout << " types=";
for (const auto& [type, count] : types) {
std::cout << static_cast<unsigned>(type) << ':' << count << ',';
}
std::cout << '\n';
}
return okay ? 0 : 1;
}
+50 -1
View File
@@ -39,12 +39,29 @@ bool probe(const std::string& path) {
size_t flashingObjects = 0;
size_t noGlobalFadeObjects = 0;
size_t translucentObjects = 0;
size_t objectAnimationKeys = 0;
size_t objectRepeatKeys = 0;
size_t objectInterpolateKeys = 0;
size_t objectDifferingFlagKeys = 0;
const auto countAnimationKeys = [&](const auto& keys) {
objectAnimationKeys += keys.size();
for (const auto& key : keys) {
objectRepeatKeys += key.repeat ? 1 : 0;
objectInterpolateKeys += key.interpolate ? 1 : 0;
objectDifferingFlagKeys += key.repeat != key.interpolate ? 1 : 0;
}
};
for (const gc::StageObject& object : stage.objects) {
if (object.parentIndex >= 0) ++parentedObjects;
if (object.wireframe) ++wireframeObjects;
if (object.flashing) ++flashingObjects;
if (object.unknownFlag) ++noGlobalFadeObjects;
if (object.color[3] < 0.999f) ++translucentObjects;
countAnimationKeys(object.visibility);
countAnimationKeys(object.movement);
countAnimationKeys(object.scaling);
countAnimationKeys(object.rotations);
countAnimationKeys(object.colorChanges);
}
std::cout << path
@@ -67,6 +84,8 @@ bool probe(const std::string& path) {
<< " noteNames=" << stage.noteNames.size()
<< "\n background: particles=" << stage.particles.size()
<< " visualizerKeys=" << stage.visualizer.size()
<< " imageTextures=" << stage.backgroundTextureNames.size()
<< " imageKeys=" << stage.backgroundImages.size()
<< " colorKeys=" << stage.backgroundColors.size()
<< " models=" << stage.modelNames.size()
<< " objects=" << stage.objects.size()
@@ -74,7 +93,11 @@ bool probe(const std::string& path) {
<< " wire=" << wireframeObjects
<< " flashing=" << flashingObjects
<< " noGlobalFade=" << noGlobalFadeObjects
<< " translucent=" << translucentObjects;
<< " translucent=" << translucentObjects
<< " objectKeys=" << objectAnimationKeys
<< " repeat=" << objectRepeatKeys
<< " interpolate=" << objectInterpolateKeys
<< " differingFlags=" << objectDifferingFlagKeys;
if (!stage.notes.empty()) {
const auto [lo, hi] = std::minmax_element(
stage.notes.begin(), stage.notes.end(),
@@ -96,6 +119,32 @@ bool probe(const std::string& path) {
for (const auto& key : stage.visualizer) ++visualizerTypes[key.type];
std::cout << "\n visualizerTypes:";
for (const auto& [type, count] : visualizerTypes) std::cout << ' ' << type << '=' << count;
std::cout << "\n visualizerKeys:";
for (const auto& key : stage.visualizer) {
std::cout << ' ' << key.timeMs << ":type=" << key.type
<< ",rgba=" << static_cast<unsigned>(key.color.r) << ','
<< static_cast<unsigned>(key.color.g) << ','
<< static_cast<unsigned>(key.color.b) << ','
<< static_cast<unsigned>(key.color.a);
}
}
if (!stage.backgroundTextureNames.empty()) {
std::cout << "\n backgroundTextures:";
for (size_t i = 0; i < stage.backgroundTextureNames.size(); ++i) {
std::cout << ' ' << i << '=' << stage.backgroundTextureNames[i];
}
}
if (!stage.backgroundImages.empty()) {
std::cout << "\n backgroundImageKeys:";
for (const auto& image : stage.backgroundImages) {
std::cout << ' ' << image.timeMs << ":mode=" << image.mode
<< ",atlas=" << image.atlasIndex
<< ",texture=" << image.textureIndex
<< ",rgba=" << static_cast<unsigned>(image.color.r) << ','
<< static_cast<unsigned>(image.color.g) << ','
<< static_cast<unsigned>(image.color.b) << ','
<< static_cast<unsigned>(image.color.a);
}
}
if (!stage.drawDistances.empty()) {
std::cout << "\n drawDistance=" << stage.drawDistances.front().timeMs << ':'