Files
ReSkate/Extension/Profile/local_profile_runtime.cpp
T
AyeZeeBB abdda77946 Give each mod archive its own index; unblock launches and saves
Mod archives (Engine/Vfs/mod_merge*)
- A free archive index no longer has to sit at or below the highest index
  the layout already declares. The engine keeps archives in a table keyed
  by layer, install chunk and a 16-bit index, with no range to stay
  inside; what it cannot survive is a reference to an archive the layout
  never declared. Indices are taken lowest first, per package directory.
- So a mod that collides takes an index of its own and its file is hard
  linked rather than concatenated, and no two mods share one archive's
  4 GB of addressable bytes. Sharing is left for a mod added while the
  game runs, which has no declared index to take, and for a directory
  with no index left.
- A placement addresses its payload with a 32-bit offset, so both append
  paths now refuse to cross 4 GB instead of wrapping round and pointing
  the game at some other payload: CasStore::write fails with the archive
  that is full, and the live-apply path checks archive 1's size plus the
  mod's before appending and asks for a restart, which gives the mod an
  archive of its own anyway.

Injection through a hooked LoadLibraryW (Engine/Core/Platform)
- A player could not launch at all: "Child LoadLibraryW entry bytes match
  neither Windows' kernel32 file nor this process". Anti-virus and
  overlays hook the entry with a jump to a per-process trampoline, so the
  bytes differ in the launcher and in the child and neither comparison
  could match. The guard already allowed the same hook when both
  processes carried it identically, so it refused on an accident of how
  the hook was written rather than on intent.
- inline_jump_length recognises the forms a hooking engine writes (rel32
  jmp or call, push/ret, jmp [rip], mov rax/jmp rax, short jmp). When one
  is present and every byte after it still matches the clean kernel32
  file, the launch goes ahead: the body is Windows' own, and the provider
  image already matched on timestamp, checksum and size. A wholesale
  replacement is still refused, and now says so.
- The failure names the modules loaded into Skate that came from neither
  Windows nor the game folder, instead of guessing at "security
  software". An accepted hook is logged as a warning with the same list.

Launching (Launcher/launch.cpp)
- Exporting loose scripts is a convenience, and a folder that would not
  take it was failing the whole launch: fs::create_directories threw and
  nothing caught it. The export is skipped, RESKATE_LOOSE_FILES goes to
  0 so the runtime does not look for files that are not there, and Skate
  starts. Someone whose install is under Program Files can play; only
  editing loose scripts is lost.

Failures worth reading (Launcher/problem.h)
- A hooked LoadLibraryW, a refused write and ERROR_ELEVATION_REQUIRED
  each explain themselves now. Windows reports the last two in the user's
  own language -- "Zugriff verweigert", "La operación solicitada requiere
  elevación" -- so the rules key on the operation names and the error
  number, which are never translated.

Saved skaters (Extension/Customization, Extension/Profile)
- Outfits stopped saving and every slot showed the standard skater. The
  load hook marks a preset blocked on entry and clears it only on a full
  success, so a load refused for a catalog that was not ready yet left
  the preset blocked for the session and every later save was refused as
  previous_load_rejected. The game asks once per level load and does not
  ask again, so one lost race cost a skater until a restart. Logs show
  the catalog arriving three seconds after the refused load, at startup
  and again on a map switch 17 minutes later.
- The record is remembered in CosmeticRuntime::pending_loads and applied
  by retry_pending_cosmetic_loads from the customization pump, which
  already waits for refresh_cosmetic_catalog. The preset stays blocked
  until a load really succeeds, so nothing is written over the outfit on
  disk.
- Those records belong to the loadout manager's array, which can be
  replaced in between, so each pointer is checked against the live array
  exactly as a save checks one -- manager current, begin/end sane, 0x48
  stride, at most 10 records, in range and aligned. A record that no
  longer belongs is dropped as retry_record_moved rather than written
  through.
2026-10-03 18:30:28 -05:00

584 lines
35 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/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_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();
// Outfits the game asked for before the catalog was ready: it does
// not ask again, so they are applied here instead of the player
// finding a standard skater that will not save over itself.
retry_pending_cosmetic_loads();
}
} 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 {
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);
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; }
}
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) {
auto& s = local_runtime();
if (!authored_offline) return false;
if (s.attempted) return s.active.load() && s.base == base;
s.attempted = true;
std::vector<void*> created;
bool enable_attempted{};
try {
if (reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr)) != base) return false;
std::array<wchar_t, 32768> exe{};
const auto n = GetModuleFileNameW(nullptr, exe.data(), static_cast<DWORD>(exe.size()));
if (!n || n >= exe.size()) return false;
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})
if (!matches(base, f)) return false;
if (!neighborhood_type_contract(base) || !rip_score_type_contract(base) ||
!challenge_map_type_contract(base)) return false;
if (!prepare_main_mission_override(base)) return false;
if (!game::initialize_native_data(base)) return false;
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, &register_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);
if (music_ready)
hook(music_ui_initialize_contract, &music_ui_initialize_hook, music_ui_runtime().functions.initialize);
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");
}
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);
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}");
dingosdk::logging::log(dingosdk::logging::Level::error, dingosdk::logging::Channel::profile, "Profile initialization failed: {}", e.what());
return false;
}
}
}