#include "trainer_camera.h" #include "Engine/Core/Log/logging.h" #include #include #include #include #include #include #include #include #include namespace dingosdk::trainer { namespace { // Game build 25414733. A camera framing set (CameraFramingSetAsset) holds its framings twice: // as separate objects, which the running camera does not read, and compiled into its root as an // array sorted by pitch, which it reads every frame (measured: writing the array moves the // camera at once). One array element is 0x50 bytes: +0x00 offset x y z (right, up, back), // +0x40 the camera pitch in radians that the framing is for. // The root itself starts with the word below and holds, at +0x1c, the point the framings turn // about (x y z), with its pitch limits 89 and 90 a little further on. constexpr std::size_t element_size = 0x50, element_pitch = 0x40; constexpr std::uint32_t root_word = 0x0006b100; constexpr std::size_t pivot_offset = 0x1c, root_span = 0x90; constexpr float degrees = 0.01745329252f; struct Element { float x, y, z, pitch; // pitch in degrees }; struct Set { CameraRig rig; std::size_t count; Element elements[3]; }; constexpr Set sets[]{ {CameraRig::low, 3, {{-1.35f, -0.1f, 2.0f, -45.0f}, {-1.35f, 0.0f, 1.9f, 0.0f}, {-1.35f, 0.2f, 2.0f, 45.0f}}}, // also the mega park's {CameraRig::high, 1, {{-1.0f, 0.0f, 3.0f, -12.0f}}}, {CameraRig::high, 3, {{-1.0f, 0.0f, 3.0f, -75.0f}, {-1.0f, 0.0f, 3.0f, 0.0f}, {-1.0f, 0.0f, 3.0f, 75.0f}}}, // mega park {CameraRig::foot, 3, {{0.0f, 0.75f, 2.5f, -75.0f}, {0.0f, 0.0f, 3.0f, 0.0f}, {0.0f, 0.45f, 1.25f, 75.0f}}}, }; struct Pivot { CameraRig rig; float y; }; constexpr Pivot pivots[]{{CameraRig::low, 0.85f}, {CameraRig::high, 1.0f}, {CameraRig::high, 1.2f}}; struct Found { std::uintptr_t address{}; // of the three numbers CameraRig rig{}; bool pivot{}; std::array stock{}; float pitch{}; // radians: what a framing must still hold beside its numbers }; struct State { std::mutex mutex; std::array wanted{}; std::vector found; std::atomic searching{}; std::uint64_t next_search{}; int searches{}; }; State &state() { static auto *value = new State; return *value; } bool about(float a, float b) { return std::abs(a - b) < 2e-4f; } template bool peek(std::uintptr_t address, T &value) noexcept { __try { std::memcpy(&value, reinterpret_cast(address), sizeof(T)); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool put(std::uintptr_t address, float value) noexcept { __try { *reinterpret_cast(address) = value; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool element_at(const float *f, const Element &e) noexcept { return about(f[0], e.x) && about(f[1], e.y) && about(f[2], e.z) && about(f[element_pitch / 4], e.pitch * degrees); } // One hit: what is at word `i` of a region, if anything known. Plain data only (it runs under __try). struct Hit { const Set *set{}; const Pivot *pivot{}; }; Hit classify(const std::uint32_t *words, const float *floats, std::size_t i) noexcept { const auto first = words[i]; // The first number of every array is -1.35, -1 or 0; of a root, its word. if (first == 0xbfaccccd || first == 0xbf800000 || first == 0) { if (floats[i + 2] < 1.0f || floats[i + 2] > 3.5f) return {}; // no array's first distance for (const auto &set : sets) { bool all = true; for (std::size_t k = 0; k < set.count && all; ++k) all = element_at(floats + i + k * element_size / 4, set.elements[k]); if (all) return {&set, nullptr}; } } else if (first == root_word && floats[i + pivot_offset / 4] == 0.0f && floats[i + pivot_offset / 4 + 2] == 0.0f) { for (const auto &known : pivots) { if (!about(floats[i + pivot_offset / 4 + 1], known.y)) continue; for (std::size_t k = pivot_offset / 4 + 3; k + 1 < root_span / 4; ++k) if (floats[i + k] == 89.0f && floats[i + k + 1] == 90.0f) return {nullptr, &known}; return {}; } } return {}; } // Fills `hits` (word index and what it is) for one region; returns how many. std::size_t search_region(std::uintptr_t start, std::size_t size, std::size_t *indices, Hit *hits, std::size_t capacity) noexcept { std::size_t count{}; __try { const auto *words = reinterpret_cast(start); const auto *floats = reinterpret_cast(start); const std::size_t total = size / 4, tail = 3 * element_size / 4 + root_span / 4; for (std::size_t i = 0; i + tail < total && count < capacity; ++i) { const auto first = words[i]; if (first != 0xbfaccccd && first != 0xbf800000 && first != 0 && first != root_word) continue; if (const auto hit = classify(words, floats, i); hit.set || hit.pivot) { indices[count] = i; hits[count++] = hit; } } } __except (EXCEPTION_EXECUTE_HANDLER) {} return count; } void search() { auto &s = state(); std::vector fresh; const auto started = GetTickCount64(); MEMORY_BASIC_INFORMATION info{}; std::array indices{}; std::array hits{}; for (std::uintptr_t at = 0x10000; at < 0x7fffffff0000ull && VirtualQuery(reinterpret_cast(at), &info, sizeof(info)) == sizeof(info); at = reinterpret_cast(info.BaseAddress) + info.RegionSize) { if (info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE || info.Type == MEM_IMAGE) continue; const auto start = reinterpret_cast(info.BaseAddress); const auto count = search_region(start, info.RegionSize, indices.data(), hits.data(), hits.size()); for (std::size_t h = 0; h < count && fresh.size() < 256; ++h) { const auto address = start + indices[h] * 4; if (const auto *set = hits[h].set) for (std::size_t k = 0; k < set->count; ++k) fresh.push_back({address + k * element_size, set->rig, false, {set->elements[k].x, set->elements[k].y, set->elements[k].z}, set->elements[k].pitch * degrees}); else if (const auto *pivot = hits[h].pivot) fresh.push_back({address + pivot_offset, pivot->rig, true, {0.0f, pivot->y, 0.0f}, 0.0f}); } } { std::lock_guard lock(s.mutex); // What was already written no longer holds the game's numbers: it stays known. for (const auto &old : s.found) if (std::ranges::find(fresh, old.address, &Found::address) == fresh.end()) fresh.push_back(old); s.found = std::move(fresh); } logging::write(logging::Level::info, logging::Channel::skater, std::format("Trainer: camera framing data found: {} ({} ms).", camera_rigs_summary(), GetTickCount64() - started)); s.searching.store(false, std::memory_order_release); } } // namespace void want_camera_rigs(const std::array &settings) noexcept { auto &s = state(); std::lock_guard lock(s.mutex); s.wanted = settings; } void forget_camera_rigs() noexcept { auto &s = state(); std::lock_guard lock(s.mutex); s.found.clear(); s.searches = 0; s.next_search = 0; } std::size_t apply_camera_rigs(std::uint64_t now) noexcept { auto &s = state(); std::size_t written{}; try { std::lock_guard lock(s.mutex); const bool any = std::ranges::any_of(s.wanted, [](const RigSetting &r) { return r != RigSetting{}; }); if (s.found.empty()) { // The game's own numbers are in place until something else is wanted: only then look. if (any && !s.searching.load(std::memory_order_acquire) && now >= s.next_search && s.searches < 4) { ++s.searches; s.next_search = now + 15000; s.searching.store(true, std::memory_order_release); std::thread(search).detach(); } return 0; } const auto finite = [](float value, float low, float high, float otherwise) { return std::isfinite(value) ? std::clamp(value, low, high) : otherwise; }; for (const auto &found : s.found) { // The level may have been unloaded under us: write only into what is still a framing. std::uint32_t word{}; float pitch{}; if (found.pivot ? !peek(found.address - pivot_offset, word) || word != root_word : !peek(found.address + element_pitch, pitch) || !about(pitch, found.pitch)) continue; const auto &want = s.wanted[static_cast(found.rig)]; const bool foot = found.rig == CameraRig::foot; const float distance = finite(want.distance, -1000.0f, 1000.0f, 1.0f), side = finite(want.side, -1000.0f, 1000.0f, 1.0f); const float height = finite(want.height, -1000.0f, 1000.0f, 0.0f), raise = finite(want.raise, -1000.0f, 1000.0f, 0.0f); std::array target = found.stock; if (found.pivot) target[1] += height; else { target[0] = foot ? side - 1.0f : target[0] * side; // on foot the game centres the camera: metres to the right instead target[1] += raise + (foot ? height : 0.0f); // the on-foot camera turns about the skater's root: nothing to move target[2] *= distance; } for (std::size_t i = 0; i < 3; ++i) { float current{}; if (peek(found.address + i * 4, current) && current != target[i] && put(found.address + i * 4, target[i])) ++written; } } } catch (...) {} return written; } std::string camera_rigs_summary() { auto &s = state(); std::array framings{}, roots{}; { std::unique_lock lock(s.mutex, std::try_to_lock); // also called with the lock held if (lock.owns_lock()) for (const auto &found : s.found) ++(found.pivot ? roots : framings)[static_cast(found.rig)]; } return std::format("low camera {} framings and {} pivots, high camera {} and {}, on foot {} framings", framings[0], roots[0], framings[1], roots[1], framings[2]); } } // namespace dingosdk::trainer