From 4d2488a50ec50d7f06d641d58f9b51a63a966b76 Mon Sep 17 00:00:00 2001 From: craftablescience Date: Sun, 18 Oct 2026 13:21:18 -0700 Subject: [PATCH 1/2] vcdpp: add library --- CMakeLists.txt | 7 +- README.md | 19 ++ docs/index.html | 18 ++ include/vcdpp/IMAGE.h | 1 + include/vcdpp/VCD.h | 271 +++++++++++++++++++ include/vcdpp/vcdpp.h | 9 + src/vcdpp/IMAGE.cpp | 1 + src/vcdpp/VCD.cpp | 582 +++++++++++++++++++++++++++++++++++++++++ src/vcdpp/_vcdpp.cmake | 9 + test/vcdpp.cpp | 73 ++++++ 10 files changed, 989 insertions(+), 1 deletion(-) create mode 100644 include/vcdpp/IMAGE.h create mode 100644 include/vcdpp/VCD.h create mode 100644 include/vcdpp/vcdpp.h create mode 100644 src/vcdpp/IMAGE.cpp create mode 100644 src/vcdpp/VCD.cpp create mode 100644 src/vcdpp/_vcdpp.cmake create mode 100644 test/vcdpp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 395d47b84..074b406b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ option(SOURCEPP_USE_MDLPP "Build mdlpp library" ${SOURC option(SOURCEPP_USE_SNDPP "Build sndpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_STEAMPP "Build steampp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_TOOLPP "Build toolpp library" ${SOURCEPP_LIBS_START_ENABLED}) +option(SOURCEPP_USE_VCDPP "Build vcdpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VCRYPTPP "Build vcryptpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VPKPP "Build vpkpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VTFPP "Build vtfpp library" ${SOURCEPP_LIBS_START_ENABLED}) @@ -74,6 +75,9 @@ endif() if(SOURCEPP_USE_TOOLPP) set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE) endif() +if(SOURCEPP_USE_VCDPP) + set(SOURCEPP_USE_VPKPP ON CACHE INTERNAL "" FORCE) +endif() if(SOURCEPP_USE_VPKPP) set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE) endif() @@ -164,6 +168,7 @@ add_sourcepp_library(mdlpp TEST ) # sourcepp::mdlpp add_sourcepp_library(sndpp TEST ) # sourcepp::sndpp add_sourcepp_library(steampp C CSHARP PYTHON TEST ) # sourcepp::steampp add_sourcepp_library(toolpp PYTHON TEST ) # sourcepp::toolpp +add_sourcepp_library(vcdpp TEST ) # sourcepp::vcdpp add_sourcepp_library(vcryptpp C CSHARP PYTHON TEST ) # sourcepp::vcryptpp add_sourcepp_library(vpkpp C CSHARP PYTHON TEST ) # sourcepp::vpkpp add_sourcepp_library(vtfpp C CSHARP PYTHON TEST BENCH) # sourcepp::vtfpp @@ -179,7 +184,7 @@ sourcepp_test_setup_post() # Print options print_options(OPTIONS - USE_BSPPP USE_DMXPP USE_FSPP USE_GAMEPP USE_KVPP USE_MDLPP USE_SNDPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP "\n" + USE_BSPPP USE_DMXPP USE_FSPP USE_GAMEPP USE_KVPP USE_MDLPP USE_SNDPP USE_STEAMPP USE_TOOLPP USE_VCDPP USE_VCRYPTPP USE_VPKPP USE_VTFPP "\n" BUILD_C_WRAPPERS BUILD_CSHARP_WRAPPERS BUILD_PYTHON_WRAPPERS BUILD_WASM_WRAPPERS "\n" BUILD_BENCHMARKS BUILD_WITH_TBB BUILD_WITH_THREADS BUILD_TESTS BUILD_TESTS_EXTRA BUILD_WIN7_COMPAT LINK_STATIC_MSVC_RUNTIME "\n") diff --git a/README.md b/README.md index befcb20be..f3fde5175 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,25 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj ✅ + + vcdpp* + IMAGE (Scenes) + ✅ + ✅ + + + + + + VCD v1 +
• BVCD v1-4 +
• VCE v1 +
• XCD v??TODO?? (Xbox, Half-Life 2) + + ✅ + ✅ + + vcryptpp VICE encrypted files diff --git a/docs/index.html b/docs/index.html index ef7854281..04fb7d95d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -584,6 +584,24 @@

Supported Formats

✅ ✅ + + vcdpp* + IMAGE (Scenes) + ✅ + ✅ + + + + VCD v1 + + + ✅ + ✅ + vcryptpp diff --git a/include/vcdpp/IMAGE.h b/include/vcdpp/IMAGE.h new file mode 100644 index 000000000..6f70f09be --- /dev/null +++ b/include/vcdpp/IMAGE.h @@ -0,0 +1 @@ +#pragma once diff --git a/include/vcdpp/VCD.h b/include/vcdpp/VCD.h new file mode 100644 index 000000000..167feddf5 --- /dev/null +++ b/include/vcdpp/VCD.h @@ -0,0 +1,271 @@ +// ReSharper disable CppUnusedIncludeDirective + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace vcdpp { + +struct Event { + enum class Type : uint8_t { + UNSPECIFIED = 0, + SECTION = 1, + EXPRESSION = 2, + LOOK_AT = 3, + MOVE_TO = 4, + SPEAK = 5, + GESTURE = 6, + SEQUENCE = 7, + FACE = 8, + FIRE_TRIGGER = 9, + FLEX_ANIMATION = 10, + SUBSCENE = 11, + LOOP = 12, + INTERRUPT = 13, + STOP_POINT = 14, + PERMIT_RESPONSES = 15, + GENERIC = 16, + CAMERA = 17, + SCRIPT = 18, + } type; + std::string name; + float startTime = 0.f; + float endTime = 0.f; + std::string param; + std::string param2; + std::string param3; + + enum Flags : uint8_t { + FLAG_NONE = 0, + FLAG_RESUME_CONDITION = 1 << 0, + FLAG_LOCK_BODY_FACING = 1 << 1, + FLAG_FIXED_LENGTH = 1 << 2, + FLAG_ACTIVE = 1 << 3, + FLAG_FORCE_SHORT_MOVEMENT = 1 << 4, + FLAG_PLAY_OVER_SCRIPT = 1 << 5, + FLAG_SYNC_TO_FOLLOWING_GESTURE = 1 << 6, + } flags = FLAG_ACTIVE; + + float pitch = 0.f; + float yaw = 0.f; + + float distanceToTarget = 0.f; + + enum class Interpolate : uint8_t { + DEFAULT = 0, + CATMULLROM_NORMALIZE_X, + EASEIN, + EASEOUT, + EASEINOUT, + BSPLINE, + LINEAR_INTERP, + KOCHANEK, + KOCHANEK_EARLY, + KOCHANEK_LATE, + SIMPLE_CUBIC, + CATMULLROM, + CATMULLROM_NORMALIZE, + CATMULLROM_TANGENT, + EXPONENTIAL_DECAY, + HOLD, + COUNT, + }; + + struct Curve { + Interpolate from = Interpolate::DEFAULT; + Interpolate to = Interpolate::DEFAULT; + }; + + struct Edge { + Curve curve; + float time = 0.f; + }; + + struct Sample { + float time = 0.f; + float value = 0.f; + std::optional curve; + }; + + struct RampLegacy { + float attack = 1.f; + float sustain = 1.f; + float decay = 1.f; + }; + std::optional eventRampLegacy; + + struct Ramp { + std::optional curve; + std::optional leftEdge; + std::optional rightEdge; + std::vector samples; + }; + std::optional eventRamp; + + struct RelativeTagLegacy { + std::string name; + std::string filename; + }; + + std::optional relativeTagLegacy; + + struct Tag { + std::string name; + float value = 0.f; + }; + + std::vector relativeTags; + std::vector absoluteTagsPlaybackTimes; + std::vector absoluteTagsShiftedTimes; + + struct FlexTimingTag { + Tag tag; + bool locked = false; + }; + + std::vector flexTimingTags; + + struct FlexAnimation { + bool samplesUseTime = false; + std::optional curve; + + struct Track { + std::string name; + bool disabled = false; + float minRange = 0.f; + float maxRange = 1.f; + std::optional leftEdge; + std::optional rightEdge; + std::vector primary; + std::optional> secondary; + }; + + std::vector tracks; + }; + + std::vector flexAnimations; + + float sequenceDuration = 0.f; + + struct SpeechData { + enum class CCType : uint8_t { + CC_MASTER = 0, + CC_SLAVE = 1, + CC_DISABLED = 2, // unused + } ccType = CCType::CC_MASTER; + std::string ccToken; + enum CCFlags : uint8_t { + FLAG_NONE = 0, + FLAG_USING_COMBINED_FILE = 1 << 0, // unused + FLAG_COMBINED_USING_GENDER_TOKEN = 1 << 1, // unused + FLAG_NO_ATTENUATE = 1 << 2, + } ccFlags = FLAG_NONE; + } speechData; + + int8_t loopCount = 0; +}; +SOURCEPP_BITFLAGS_ENUM(Event::Flags); +SOURCEPP_BITFLAGS_ENUM(Event::SpeechData::CCFlags); + +struct Actor { + struct Channel { + std::string name; + std::vector events; + bool active = true; + }; + + std::string name; + std::string faceposerModel; + std::vector channels; + bool active = true; +}; + +struct FaceposerSettings { + struct ScaleSettings { + int choreoView = 100; + int expressionTool = 100; + int gestureTool = 100; + int rampTool = 100; + int sceneRampTool = 100; + } scaleSettings; + std::string mapName; + uint8_t fps = 60; + bool snap = false; + std::optional ignorePhonemes; +}; + +class VCD { +public: + enum class Platform { + UNKNOWN = 0, + VCD = 1, + VCE = 2, + BVCD = sourcepp::parser::binary::makeFourCC("bvcd"), + XVCD = sourcepp::parser::binary::makeFourCC("xvcd"), + }; + + enum class ParseError { + OK = 0, + EXPECTED_OPENING_BRACE, + UNKNOWN_EVENT_TYPE, + UNKNOWN_ABSOLUTETAGS_TYPE, + UNKNOWN_CC_TYPE, + INVALID_CURVE_TOKEN, + UNKNOWN_CURVE_TYPE, + UNKNOWN_EVENT_KEY, + UNKNOWN_CHANNEL_KEY, + UNKNOWN_ACTOR_KEY, + UNKNOWN_SCENE_SETTINGS_KEY, + UNKNOWN_TOP_LEVEL_KEY, + }; + + [[nodiscard]] static std::string getMessageForParseErrorCode(ParseError error, std::string_view token); + + class SyntaxError : public std::runtime_error { + public: + explicit SyntaxError(ParseError error, std::string_view token) : std::runtime_error{getMessageForParseErrorCode(error, token)} {} + }; + + VCD() = default; + + explicit VCD(std::string_view vcdData); + + [[nodiscard]] Platform getPlatform() const; + + [[nodiscard]] uint8_t getVersion() const; + + [[nodiscard]] uint32_t getChecksum() const; + + [[nodiscard]] const std::vector& getGlobalEvents() const; + + [[nodiscard]] const std::vector& getActors() const; + + [[nodiscard]] const std::optional& getSceneRamp() const; + + [[nodiscard]] const FaceposerSettings& getFaceposerSettings() const; + +protected: + Platform platform = Platform::UNKNOWN; + uint8_t version = 1; + uint32_t checksum = 0; + std::vector globalEvents; + std::vector actors; + std::optional sceneRamp; + FaceposerSettings faceposerSettings; +}; + +namespace literals { + +inline VCD operator""_vcd(const char* str, const std::size_t len) { + return VCD{std::string_view{str, len}}; +} + +} // namespace literals + +} // namespace vcdpp diff --git a/include/vcdpp/vcdpp.h b/include/vcdpp/vcdpp.h new file mode 100644 index 000000000..5977d980b --- /dev/null +++ b/include/vcdpp/vcdpp.h @@ -0,0 +1,9 @@ +#pragma once + +/* + * This header is just included so consumers of this library can + * include it the same way as any of the other SourcePP libraries. + */ + +#include "IMAGE.h" +#include "VCD.h" diff --git a/src/vcdpp/IMAGE.cpp b/src/vcdpp/IMAGE.cpp new file mode 100644 index 000000000..f9fc7b9f0 --- /dev/null +++ b/src/vcdpp/IMAGE.cpp @@ -0,0 +1 @@ +#include diff --git a/src/vcdpp/VCD.cpp b/src/vcdpp/VCD.cpp new file mode 100644 index 000000000..c043102f7 --- /dev/null +++ b/src/vcdpp/VCD.cpp @@ -0,0 +1,582 @@ +// ReSharper disable CppUseStructuredBinding + +#include + +#include +#include + +#include + +#include + +using namespace sourcepp; +using namespace vcdpp; + +namespace { + +template +concept ValueType = std::same_as || std::integral || std::floating_point || std::same_as || std::same_as; + +} // namespace + +std::string VCD::getMessageForParseErrorCode(ParseError error, std::string_view token) { + switch (error) { + case ParseError::OK: + break; + case ParseError::EXPECTED_OPENING_BRACE: + return std::format("Expected opening brace '{{', got \"{}\"", token); + case ParseError::UNKNOWN_EVENT_TYPE: + return std::format("Unknown event type \"{}\"", token); + case ParseError::UNKNOWN_ABSOLUTETAGS_TYPE: + return std::format("Unknown absolutetags type \"{}\"", token); + case ParseError::UNKNOWN_CC_TYPE: + return std::format("Unknown cc type \"{}\"", token); + case ParseError::INVALID_CURVE_TOKEN: + return std::format("Invalid curve token \"{}\"", token); + case ParseError::UNKNOWN_CURVE_TYPE: + return std::format("Unknown curve type starting at \"{}\"", token); + case ParseError::UNKNOWN_EVENT_KEY: + return std::format("Unknown event key \"{}\"", token); + case ParseError::UNKNOWN_CHANNEL_KEY: + return std::format("Unknown channel key \"{}\"", token); + case ParseError::UNKNOWN_ACTOR_KEY: + return std::format("Unknown actor key \"{}\"", token); + case ParseError::UNKNOWN_SCENE_SETTINGS_KEY: + return std::format("Unknown scene settings key \"{}\"", token); + case ParseError::UNKNOWN_TOP_LEVEL_KEY: + return std::format("Unknown top level key \"{}\"", token); + } + return ""; +} + +VCD::VCD(std::string_view vcdData) { + this->platform = Platform::VCD; + + // Version + { + BufferStreamReadOnly stream{vcdData}; + parser::text::eatWhitespace(stream); + if (parser::text::readUnquotedString(stream) == "//") { + parser::text::eatWhitespace(stream); + if (string::iequals(parser::text::readUnquotedString(stream), "Choreo")) { + parser::text::eatWhitespace(stream); + if (string::iequals(parser::text::readUnquotedString(stream), "version")) { + parser::text::eatWhitespace(stream); + auto versionStr = parser::text::readUnquotedString(stream); + + // Handle the VCE comment header + if (versionStr.ends_with(':')) { + versionStr.pop_back(); + parser::text::eatWhitespace(stream); + if ( + const auto eventCount = parser::text::readUnquotedString(stream); + eventCount.size() >= 3 && eventCount.starts_with('<') && eventCount.ends_with('>') && string::isInt(eventCount.substr(1, eventCount.size() - 2)) + ) { + this->platform = Platform::VCE; + } + } + + // Check/set version + if (!string::toInt(versionStr, this->version)) { + this->platform = Platform::UNKNOWN; + return; + } + } + } + } + } + + // Checksum doesn't exist in text format + this->checksum = 0; + + // Ok now start parsing tokens + std::string backingString; + auto tokens = parser::text::Tokenizer{} + .setMultiLineCommentStart("") + .setMultiLineCommentEnd("") + .parse(vcdData, backingString); + + const auto parseValue = [&tokens](V& value) requires ValueType || (requires { typename V::value_type; } && std::same_as> && ValueType) { + const auto token = tokens.next(); + if constexpr (std::same_as) { // NOLINT(*-branch-clone) + value = string::iequals(token, "on") || string::iequals(token, "1"); + } else if constexpr (std::integral) { + string::toInt(token, value); + } else if constexpr (std::floating_point) { + string::toFloat(token, value); + } else if constexpr (std::same_as || std::same_as) { // NOLINT(*-branch-clone) + value = token; + } else if constexpr (std::same_as) { + value = string::iequals(token, "on") || string::iequals(token, "1"); + } else if constexpr (std::integral) { + typename V::value_type v; + string::toInt(token, v); + value = v; + } else if constexpr (std::floating_point) { + typename V::value_type v; + string::toFloat(token, v); + value = v; + } else if constexpr (std::same_as || std::same_as) { + value = token; + } else { + // you fucked up! + value = token; + } + }; + + static constexpr auto parseCurve = [](Event::Curve& curve, std::string_view token) { + auto curveToken = token; + + const auto parseInterpolateName = [token, &curveToken](Event::Interpolate& interpolate) { + // Ordered specifically to check for substrings last! + static constexpr std::array, 16> INTERPOLATE{{ + {Event::Interpolate::DEFAULT, "default"}, + {Event::Interpolate::CATMULLROM_NORMALIZE_X, "catmullrom_normalize_x"}, + {Event::Interpolate::CATMULLROM_NORMALIZE, "catmullrom_normalize"}, + {Event::Interpolate::CATMULLROM_TANGENT, "catmullrom_tangent"}, + {Event::Interpolate::CATMULLROM, "catmullrom"}, + {Event::Interpolate::EASEINOUT, "easeinout"}, + {Event::Interpolate::EASEIN, "easein"}, + {Event::Interpolate::EASEOUT, "easeout"}, + {Event::Interpolate::BSPLINE, "bspline"}, + {Event::Interpolate::LINEAR_INTERP, "linear_interp"}, + {Event::Interpolate::KOCHANEK_EARLY, "kochanek_early"}, + {Event::Interpolate::KOCHANEK_LATE, "kochanek_late"}, + {Event::Interpolate::KOCHANEK, "kochanek"}, + {Event::Interpolate::SIMPLE_CUBIC, "simple_cubic"}, + {Event::Interpolate::EXPONENTIAL_DECAY, "exponential_decay"}, + {Event::Interpolate::HOLD, "hold"}, + }}; + static_assert(static_cast(Event::Interpolate::COUNT) == INTERPOLATE.size()); + + if (!curveToken.starts_with("curve_")) { + throw SyntaxError{ParseError::INVALID_CURVE_TOKEN, token}; + } + curveToken = curveToken.substr(6); + + for (auto [currentInterpolate, currentName] : INTERPOLATE) { + if (curveToken.starts_with(currentName)) { + interpolate = currentInterpolate; + curveToken = curveToken.substr(currentName.size()); + return; + } + } + throw SyntaxError{ParseError::UNKNOWN_CURVE_TYPE, curveToken}; + }; + + parseInterpolateName(curve.from); + + if (!curveToken.starts_with("_to_")) { + throw SyntaxError{ParseError::INVALID_CURVE_TOKEN, token}; + } + curveToken = curveToken.substr(4); + + parseInterpolateName(curve.to); + }; + + const auto parseEdge = [&tokens, &parseValue](Event::Edge& edge) { + parseCurve(edge.curve, tokens.next()); + parseValue(edge.time); + }; + + const auto parseSamples = [&tokens, &parseValue](std::vector& samples) { + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.peek()) != "}") { + auto& sample = samples.emplace_back(); + parseValue(sample.time); + parseValue(sample.value); + if (tokens.peek() != "}" && !string::isFloat(tokens.peek())) { + Event::Curve curve; + parseCurve(curve, tokens.next()); + sample.curve = curve; + } + } + tokens.skip(); + }; + + const auto parseTags = [&tokens, &parseValue](std::vector& tags) { + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + auto& tag = tags.emplace_back(); + tag.name = token; + parseValue(tag.value); + } + }; + + const auto parseFlexTimingTags = [&tokens, &parseValue](std::vector& tags) { + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + auto& tag = tags.emplace_back(); + tag.tag.name = token; + parseValue(tag.tag.value); + parseValue(tag.locked); + } + }; + + const auto parseRamp = [&tokens, &parseEdge, &parseSamples](Event::Ramp& ramp) { + if (tokens.peek() == "leftedge") { + tokens.skip(); + Event::Edge edge; + parseEdge(edge); + ramp.leftEdge = edge; + } + + if (tokens.peek() == "rightedge") { + tokens.skip(); + Event::Edge edge; + parseEdge(edge); + ramp.rightEdge = edge; + } + + parseSamples(ramp.samples); + }; + + const auto parseFlexAnimation = [&tokens, &parseValue, &parseEdge, &parseSamples](Event::FlexAnimation& flexAnimation) { + if (tokens.peek() == "samples_use_time") { + flexAnimation.samplesUseTime = true; + tokens.skip(); + } + + static constexpr std::string_view DEFAULT_CURVE_TYPE_PREFIX = "defaultcurvetype="; + if (tokens.peek().starts_with(DEFAULT_CURVE_TYPE_PREFIX)) { + Event::Curve curve; + parseCurve(curve, tokens.next().substr(DEFAULT_CURVE_TYPE_PREFIX.size())); + flexAnimation.curve = curve; + } + + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + auto& track = flexAnimation.tracks.emplace_back(); + track.name = token; + + if (tokens.peek() == "disabled") { + track.disabled = true; + tokens.skip(); + } + + bool hasSecondary = false; + if (tokens.peek() == "combo") { + hasSecondary = true; + tokens.skip(); + } + + if (tokens.peek() == "range") { + tokens.skip(); + parseValue(track.minRange); + parseValue(track.maxRange); + } + + if (tokens.peek() == "leftedge") { + tokens.skip(); + Event::Edge edge; + parseEdge(edge); + track.leftEdge = edge; + } + + if (tokens.peek() == "rightedge") { + tokens.skip(); + Event::Edge edge; + parseEdge(edge); + track.rightEdge = edge; + } + + if (tokens.peek() != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, tokens.peek()}; + } + parseSamples(track.primary); + if (hasSecondary) { + if (tokens.peek() != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, tokens.peek()}; + } + std::vector secondary; + parseSamples(secondary); + track.secondary = std::move(secondary); + } + } + }; + + const auto parseEvent = [&tokens, &parseValue, &parseTags, &parseFlexTimingTags, &parseRamp, &parseFlexAnimation](Event& event) { + // Type + { + if ( + const auto token = tokens.next(); + string::iequals(token, "unspecified") + ) { + event.type = Event::Type::UNSPECIFIED; + } else if (string::iequals(token, "section")) { + event.type = Event::Type::SECTION; + } else if (string::iequals(token, "expression")) { + event.type = Event::Type::EXPRESSION; + } else if (string::iequals(token, "lookat")) { + event.type = Event::Type::LOOK_AT; + } else if (string::iequals(token, "moveto")) { + event.type = Event::Type::MOVE_TO; + } else if (string::iequals(token, "speak")) { + event.type = Event::Type::SPEAK; + } else if (string::iequals(token, "gesture")) { + event.type = Event::Type::GESTURE; + } else if (string::iequals(token, "sequence")) { + event.type = Event::Type::SEQUENCE; + } else if (string::iequals(token, "face")) { + event.type = Event::Type::FACE; + } else if (string::iequals(token, "firetrigger")) { + event.type = Event::Type::FIRE_TRIGGER; + } else if (string::iequals(token, "flexanimation")) { + event.type = Event::Type::FLEX_ANIMATION; + } else if (string::iequals(token, "subscene")) { + event.type = Event::Type::SUBSCENE; + } else if (string::iequals(token, "loop")) { + event.type = Event::Type::LOOP; + } else if (string::iequals(token, "interrupt")) { + event.type = Event::Type::INTERRUPT; + } else if (string::iequals(token, "stoppoint")) { + event.type = Event::Type::STOP_POINT; + } else if (string::iequals(token, "permitresponses")) { + event.type = Event::Type::PERMIT_RESPONSES; + } else if (string::iequals(token, "generic")) { + event.type = Event::Type::GENERIC; + } else if (string::iequals(token, "camera")) { + event.type = Event::Type::CAMERA; + } else if (string::iequals(token, "script")) { + event.type = Event::Type::SCRIPT; + } else { + event.type = Event::Type::UNSPECIFIED; + throw SyntaxError{ParseError::UNKNOWN_EVENT_TYPE, token}; + } + parseValue(event.name); + } + + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + if (string::iequals(token, "time")) { + parseValue(event.startTime); + if (string::isFloat(tokens.peek())) { + parseValue(event.endTime); + } + } else if (string::iequals(token, "param")) { + parseValue(event.param); + } else if (string::iequals(token, "param2")) { + parseValue(event.param2); + } else if (string::iequals(token, "param3")) { + parseValue(event.param3); + } else if (string::iequals(token, "resumecondition")) { + event.flags |= Event::FLAG_RESUME_CONDITION; + } else if (string::iequals(token, "lockbodyfacing")) { + event.flags |= Event::FLAG_LOCK_BODY_FACING; + } else if (string::iequals(token, "fixedlength")) { + event.flags |= Event::FLAG_FIXED_LENGTH; + } else if (string::iequals(token, "active")) { + bool active = true; + parseValue(active); + event.flags = active + ? event.flags | Event::FLAG_ACTIVE + : event.flags & ~Event::FLAG_ACTIVE; + } else if (string::iequals(token, "forceshortmovement")) { + event.flags |= Event::FLAG_FORCE_SHORT_MOVEMENT; + } else if (string::iequals(token, "playoverscript")) { + event.flags |= Event::FLAG_PLAY_OVER_SCRIPT; + } else if (string::iequals(token, "synctofollowinggesture")) { + event.flags |= Event::FLAG_SYNC_TO_FOLLOWING_GESTURE; + } else if (string::iequals(token, "pitch")) { + parseValue(event.pitch); + } else if (string::iequals(token, "yaw")) { + parseValue(event.yaw); + } else if (string::iequals(token, "distancetotarget")) { + parseValue(event.distanceToTarget); + } else if (string::iequals(token, "ramp")) { + Event::RampLegacy ramp; + parseValue(ramp.attack); + if (string::isFloat(tokens.peek())) { + parseValue(ramp.sustain); + if (string::isFloat(tokens.peek())) { + parseValue(ramp.decay); + } + } + event.eventRampLegacy = ramp; + } else if (string::iequals(token, "event_ramp")) { + Event::Ramp ramp; + parseRamp(ramp); + event.eventRamp = ramp; + } else if (string::iequals(token, "relativetag")) { + Event::RelativeTagLegacy relativeTag; + parseValue(relativeTag.name); + parseValue(relativeTag.filename); + event.relativeTagLegacy = relativeTag; + } else if (string::iequals(token, "tags")) { + parseTags(event.relativeTags); + } else if (string::iequals(token, "absolutetags")) { + token = tokens.next(); + if (string::iequals(token, "playback_time")) { + parseTags(event.absoluteTagsPlaybackTimes); + } else if (string::iequals(token, "shifted_time")) { + parseTags(event.absoluteTagsShiftedTimes); + } else { + throw SyntaxError{ParseError::UNKNOWN_ABSOLUTETAGS_TYPE, token}; + } + } else if (string::iequals(token, "flextimingtags")) { + parseFlexTimingTags(event.flexTimingTags); + } else if (string::iequals(token, "flexanimations")) { + parseFlexAnimation(event.flexAnimations.emplace_back()); + } else if (string::iequals(token, "sequenceduration")) { + parseValue(event.sequenceDuration); + } else if (string::iequals(token, "cctype")) { + if ( + token = tokens.next(); + string::iequals(token, "cc_master") + ) { + event.speechData.ccType = Event::SpeechData::CCType::CC_MASTER; + } else if (string::iequals(token, "cc_slave")) { + event.speechData.ccType = Event::SpeechData::CCType::CC_SLAVE; + } else if (string::iequals(token, "cc_disabled")) { + event.speechData.ccType = Event::SpeechData::CCType::CC_DISABLED; + } else { + throw SyntaxError{ParseError::UNKNOWN_CC_TYPE, token}; + } + } else if (string::iequals(token, "cctoken")) { + parseValue(event.speechData.ccToken); + } else if (string::iequals(token, "cc_usingcombinedfile")) { + event.speechData.ccFlags |= Event::SpeechData::FLAG_USING_COMBINED_FILE; + } else if (string::iequals(token, "cc_combinedusesgender")) { + event.speechData.ccFlags |= Event::SpeechData::FLAG_COMBINED_USING_GENDER_TOKEN; + } else if (string::iequals(token, "cc_noattenuate") || string::iequals(token, "cc_noattentuate")) { + // cc_noattentuate??? lol??? it shows up in HL2 and P1 but this string doesn't exist in newer games... + event.speechData.ccFlags |= Event::SpeechData::FLAG_NO_ATTENUATE; + } else if (string::iequals(token, "loopcount")) { + parseValue(event.loopCount); + } else { + throw SyntaxError{ParseError::UNKNOWN_EVENT_KEY, token}; + } + } + }; + + const auto parseActor = [&tokens, &parseValue, &parseEvent](Actor& actor) { + const auto parseChannel = [&tokens, &parseValue, &parseEvent](Actor::Channel& channel) { + parseValue(channel.name); + + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + if (string::iequals(token, "event")) { + parseEvent(channel.events.emplace_back()); + } else if (string::iequals(token, "active")) { + parseValue(channel.active); + } else { + throw SyntaxError{ParseError::UNKNOWN_CHANNEL_KEY, token}; + } + } + }; + + parseValue(actor.name); + + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + if (string::iequals(token, "faceposermodel")) { + parseValue(actor.faceposerModel); + } else if (string::iequals(token, "channel")) { + parseChannel(actor.channels.emplace_back()); + } else if (string::iequals(token, "active")) { + parseValue(actor.active); + } else { + throw SyntaxError{ParseError::UNKNOWN_ACTOR_KEY, token}; + } + } + }; + + const auto parseScaleSettings = [&tokens, &parseValue](FaceposerSettings::ScaleSettings& scaleSettings) { + auto token = tokens.next(); + if (token != "{") { + throw SyntaxError{ParseError::EXPECTED_OPENING_BRACE, token}; + } + while ((token = tokens.next()) != "}") { + if (string::iequals(token, "CChoreoView")) { + parseValue(scaleSettings.choreoView); + } else if (string::iequals(token, "ExpressionTool")) { + parseValue(scaleSettings.expressionTool); + } else if (string::iequals(token, "RampTool")) { + parseValue(scaleSettings.rampTool); + } else if (string::iequals(token, "GestureTool")) { + parseValue(scaleSettings.gestureTool); + } else if (string::iequals(token, "SceneRampTool")) { + parseValue(scaleSettings.sceneRampTool); + } else { + throw SyntaxError{ParseError::UNKNOWN_SCENE_SETTINGS_KEY, token}; + } + } + }; + + try { + while (true) { + if (const auto token = tokens.next(); string::iequals(token, "event")) { + parseEvent(this->globalEvents.emplace_back()); + } else if (string::iequals(token, "actor")) { + parseActor(this->actors.emplace_back()); + } else if (string::iequals(token, "scalesettings")) { + FaceposerSettings::ScaleSettings scaleSettings; + parseScaleSettings(scaleSettings); + this->faceposerSettings.scaleSettings = scaleSettings; + } else if (string::iequals(token, "mapname")) { + parseValue(this->faceposerSettings.mapName); + } else if (string::iequals(token, "fps")) { + parseValue(this->faceposerSettings.fps); + } else if (string::iequals(token, "snap")) { + parseValue(this->faceposerSettings.snap); + } else if (string::iequals(token, "ignorePhonemes")) { + parseValue(this->faceposerSettings.ignorePhonemes); + } else if (string::iequals(token, "scene_ramp")) { + Event::Ramp ramp; + parseRamp(ramp); + this->sceneRamp = ramp; + } else { + throw SyntaxError{ParseError::UNKNOWN_TOP_LEVEL_KEY, token}; + } + } + } catch (const std::overflow_error&) {} +} + +VCD::Platform VCD::getPlatform() const { + return this->platform; +} + +uint8_t VCD::getVersion() const { + return this->version; +} + +uint32_t VCD::getChecksum() const { + return this->checksum; +} + +const std::vector& VCD::getGlobalEvents() const { + return this->globalEvents; +} + +const std::vector& VCD::getActors() const { + return this->actors; +} + +const std::optional& VCD::getSceneRamp() const { + return this->sceneRamp; +} + +const FaceposerSettings& VCD::getFaceposerSettings() const { + return this->faceposerSettings; +} diff --git a/src/vcdpp/_vcdpp.cmake b/src/vcdpp/_vcdpp.cmake new file mode 100644 index 000000000..47d5dac4a --- /dev/null +++ b/src/vcdpp/_vcdpp.cmake @@ -0,0 +1,9 @@ +add_pretty_parser(vcdpp + DEPS_PUBLIC sourcepp::vpkpp + PRECOMPILED_HEADERS + "${CMAKE_CURRENT_SOURCE_DIR}/include/vcdpp/IMAGE.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vcdpp/VCD.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vcdpp/vcdpp.h" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/IMAGE.cpp" + "${CMAKE_CURRENT_LIST_DIR}/VCD.cpp") diff --git a/test/vcdpp.cpp b/test/vcdpp.cpp new file mode 100644 index 000000000..15dd5d291 --- /dev/null +++ b/test/vcdpp.cpp @@ -0,0 +1,73 @@ +#include + +#include + +using namespace sourcepp; +using namespace vcdpp; +using namespace vcdpp::literals; + +TEST(vcdpp, read_vce) { + const auto vce = R"( +// Choreo version 1: <1> Exported Events +event gesture "Gesture event 1" +{ + time 0.000000 -1.000000 + param "G_shrug" + absolutetags playback_time + { + "apex" 0.160000 + "accent" 0.240000 + "loop" 0.320000 + "end" 0.440000 + } + absolutetags shifted_time + { + "apex" 0.160000 + "accent" 0.240000 + "loop" 0.320000 + "end" 0.440000 + } + sequenceduration 1.700000 +})"_vcd; + ASSERT_EQ(vce.getPlatform(), VCD::Platform::VCE); + ASSERT_EQ(vce.getGlobalEvents().size(), 1); +} + +#if 0 + +TEST(vcdpp, blah) { + std::vector files; + for (const auto& it : std::filesystem::recursive_directory_iterator{"/home/lxlewis/Steam/steamapps/common", std::filesystem::directory_options::skip_permission_denied}) { + if (it.path().extension() == ".vcd") { + files.push_back(it.path()); + } + } + + // blacklist corrupted vcds + std::array blacklist{ + "/home/lxlewis/Steam/steamapps/common/GarrysMod/garrysmod/scenes/chell/death.vcd", + "/home/lxlewis/Steam/steamapps/common/GarrysMod/garrysmod/scenes/chell/idle.vcd", + "/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/update/scenes/gambler/warnsmoker01.vcd", + "/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/update/scenes/gambler/warnsmoker02.vcd", + "/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/update/scenes/gambler/warnsmoker03.vcd", + "/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/update/scenes/gambler/warnsmoker04.vcd", + "/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/update/scenes/mechanic/warnboomer06.vcd", + }; + + for (const auto& file : files) { + if (std::ranges::find(blacklist, file) != blacklist.end()) continue; + EXPECT_NO_THROW(VCD{fs::readFileText(file)}) << file; + } +} + +TEST(vcdpp, parse_event_ramp_extras) { + const VCD vcd{fs::readFileText("/home/lxlewis/Steam/steamapps/common/Dino D-Day/bin/unittests/dmxtest.vcd")}; + ASSERT_EQ(vcd.getGlobalEvents().size(), 3); +} + +TEST(vcdpp, fuzz) { + const VCD vcd{fs::readFileText("/home/lxlewis/Steam/steamapps/common/Left 4 Dead 2/left4dead2/scenes/coach/laughter01.vcd")}; + ASSERT_EQ(vcd.getGlobalEvents().size(), 1); +} + +#endif From b34d5270ff1df7b6481bad60b67f9f2f72dde77b Mon Sep 17 00:00:00 2001 From: craftablescience Date: Fri, 30 Oct 2026 01:00:51 -0700 Subject: [PATCH 2/2] vcdpp: add VCD writing --- include/vcdpp/VCD.h | 12 ++ src/vcdpp/VCD.cpp | 284 ++++++++++++++++++++++++++++++++++++++++++++ test/vcdpp.cpp | 106 ++++++++++++++++- 3 files changed, 400 insertions(+), 2 deletions(-) diff --git a/include/vcdpp/VCD.h b/include/vcdpp/VCD.h index 167feddf5..d1b85ad93 100644 --- a/include/vcdpp/VCD.h +++ b/include/vcdpp/VCD.h @@ -244,12 +244,24 @@ class VCD { [[nodiscard]] const std::vector& getGlobalEvents() const; + [[nodiscard]] std::vector& getGlobalEvents(); + [[nodiscard]] const std::vector& getActors() const; + [[nodiscard]] std::vector& getActors(); + [[nodiscard]] const std::optional& getSceneRamp() const; + [[nodiscard]] std::optional& getSceneRamp(); + [[nodiscard]] const FaceposerSettings& getFaceposerSettings() const; + [[nodiscard]] FaceposerSettings& getFaceposerSettings(); + + [[nodiscard]] std::vector bake() const; + + bool bake(const std::filesystem::path& vcdPath) const; // NOLINT(*-use-nodiscard) + protected: Platform platform = Platform::UNKNOWN; uint8_t version = 1; diff --git a/src/vcdpp/VCD.cpp b/src/vcdpp/VCD.cpp index c043102f7..0fcc98f66 100644 --- a/src/vcdpp/VCD.cpp +++ b/src/vcdpp/VCD.cpp @@ -10,6 +10,7 @@ #include using namespace sourcepp; +using namespace std::string_view_literals; using namespace vcdpp; namespace { @@ -569,14 +570,297 @@ const std::vector& VCD::getGlobalEvents() const { return this->globalEvents; } +std::vector& VCD::getGlobalEvents() { + return this->globalEvents; +} + const std::vector& VCD::getActors() const { return this->actors; } +std::vector& VCD::getActors() { + return this->actors; +} + const std::optional& VCD::getSceneRamp() const { return this->sceneRamp; } +std::optional& VCD::getSceneRamp() { + return this->sceneRamp; +} + const FaceposerSettings& VCD::getFaceposerSettings() const { return this->faceposerSettings; } + +FaceposerSettings& VCD::getFaceposerSettings() { + return this->faceposerSettings; +} + +std::vector VCD::bake() const { + // todo: pretty much everything + + std::vector out; + BufferStream stream{out}; + + switch (this->platform) { + case Platform::VCD: + case Platform::VCE: { + if (this->platform == Platform::VCE) { + stream.write(std::format("// Choreo version {}: <{}> Exported Events\n", this->version, this->globalEvents.size()), false); + } else { + stream.write(std::format("// Choreo version {}\n", this->version), false); + } + + const auto writeEvent = [&stream](const Event& event, bool insideActor) { + const auto indentLine = [&stream, insideActor] { + if (insideActor) { + stream.write("\t\t"sv, false); + } + }; + const auto indentProperty = [&stream, &indentLine] { + indentLine(); + stream.write('\t'); + }; + + indentLine(); + stream.write("event "sv, false); + switch (event.type) { + case Event::Type::UNSPECIFIED: stream.write("unspecified"sv, false); break; + case Event::Type::SECTION: stream.write("section"sv, false); break; + case Event::Type::EXPRESSION: stream.write("expression"sv, false); break; + case Event::Type::LOOK_AT: stream.write("lookat"sv, false); break; + case Event::Type::MOVE_TO: stream.write("moveto"sv, false); break; + case Event::Type::SPEAK: stream.write("speak"sv, false); break; + case Event::Type::GESTURE: stream.write("gesture"sv, false); break; + case Event::Type::SEQUENCE: stream.write("sequence"sv, false); break; + case Event::Type::FACE: stream.write("face"sv, false); break; + case Event::Type::FIRE_TRIGGER: stream.write("firetrigger"sv, false); break; + case Event::Type::FLEX_ANIMATION: stream.write("flexanimation"sv, false); break; + case Event::Type::SUBSCENE: stream.write("subscene"sv, false); break; + case Event::Type::LOOP: stream.write("loop"sv, false); break; + case Event::Type::INTERRUPT: stream.write("interrupt"sv, false); break; + case Event::Type::STOP_POINT: stream.write("stoppoint"sv, false); break; + case Event::Type::PERMIT_RESPONSES: stream.write("permitresponses"sv, false); break; + case Event::Type::GENERIC: stream.write("generic"sv, false); break; + case Event::Type::CAMERA: stream.write("camera"sv, false); break; + case Event::Type::SCRIPT: stream.write("script"sv, false); break; + } + stream.write(std::format(" \"{}\"\n", event.name), false); + indentLine(); + stream.write("{\n"sv, false); + + indentProperty(); + stream.write(std::format("time {}", event.startTime), false); + if (event.endTime != 0.f) { + stream.write(std::format(" {}", event.endTime), false); + } + stream.write('\n'); + + { + indentProperty(); + stream.write(std::format("param \"{}\"\n", event.param), false); + } + if (!event.param2.empty()) { + indentProperty(); + stream.write(std::format("param2 \"{}\"\n", event.param2), false); + } + if (!event.param3.empty()) { + indentProperty(); + stream.write(std::format("param3 \"{}\"\n", event.param3), false); + } + + if (event.flags & Event::FLAG_RESUME_CONDITION) { + indentProperty(); + stream.write("resumecondition\n"sv, false); + } + if (event.flags & Event::FLAG_LOCK_BODY_FACING) { + indentProperty(); + stream.write("lockbodyfacing\n"sv, false); + } + if (event.flags & Event::FLAG_FIXED_LENGTH) { + indentProperty(); + stream.write("fixedlength\n"sv, false); + } + if (!(event.flags & Event::FLAG_ACTIVE)) { + indentProperty(); + stream.write("active 0\n"sv, false); + } + if (event.flags & Event::FLAG_FORCE_SHORT_MOVEMENT) { + indentProperty(); + stream.write("forceshortmovement\n"sv, false); + } + if (event.flags & Event::FLAG_PLAY_OVER_SCRIPT) { + indentProperty(); + stream.write("playoverscript\n"sv, false); + } + if (event.flags & Event::FLAG_SYNC_TO_FOLLOWING_GESTURE) { + indentProperty(); + stream.write("synctofollowinggesture\n"sv, false); + } + + // todo: do the rest of the properties + + switch (event.type) { + case Event::Type::SPEAK: + stream.write('\n'); + + indentProperty(); + stream.write("cctype \""sv, false); + switch (event.speechData.ccType) { + case Event::SpeechData::CCType::CC_MASTER: stream.write("cc_master"sv, false); break; + case Event::SpeechData::CCType::CC_SLAVE: stream.write("cc_slave"sv, false); break; + case Event::SpeechData::CCType::CC_DISABLED: stream.write("cc_disabled"sv, false); break; + } + stream.write("\"\n"sv, false); + + indentProperty(); + stream.write(std::format("cctoken \"{}\"\n", event.speechData.ccToken), false); + + if (event.speechData.ccFlags & Event::SpeechData::FLAG_USING_COMBINED_FILE) { + indentProperty(); + stream.write("cc_usingcombinedfile\n"sv, false); + } + if (event.speechData.ccFlags & Event::SpeechData::FLAG_COMBINED_USING_GENDER_TOKEN) { + indentProperty(); + stream.write("cc_combinedusesgender\n"sv, false); + } + if (event.speechData.ccFlags & Event::SpeechData::FLAG_NO_ATTENUATE) { + indentProperty(); + stream.write("cc_noattenuate\n"sv, false); + } + break; + case Event::Type::GESTURE: + if (event.sequenceDuration != 0.f) { + stream.write('\n'); + + indentProperty(); + stream.write(std::format("sequenceduration {}\n", event.sequenceDuration), false); + } + break; + case Event::Type::LOOP: + stream.write('\n'); + + indentProperty(); + stream.write(std::format("loopcount \"{}\"\n", event.loopCount), false); + break; + default: + break; + } + + indentLine(); + stream.write("}\n"sv, false); + }; + + for (const auto& event : this->globalEvents) { + writeEvent(event, false); + } + if (this->platform == Platform::VCE) { + for (const auto& actor : this->actors) { + for (const auto& channel : actor.channels) { + for (const auto& event : channel.events) { + writeEvent(event, false); + } + } + } + break; + } + + const auto writeActor = [&stream, &writeEvent](const Actor& actor) { + const auto writeChannel = [&stream, &writeEvent](const Actor::Channel& channel) { + const auto indentLine = [&stream] { + stream.write('\t'); + }; + const auto indentProperty = [&stream, &indentLine] { + indentLine(); + stream.write('\t'); + }; + + indentLine(); + stream.write(std::format("channel \"{}\"\n", channel.name), false); + indentLine(); + stream.write("{\n"sv, false); + + for (const auto& event : channel.events) { + writeEvent(event, true); + } + + if (!channel.active) { + indentProperty(); + stream.write("active 0\n"sv, false); + } + + indentLine(); + stream.write("}\n"sv, false); + }; + + const auto indentProperty = [&stream] { + stream.write('\t'); + }; + + stream.write(std::format("actor \"{}\"\n", actor.name), false); + stream.write("{\n"sv, false); + + for (const auto& channel : actor.channels) { + writeChannel(channel); + } + + if (!actor.faceposerModel.empty()) { + indentProperty(); + stream.write(std::format("faceposermodel \"{}\"\n", actor.faceposerModel), false); + } + + if (!actor.active) { + indentProperty(); + stream.write("active 0\n"sv, false); + } + + stream.write("}\n"sv, false); + }; + + for (const auto& actor : this->actors) { + writeActor(actor); + } + + const auto writeFaceposerSettings = [&stream](const FaceposerSettings& settings) { + stream + .write("scalesettings\n{\n"sv, false) + .write(std::format("\t\"CChoreoView\" \"{}\"", settings.scaleSettings.choreoView), false) + .write(std::format("\t\"SceneRampTool\" \"{}\"", settings.scaleSettings.sceneRampTool), false) + .write(std::format("\t\"ExpressionTool\" \"{}\"", settings.scaleSettings.expressionTool), false) + .write(std::format("\t\"GestureTool\" \"{}\"", settings.scaleSettings.gestureTool), false) + .write(std::format("\t\"RampTool\" \"{}\"", settings.scaleSettings.rampTool), false) + .write("}\n"sv, false); + + if (!settings.mapName.empty()) { + stream.write(std::format("mapname \"{}\"\n", settings.mapName), false); + } + + stream.write(std::format("fps {}\n", settings.fps), false); + + stream.write(std::format("snap {}\n", settings.snap ? "on" : "off"), false); + + if (settings.ignorePhonemes.has_value()) { + stream.write(std::format("ignorePhonemes {}\n", *settings.ignorePhonemes ? "on" : "off"), false); + } + }; + + writeFaceposerSettings(this->faceposerSettings); + + break; + } + case Platform::UNKNOWN: + case Platform::BVCD: + case Platform::XVCD: { + return {}; + } + } + + out.resize(stream.size()); + return out; +} + +bool VCD::bake(const std::filesystem::path& vcdPath) const { + return fs::writeFileBuffer(vcdPath, this->bake()); +} diff --git a/test/vcdpp.cpp b/test/vcdpp.cpp index 15dd5d291..cfba4e829 100644 --- a/test/vcdpp.cpp +++ b/test/vcdpp.cpp @@ -6,6 +6,108 @@ using namespace sourcepp; using namespace vcdpp; using namespace vcdpp::literals; +TEST(vcdpp, read_vcd_speak) { + // todo: move to test assets + // scenes/escape/ai_escape_speaker01.vcd + const auto vcd = R"(//Choreo version 1 +actor "Aperture_AI" +{ + channel "audio" + { + event speak "Portal.escape_00_part1_nag01-1" + { + time 0.12 0.952268 + param "Portal.escape_00_part1_nag01-1" + fixedlength + + cctype "cc_master" + cctoken "" + cc_noattentuate + } + event speak "Portal.escape_00_part1_nag02-1" + { + time 1.433334 2.543016 + param "Portal.escape_00_part1_nag02-1" + fixedlength + + cctype "cc_master" + cctoken "" + cc_noattentuate + } + event speak "Portal.escape_00_part1_nag03-1" + { + time 2.626667 5.995397 + param "Portal.escape_00_part1_nag03-1" + fixedlength + + cctype "cc_master" + cctoken "" + cc_noattentuate + } + event speak "Portal.escape_00_part1_nag01-1" + { + time 6.526667 7.358935 + param "Portal.escape_00_part1_nag01-1" + fixedlength + + cctype "cc_master" + cctoken "" + cc_noattentuate + } + } +} +"scalesettings" +{ + "CChoreoView" "100" + "SceneRampTool" "100" + "ExpressionTool" "100" + "GestureTool" "100" + "RampTool" "100" +} +fps 60 +snap off +)"_vcd; + EXPECT_EQ(vcd.getPlatform(), VCD::Platform::VCD); + EXPECT_EQ(vcd.getVersion(), 1); + EXPECT_EQ(vcd.getChecksum(), 0); + EXPECT_TRUE(vcd.getGlobalEvents().empty()); + ASSERT_EQ(vcd.getActors().size(), 1); + EXPECT_EQ(vcd.getActors()[0].name, "Aperture_AI"); + ASSERT_EQ(vcd.getActors()[0].channels.size(), 1); + EXPECT_EQ(vcd.getActors()[0].channels[0].name, "audio"); + ASSERT_EQ(vcd.getActors()[0].channels[0].events.size(), 4); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[0].name, "Portal.escape_00_part1_nag01-1"); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[0].startTime, 0.12); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[0].endTime, 0.952268); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[0].param, "Portal.escape_00_part1_nag01-1"); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[1].name, "Portal.escape_00_part1_nag02-1"); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[1].startTime, 1.433334); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[1].endTime, 2.543016); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[1].param, "Portal.escape_00_part1_nag02-1"); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[2].name, "Portal.escape_00_part1_nag03-1"); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[2].startTime, 2.626667); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[2].endTime, 5.995397); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[2].param, "Portal.escape_00_part1_nag03-1"); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[3].name, "Portal.escape_00_part1_nag01-1"); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[3].startTime, 6.526667); + EXPECT_FLOAT_EQ(vcd.getActors()[0].channels[0].events[3].endTime, 7.358935); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[3].param, "Portal.escape_00_part1_nag01-1"); + for (int i = 0; i < 4; i++) { + EXPECT_EQ(vcd.getActors()[0].channels[0].events[i].type, Event::Type::SPEAK); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[i].flags, Event::FLAG_FIXED_LENGTH | Event::FLAG_ACTIVE); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[i].speechData.ccType, Event::SpeechData::CCType::CC_MASTER); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[i].speechData.ccToken, ""); + EXPECT_EQ(vcd.getActors()[0].channels[0].events[i].speechData.ccFlags, Event::SpeechData::FLAG_NO_ATTENUATE); + } + EXPECT_EQ(vcd.getFaceposerSettings().scaleSettings.choreoView, 100); + EXPECT_EQ(vcd.getFaceposerSettings().scaleSettings.sceneRampTool, 100); + EXPECT_EQ(vcd.getFaceposerSettings().scaleSettings.expressionTool, 100); + EXPECT_EQ(vcd.getFaceposerSettings().scaleSettings.gestureTool, 100); + EXPECT_EQ(vcd.getFaceposerSettings().scaleSettings.rampTool, 100); + EXPECT_EQ(vcd.getFaceposerSettings().fps, 60); + EXPECT_FALSE(vcd.getFaceposerSettings().snap); +} + TEST(vcdpp, read_vce) { const auto vce = R"( // Choreo version 1: <1> Exported Events @@ -29,8 +131,8 @@ event gesture "Gesture event 1" } sequenceduration 1.700000 })"_vcd; - ASSERT_EQ(vce.getPlatform(), VCD::Platform::VCE); - ASSERT_EQ(vce.getGlobalEvents().size(), 1); + EXPECT_EQ(vce.getPlatform(), VCD::Platform::VCE); + EXPECT_EQ(vce.getGlobalEvents().size(), 1); } #if 0