#include "game_clips.h" #include "Engine/Resource/ebx_document.h" #include "Engine/Vfs/game_bundles.h" #include #include #include #include #include #include #include #include #include #include namespace dingosdk::anim_studio::game_clips { namespace { namespace ebx = frostbite::ebx; namespace fb = frostbite; // Game build 20260929: the level root carries the clips and their channel maps (each named // "_af//toolchanneltodofassetdata"); the game configuration carries // the skater's rig, its DOF sets and its skeleton. constexpr std::string_view level_toc = "Win32/levels/game/bam_levelroot/bam_levelroot.toc"; constexpr std::string_view level_bundle = "win32/levels/game/bam_levelroot/bam_levelroot"; constexpr std::string_view config_toc = "Win32/globals.toc"; constexpr std::string_view config_bundle = "win32/configurations/gameconfigurations/delmargameconfiguration"; constexpr std::string_view rig_name = "animation/dingo/animbase_default_rig"; constexpr std::uint32_t eclipse_resource_type = 0x4088BF7E; std::string lower(std::string_view text) { std::string out(text); for (auto& c : out) c = static_cast(c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c); return out; } // ---- EBX values ------------------------------------------------------------------------------------ const ebx::Value& field(const ebx::Object& object, std::string_view name) { const auto* found = object.find(name); if (!found) throw std::runtime_error("its " + std::string(name) + " is missing"); return found->value; } std::optional number_of(const ebx::Value& value) { if (const auto* v = std::get_if(&value.data)) return static_cast(*v); if (const auto* v = std::get_if(&value.data)) return static_cast(*v); if (const auto* v = std::get_if(&value.data)) return *v; if (const auto* v = std::get_if(&value.data)) return *v ? 1.0 : 0.0; return std::nullopt; } double number(const ebx::Object& object, std::string_view name) { const auto n = number_of(field(object, name)); if (!n || !std::isfinite(*n)) throw std::runtime_error("its " + std::string(name) + " is not a number"); return *n; } std::uint32_t index_of(const ebx::Value& value) { const auto n = number_of(value); if (!n || *n < 0 || *n > 4294967295.0 || *n != std::floor(*n)) throw std::runtime_error("an index or id is not one"); return static_cast(*n); } const ebx::Value::Array& array(const ebx::Object& object, std::string_view name) { const auto* a = std::get_if(&field(object, name).data); if (!a) throw std::runtime_error("its " + std::string(name) + " is not a list"); return *a; } const ebx::Object* object_of(const ebx::Value& value) { const auto* p = std::get_if>(&value.data); return p && *p ? p->get() : nullptr; } std::string type_of(const ebx::Document& doc, const ebx::InstanceRecord& instance) { return instance.descriptor >= 0 && static_cast(instance.descriptor) < doc.types.size() ? doc.types[static_cast(instance.descriptor)].name : std::string{}; } // An instance a pointer names: in the same document, or exported by another found with `find`. struct Ref { std::shared_ptr document; std::size_t instance{}; const ebx::Object& object() const { const auto& o = document->instances.at(instance).object; if (!o) throw std::runtime_error("an instance has no object"); return *o; } std::string type() const { return type_of(*document, document->instances.at(instance)); } }; template Ref resolve(const std::shared_ptr& doc, const ebx::Value& value, Find&& find, std::string_view what) { const auto* pointer = std::get_if(&value.data); if (!pointer || pointer->kind == ebx::PointerKind::null) throw std::runtime_error("its " + std::string(what) + " is not set"); if (pointer->kind == ebx::PointerKind::internal) { if (pointer->index < 0 || static_cast(pointer->index) >= doc->instances.size()) throw std::runtime_error("its " + std::string(what) + " points nowhere"); return {doc, static_cast(pointer->index)}; } const auto& import = doc->imports.at(static_cast(pointer->index)); const std::shared_ptr target = find(import.fileGuid); if (!target) throw std::runtime_error("its " + std::string(what) + " (" + import.fileGuid.string() + ") is not in the game's data"); for (std::size_t i = 0; i < target->instances.size(); ++i) if (target->instances[i].exported && target->instances[i].instanceGuid == import.classGuid) return {target, i}; throw std::runtime_error("its " + std::string(what) + " is not in its partition"); } // ---- the Eclipse codec (ReSkate Studio's sdk/resources/eclipse_animation.cpp) ------------------------- struct Channel { std::vector frames; std::vector> values; }; struct Eclipse { std::vector rotations, vectors, scalars; }; template T read(std::span bytes, std::size_t at) { if (at > bytes.size() || sizeof(T) > bytes.size() - at) throw std::runtime_error("its animation is cut short"); T value; std::memcpy(&value, bytes.data() + at, sizeof(value)); return value; } std::uint16_t big16(std::span bytes, std::size_t at) { return static_cast((read(bytes, at) << 8) | read(bytes, at + 1)); } // The smallest-three quaternion: three components and the lane of the largest, which is made up. std::array quaternion(std::uint32_t largest, std::array c) { constexpr std::array, 4> lanes{{{1, 2, 3}, {0, 2, 3}, {0, 1, 3}, {0, 1, 2}}}; std::array q{}; double sum{}; for (std::size_t i = 0; i < 3; ++i) { q[lanes[largest & 3][i]] = c[i]; sum += double(c[i]) * c[i]; } if (!std::isfinite(sum) || sum > 1.0001) throw std::runtime_error("its animation has a broken rotation"); q[largest & 3] = static_cast(std::sqrt(std::max(0.0, 1 - sum))); return q; } std::array quaternion64(std::uint64_t p) { return quaternion(static_cast(p & 3), {(static_cast(p >> 43) - 1048576.0F) * 6.743499056938163e-7F, (static_cast((p >> 22) & 0x1FFFFF) - 1048576.0F) * 6.743499056938163e-7F, (static_cast((p >> 2) & 0xFFFFF) - 524288.0F) * 1.3487003798218211e-6F}); } Eclipse read_eclipse(std::span bytes) { if (bytes.size() < 80) throw std::runtime_error("its animation is cut short"); const auto data = read(bytes, 0x3C), size = read(bytes, 0x38); const auto q_format = read(bytes, 0x24), v_format = read(bytes, 0x28); const auto f_format = read(bytes, 0x2C), time_format = read(bytes, 0x30); if (data > bytes.size() || size > bytes.size() - data || data < 80 || q_format > 3 || v_format > 2 || f_format > 2 || time_format > 1) throw std::runtime_error("its animation's layout is not one this reads"); const auto payload = bytes.subspan(data, size); Eclipse out; const auto table = [&](std::size_t count_at, std::size_t offset_at, std::size_t stride, std::vector& output, unsigned kind) { const auto count = read(bytes, count_at), start = read(bytes, offset_at); if (count > 65535 || (count && (start < 80 || start > data || count > (data - start) / stride))) throw std::runtime_error("its animation's channels are out of bounds"); for (std::uint32_t c = 0; c < count; ++c) { const std::size_t at = start + c * stride; const auto keys = read(bytes, at); if (!keys || keys > 65536) throw std::runtime_error("an animation channel has no keys"); Channel channel; if (kind == 0 && keys == 1) { channel.frames.push_back(0); channel.values.push_back(quaternion64(read(bytes, at + 4))); } else if (kind && keys == 2) { const auto first = read(bytes, at + 4), last = read(bytes, at + 8); if (first > 65535 || last > 65535) throw std::runtime_error("an animation key's time is broken"); channel.frames = {static_cast(first), static_cast(last)}; for (unsigned k = 0; k < 2; ++k) { std::array value{}; for (unsigned d = 0; d < (kind == 1 ? 3U : 1U); ++d) value[d] = read(bytes, at + (kind == 1 ? 16 + k * 16 : 12 + k * 4) + d * 4); channel.values.push_back(value); } } else { const auto times = read(bytes, at + 4), values = read(bytes, at + 8); for (std::uint32_t k = 0; k < keys; ++k) channel.frames.push_back(time_format ? big16(payload, times + k * 2ULL) : read(payload, times + k)); for (std::uint32_t k = 0; k < keys; ++k) { std::array value{}; if (kind == 0) { if (q_format == 0) { const std::size_t p = values + k * 4ULL; const unsigned a = read(payload, p), b = read(payload, p + 1); const unsigned c0 = read(payload, p + 2), d = read(payload, p + 3); value = quaternion(d & 3, {(static_cast(a | (b >> 6) << 8) - 512.0F) * 0.0013824179768562317F, (static_cast((b & 63) | (c0 >> 4) << 6) - 512.0F) * 0.0013824179768562317F, (static_cast((c0 & 15) | (d >> 2) << 4) - 512.0F) * 0.0013824179768562317F}); } else if (q_format == 1) { const std::size_t p = values + k * 6ULL; const unsigned a = big16(payload, p), b = big16(payload, p + 2), c0 = big16(payload, p + 4); value = quaternion(((a & 1) << 1) | (b & 1), {(static_cast(a & ~1U) - 32768.0F) * 2.157951530534774e-5F, (static_cast(b & ~1U) - 32768.0F) * 2.157951530534774e-5F, (static_cast(c0) - 32768.0F) * 2.157951530534774e-5F}); } else if (q_format == 2) value = quaternion64(read(payload, values + k * 8ULL)); else for (unsigned d = 0; d < 4; ++d) value[d] = read(payload, values + k * 16ULL + d * 4); } else { const auto format = kind == 1 ? v_format : f_format; const unsigned dimensions = kind == 1 ? 3U : 1U, width = format == 0 ? 1U : format == 1 ? 2U : 4U; for (unsigned d = 0; d < dimensions; ++d) { const std::size_t p = values + (std::size_t(k) * dimensions + d) * width; if (format == 2) value[d] = read(payload, p); else { const auto least = read(bytes, at + (kind == 1 ? 16 : 12) + d * 4); const auto most = read(bytes, at + (kind == 1 ? 32 : 16) + d * 4); const float share = format ? big16(payload, p) / 65535.0F : read(payload, p) / 255.0F; value[d] = least + share * (most - least); } } } channel.values.push_back(value); } } for (std::size_t k = 1; k < channel.frames.size(); ++k) if (channel.frames[k] <= channel.frames[k - 1]) throw std::runtime_error("an animation channel's keys are out of order"); output.push_back(std::move(channel)); } }; table(0, 4, 12, out.rotations, 0); table(12, 16, 48, out.vectors, 1); table(0x18, 0x1C, 20, out.scalars, 2); return out; } // A channel at `frame`: between its two keys around it, the rotation slerped. std::array sample(const Channel& c, double frame, bool rotation) { const auto next = std::upper_bound(c.frames.begin(), c.frames.end(), frame, [](double f, std::uint16_t k) { return f < k; }); if (next == c.frames.begin()) return c.values.front(); if (next == c.frames.end()) return c.values.back(); const auto i = static_cast(next - c.frames.begin()); const auto t = static_cast((frame - c.frames[i - 1]) / (c.frames[i] - c.frames[i - 1])); const auto &a = c.values[i - 1], &b = c.values[i]; if (rotation) return normalised(slerp(a, b, t)); auto out = a; for (unsigned k = 0; k < 4; ++k) out[k] += (b[k] - a[k]) * t; return out; } // ---- the rig --------------------------------------------------------------------------------------- // Where each DOF of the rig goes: a joint's rotation ('q'), position ('t') or scale ('s'). struct Dof { std::uint16_t joint{}; char part{}; }; struct Rig { fb::Guid guid; std::map dofs; std::vector rest; // each joint's default (parent-local) std::vector parents; // the skeleton's own: anim_core's table hangs Skateboard_Root elsewhere std::uint16_t board{}; // Skateboard_Root }; // `child` in `parent`'s frame, put into the frame `parent` is in. Joint compose(const Joint& parent, const Joint& child) { const auto moved = rotate(parent.rotation, {child.position[0] * parent.scale[0], child.position[1] * parent.scale[1], child.position[2] * parent.scale[2]}); Joint out = child; out.position = {parent.position[0] + moved[0], parent.position[1] + moved[1], parent.position[2] + moved[2]}; out.rotation = normalised(multiply(parent.rotation, child.rotation)); return out; } std::string string_at(std::span bytes, std::size_t at, std::size_t end) { if (end > bytes.size() || at >= end) throw std::runtime_error("a rig name is out of bounds"); const auto* start = reinterpret_cast(bytes.data() + at); const auto* nul = static_cast(std::memchr(start, 0, end - at)); if (!nul) throw std::runtime_error("a rig name has no end"); return {start, nul}; } } // namespace struct Library::Data { vfs::GameData data; vfs::GameBundle level, config; std::map> level_names; // lower case: its ebx index std::map> channel_maps; // "_af/" partitions by their guid's start std::map config_guids; Rig rig; explicit Data(const std::filesystem::path& root) : data(root) {} std::shared_ptr document(const vfs::GameBundle& bundle, std::size_t index) const { const auto* payload = bundle.payload(fb::AssetKind::ebx, index); if (!payload) throw std::runtime_error("an asset's payload is missing"); return std::make_shared(ebx::read_document(data.read(*payload))); } std::shared_ptr config_document(const fb::Guid& guid) const { const auto found = config_guids.find(guid); return found == config_guids.end() ? nullptr : document(config, found->second); } std::shared_ptr level_document(const fb::Guid& guid) const { const auto found = channel_maps.find(guid.string().substr(0, 18)); if (found == channel_maps.end()) return nullptr; auto doc = document(level, found->second); return doc->fileGuid == guid ? doc : nullptr; } void read_rig(); }; void Library::Data::read_rig() { const auto found = std::find_if(config.manifest.ebx.begin(), config.manifest.ebx.end(), [](const auto& a) { return lower(a.name) == rig_name; }); if (found == config.manifest.ebx.end()) throw std::runtime_error("the skater's rig is not in the game's data"); const auto doc = document(config, static_cast(found - config.manifest.ebx.begin())); const auto* root_instance = doc->root(); if (!root_instance || !root_instance->object || doc->rootType != "RigAsset") throw std::runtime_error("the skater's rig is not a rig"); const auto& root = *root_instance->object; rig.guid = doc->fileGuid; const auto find = [&](const fb::Guid& g) { return config_document(g); }; // The skeleton: the joints' names, in the order the skater's pose has them. const auto skeleton = resolve(doc, field(root, "SkeletonAsset"), find, "skeleton"); if (skeleton.type() != "AntSkeletonAsset") throw std::runtime_error("the rig's skeleton is not one"); std::vector table; for (const auto& c : array(skeleton.object(), "NameTable")) table.push_back(static_cast(index_of(c) & 0xff)); const auto& joints = array(skeleton.object(), "Joints"); if (joints.size() != skeleton_joints) throw std::runtime_error("the skater's skeleton has " + std::to_string(joints.size()) + " joints"); std::map> joint_of; for (std::size_t i = 0; i < joints.size(); ++i) { const auto* joint = object_of(joints[i]); if (!joint) throw std::runtime_error("a skeleton joint is not one"); joint_of.emplace(string_at(table, index_of(field(*joint, "Name")), table.size()), static_cast(i)); const auto parent = number(*joint, "Parent"); if (!(parent >= -1 && parent < static_cast(i))) throw std::runtime_error("the skeleton's joints are out of order"); rig.parents.push_back(static_cast(parent)); } const auto board = joint_of.find("Skateboard_Root"); if (board == joint_of.end()) throw std::runtime_error("the skeleton has no Skateboard_Root"); rig.board = board->second; // Each DOF set's names (its LayoutData: a header, 32 bytes a DOF, then the names), against the ids. const auto& sets = array(root, "DofSets"); const auto& starts = array(root, "DofSetIdIndices"); const auto& ids = array(root, "DofIds"); if (sets.size() != starts.size()) throw std::runtime_error("the rig's DOF lists disagree"); for (std::size_t s = 0; s < sets.size(); ++s) { const auto set = resolve(doc, sets[s], find, "DOF set"); const auto& words = array(set.object(), "LayoutData"); std::vector bytes(words.size() * 8); for (std::size_t i = 0; i < words.size(); ++i) { const auto* w = std::get_if(&words[i].data); if (!w) throw std::runtime_error("a DOF set's layout is not one this reads"); std::memcpy(bytes.data() + i * 8, w, 8); } const auto word = [&](std::size_t at) { return read(bytes, at); }; const std::size_t start = index_of(starts[s]), end = s + 1 < starts.size() ? index_of(starts[s + 1]) : ids.size(); const std::size_t count = std::size_t(word(4)) + 1, strings = word(0x10), string_end = strings + word(0x14); if (start > end || end > ids.size() || count != end - start || strings != 0x20 + count * 0x20 || string_end > bytes.size()) throw std::runtime_error("a DOF set's layout is not one this reads"); for (std::size_t i = 0; i < count; ++i) { const auto name = string_at(bytes, strings + word(0x20 + i * 0x20 + 12), string_end); if (name.size() < 3 || name[name.size() - 2] != '.') continue; const char part = name.back(); if (part != 'q' && part != 't' && part != 's') continue; if (const auto j = joint_of.find(std::string_view(name).substr(0, name.size() - 2)); j != joint_of.end()) rig.dofs.try_emplace(index_of(ids[start + i]), Dof{j->second, part}); } } // The default pose: every joint's rest rotation, position and scale. const auto pose = resolve(doc, field(root, "DefaultPoseAsset"), find, "default pose"); std::map> defaults; for (const auto& [prefix, dimensions] : {std::pair{"Vector3", 3}, std::pair{"Vector4", 4}}) { const auto* id_field = pose.object().find(std::string(prefix) + "DofIds"); const auto* value_field = pose.object().find(std::string(prefix) + "Values"); if (!id_field || !value_field) continue; const auto* dof_ids = std::get_if(&id_field->value.data); const auto* values = std::get_if(&value_field->value.data); if (!dof_ids || !values || dof_ids->size() != values->size()) throw std::runtime_error("the rig's default pose disagrees with itself"); for (std::size_t i = 0; i < dof_ids->size(); ++i) { const auto* v = object_of((*values)[i]); if (!v) continue; std::array value{}; constexpr std::array axes{"x", "y", "z", "w"}; for (int d = 0; d < dimensions; ++d) value[static_cast(d)] = static_cast(number(*v, axes[static_cast(d)])); defaults[index_of((*dof_ids)[i])] = value; } } rig.rest.assign(skeleton_joints, Joint{}); for (const auto& [id, dof] : rig.dofs) { const auto value = defaults.find(id); if (value == defaults.end()) continue; auto& j = rig.rest[dof.joint]; const auto& v = value->second; if (dof.part == 'q') j.rotation = normalised({v[0], v[1], v[2], v[3]}); else if (dof.part == 't') j.position = {v[0], v[1], v[2]}; else j.scale = {v[0], v[1], v[2]}; } } Library::Library(const std::filesystem::path& game_root) : data_(std::make_unique(game_root)) { auto& d = *data_; auto level = d.data.read_bundle(d.data.read_toc(level_toc), level_bundle); if (!level) throw std::runtime_error("the level root's bundle is missing"); d.level = std::move(*level); auto config = d.data.read_bundle(d.data.read_toc(config_toc), config_bundle); if (!config) throw std::runtime_error("the game configuration's bundle is missing"); d.config = std::move(*config); for (std::size_t i = 0; i < d.level.manifest.ebx.size(); ++i) { auto name = lower(d.level.manifest.ebx[i].name); if (name.starts_with("_af/") && name.size() > 22 && name[22] == '/') d.channel_maps.emplace(name.substr(4, 18), i); else if (name.starts_with("animation/")) d.level_names.emplace(std::move(name), i); } for (std::size_t i = 0; i < d.config.manifest.ebx.size(); ++i) if (const auto* payload = d.config.payload(fb::AssetKind::ebx, i)) try { d.config_guids.emplace(ebx::read_file_guid(d.data.read(*payload)), i); } catch (...) {} d.read_rig(); } Library::~Library() = default; std::vector Library::names(std::string_view containing, std::size_t most) const { const auto wanted = lower(containing); std::vector> found; for (const auto& [name, index] : data_->level_names) if (wanted.empty() || name.find(wanted) != std::string::npos) found.emplace_back(index, name); std::sort(found.begin(), found.end()); std::vector out; for (auto& [index, name] : found) { if (out.size() >= most) break; out.push_back(std::move(name)); } return out; } Clip Library::load(std::string_view asked) const { const auto& d = *data_; const auto name = lower(asked); const auto at = d.level_names.find(name); if (at == d.level_names.end()) throw std::runtime_error("the game has no animation " + name); const auto doc = d.document(d.level, at->second); if (doc->rootType != "ClipControllerAsset" || !doc->root() || !doc->root()->object) throw std::runtime_error(name + " is a " + doc->rootType + ", not a clip"); const auto& root = *doc->root()->object; const auto* rig = std::get_if(&field(root, "RigRef").data); if (!rig || rig->kind != ebx::PointerKind::external || doc->imports.at(static_cast(rig->index)).fileGuid != d.rig.guid) throw std::runtime_error(name + " is not on the skater's rig"); const auto find = [&](const fb::Guid& g) { return d.level_document(g); }; const auto anim = resolve(doc, field(root, "Anim"), find, "animation"); if (anim.type() != "EclipseAnimationAsset") throw std::runtime_error(name + "'s animation is a " + anim.type() + ", which this does not read"); const double fps = number(root, "FPS") * number(root, "TimeScale"); if (!(fps > 0 && fps < 1000)) throw std::runtime_error(name + "'s frame rate is broken"); const double trim = number(anim.object(), "TrimOffset"), duration = number(anim.object(), "TrimmedDuration"); if (!(duration > 0 && duration < 36000)) throw std::runtime_error(name + " has no frames"); // Its keys: the resource the animation names. const auto* reference = std::get_if(&field(anim.object(), "Resource").data); if (!reference) throw std::runtime_error(name + " has no animation data"); std::size_t index = 0; const auto& resources = d.level.manifest.resources; while (index < resources.size() && resources[index].resourceId != reference->id) ++index; if (index == resources.size() || resources[index].resourceType != eclipse_resource_type) throw std::runtime_error(name + "'s animation data is not in the game's data"); const auto* payload = d.level.payload(fb::AssetKind::resource, index); if (!payload) throw std::runtime_error(name + "'s animation data is missing"); const auto eclipse = read_eclipse(d.data.read(*payload)); // Which DOF each channel moves: rotations, then vectors, then floats. const auto map = resolve(anim.document, field(anim.object(), "ChannelToDofAsset"), find, "channel map"); const auto& ids = array(map.object(), "ChannelToDofIds"); if (ids.size() != eclipse.rotations.size() + eclipse.vectors.size() + eclipse.scalars.size()) throw std::runtime_error(name + "'s channel map disagrees with its animation"); struct Bound { const Channel* channel; Dof dof; }; std::vector bound; std::size_t next = 0; for (const auto* list : {&eclipse.rotations, &eclipse.vectors}) { const char kind = list == &eclipse.rotations ? 'q' : 'v'; for (const auto& channel : *list) { const auto found = d.rig.dofs.find(index_of(ids[next++])); if (found == d.rig.dofs.end()) continue; if ((kind == 'q') != (found->second.part == 'q')) continue; bound.push_back({&channel, found->second}); } } Clip clip; clip.joints = skeleton_joints; const auto frames = static_cast(std::lround(duration)) + 1; clip.times.reserve(frames); clip.frames.reserve(frames * skeleton_joints); std::vector board_y; for (std::size_t k = 0; k < frames; ++k) { const double frame = trim + static_cast(k); clip.times.push_back(static_cast(k / fps)); const auto first = clip.frames.size(); clip.frames.insert(clip.frames.end(), d.rig.rest.begin(), d.rig.rest.end()); const std::span pose(clip.frames.data() + first, skeleton_joints); for (const auto& b : bound) { const auto v = sample(*b.channel, frame, b.dof.part == 'q'); auto& j = pose[b.dof.joint]; if (b.dof.part == 'q') j.rotation = {v[0], v[1], v[2], v[3]}; else if (b.dof.part == 't') j.position = {v[0], v[1], v[2]}; else j.scale = {v[0], v[1], v[2]}; } // The board under the placement joint, as a recording has it: Skateboard_Root, from the // placement joint down through the skeleton's own parents. Joint rel = pose[d.rig.board]; for (auto up_to = d.rig.parents[d.rig.board]; up_to > placement_joint; up_to = d.rig.parents[static_cast(up_to)]) rel = compose(pose[static_cast(up_to)], rel); clip.board.push_back(rel); Joint up = rel; for (int up_to = placement_joint; up_to >= 0; up_to = d.rig.parents[static_cast(up_to)]) up = compose(pose[static_cast(up_to)], up); board_y.push_back(up.position[1]); } // The pop is the board's first low point (the tail strikes); the landing its deepest low point // between a quarter and about 0.85 s later (make_trick_library.py; at 60 a second). Its height is // smoothed over five frames, as numpy's convolve "same" does (nothing past the ends). std::vector smooth(board_y.size()); for (std::size_t i = 0; i < board_y.size(); ++i) { float sum = 0; for (std::size_t j = i >= 2 ? i - 2 : 0; j <= std::min(i + 2, board_y.size() - 1); ++j) sum += board_y[j]; smooth[i] = sum / 5.0f; } std::vector lows; for (std::size_t i = 2; i + 2 < smooth.size(); ++i) if (smooth[i] <= smooth[i - 1] && smooth[i] < smooth[i + 1]) lows.push_back(i); if (!lows.empty()) { const std::size_t takeoff = lows.front(); const auto per_frame = fps / 60.0; const std::size_t from = takeoff + static_cast(15 * per_frame), to = std::min(frames - 1, takeoff + static_cast(51 * per_frame)); std::optional landing; for (const auto i : lows) if (i >= from && i <= to && (!landing || smooth[i] < smooth[*landing])) landing = i; clip.takeoff = clip.times[takeoff]; clip.landing = clip.times[landing.value_or(std::min(frames - 1, takeoff + static_cast(27 * per_frame)))]; } return clip; } namespace { struct Loader { std::once_flag started; std::atomic> library; std::mutex mutex; std::string failure; }; Loader& loader() { static auto* l = new Loader; return *l; } std::filesystem::path game_folder() { std::vector path(32768); const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); if (!length || length >= path.size()) throw std::runtime_error("the game's folder is unknown"); return std::filesystem::path(std::wstring(path.data(), length)).parent_path(); } } // namespace void prepare() noexcept { try { std::call_once(loader().started, [] { std::thread([] { try { loader().library.store(std::make_shared(game_folder())); } catch (const std::exception& e) { std::lock_guard lock(loader().mutex); loader().failure = e.what(); } }).detach(); }); } catch (...) {} } std::shared_ptr library() noexcept { prepare(); return loader().library.load(); } std::string failure() { std::lock_guard lock(loader().mutex); return loader().failure; } }