mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 18:37:07 +02:00
221 lines
12 KiB
C++
221 lines
12 KiB
C++
#include "Engine/Core/Log/logging.h"
|
|
#include "Extension/Objects/local_placements_runtime.h"
|
|
#include "Extension/Profile/runtime_internal.h"
|
|
#include "level_loading.h"
|
|
#include "Extension/UI/Overlay/overlay.h"
|
|
#include "local_park_rotation.h"
|
|
#include "local_world_layers.h"
|
|
#include "Engine/Game/Abi/native_string_read.h"
|
|
#include "Engine/Game/Build/20260929/park_rotation.h"
|
|
|
|
namespace dingosdk::profile_runtime {
|
|
// Authored BAM park-pack event adapter. See analysis/rotating-parks.md.
|
|
|
|
ParkRuntime& park_runtime() { static auto* r = new ParkRuntime; return *r; }
|
|
|
|
void reset_park_session() {
|
|
auto& r = park_runtime();
|
|
r.owner = r.context = 0; r.sent = {}; r.pending_lot = static_cast<unsigned>(park_lots.size());
|
|
r.next_request = 0; r.model.ready = false;
|
|
}
|
|
|
|
bool park_owner_ready(std::uintptr_t manager, std::uintptr_t& context, bool require_park) {
|
|
const auto base = local_runtime().base;
|
|
std::uintptr_t current{}, vtable{}, reference{};
|
|
std::uint32_t context_offset{};
|
|
std::uint8_t ready{}, live{};
|
|
// The native notification resolves manager+8 by adding the registered
|
|
// context offset. Reject stale handles before entering its unchecked read.
|
|
if (!read(base + addr::park_rotation::manager, current) || manager != current || !manager ||
|
|
!read(manager, vtable) || vtable != base + addr::park_rotation::manager_vtable ||
|
|
(require_park && (!read(manager + 0x42, ready) || ready != 1)) ||
|
|
!read(manager + 8, reference) || !reference ||
|
|
!read(base + addr::park_rotation::context_offset, context_offset) || context_offset > 0x1000000 ||
|
|
reference > UINTPTR_MAX - context_offset - 0x28) return false;
|
|
context = reference + context_offset;
|
|
return read(context + 0x28, live) && live == 1;
|
|
}
|
|
|
|
void observe_park_readiness(std::uintptr_t manager, std::uint64_t now) noexcept {
|
|
auto& r = park_runtime();
|
|
if (now < r.next_observation) return;
|
|
r.next_observation = now + 250;
|
|
std::uintptr_t current{}, vt{};
|
|
std::uint8_t ready{};
|
|
if (!read(local_runtime().base + addr::park_rotation::manager, current) || current != manager ||
|
|
!read(manager, vt) || vt != local_runtime().base + addr::park_rotation::manager_vtable ||
|
|
!read(manager + 0x42, ready) || ready > 1) return;
|
|
if (r.observed_owner != manager) {
|
|
r.observed_owner = manager; r.observed_ready.reset();
|
|
}
|
|
if (!r.observed_ready || *r.observed_ready != (ready != 0)) {
|
|
const auto elapsed = r.observed_ready && now >= r.observed_since ? now - r.observed_since : 0;
|
|
logging::log(ready ? logging::Level::success : logging::Level::info, logging::Channel::park,
|
|
"Park manager {} ({}ms in previous observed state).", ready ? "ready" : "waiting for park content", elapsed);
|
|
r.observed_ready = ready != 0; r.observed_since = now; r.readiness_warned = false;
|
|
} else if (!ready && !r.readiness_warned && now >= r.observed_since && now - r.observed_since >= 30000) {
|
|
r.readiness_warned = true;
|
|
const auto destination = last_level_destination();
|
|
// Custom maps have none of the game's park lots, so the park manager can
|
|
// wait forever on a level that loaded fine. Only a level that never
|
|
// became active is a real failure worth an on-screen error.
|
|
if (level_content_active()) {
|
|
logging::write(logging::Level::info, logging::Channel::park,
|
|
"Park manager has no park content to wait for; the level itself is active." +
|
|
(destination.empty() ? std::string{} : " Level: " + destination + "."));
|
|
return;
|
|
}
|
|
// A custom level whose content never arrives sits here: name it and
|
|
// the mod behind it, so the wait can be traced to a folder.
|
|
logging::write(logging::Level::warning, logging::Channel::park,
|
|
"Park manager still waiting for content after 30 seconds." +
|
|
(destination.empty() ? std::string{}
|
|
: " Level: " + destination + custom_level_hint(destination) + "."));
|
|
if (const auto notice = custom_level_notice(destination); !notice.empty())
|
|
overlay::notify(overlay::NoticeLevel::error, "This map is not loading", notice);
|
|
}
|
|
}
|
|
|
|
// Log notifications from both the engine and SDK at their shared native entry.
|
|
// A returned notification queues graph work; it does not prove a park loaded.
|
|
void park_notify_hook(std::uintptr_t manager, const NativeString* zone, const NativeString* park) {
|
|
PreserveError native_error;
|
|
const auto incoming_error = native_error.error;
|
|
auto& r = park_runtime();
|
|
bool observed{};
|
|
std::string zone_name, park_name;
|
|
try {
|
|
if (r.active.load(std::memory_order_acquire)) {
|
|
observed = game::read_native_identifier(reinterpret_cast<std::uintptr_t>(zone), zone_name) &&
|
|
game::read_native_identifier(reinterpret_cast<std::uintptr_t>(park), park_name);
|
|
if (observed)
|
|
logging::log(logging::Level::info, logging::Channel::park,
|
|
"Park {} event: lot={}, layout={}.", park_name.empty() ? "clear" : "load",
|
|
zone_name.empty() ? "<empty>" : zone_name, park_name.empty() ? "<empty lot>" : park_name);
|
|
else logging::write(logging::Level::debug, logging::Channel::park, "Park event names unavailable.");
|
|
}
|
|
} catch (...) {}
|
|
const auto started = GetTickCount64();
|
|
SetLastError(incoming_error);
|
|
r.original_notify(manager, zone, park);
|
|
native_error.error = GetLastError();
|
|
if (observed) {
|
|
const auto elapsed = GetTickCount64() - started;
|
|
logging::log(elapsed >= 1000 ? logging::Level::warning : logging::Level::debug, logging::Channel::park,
|
|
"Park event handler returned after {}ms: lot={}, layout={}; awaiting subworld streaming.",
|
|
elapsed, zone_name, park_name.empty() ? "<empty lot>" : park_name);
|
|
}
|
|
}
|
|
|
|
void update_park_rotation(std::uintptr_t manager, std::uint64_t now) {
|
|
auto& r = park_runtime();
|
|
observe_park_readiness(manager, now);
|
|
std::uintptr_t context{};
|
|
if (!park_owner_ready(manager, context)) { reset_park_session(); return; }
|
|
if (r.owner != manager || r.context != context) reset_park_session();
|
|
r.owner = manager; r.context = context; r.model.ready = true;
|
|
if (r.launch_randomization.consume(r.model.ready, r.model.controlled_by_host))
|
|
load_random_local_parks();
|
|
// Serialize clear/load across ticks. The native graph's empty-name branch
|
|
// unloads every family at this lot, including a previously selected family.
|
|
if (now < r.next_request) return;
|
|
// Updated only after Store commits; avoid copying the full cosmetic/profile
|
|
// document on every server frame when no park selection has changed.
|
|
const auto& choices = r.model.choices;
|
|
if (r.pending_lot < park_lots.size()) {
|
|
const auto lot = r.pending_lot;
|
|
const auto layout = choices[lot];
|
|
const auto native_layout = park_native_name(layout);
|
|
if (!layout.empty() && layout != "empty") {
|
|
NativeString zone{std::string(park_lots[lot].key)}, park{native_layout};
|
|
r.notify(manager, &zone, &park);
|
|
}
|
|
r.sent[lot] = layout; r.clear_unset[lot] = false;
|
|
r.pending_lot = static_cast<unsigned>(park_lots.size());
|
|
r.model.feedback = r.model.controlled_by_host ? "Host's park selection applied." :
|
|
"Park selection applied.";
|
|
dingosdk::logging::event(dingosdk::logging::Channel::world, dingosdk::Json{{"event", "local_park_selection_sent"}, {"lot", park_lots[lot].key},
|
|
{"layout", layout}, {"native_layout", native_layout}, {"context", context}}.dump().c_str());
|
|
r.next_request = now + 1000;
|
|
return;
|
|
}
|
|
for (unsigned lot = 0; lot < choices.size(); ++lot) {
|
|
if (!r.clear_unset[lot] && (choices[lot].empty() || choices[lot] == r.sent[lot])) continue;
|
|
NativeString zone{std::string(park_lots[lot].key)}, empty{std::string{}};
|
|
r.notify(manager, &zone, &empty);
|
|
r.pending_lot = lot; r.next_request = now + 1000;
|
|
r.model.feedback = "Changing " + std::string(park_lots[lot].label) + "...";
|
|
dingosdk::logging::event(dingosdk::logging::Channel::world, dingosdk::Json{{"event", "local_park_clear_sent"}, {"lot", park_lots[lot].key}}.dump().c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
void park_tick_hook(std::uintptr_t manager, float delta) {
|
|
auto& r = park_runtime(); auto& s = local_runtime();
|
|
r.tick(manager, delta);
|
|
if (!r.active.load(std::memory_order_acquire) || !s.active.load(std::memory_order_acquire)) return;
|
|
PreserveError preserve;
|
|
try {
|
|
std::lock_guard lock(s.native_mutex);
|
|
update_park_rotation(manager, GetTickCount64());
|
|
update_world_layers(manager, GetTickCount64());
|
|
update_placement_poses();
|
|
} catch (...) {
|
|
dingosdk::logging::event(dingosdk::logging::Channel::world, "{\"event\":\"local_park_update_failed\"}");
|
|
}
|
|
}
|
|
|
|
void park_construct_hook(std::uintptr_t context) {
|
|
park_runtime().construct(context);
|
|
if (!park_runtime().active.load(std::memory_order_acquire)) return;
|
|
PreserveError preserve;
|
|
std::lock_guard lock(local_runtime().native_mutex);
|
|
reset_park_session(); // also handles allocators reusing a previous manager
|
|
auto& r = park_runtime();
|
|
r.observed_owner = 0; r.observed_ready.reset(); r.next_observation = 0;
|
|
logging::write(logging::Level::info, logging::Channel::park, "Park context constructed; waiting for park content.");
|
|
}
|
|
|
|
void start_park_rotation() noexcept {
|
|
auto& r = park_runtime(); const auto base = local_runtime().base;
|
|
const auto& sites = addr::park_rotation::sites;
|
|
std::vector<void*> created;
|
|
try {
|
|
r.model.choices = profile::park_choices(*local_runtime().store->shared_snapshot());
|
|
r.model.randomize_on_launch = local_preference("RandomizeParksOnLaunch").value_or(false);
|
|
r.launch_randomization.pending = r.model.randomize_on_launch;
|
|
for (const auto& site : sites) {
|
|
std::array<unsigned char, 16> bytes{};
|
|
if (!read(base + site.rva, bytes) || bytes != site.bytes)
|
|
throw std::runtime_error("Park adapter fingerprint mismatch");
|
|
}
|
|
const auto hook = [&](unsigned i, auto detour, auto& original) {
|
|
auto* target = reinterpret_cast<void*>(base + sites[i].rva);
|
|
if (hook_prepare(target, reinterpret_cast<void*>(detour), reinterpret_cast<void**>(&original)) != HookOk)
|
|
throw std::runtime_error("Cannot prepare park adapter");
|
|
created.push_back(target);
|
|
};
|
|
r.notify = reinterpret_cast<ParkRuntime::Notify>(base + sites[2].rva);
|
|
hook(0, &park_tick_hook, r.tick); hook(1, &park_construct_hook, r.construct);
|
|
for (auto* target : created)
|
|
if (hook_enable(target) != HookOk) throw std::runtime_error("Cannot enable park adapter");
|
|
auto* notify_target = reinterpret_cast<void*>(base + sites[2].rva);
|
|
void* notify_original{};
|
|
bool notify_logging{};
|
|
if (hook_prepare(notify_target, reinterpret_cast<void*>(&park_notify_hook), ¬ify_original) == HookOk) {
|
|
r.original_notify = reinterpret_cast<ParkRuntime::Notify>(notify_original);
|
|
notify_logging = hook_enable(notify_target) == HookOk;
|
|
}
|
|
logging::write(notify_logging ? logging::Level::info : logging::Level::warning, logging::Channel::park,
|
|
notify_logging ? "Native park load/clear event logging enabled." :
|
|
"Park event hook unavailable; park readiness sampling remains enabled.");
|
|
r.active.store(true, std::memory_order_release);
|
|
dingosdk::logging::event(dingosdk::logging::Channel::world, "{\"event\":\"local_parks_initialized\",\"active\":true}");
|
|
} catch (...) {
|
|
for (auto* target : created) hook_disable(target);
|
|
r.model.feedback = "Park selection is unavailable in this build.";
|
|
dingosdk::logging::event(dingosdk::logging::Channel::world, "{\"event\":\"local_parks_initialized\",\"active\":false}");
|
|
}
|
|
}
|
|
}
|