mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-27 23:07:08 +01:00
DingoSDKDebugInitialize answers the launcher with a bare TRUE or FALSE. On FALSE the launcher threw "DingoSDKDebugInitialize returned FALSE", which the STATUS tile turned into "ReSkate could not attach to Skate" with anti-virus advice, although the attach had worked and the runtime had logged its reason. ReSkate.dll now exports DingoSDKStartupFailure, a 1 KB buffer that every FALSE path fills with its reason (the first one stays). The launcher reads it out of Skate before closing it. With an older ReSkate.dll it reads the ERROR lines that Skate process wrote to ReSkate.log during the call instead, and with neither it says it was not told why. problem.h explains these messages before the attach rules: a save in use or a folder another game holds says Skate is already running, hook and overlay failures keep the attach advice, and anything else leads with the runtime's own reason. Bootstrap and the local profile hand their reason back, and the profile checks that used to return false without a word now log one. Fixes #90, fixes #346. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
651 lines
40 KiB
C++
651 lines
40 KiB
C++
#include "Extension/News/live_news.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Vfs/mod_catalog.h"
|
|
#include "Extension/Customization/local_cosmetic_catalog.h"
|
|
#include "Extension/Customization/local_customization_runtime.h"
|
|
#include "Extension/Customization/local_player_card_runtime.h"
|
|
#include "Extension/Music/local_music_ui.h"
|
|
#include "Extension/Music/local_music_shelf.h"
|
|
#include "Extension/UI/NativeMenu/native_menu_data.h"
|
|
#include "Extension/Customization/item_browser.h"
|
|
#include "Extension/Music/local_music_playback.h"
|
|
#include "Extension/News/local_news_runtime.h"
|
|
#include "Extension/Objects/local_buildkit_labels.h"
|
|
#include "Extension/Objects/local_buildkit_limits.h"
|
|
#include "Extension/Objects/local_object_runtime.h"
|
|
#include "Extension/Objects/local_placements_runtime.h"
|
|
#include "Extension/Objects/ParkEditor/park_editor_runtime.h"
|
|
#include "runtime_internal.h"
|
|
#include "Extension/Progression/local_challenge_runtime.h"
|
|
#include "Extension/Progression/local_entitlement_trigger_runtime.h"
|
|
#include "Extension/Progression/local_neighborhood_runtime.h"
|
|
#include "Extension/Progression/local_rip_score_runtime.h"
|
|
#include "Extension/Rendering/local_graphics_controls.h"
|
|
#include "Extension/Settings/local_gameplay_settings.h"
|
|
#include "Extension/Settings/local_native_settings.h"
|
|
#include "Extension/Settings/local_user_settings.h"
|
|
#include "Extension/Travel/local_location_travel.h"
|
|
#include "Extension/World/local_park_rotation.h"
|
|
#include "Extension/World/local_world_controls.h"
|
|
#include "Extension/World/local_world_layers.h"
|
|
|
|
namespace dingosdk::profile_runtime {
|
|
LocalRuntime& local_runtime() { static auto* s = new LocalRuntime; return *s; }
|
|
|
|
thread_local bool hydrating{};
|
|
|
|
// Runs inside the game's expression VM hooks and the challenge map walks, many times a frame:
|
|
// peeked, not read, which cost one ReadProcessMemory call per character (profiled 2026-10-01).
|
|
bool identifier(const void* wrapper, std::string& value) {
|
|
std::uintptr_t p{};
|
|
if (!memory::peek(reinterpret_cast<std::uintptr_t>(wrapper), p)) return false;
|
|
value.clear();
|
|
char text[256];
|
|
const auto length = memory::peek_cstring(p, text, sizeof(text));
|
|
if (length <= 0) return false;
|
|
for (std::ptrdiff_t i = 0; i < length; ++i) {
|
|
const auto c = static_cast<unsigned char>(text[i]);
|
|
if (c < 32 || c == 127) return false;
|
|
}
|
|
value.assign(text, static_cast<std::size_t>(length));
|
|
return true;
|
|
}
|
|
|
|
bool entitlements_ready_hook(std::uintptr_t manager) {
|
|
auto& s = local_runtime();
|
|
// Active is published only after the validated JSON profile has loaded and
|
|
// all hooks are installed. UI consumers may now query that local provider.
|
|
// This is readiness, not ownership: unknown IDs still use the native lookup.
|
|
if (s.active.load(std::memory_order_acquire)) {
|
|
PreserveError preserve;
|
|
if (!s.logged_entitlements_ready.exchange(true))
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_entitlements_ready\",\"source\":\"saved_profile\"}");
|
|
return true;
|
|
}
|
|
return s.entitlements_ready(manager);
|
|
}
|
|
|
|
bool has_entitlement_hook(std::uintptr_t manager, const void* wrapper) {
|
|
auto& s = local_runtime();
|
|
if (s.active.load(std::memory_order_acquire)) {
|
|
PreserveError preserve;
|
|
try {
|
|
std::string id;
|
|
if (identifier(wrapper, id)) {
|
|
const auto saved = s.store->entitlement(id);
|
|
if (saved) {
|
|
std::lock_guard lock(s.native_mutex);
|
|
if (s.logged_entitlements.size() < 256 &&
|
|
s.logged_entitlements.try_emplace(id, *saved).second) {
|
|
std::ostringstream event;
|
|
event << "{\"event\":\"local_profile_entitlement\",\"id\":" << std::quoted(id)
|
|
<< ",\"owned\":" << (*saved ? "true" : "false");
|
|
if (id.starts_with("unlock_busstop_")) append_entitlement_trigger_context(event);
|
|
event << '}';
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, event.str().c_str());
|
|
}
|
|
return *saved;
|
|
}
|
|
}
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_entitlement_failed\"}"); }
|
|
// Offline readiness can expose this lookup before the online manager
|
|
// exists. Unknown IDs are unowned; the native lookup dereferences +1e0
|
|
// without a null guard and is only safe with an actual manager.
|
|
if (!manager) return false;
|
|
}
|
|
return s.has_entitlement(manager, wrapper);
|
|
}
|
|
|
|
std::uintptr_t onboarding_manager(std::uintptr_t context) {
|
|
// The native context accessor uses this registered system offset.
|
|
// Resolve it afresh; the level owns the system and its entity handle table.
|
|
std::uint32_t offset{}, bucket_count{};
|
|
std::uintptr_t owner{}, buckets{};
|
|
std::uint8_t initialized{};
|
|
if (context < 0x10000 || !read(local_runtime().base + addr::profile::onboarding_system_offset, offset) ||
|
|
offset > 0x1000000) return 0;
|
|
const auto manager = context + offset;
|
|
if (!read(manager + 0x18, owner) || owner != context ||
|
|
!read(manager + 0x90, initialized) || initialized != 1 ||
|
|
!read(manager + 0x58, buckets) || buckets < 0x10000 ||
|
|
!read(manager + 0x60, bucket_count) || !bucket_count || bucket_count > 1048576) return 0;
|
|
return manager;
|
|
}
|
|
|
|
void apply_onboarding_event(std::uintptr_t manager, const profile::PlayEvent& event) {
|
|
if (!event.id.ends_with(":start") && !event.id.ends_with(":complete")) return;
|
|
auto& s = local_runtime();
|
|
alignas(8) std::array<std::byte, 40> error{};
|
|
s.success(error.data());
|
|
struct Release {
|
|
DestroyString destroy; void* error;
|
|
~Release() { destroy(error); }
|
|
} release{s.destroy_string, error.data()};
|
|
const char* id = event.id.c_str();
|
|
// Unlike the history cache, this consumer publishes the runtime component's
|
|
// started/completed fields and the ECS data-change notification.
|
|
s.apply_onboarding(manager, error.data(), &id, event.timestamp, event.count);
|
|
}
|
|
|
|
void register_onboarding_hook(std::uintptr_t manager, const void* wrapper, std::uint64_t handle) {
|
|
auto& s = local_runtime();
|
|
s.register_onboarding(manager, wrapper, handle);
|
|
if (!s.active.load(std::memory_order_acquire)) return;
|
|
PreserveError preserve;
|
|
try {
|
|
std::lock_guard lock(s.native_mutex);
|
|
std::uintptr_t context{};
|
|
std::string id;
|
|
if (!(handle >> 32) || !read(manager + 0x18, context) ||
|
|
onboarding_manager(context) != manager || !identifier(wrapper, id)) return;
|
|
const auto snapshot = s.store->mission_state();
|
|
unsigned applied{};
|
|
for (const auto* suffix : {":start", ":complete"}) {
|
|
const auto found = snapshot->play_events.find(id + suffix);
|
|
if (found == snapshot->play_events.end()) continue;
|
|
apply_onboarding_event(manager, found->second);
|
|
++applied;
|
|
}
|
|
if (applied && s.logged_onboarding[id] != handle) {
|
|
s.logged_onboarding[id] = handle;
|
|
std::ostringstream event;
|
|
event << "{\"event\":\"local_profile_onboarding_replayed\",\"id\":" << std::quoted(id)
|
|
<< ",\"events\":" << applied << '}';
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, event.str().c_str());
|
|
}
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_onboarding_replay_failed\"}"); }
|
|
}
|
|
|
|
bool valid_manager(std::uintptr_t manager) {
|
|
PreserveError preserve;
|
|
std::uintptr_t vtable{}, context{}, begin{}, end{}, buckets{};
|
|
std::uint32_t bucket_count{};
|
|
std::uint8_t initialized{};
|
|
return read(manager, vtable) && vtable == local_runtime().base + addr::profile::play_event_manager_vtable &&
|
|
read(manager + 0xc0, initialized) && initialized == 1 &&
|
|
read(manager + 0x18, context) && context >= 0x10000 &&
|
|
read(manager + 0x38, begin) && read(manager + 0x40, end) && end >= begin &&
|
|
(end - begin) % 0x40 == 0 && (end - begin) / 0x40 <= 4096 &&
|
|
read(manager + 0x58, buckets) && buckets >= 0x10000 &&
|
|
read(manager + 0x60, bucket_count) && bucket_count > 0 && bucket_count <= 1048576;
|
|
}
|
|
|
|
void cache_event(std::uintptr_t manager, const profile::PlayEvent& event) {
|
|
const NativeEvent native(event);
|
|
local_runtime().cache(manager, &native);
|
|
}
|
|
|
|
void refresh_graphs(std::uintptr_t manager) {
|
|
// The native acknowledgement takes a synchronous snapshot of this vector.
|
|
std::uintptr_t begin{}, end{};
|
|
if (!read(manager + 0x38, begin) || !read(manager + 0x40, end) || end < begin ||
|
|
(end - begin) % 0x40 || (end - begin) / 0x40 > 4096) throw std::runtime_error("Invalid graph vector");
|
|
for (auto at = begin; at != end; at += 0x40) local_runtime().refresh_graph(at);
|
|
}
|
|
|
|
bool hydrate(std::uintptr_t manager) {
|
|
if (hydrating) return false;
|
|
struct Guard { Guard() { hydrating = true; } ~Guard() { hydrating = false; } } guard;
|
|
auto& s = local_runtime();
|
|
const auto snapshot = s.store->mission_state();
|
|
for (const auto& [id, event] : snapshot->play_events) { (void)id; cache_event(manager, event); }
|
|
// Use the normal response consumer for ready publication and graph refresh.
|
|
// It consumes its native Error and a null shared list (no invented refcounts).
|
|
alignas(8) std::array<std::byte, 40> error{};
|
|
std::array<std::uintptr_t, 2> empty_list{};
|
|
s.success(error.data());
|
|
s.complete_load(&manager, error.data(), empty_list.data());
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_hydrated\"}");
|
|
return true;
|
|
}
|
|
|
|
void load_events_hook(std::uintptr_t manager) {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire) || !valid_manager(manager)) { s.load(manager); return; }
|
|
PreserveError preserve;
|
|
try { std::lock_guard lock(s.native_mutex); hydrate(manager); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_load_failed\"}"); }
|
|
}
|
|
|
|
bool ready_hook(std::uintptr_t manager) {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire) || !valid_manager(manager)) return s.ready(manager);
|
|
PreserveError preserve;
|
|
try {
|
|
std::lock_guard lock(s.native_mutex);
|
|
if (hydrating) return false;
|
|
std::uint8_t loaded{};
|
|
return read(manager + 0xb8, loaded) && (loaded == 1 || hydrate(manager));
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_load_failed\"}"); return false; }
|
|
}
|
|
|
|
void send_event_hook(std::uintptr_t manager, const void* wrapper) {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire) || !valid_manager(manager)) { s.send(manager, wrapper); return; }
|
|
PreserveError preserve;
|
|
try {
|
|
std::lock_guard lock(s.native_mutex);
|
|
std::string id;
|
|
if (!identifier(wrapper, id)) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_event_rejected\"}"); return; }
|
|
std::uint8_t loaded{};
|
|
if (!read(manager + 0xb8, loaded) || (!loaded && !hydrate(manager))) return;
|
|
// Acknowledge only after the durable transaction. Native cache insertion
|
|
// publishes the same absolute count used by the backend load consumer.
|
|
const auto event = s.store->record_play_event(id);
|
|
cache_event(manager, event);
|
|
refresh_graphs(manager);
|
|
std::uintptr_t context{};
|
|
if (read(manager + 0x18, context))
|
|
if (const auto onboarding = onboarding_manager(context)) apply_onboarding_event(onboarding, event);
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_event_saved\"}");
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_save_failed\",\"acknowledged\":false}"); }
|
|
}
|
|
|
|
bool get_bool_hook(std::uint64_t user, const void* key, bool fallback) {
|
|
auto& s = local_runtime();
|
|
if (s.active.load(std::memory_order_acquire)) {
|
|
PreserveError preserve;
|
|
try {
|
|
std::string id;
|
|
if (identifier(key, id)) if (const auto value = s.store->bool_option(id)) return *value;
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_option_read_failed\"}"); }
|
|
}
|
|
return s.get_bool(user, key, fallback);
|
|
}
|
|
|
|
void set_bool_hook(std::uint64_t user, const void* key, bool value) {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) { s.set_bool(user, key, value); return; }
|
|
PreserveError preserve;
|
|
if (gameplay_expression_kind(executing_expression) == 2 && !gameplay_user_edit()) return;
|
|
try {
|
|
std::string id;
|
|
if (identifier(key, id)) { s.store->set_bool_option(id, value); }
|
|
else dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_option_rejected\"}");
|
|
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_save_failed\",\"acknowledged\":false}"); }
|
|
}
|
|
|
|
bool saved_quest(std::string_view id, std::int32_t& value) noexcept {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return false;
|
|
try {
|
|
const auto saved = s.store->quest_state(id);
|
|
if (saved && (*saved == 0 || *saved == 5)) { value = *saved; return true; }
|
|
} catch (...) {}
|
|
return false;
|
|
}
|
|
|
|
bool save_quest_completion(std::string_view id) noexcept {
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return false;
|
|
try { s.store->set_quest_state(id, 5); return true; }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_quest_save_failed\"}"); return false; }
|
|
}
|
|
|
|
bool matches(std::uintptr_t base, const Fingerprint& f) {
|
|
std::array<unsigned char, 32> bytes{};
|
|
return read(base + f.rva, bytes) && bytes == f.bytes;
|
|
}
|
|
}
|
|
|
|
namespace dingosdk {
|
|
using namespace profile_runtime;
|
|
|
|
void set_local_location_travel_queue(LocalLocationTravelQueue queue) noexcept {
|
|
location_travel_queue.store(queue, std::memory_order_release);
|
|
}
|
|
void update_local_item_browser() noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return;
|
|
std::lock_guard lock(s.native_mutex);
|
|
if (cosmetic_runtime().update_thread != GetCurrentThreadId()) return;
|
|
item_browser::update(s.base);
|
|
}
|
|
|
|
void update_local_customization() noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return;
|
|
std::lock_guard lock(s.native_mutex);
|
|
try {
|
|
auto& c = cosmetic_runtime();
|
|
const auto thread = GetCurrentThreadId();
|
|
if (!c.update_thread) c.update_thread = thread;
|
|
if (c.update_thread != thread) return;
|
|
if (!c.catalog_failed && refresh_cosmetic_catalog()) {
|
|
publish_cosmetic_catalog();
|
|
publish_cosmetic_inventory();
|
|
}
|
|
} catch (const std::exception& failure) {
|
|
// (Why, in the log: a bare "failed" left a player with no items and nothing to go on.)
|
|
cosmetic_runtime().catalog_failed = true;
|
|
const dingosdk::Json event{{"event", "local_cosmetic_catalog_failed"}, {"reason", failure.what()}};
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, event.dump().c_str());
|
|
} catch (...) {
|
|
cosmetic_runtime().catalog_failed = true;
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_cosmetic_catalog_failed\"}");
|
|
}
|
|
try { update_challenge_catalog(); update_challenge_requests(); update_selected_challenge_progress(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_challenge_update_failed\"}"); }
|
|
try { update_location_travel(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_location_travel_failed\"}"); }
|
|
try { update_position_teleport(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"position_teleport_failed\"}"); }
|
|
try { update_placement_restore(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_placement_restore_failed\"}"); }
|
|
try { update_buildkit_limits(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_buildkit_limits_failed\"}"); }
|
|
try { update_rip_score(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_rip_score_failed\"}"); }
|
|
try { update_news_subscriptions(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_news_failed\",\"operation\":\"update\"}"); }
|
|
try { update_object_categories(); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_object_categories_failed\",\"operation\":\"update\"}"); }
|
|
update_music_catalog();
|
|
try { update_music_catalog_refresh(); } catch (...) {}
|
|
try { update_spotify_cover(); } catch (...) {}
|
|
update_music_shelf();
|
|
try {
|
|
if (cosmetic_runtime().update_thread == GetCurrentThreadId() && !cosmetic_runtime().items.empty())
|
|
update_player_card();
|
|
} catch (...) {
|
|
player_card_runtime().failed = true;
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_player_card_failed\"}");
|
|
}
|
|
}
|
|
void local_profile_before_level_transition(unsigned next) noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return;
|
|
std::lock_guard lock(s.native_mutex);
|
|
music_shelf_before_level_transition(next);
|
|
item_browser::before_level_transition(next, s.base);
|
|
news_runtime().pending.before_transition(next);
|
|
object_runtime().pending.before_transition(next);
|
|
auto& placements = placements_runtime();
|
|
if (next == 3 || next == 14 || next == 22 || next == 24 || next == 25) {
|
|
placements.creates_enabled = false;
|
|
placements.creates.clear();
|
|
placements.creation.reset();
|
|
} else if (next == 13 || next == 21) placements.creates_enabled = true;
|
|
}
|
|
std::uint32_t local_customization_selected_preset() noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return 0;
|
|
try { return s.store->selected_cosmetic_preset(); } catch (...) { return 0; }
|
|
}
|
|
bool local_customization_outfits_loadable() noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire)) return true;
|
|
std::lock_guard lock(s.native_mutex);
|
|
try {
|
|
if (!cosmetic_runtime().items.empty() || refresh_cosmetic_catalog()) return true;
|
|
// Nothing saved is nothing to check: a new profile's slots are built
|
|
// from the game's own defaults and need no catalog.
|
|
const auto snapshot = s.store->shared_snapshot();
|
|
return snapshot->customization.value("loadouts", dingosdk::Json::object()).empty();
|
|
} catch (...) { return true; }
|
|
}
|
|
void observe_local_customization_selection(std::int32_t index) noexcept {
|
|
PreserveError preserve;
|
|
auto& s = local_runtime();
|
|
if (!s.active.load(std::memory_order_acquire) || index < 0 || index >= 10) return;
|
|
std::lock_guard lock(s.native_mutex);
|
|
auto& c = cosmetic_runtime();
|
|
if (c.update_thread != GetCurrentThreadId() || c.observed_selection == index) return;
|
|
c.observed_selection = index;
|
|
try { s.store->set_selected_cosmetic_preset(static_cast<std::uint32_t>(index)); }
|
|
catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_cosmetic_selection_save_failed\"}"); }
|
|
}
|
|
|
|
bool initialize_local_profile(std::uintptr_t base, bool authored_offline,
|
|
const std::filesystem::path& path, std::string& failure) {
|
|
const auto refuse = [&failure](std::string_view why) {
|
|
failure = "Profile initialization failed: " + std::string(why);
|
|
dingosdk::logging::write(dingosdk::logging::Level::error, dingosdk::logging::Channel::profile, failure);
|
|
return false;
|
|
};
|
|
auto& s = local_runtime();
|
|
if (!authored_offline) return refuse("the offline runtime is not active");
|
|
if (s.attempted) return s.active.load() && s.base == base ? true : refuse("an earlier attempt in this game did not finish");
|
|
s.attempted = true;
|
|
std::vector<void*> created;
|
|
bool enable_attempted{};
|
|
try {
|
|
if (reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr)) != base) return refuse("the game image moved");
|
|
std::array<wchar_t, 32768> exe{};
|
|
const auto n = GetModuleFileNameW(nullptr, exe.data(), static_cast<DWORD>(exe.size()));
|
|
if (!n || n >= exe.size()) return refuse("cannot locate Skate.exe");
|
|
launcher::validate_game_file(exe.data());
|
|
for (const auto& f : {load_contract, ready_contract, send_contract, get_bool_contract,
|
|
user_get_bool_contract, user_get_number_contract, user_get_integer_contract, user_get_string_contract,
|
|
user_set_bool_contract, user_set_number_contract, user_set_integer_contract, user_set_text_contract,
|
|
user_set_string_contract, user_success_callback_contract, user_error_callback_contract,
|
|
gameplay_get_bool_contract, gameplay_get_number_contract, gameplay_get_integer_contract, gameplay_get_string_contract,
|
|
gameplay_set_bool_contract, gameplay_set_number_contract, gameplay_set_integer_contract, gameplay_set_string_contract,
|
|
native_setting_get_contract, native_setting_set_contract,
|
|
set_bool_contract, cache_contract, complete_load_contract, success_contract, refresh_graph_contract,
|
|
register_onboarding_contract, apply_onboarding_contract, destroy_string_contract, onboarding_accessor_contract,
|
|
has_entitlement_contract, entitlements_ready_contract, neighborhood_update_contract, neighborhood_publish_contract,
|
|
neighborhood_ctor_contract, model_manager_contract, model_create_contract, model_value_contract,
|
|
model_find_contract, model_lock_contract, model_unlock_contract,
|
|
execute_expression_contract, execute_profiled_expression_contract,
|
|
trigger_online_contract, trigger_session_contract,
|
|
load_cosmetic_contract, save_cosmetic_contract, copy_cosmetic_contract,
|
|
cosmetic_allocate_contract, cosmetic_construct_contract, cosmetic_decode_contract,
|
|
cosmetic_create_catalog_contract, cosmetic_complete_catalog_contract, cosmetic_ui_ready_contract,
|
|
cosmetic_control_destroy_contract, cosmetic_control_free_contract, cosmetic_adapter_allocate_contract,
|
|
cosmetic_adapter_aligned_contract, cosmetic_adapter_free_contract, cosmetic_populate_inventory_contract,
|
|
card_recipe_ctor_contract, card_recipe_destroy_contract, card_recipe_get_contract,
|
|
card_recipe_set_contract, card_recipe_ready_contract, card_info_ctor_contract,
|
|
card_model_field_contract, card_local_info_contract,
|
|
news_first_contract, news_list_contract, news_ctor_contract, news_destroy_contract,
|
|
news_array_ctor_contract, news_array_destroy_contract, news_append_contract, news_assign_contract,
|
|
buildkit_text_exists_contract, buildkit_text_translate_contract,
|
|
buildkit_settings_lookup_contract, buildkit_grabber_settings_contract,
|
|
placement_server_create_contract, placement_message_contract, placement_apply_contract,
|
|
placement_construct_contract, placement_destroy_contract, placement_resolve_contract, placement_valid_contract, placement_query_contract, placement_pose_contract, placement_context_contract, placement_accessor_contract, placement_connection_contract, placement_channel_contract,
|
|
placement_delete_ctor_contract, placement_message_destroy_contract, placement_send_contract,
|
|
placement_teleport_contract, placement_transition_contract,
|
|
news_subscribe_contract, news_delegate_copy_contract, news_delegate_destroy_contract,
|
|
news_invoke_contract, news_reference_contract,
|
|
object_subscribe_contract, object_allocate_contract, object_ctor_contract,
|
|
object_register_contract, object_parse_contract, object_release_contract,
|
|
challenge_begin_contract, challenge_end_contract, challenge_available_contract, challenge_select_contract, challenge_lookup_contract, challenge_end_invoke_contract, challenge_error_contract, challenge_counts_contract, challenge_index_insert_contract, challenge_index_append_contract, challenge_insert_contract, challenge_dispatch_contract, challenge_request_create_contract,
|
|
rip_score_context_contract, multiplayer::menu_data::model_record_load_contract})
|
|
if (!matches(base, f)) return refuse(std::format("the game code at {:#x} is not what this build expects", f.rva));
|
|
if (!neighborhood_type_contract(base) || !rip_score_type_contract(base) ||
|
|
!challenge_map_type_contract(base)) return refuse("the game's profile data types are not what this build expects");
|
|
if (!prepare_main_mission_override(base)) return refuse("the main mission override could not be prepared");
|
|
if (!game::initialize_native_data(base)) return refuse("the game's data model could not be reached");
|
|
s.base = base;
|
|
s.store = std::make_unique<profile::Store>(path);
|
|
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::profile,
|
|
L"Local profile loaded: " + path.filename().wstring()); // the folder holds the Windows user name
|
|
buildkit_limits_runtime().config = buildkit_limits_from(*s.store->shared_snapshot());
|
|
buildkit_limits_runtime().lookup = reinterpret_cast<decltype(buildkit_limits_runtime().lookup)>(base + buildkit_settings_lookup_contract.rva);
|
|
s.cache = reinterpret_cast<Cache>(base + cache_contract.rva);
|
|
s.complete_load = reinterpret_cast<CompleteLoad>(base + complete_load_contract.rva);
|
|
s.success = reinterpret_cast<Success>(base + success_contract.rva);
|
|
s.refresh_graph = reinterpret_cast<RefreshGraph>(base + refresh_graph_contract.rva);
|
|
s.apply_onboarding = reinterpret_cast<ApplyOnboarding>(base + apply_onboarding_contract.rva);
|
|
s.destroy_string = reinterpret_cast<DestroyString>(base + destroy_string_contract.rva);
|
|
s.copy_cosmetic = reinterpret_cast<CopyCosmeticRecipes>(base + copy_cosmetic_contract.rva);
|
|
initialize_cosmetic_catalog_functions(base);
|
|
if (!matches(base, starter_profile_contract)) throw std::runtime_error("Starter profile contract mismatch");
|
|
cosmetic_runtime().initialize_starter = reinterpret_cast<decltype(cosmetic_runtime().initialize_starter)>(base + starter_profile_contract.rva);
|
|
initialize_player_card_functions(base);
|
|
initialize_news_functions(base);
|
|
news::start_live_news();
|
|
initialize_object_functions(base);
|
|
initialize_challenge_functions(base);
|
|
for (const auto& f : {travel_location_ctor_contract, travel_access_ctor_contract, travel_request_contract,
|
|
travel_prepare_contract, travel_attribute_request_contract})
|
|
if (!matches(base, f)) throw std::runtime_error("Location travel native contract mismatch");
|
|
if (!location_travel_type_contract(base)) throw std::runtime_error("Location travel value contract mismatch");
|
|
auto& travel = location_travel_runtime();
|
|
travel.policy = location_travel_policy((*s.store->shared_snapshot()).extensions);
|
|
travel.construct_location = reinterpret_cast<decltype(travel.construct_location)>(base + travel_location_ctor_contract.rva);
|
|
travel.construct_access = reinterpret_cast<decltype(travel.construct_access)>(base + travel_access_ctor_contract.rva);
|
|
const bool music_ready = initialize_music_functions(base);
|
|
if (!music_ready) dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"native_music_ui_contract_mismatch\"}");
|
|
rip_score_runtime().get_context = reinterpret_cast<decltype(rip_score_runtime().get_context)>(base + rip_score_context_contract.rva);
|
|
auto hook = [&](const Fingerprint& f, auto detour, auto& original) {
|
|
void* trampoline{};
|
|
auto* target = reinterpret_cast<void*>(base + f.rva);
|
|
const auto status = hook_prepare(target, reinterpret_cast<void*>(detour), &trampoline);
|
|
if (status != HookOk)
|
|
throw std::runtime_error("Cannot prepare local profile hook at RVA " + std::to_string(f.rva) +
|
|
": " + hook_status_string(status));
|
|
created.push_back(target);
|
|
if (!trampoline) throw std::runtime_error("Missing local profile trampoline");
|
|
original = reinterpret_cast<std::remove_reference_t<decltype(original)>>(trampoline);
|
|
};
|
|
hook(load_contract, &load_events_hook, s.load);
|
|
hook(travel_request_contract, &location_travel_request_hook, travel.request);
|
|
hook(travel_prepare_contract, &prepare_location_travel_hook, travel.prepare_request);
|
|
hook(travel_attribute_request_contract, &location_travel_attribute_request_hook, travel.attribute_request);
|
|
hook(ready_contract, &ready_hook, s.ready);
|
|
hook(send_contract, &send_event_hook, s.send);
|
|
hook(get_bool_contract, &get_bool_hook, s.get_bool);
|
|
hook(set_bool_contract, &set_bool_hook, s.set_bool);
|
|
auto& values = user_values();
|
|
values.assign_string = reinterpret_cast<decltype(values.assign_string)>(base + news_assign_contract.rva);
|
|
values.success_callback = reinterpret_cast<decltype(values.success_callback)>(base + user_success_callback_contract.rva);
|
|
values.error_callback = reinterpret_cast<decltype(values.error_callback)>(base + user_error_callback_contract.rva);
|
|
hook(user_get_bool_contract, &user_get_bool, values.get_bool);
|
|
hook(user_get_number_contract, &user_get_number, values.get_number);
|
|
hook(user_get_integer_contract, &user_get_integer, values.get_integer);
|
|
hook(user_get_string_contract, &user_get_string, values.get_string);
|
|
hook(user_set_bool_contract, &user_set_bool, values.set_bool);
|
|
hook(user_set_number_contract, &user_set_number, values.set_number);
|
|
hook(user_set_integer_contract, &user_set_integer, values.set_integer);
|
|
hook(user_set_text_contract, &user_set_text, values.set_text);
|
|
hook(user_set_string_contract, &user_set_string, values.set_string);
|
|
auto& gameplay = gameplay_settings();
|
|
hook(gameplay_get_bool_contract, &gameplay_get_bool, gameplay.get_bool);
|
|
hook(gameplay_get_number_contract, &gameplay_get_number, gameplay.get_number);
|
|
hook(gameplay_get_integer_contract, &gameplay_get_integer, gameplay.get_integer);
|
|
hook(gameplay_get_string_contract, &gameplay_get_string, gameplay.get_string);
|
|
hook(gameplay_set_bool_contract, &gameplay_set_bool, gameplay.set_bool);
|
|
hook(gameplay_set_number_contract, &gameplay_set_number, gameplay.set_number);
|
|
hook(gameplay_set_integer_contract, &gameplay_set_integer, gameplay.set_integer);
|
|
hook(gameplay_set_string_contract, &gameplay_set_string, gameplay.set_string);
|
|
auto& native = native_settings();
|
|
hook(native_setting_get_contract, &native_setting_get, native.get);
|
|
hook(native_setting_set_contract, &native_setting_set, native.set);
|
|
hook(register_onboarding_contract, ®ister_onboarding_hook, s.register_onboarding);
|
|
hook(has_entitlement_contract, &has_entitlement_hook, s.has_entitlement);
|
|
hook(entitlements_ready_contract, &entitlements_ready_hook, s.entitlements_ready);
|
|
hook(execute_expression_contract, &execute_expression_hook, s.execute_expression);
|
|
hook(execute_profiled_expression_contract, &execute_profiled_expression_hook, s.execute_profiled_expression);
|
|
hook(trigger_online_contract, &trigger_online_hook, s.trigger_online);
|
|
hook(trigger_session_contract, &trigger_session_hook, s.trigger_session);
|
|
hook(load_cosmetic_contract, &load_cosmetic_hook, s.load_cosmetic);
|
|
hook(save_cosmetic_contract, &save_cosmetic_hook, s.save_cosmetic);
|
|
hook(card_local_info_contract, &local_player_info_hook, player_card_runtime().functions.get_local_info);
|
|
hook(news_first_contract, &news_first_hook, news_runtime().functions.first);
|
|
hook(news_list_contract, &news_list_hook, news_runtime().functions.list);
|
|
hook(news_subscribe_contract, &news_subscribe_hook, news_runtime().functions.subscribe);
|
|
hook(object_subscribe_contract, &object_categories_hook, object_runtime().functions.subscribe);
|
|
hook(multiplayer::menu_data::model_record_load_contract,
|
|
&multiplayer::menu_data::model_record_load_hook,multiplayer::menu_data::model_record_load_original);
|
|
if (music_ready) {
|
|
hook(music_ui_initialize_contract, &music_ui_initialize_hook, music_ui_runtime().functions.initialize);
|
|
if (music_ui_runtime().functions.favorite_apply)
|
|
hook(addr::local_music::favorite_change_contract, &music_favorite_change_hook,
|
|
music_ui_runtime().functions.favorite_change);
|
|
std::array<unsigned char, 32> construct_bytes{};
|
|
if (read(base + music_model_construct_contract.rva, construct_bytes) &&
|
|
construct_bytes == music_model_construct_contract.bytes)
|
|
hook(music_model_construct_contract, &music_model_construct_hook, music_model_construct_original);
|
|
else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
|
"{\"event\":\"music_model_construct_contract_mismatch\"}");
|
|
}
|
|
const bool playback_ready = initialize_music_playback(base,
|
|
local_preference("MusicShuffle").value_or(false));
|
|
if (playback_ready) {
|
|
std::array<unsigned char, 32> channel_bind_bytes{};
|
|
if (read(base + addr::local_music::playback_channel_playlist_bind_contract.rva,
|
|
channel_bind_bytes) &&
|
|
channel_bind_bytes == addr::local_music::playback_channel_playlist_bind_contract.bytes)
|
|
hook(addr::local_music::playback_channel_playlist_bind_contract,
|
|
&music_channel_playlist_bind_hook, music_channel_playlist_bind_original());
|
|
else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
|
"{\"event\":\"music_channel_playlist_binding_contract_mismatch\"}");
|
|
hook(addr::local_music::playback_select_next_contract,
|
|
&music_select_next_hook, music_select_next_original());
|
|
hook(addr::local_music::playback_song_score_contract,
|
|
&music_song_score_hook, music_song_score_original());
|
|
} else dingosdk::logging::event(dingosdk::logging::Channel::music,
|
|
"{\"event\":\"music_playback_order_contract_mismatch\"}");
|
|
hook(buildkit_text_exists_contract, &buildkit_text_exists, buildkit_text_functions().exists);
|
|
hook(buildkit_text_translate_contract, &buildkit_text_translate, buildkit_text_functions().translate);
|
|
hook(buildkit_grabber_settings_contract, &buildkit_grabber_settings_hook, buildkit_limits_runtime().grabber_settings);
|
|
auto& placements = placements_runtime();
|
|
placements.server_create = reinterpret_cast<decltype(placements.server_create)>(base + placement_server_create_contract.rva);
|
|
placements.teleport = reinterpret_cast<decltype(placements.teleport)>(base + placement_teleport_contract.rva);
|
|
placements.transition_ctor = reinterpret_cast<decltype(placements.transition_ctor)>(base + placement_transition_contract.rva);
|
|
placements.transition_destroy = reinterpret_cast<decltype(placements.transition_destroy)>(base + object_release_contract.rva);
|
|
placements.delete_ctor = reinterpret_cast<decltype(placements.delete_ctor)>(base + placement_delete_ctor_contract.rva);
|
|
placements.message_destroy = reinterpret_cast<decltype(placements.message_destroy)>(base + placement_message_destroy_contract.rva);
|
|
placements.send = reinterpret_cast<decltype(placements.send)>(base + placement_send_contract.rva);
|
|
|
|
placements.resolve = reinterpret_cast<decltype(placements.resolve)>(base + placement_resolve_contract.rva);
|
|
placements.valid = reinterpret_cast<decltype(placements.valid)>(base + placement_valid_contract.rva);
|
|
placements.query = reinterpret_cast<decltype(placements.query)>(base + placement_query_contract.rva);
|
|
placements.pose = reinterpret_cast<decltype(placements.pose)>(base + placement_pose_contract.rva);
|
|
placements.context = reinterpret_cast<decltype(placements.context)>(base + placement_context_contract.rva);
|
|
hook(placement_construct_contract, &placement_construct_hook, placements.construct);
|
|
hook(placement_destroy_contract, &placement_destroy_hook, placements.destroy);
|
|
hook(placement_message_contract, &placement_message_hook, placements.message);
|
|
hook(placement_apply_contract, &placement_apply_hook, placements.apply);
|
|
hook(challenge_begin_contract, &challenge_begin_hook, challenge_runtime().f.begin);
|
|
hook(challenge_end_contract, &challenge_end_hook, challenge_runtime().f.end);
|
|
hook(challenge_available_contract, &challenge_available_hook, challenge_runtime().f.available);
|
|
hook(challenge_select_contract, &challenge_select_hook, challenge_runtime().f.select);
|
|
hook(challenge_lookup_contract, &challenge_lookup_hook, challenge_runtime().f.lookup);
|
|
hook(challenge_counts_contract, &challenge_counts_hook, challenge_runtime().f.counts);
|
|
auto& neighborhoods = neighborhood_runtime();
|
|
neighborhoods.construct = reinterpret_cast<ConstructNeighborhood>(base + neighborhood_ctor_contract.rva);
|
|
hook(neighborhood_publish_contract, &publish_neighborhood_hook, game::native_data().models.publish);
|
|
hook(neighborhood_update_contract, &update_neighborhoods_hook, neighborhoods.update);
|
|
for (auto* target : created) {
|
|
enable_attempted = true;
|
|
if (hook_enable(target) != HookOk) throw std::runtime_error("Cannot enable local profile hook");
|
|
}
|
|
if (playback_ready) activate_music_playback();
|
|
initialize_placement_store(path);
|
|
initialize_park_editor(path.parent_path());
|
|
set_park_mods_root(mods::engine_data_root());
|
|
s.active.store(true, std::memory_order_release);
|
|
start_world_layers();
|
|
start_park_rotation();
|
|
start_world_controls();
|
|
start_graphics_controls();
|
|
set_local_profile_event_provider(base, true);
|
|
set_local_object_browser_provider(base, &local_object_browser_ready);
|
|
set_main_mission_profile_provider(base, &saved_quest, &save_quest_completion);
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_initialized\",\"active\":true,\"schema\":1,\"format\":\"sqlite\"}");
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
s.active.store(false, std::memory_order_release);
|
|
deactivate_music_playback();
|
|
set_local_profile_event_provider(base, false);
|
|
set_local_object_browser_provider(base, nullptr);
|
|
for (auto* target : created) {
|
|
if (enable_attempted) hook_disable(target);
|
|
else hook_remove(target);
|
|
}
|
|
// Retain attempted trampolines/store for any already-entered detours.
|
|
if (!enable_attempted) s.store.reset();
|
|
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_profile_initialized\",\"active\":false}");
|
|
return refuse(e.what());
|
|
}
|
|
}
|
|
|
|
}
|