Files
ReSkate/Runtime/client_tick.cpp
T
AyeZeeBB 5d37d0bc50 Drone: follow mode flies the drone behind the skater
An autopilot flies the real flight model behind the skater while they skate: it
works the sticks a pilot would (angle mode with height hold), reacts a moment late,
eases toward the lie of the ground instead of every ollie, closes gaps no faster than
it can slow down gently, and climbs a little over what blocks the shot. Its camera
sits on a tilt gimbal so leaning to speed up or brake keeps the skater in frame.

Started from FOLLOW ME on the DRONE page or the dronefollow command; it hides the
game's interface like Start flying does. Tests cover straight, turns, hard stops,
ollies and catching up.
2026-10-16 16:43:56 -05:00

1474 lines
89 KiB
C++

#include "runtime_internal.h"
#include "Extension/Customization/developer_hoodie.h"
#include "Extension/Customization/developer_board.h"
#include "Extension/Assets/live_mods.h"
#include "Extension/Assets/map_download.h"
#include "Extension/Assets/map_icons.h"
#include "Extension/Music/spotify_station.h"
#include "Extension/Multiplayer/Hud/game_ui_state.h"
#include "Extension/Music/music_inject.h"
#include "Extension/World/texture_handle_limit.h"
#include "Extension/Assets/native_render_resource_pool.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Profiling/profiler.h"
#include "Extension/Console/memory_ledger.h"
#include "Extension/Settings/job_spin.h"
#include "Extension/Settings/replay_drafts.h"
#include "Engine/Game/World/client_state.h"
#include "Extension/HallOfMeat/hall_of_meat.h"
#include "Extension/RoadRash/road_rash.h"
#include "Extension/BoardBreak/board_break.h"
#include "Extension/Skater/grind_exit_control.h"
#include "Extension/Skater/camera_observer.h"
#include "Extension/UI/NativeMenu/native_menu.h"
#include "Extension/Multiplayer/Hud/native_party.h"
#include "Extension/Multiplayer/Session/session.h"
#include "Extension/Multiplayer/Remote/native_prop.h"
#include "Extension/Multiplayer/Remote/drone_sound.h"
#include "Extension/Boot/discord_presence.h"
#include "Engine/Game/World/world_names.h"
#include "Extension/Multiplayer/Hud/custom_nametags.h"
#include "Extension/Multiplayer/developer_identity.h"
#include "Extension/Multiplayer/Hud/follow_camera.h"
#include "Engine/Game/UI/game_view.h"
#include "Extension/Multiplayer/Steam/steam_friend_join.h"
#include "Extension/Objects/network_object_runtime.h"
#include "Extension/Profile/local_profile_runtime.h"
#include "Extension/Progression/fixed_stop_entitlement_provider.h"
#include "Extension/Progression/mission_progression_override.h"
#include "Extension/Progression/neighborhood_unlock_override.h"
#include "Extension/Settings/gameplay_settings_override.h"
#include "Extension/Settings/named_settings.h"
#include "Extension/Skater/ai_skaters.h"
#include "Extension/Skater/client_source_spawn.h"
#include "Extension/Skater/skater_slot_override.h"
#include "Extension/Skater/drone_flight.h"
#include "Extension/Skater/drone_outfit.h"
#include "Extension/Throwdowns/native_throwdowns.h"
#include "Extension/Trainer/trainer.h"
#include "Extension/World/level_loading.h"
#include "Extension/World/loading_screen.h"
#include <Windows.h>
#include <dxgi.h>
#include <Psapi.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <chrono>
#include <iomanip>
#include <optional>
#include <sstream>
#include <format>
#include <thread>
namespace dingosdk::runtime::detail {
namespace {
using HostedSelector = bool (*)(const char*);
const char* state_name(DWORD state) { return dingosdk::client_state_name(state); }
// What update_model learned about this tick's client, reused by the rest of the
// tick instead of reading the same native chains again.
struct TickState {
bool valid{};
DWORD state{}, game_type{};
std::optional<bool> context; // native_context
std::optional<const char*> camera_issue; // camera_probe_unavailable_reason, nullptr when ready
};
void refresh_interactive_model(std::uintptr_t client, DWORD state, DWORD game_type) {
auto& r = runtime();
auto world = dingosdk::local_profile_world_layers();
bool ready{};
{
std::lock_guard lock(r.mutex);
ready = !r.observer_failed && !r.requests.loading() && r.model.can_queue_load &&
state == r.previous_state && (state == 13 || state == 21) && world.ready;
}
const auto map = ready && native_context(r.base, client, game_type) ? world.map : dingosdk::WorldMap::none;
// Commands already run on the verified client thread. Apply their settings
// and publish the result now, without waiting for the 500 ms catalog scan.
dingosdk::update_local_world_controls(map);
dingosdk::update_local_graphics_controls();
auto controls = dingosdk::local_profile_world_controls();
auto graphics = dingosdk::local_profile_graphics_controls();
auto parks = dingosdk::local_profile_parks();
auto progression = dingosdk::local_profile_progression();
auto player_card = dingosdk::local_profile_player_card();
auto object_persistence = dingosdk::local_profile_object_persistence();
auto editor = dingosdk::local_park_editor();
auto bindings = dingosdk::local_profile_controller_bindings();
const auto missions = dingosdk::local_profile_missions();
std::vector<dingosdk::overlay::MissionRow> rows;
for (const auto& row : missions.rows) rows.push_back({row.id, row.group, row.completed});
std::lock_guard lock(r.mutex);
r.model.world = std::move(world);
r.model.world_controls = std::move(controls);
r.model.graphics = std::move(graphics);
r.model.parks = std::move(parks);
r.model.progression = std::move(progression);
r.model.player_card = std::move(player_card);
r.model.object_persistence = std::move(object_persistence);
r.model.editor = std::move(editor);
r.model.bindings = std::move(bindings);
r.model.missions_available = missions.available;
r.model.mission_feedback = missions.feedback;
r.model.missions = std::move(rows);
++r.model_revision;
}
// Engine settings that cost simulation time for nothing here, set once through the named
// setter (retried until the settings registry answers):
// - telemetry and the performance tracker only feed EA's backend, absent offline;
// - EA's error reporting (EadpErrorsData) only builds reports for EA's servers, which
// ea_service_block keeps unreachable. Set here because the same values given on the game's
// command line do not take;
// - the in-game advertising (billboards served by EA's ad service, with a render pass that
// measures how long each was on screen) has nothing to show offline and still costs frame
// time: its three switches, the renderer's, the service's and the game's own;
// - the world-transform update, which grows with every remote player's entities, is split
// over more jobs than Game.cfg's 2.
void apply_performance_settings() {
static bool applied = false;
static ULONGLONG next_attempt = 0;
if (applied) return;
const auto now = GetTickCount64();
if (now < next_attempt) return;
next_attempt = now + 500;
try {
const auto jobs = std::to_string(std::clamp(std::thread::hardware_concurrency() / 2, 2u, 8u));
const std::array<std::pair<const char*, std::string>, 13> settings{{
{"DingoTelemetry.Enable", "0"},
{"DingoTelemetry.EnablePlayerTickEvents", "0"},
{"DingoTelemetry.EnablePerformanceEvents", "0"},
{"DingoTelemetry.EnableCPUBenchmark", "0"},
{"EadpErrorsData.DisableAllEventsReporting", "1"},
{"EadpErrorsData.EnableReportCrashes", "0"},
{"EadpErrorsData.AutoSendEventFromPersistence", "0"},
{"PerformanceTracker.Enabled", "0"},
{"PerformanceTracker.JuiceLogPerformance", "0"},
{"AdTech.AdTechEnable", "0"},
{"AdTechService.AdTechEnable", "0"},
{"DingoAdTech.EnableAdTech", "0"},
{"EcsWorldTransform.ParallelWorldTransformUpdateJobCount", jobs},
}};
bool pending = false;
std::string results;
for (const auto& [name, value] : settings) {
const auto result = dingosdk::change_named_setting(name, value, false);
pending = pending || result.starts_with("error: ");
results += (results.empty() ? "" : " | ") + result;
}
if (!pending) {
applied = true;
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime,
"Performance settings: {}", results);
}
} catch (...) {}
}
// The game's own connection is a loopback to the world it hosts in the same process (ReSkate's
// multiplayer runs beside it), so its in-game timeouts (Game.cfg 15 s, Branch.cfg 5 s) can only
// fire when the game itself stalls that long; a long replay export does (the encoder sets the
// pace of every frame), and the game then dropped the player to the splash screen with
// SecureReason_TimedOut while the export went on. -1 turns them off, as the game's own
// development config does. Kept apart from the settings above so an unknown name here cannot
// hold those back; tried for a minute.
void apply_local_connection_timeouts() {
static int attempts = 0;
static ULONGLONG next_attempt = 0;
if (attempts >= 120) return;
const auto now = GetTickCount64();
if (now < next_attempt) return;
next_attempt = now + 500;
try {
bool pending = false;
std::string results;
for (const auto* name : {"Client.IngameTimeout", "Server.IngameTimeout"}) {
const auto result = dingosdk::change_named_setting(name, "-1", false);
pending = pending || result.starts_with("error: ");
results += (results.empty() ? "" : " | ") + result;
}
if (!pending || ++attempts == 120) {
attempts = 120;
dingosdk::logging::log(pending ? dingosdk::logging::Level::warning : dingosdk::logging::Level::info,
dingosdk::logging::Channel::runtime, "Local connection timeouts: {}", results);
}
} catch (...) {}
}
// The mesh streaming pool holds every render mesh the world has loaded, in video memory.
// GraphicsPC.lua fixes it at 532960 KB (520 MB) on every PC quality level, sized for San Van's
// streamed cells; a big custom map's meshes do not fit, so its level either never finishes
// loading or waits out the transition's 180 s world-quality timeout
// (MeshStreamingLoadStateProvider never reports high) and spawns the skater invisible. The pools
// are re-created from this setting at every level start (FUN_14507a2d0), so raising it before a
// load is enough; the quality script sets it again whenever graphics settings are applied, so it
// is kept here. The engine turns it into bytes in 32 bits (PoolSize << 10, plus
// PoolHeadroomSize), so the combined allocation must remain below 4 GiB.
// How far it is raised depends on the card: 1.0.0 set 3.5 GiB on every card, the likely cause
// of older cards resetting the device while loading (DXGI_ERROR_DEVICE_RESET in
// gameRendBeginFrame on an RX 580, 2026-10-03). A quarter of the card's dedicated memory, from
// the game's own 520 MB up to 3.5 GiB (from 16 GB cards up); cards of 6 GB or less keep the
// game's value. Cards of up to 12 GB stop at 2 GiB: a quarter was 3 GiB of a 12 GB card, and
// with a big map and texture mods on top the game ran out of video memory (E_OUTOFMEMORY from
// CreateCommittedResource on an RTX 4070 SUPER, 2026-10-04).
namespace mesh_pool {
constexpr std::uint32_t stock_kb = 532960, most_kb = 3584u * 1024u, most_to_12gb_kb = 2048u * 1024u;
static_assert((std::uint64_t{most_kb} + 24576u) * 1024u <= 0xffffffffull);
struct Card { std::uint64_t memory{}; std::string name; };
// The hardware adapter with the most dedicated memory: the one the game renders on (a laptop's
// integrated GPU has little or none). The game's own dxgi.dll, so nothing new is loaded.
Card largest_card() {
Card card;
const auto module = GetModuleHandleW(L"dxgi.dll");
using Create = HRESULT(WINAPI*)(REFIID, void**);
const auto create = module ? reinterpret_cast<Create>(GetProcAddress(module, "CreateDXGIFactory1")) : nullptr;
IDXGIFactory1* factory{};
if (!create || FAILED(create(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(&factory))) || !factory) return card;
IDXGIAdapter1* adapter{};
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) {
DXGI_ADAPTER_DESC1 desc{};
if (SUCCEEDED(adapter->GetDesc1(&desc)) && !(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) &&
desc.DedicatedVideoMemory > card.memory) {
card.memory = desc.DedicatedVideoMemory;
card.name.clear();
for (const auto* c = desc.Description; *c; ++c) card.name.push_back(*c < 128 ? static_cast<char>(*c) : '?');
}
adapter->Release();
}
factory->Release();
return card;
}
// RESKATE_MESH_POOL_MB: the pool's size for this session in MB, whatever the card (520 to 3584);
// for finding what size a map that fails needs.
std::uint32_t override_kb() {
wchar_t value[16]{};
const auto length = GetEnvironmentVariableW(L"RESKATE_MESH_POOL_MB", value, 16);
if (!length || length >= 16) return 0;
const auto megabytes = std::wcstoul(value, nullptr, 10);
return megabytes ? static_cast<std::uint32_t>(std::clamp<std::uint64_t>(std::uint64_t{megabytes} * 1024, stock_kb, most_kb)) : 0;
}
// KiB for the pool on this card, or 0 to keep the game's own value.
std::uint32_t size_kb(std::uint64_t memory) {
if (const auto chosen = override_kb()) return chosen;
// Cards report a little under or over their nominal size.
constexpr std::uint64_t six_gib = 6ull << 30, twelve_gib = 12ull << 30, slack = 256ull << 20;
if (memory <= six_gib + slack) return 0;
const auto most = memory <= twelve_gib + slack ? most_to_12gb_kb : most_kb;
return static_cast<std::uint32_t>(std::clamp<std::uint64_t>(memory / 4 / 1024, stock_kb, most));
}
}
void apply_mesh_streaming_pool() {
static const auto card = mesh_pool::largest_card();
static const auto pool_kb = mesh_pool::size_kb(card.memory);
static bool reported = false;
if (!reported) {
reported = true;
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime,
"Mesh streaming pool: {} for {} ({:.1f} GB of video memory).",
pool_kb ? std::format("{} MB", pool_kb / 1024) : std::string("the game's own 520 MB"),
card.name.empty() ? std::string("an unknown card") : card.name, static_cast<double>(card.memory) / (1ull << 30));
}
if (!pool_kb) return;
static ULONGLONG next_check = 0;
static std::string last_result;
const auto now = GetTickCount64();
if (now < next_check) return;
next_check = now + 2000;
try {
const auto result = dingosdk::change_named_setting("MeshStreaming.PoolSize", std::to_string(pool_kb), false);
if (result.starts_with("error: ")) { next_check = now + 500; return; }
if (result != last_result && !result.ends_with("(unchanged)"))
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime,
"Mesh streaming pool raised for big custom maps (applies from the next level load): {}", result);
last_result = result;
} catch (...) {}
}
void apply_throwdown_modes() {
// The throwdowner flow picks between the mode-select page and straight Jam
// placement by reading these by name (GetBoolSetting). Retail receives them
// from live config; S.K.A.T.E. ships off in the native constructor. Go
// through the named setter so registered setting listeners are notified.
static bool applied = false;
static ULONGLONG next_attempt = 0;
static std::string last_result;
if (applied) return;
const auto now = GetTickCount64();
if (now < next_attempt) return;
next_attempt = now + 500;
try {
bool pending = false;
std::string results;
// EnableAdvancedParameters adds the host's Customize button and the
// parameter panel (players, timer, privacy, scored actions) to the
// throwdown details page; it also ships off.
for (const auto* name : {"DingoThrowdowns.EnableSpotBattle", "DingoThrowdowns.EnableSKATE",
"DingoThrowdowns.EnableAdvancedParameters"}) {
const auto result = dingosdk::change_named_setting(name, "1", false);
pending = pending || result.starts_with("error: ");
results += (results.empty() ? "" : " | ") + result;
}
{
// Local play has nobody to wait for: with the single-player queue
// enabled (native_throwdowns.cpp) the 120 s join countdown would only
// delay the host. Revisit when throwdowns are relayed to other players.
const auto result = dingosdk::change_named_setting("DingoThrowdowns.QueueStartTimer", "1", false);
pending = pending || result.starts_with("error: ");
results += " | " + result;
}
if (results != last_result) {
last_result = results;
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::progression,
"Throwdown modes: {}", results);
}
applied = !pending;
} catch (...) {}
}
void update_model(std::uintptr_t client, TickState& frame) {
auto& r = runtime();
std::uintptr_t vtable{};
DWORD state{}, game_type{};
// Every client tick: peeked, not read (three ReadProcessMemory calls a tick, profiled 2026-10-02).
if (!memory::peek(client, vtable) || vtable != r.base + engine::client_vtable || client > highest - 0x2a0 ||
!memory::peek(client + 0xc4, state) || state > 26 || !memory::peek(client + 0xc0, game_type) || game_type > 3) return;
DWORD no_thread{};
r.engine_thread.compare_exchange_strong(no_thread, GetCurrentThreadId());
if (r.engine_thread.load() != GetCurrentThreadId()) return;
frame.valid = true;
frame.state = state;
frame.game_type = game_type;
// Some two dozen native reads: one result serves the whole tick.
const auto native_context_ready = [&] {
if (!frame.context) frame.context = native_context(r.base, client, game_type);
return *frame.context;
};
// Engine settings are only shown by the menu and console; refresh them
// quickly (and hand them over) only while one of them is reading.
const auto settings_now = GetTickCount64();
const bool settings_wanted = settings_now < r.named_settings_wanted_until.load(std::memory_order_relaxed);
dingosdk::refresh_named_settings(settings_wanted);
apply_throwdown_modes();
apply_performance_settings();
apply_local_connection_timeouts();
dingosdk::job_spin::apply_default();
apply_mesh_streaming_pool();
dingosdk::multiplayer::apply_throwdown_strings(r.base);
const bool named_context_ready = (state == 13 || state == 21) && native_context_ready();
{
std::lock_guard lock(r.mutex);
r.named_context_ready = named_context_ready;
}
if (settings_wanted && settings_now >= r.next_named_settings_publish) {
r.next_named_settings_publish = settings_now + 250;
publish_named_settings(r);
}
// RESKATE_STARTUP_COMMANDS="load skate1map;noclip 1" (development, and the launcher's
// -map <name>, which is a "load" at the front of it): console commands
// queued in order once the world is up and can accept them, so a build can be exercised
// without anyone driving the overlay. A load waits for its destination to be listed.
{
static std::optional<std::vector<std::string>> startup_commands;
if (!startup_commands) {
startup_commands.emplace();
std::wstring value(4096, L'\0');
const auto length = GetEnvironmentVariableW(L"RESKATE_STARTUP_COMMANDS", value.data(), 4096);
if (length && length < 4096) {
std::string narrow;
for (const auto character : value.substr(0, length))
narrow.push_back(character < 128 ? static_cast<char>(character) : '?');
for (std::size_t start = 0; start <= narrow.size();) {
auto end = narrow.find(';', start);
if (end == std::string::npos) end = narrow.size();
auto piece = narrow.substr(start, end - start);
const auto first = piece.find_first_not_of(" \t");
const auto last = piece.find_last_not_of(" \t");
if (first != std::string::npos) startup_commands->push_back(piece.substr(first, last - first + 1));
start = end + 1;
}
}
}
if (!startup_commands->empty() && named_context_ready) {
std::lock_guard lock(r.mutex);
const auto& text = startup_commands->front();
const auto lower = [](std::string value) {
for (auto& character : value) character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
return value;
};
// "wait <seconds>" pauses the list, e.g. to let a multiplayer join finish.
static std::optional<std::chrono::steady_clock::time_point> wait_until;
const bool wait = lower(text).starts_with("wait ");
if (wait) {
const auto now = std::chrono::steady_clock::now();
if (!wait_until) wait_until = now + std::chrono::milliseconds(static_cast<int>(1000 * std::atof(text.c_str() + 5)));
if (now >= *wait_until) {
wait_until.reset();
startup_commands->erase(startup_commands->begin());
}
}
bool ready = !wait && r.model.can_queue_load && !r.requests.loading();
if (ready && lower(text).starts_with("load ")) {
const auto wanted = lower(text.substr(5, text.find(' ', 5) == std::string::npos ? std::string::npos : text.find(' ', 5) - 5));
ready = std::ranges::any_of(r.model.levels, [&](const dingosdk::overlay::Level& level) {
return level.can_load && (lower(level.asset).find(wanted) != std::string::npos ||
lower(level.display_name) == wanted);
});
}
if (ready && r.requests.enqueue(ConsoleRequest{text}, request_context(r), GetCurrentThreadId())) {
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level,
"Startup command queued: {}", text);
startup_commands->erase(startup_commands->begin());
}
}
}
std::optional<ConsoleRequest> console_command;
{
std::lock_guard lock(r.mutex);
console_command = r.requests.take<ConsoleRequest>(GetCurrentThreadId());
}
if (console_command) {
ScheduledWorkScope work{r, true};
const auto started = std::chrono::steady_clock::now();
execute_console_command(console_command->text);
const auto executed = std::chrono::steady_clock::now();
refresh_interactive_model(client, state, game_type);
const auto published = std::chrono::steady_clock::now();
const auto milliseconds = [](auto duration) { return std::chrono::duration<double, std::milli>(duration).count(); };
record("{\"event\":\"menu_command_timing\",\"queue_ms\":" +
std::to_string(milliseconds(started - console_command->queued_at)) + ",\"execute_ms\":" +
std::to_string(milliseconds(executed - started)) + ",\"publish_ms\":" +
std::to_string(milliseconds(published - executed)) + "}");
}
const auto now = GetTickCount64();
bool has_request{};
{ std::lock_guard lock(r.mutex); has_request = r.requests.pending_load(); }
// Camera phase validation is necessary before camera/motion operations,
// not on every idle frame. Validate afresh after any action in this tick
// that can have changed the native camera's identity (frame.camera_issue is
// cleared then); until one runs, the first check serves the whole tick.
const auto camera_phase_issue = [&]() -> const char* {
if (!frame.camera_issue) {
std::uintptr_t context{};
frame.camera_issue = read(client + 8, context) ? dingosdk::camera_probe_unavailable_reason(context) :
"Waiting for the local camera context.";
}
return *frame.camera_issue;
};
const auto camera_phase_ready = [&] { return camera_phase_issue() == nullptr; };
std::optional<dingosdk::overlay::DebugRequest> debug_request;
bool debug_busy{}, controller_busy{};
std::uint32_t freecam_controller_combo{}, freecam_combo{}, tp_to_freecam_combo{}, noclip_combo{}, forward_velocity_combo{}, up_velocity_combo{}, offboard_up_velocity_combo{};
// Yes and No in a dedicated server's vote: read only while one is running.
std::uint32_t vote_yes_combo{}, vote_no_combo{};
// The switches on buttons (action_binds): each runs its console command.
std::array<std::uint32_t, dingosdk::action_binds.size()> action_combos{};
const bool vote_open = dingosdk::multiplayer::server_vote_open();
// The map download's card (map_download.h) is answered with the same two binds, and by a
// controller, whose buttons are kept from the game while it asks.
const bool map_prompt = dingosdk::map_download::asking();
// The vote for the next map comes up the same way (overlay.h: map_vote_asking), when no map is being asked about.
const bool map_vote = !map_prompt && dingosdk::overlay::map_vote_asking();
// And a message to say OK to (overlay.h: message_asking), over either.
const bool message_up = dingosdk::overlay::message_asking();
// The hold on the game's input outlasts the card's question until everything pressed is let
// go (or a moment has passed): the button that answered must not also be a press in the
// game, or in the pause menu behind the card.
static bool prompt_held{};
static ULONGLONG prompt_release_by{};
if (map_prompt || map_vote || message_up) {
prompt_held = true;
prompt_release_by = 0;
} else if (prompt_held) {
if (!prompt_release_by) prompt_release_by = GetTickCount64() + 1500;
dingosdk::ControllerInput held;
DingoSDKOverlayReadControllerInput(&held, true);
const bool pressed = (held.available && held.buttons) || DingoSDKOverlayReadPromptKeys() ||
std::ranges::any_of(held.keys, [](std::uint64_t word) { return word != 0; });
if (!pressed || GetTickCount64() >= prompt_release_by) prompt_held = false;
}
DingoSDKOverlaySetPromptInputCapture(map_prompt || map_vote || message_up || prompt_held);
const unsigned poll_answers = dingosdk::multiplayer::server_poll_answers();
bool freecam_controller = dingosdk::local_freecam_controller();
{
std::lock_guard lock(r.mutex);
debug_busy = r.requests.loading();
controller_busy = !r.requests.idle();
freecam_controller_combo = r.model.bindings.available ? r.model.bindings.freecam_controller_combo : 0;
freecam_combo = r.model.bindings.available ? r.model.bindings.freecam_combo : 0;
tp_to_freecam_combo = r.model.bindings.available ? r.model.bindings.tp_to_freecam_combo : 0;
noclip_combo = r.model.bindings.available ? r.model.bindings.noclip_combo : 0;
forward_velocity_combo = r.model.bindings.available ? r.model.bindings.forward_velocity_combo : 0;
up_velocity_combo = r.model.bindings.available ? r.model.bindings.up_velocity_combo : 0;
offboard_up_velocity_combo = r.model.bindings.available ? r.model.bindings.offboard_up_velocity_combo : 0;
if (r.model.bindings.available) action_combos = r.model.bindings.action_combos;
if ((vote_open || map_prompt) && r.model.bindings.available) {
vote_yes_combo = r.model.bindings.vote_yes_combo;
vote_no_combo = r.model.bindings.vote_no_combo;
}
if (r.model.bindings.available)
dingosdk::map_download::set_binds(r.model.bindings.vote_yes_combo, r.model.bindings.vote_no_combo);
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
// Capture belongs to the freecam state itself. Keep it active across brief
// request/loading phases instead of opening a path back to player input.
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && (freecam_controller || dingosdk::drone::controller_capture()));
dingosdk::ControllerInput controller;
if (freecam_controller_combo || freecam_combo || tp_to_freecam_combo || noclip_combo || forward_velocity_combo || up_velocity_combo || offboard_up_velocity_combo ||
vote_yes_combo || vote_no_combo || poll_answers || map_prompt || map_vote || message_up ||
std::ranges::any_of(action_combos, [](auto combo) { return combo != 0; }))
// (The card that asks has the pointer, as a menu does: it is still read for.)
DingoSDKOverlayReadControllerInput(&controller, map_prompt || map_vote || message_up);
for (std::size_t i = 0; i < action_combos.size(); ++i)
if (r.action_bind_latches[i].update(action_combos[i], controller, r.observer_failed)) {
std::array<char, 256> result{};
const std::string command(dingosdk::action_binds[i].command);
if (queue_console_command(nullptr, command.c_str(), result.data(), result.size()))
record(("{\"event\":\"controller_binding_triggered\",\"action\":\"" + std::string(dingosdk::action_binds[i].key) + "\"}").c_str());
}
// (The input reads as nothing while the menu, the console or the chat box is open, so typing
// a bound key answers no vote.)
if (r.vote_yes_bind_latch.update(vote_yes_combo, controller, !vote_open && !map_prompt)) {
if (map_prompt) dingosdk::map_download::answer(true);
else if (dingosdk::multiplayer::queue_command("vote", "yes", ""))
record("{\"event\":\"controller_binding_triggered\",\"action\":\"vote_yes\"}");
}
if (r.vote_no_bind_latch.update(vote_no_combo, controller, !vote_open && !map_prompt)) {
if (map_prompt) dingosdk::map_download::answer(false);
else if (dingosdk::multiplayer::queue_command("vote", "no", ""))
record("{\"event\":\"controller_binding_triggered\",\"action\":\"vote_no\"}");
}
{
// A controller on the card: D-pad left and right pick, A takes the pick, B is no. Only a
// button pressed since the card came up counts, so the one that was held does not answer.
static bool prompt_before{};
static std::uint32_t buttons_before{};
// The keyboard's arrows, Enter and Esc do the same, above the pad's bits.
const bool prompt = map_prompt || map_vote || message_up;
const std::uint32_t buttons = !prompt ? 0
: (controller.available ? controller.buttons : 0) | (DingoSDKOverlayReadPromptKeys() << 20);
// The message is over the others and is answered first: A or B, Enter or Esc, is its OK.
static bool message_before{};
if (message_up && message_before) {
if ((buttons & ~buttons_before) & (0x1000 | 0x2000 | 1u << 22 | 1u << 23)) dingosdk::overlay::message_dismiss();
} else if (map_prompt && prompt_before) {
const auto pressed = buttons & ~buttons_before;
if (pressed & (0x0004 | 1u << 20)) dingosdk::map_download::pick(true);
if (pressed & (0x0008 | 1u << 21)) dingosdk::map_download::pick(false);
if (pressed & (0x1000 | 1u << 22)) dingosdk::map_download::answer(dingosdk::map_download::picked());
else if (pressed & (0x2000 | 1u << 23)) dingosdk::map_download::answer(false);
}
// The vote's popup: left and right move the pick, A votes for it, B closes it.
static bool vote_before{};
if (!message_up && map_vote && vote_before) {
const auto pressed = buttons & ~buttons_before;
if (pressed & (0x0004 | 1u << 20)) dingosdk::overlay::map_vote_move(-1);
if (pressed & (0x0008 | 1u << 21)) dingosdk::overlay::map_vote_move(1);
if (pressed & (0x1000 | 1u << 22)) dingosdk::overlay::map_vote_take();
else if (pressed & (0x2000 | 1u << 23)) dingosdk::overlay::map_vote_dismiss();
}
vote_before = map_vote;
message_before = message_up;
prompt_before = map_prompt;
buttons_before = buttons;
}
// While it downloads, the No bind stops it.
if (!map_prompt && !vote_open && dingosdk::map_download::view().stage == dingosdk::map_download::Stage::downloading) {
std::uint32_t stop_combo{};
{
std::lock_guard lock(r.mutex);
if (r.model.bindings.available) stop_combo = r.model.bindings.vote_no_combo;
}
if (stop_combo) {
dingosdk::ControllerInput input;
DingoSDKOverlayReadControllerInput(&input);
static dingosdk::ControllerComboLatch stop_latch;
if (stop_latch.update(stop_combo, input, false)) dingosdk::map_download::answer(false);
}
}
// A poll's answers are on the number keys, 1 for the first: only while one is running, so
// the keys are the game's own the rest of the time.
for (unsigned answer = 0; answer < r.poll_answer_latches.size(); ++answer) {
const std::uint32_t key = answer < poll_answers ? dingosdk::keyboard_binding_tag | ('1' + answer) : 0U;
const char number[2]{static_cast<char>('1' + answer), 0};
if (r.poll_answer_latches[answer].update(key, controller, !poll_answers) && dingosdk::multiplayer::queue_command("vote", number, ""))
record("{\"event\":\"controller_binding_triggered\",\"action\":\"poll_answer\"}");
}
if (r.freecam_controller_bind_latch.update(freecam_controller_combo, controller, r.observer_failed || controller_busy || !r.debug_model.free_camera)) {
if (dingosdk::set_local_freecam_controller(!freecam_controller))
freecam_controller = !freecam_controller;
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && (freecam_controller || dingosdk::drone::controller_capture()));
record("{\"event\":\"controller_binding_triggered\",\"action\":\"freecam_controller\"}");
}
if (r.tp_to_freecam_bind_latch.update(tp_to_freecam_combo, controller, r.observer_failed || controller_busy || !r.debug_model.free_camera || !r.debug_model.camera_position_valid || debug_request.has_value())) {
if (dingosdk::teleport_local_skater(r.debug_model.camera_position)) {
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::set_free_camera, false};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"tp_to_freecam\"}");
}
const bool freecam_bind_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(state != 13 && state != 21) || !r.debug_model.camera_available ||
(freecam_combo && !camera_phase_ready());
if (r.freecam_bind_latch.update(freecam_combo, controller, freecam_bind_blocked)) {
{
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::set_free_camera, !r.debug_model.free_camera};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"freecam\"}");
}
const bool bind_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(state != 13 && state != 21) || !r.debug_model.noclip_available ||
(noclip_combo && !camera_phase_ready());
const bool noclip_triggered = r.noclip_bind_latch.update(noclip_combo, controller, bind_blocked);
if (noclip_triggered) {
{
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::set_noclip, !r.debug_model.noclip};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"noclip\"}");
}
const bool forward_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.offboard_up_velocity_available && forward_velocity_combo == offboard_up_velocity_combo) ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.forward_velocity_available ||
(forward_velocity_combo && forward_velocity_combo == noclip_combo);
if (r.forward_velocity_bind_latch.update(forward_velocity_combo, controller, forward_velocity_blocked)) {
{
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::add_forward_velocity};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId()))
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
}
record("{\"event\":\"controller_binding_triggered\",\"action\":\"forward_velocity\"}");
}
const bool up_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.offboard_up_velocity_available && up_velocity_combo == offboard_up_velocity_combo) ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.up_velocity_available ||
(up_velocity_combo && (up_velocity_combo == noclip_combo || up_velocity_combo == forward_velocity_combo));
if (r.up_velocity_bind_latch.update(up_velocity_combo, controller, up_velocity_blocked)) {
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::add_up_velocity};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId())) {
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
record("{\"event\":\"controller_binding_triggered\",\"action\":\"up_velocity\"}");
}
}
const bool offboard_up_velocity_blocked = r.observer_failed || controller_busy || debug_request.has_value() ||
(r.debug_model.free_camera && freecam_controller) ||
(state != 13 && state != 21) || !r.debug_model.offboard_up_velocity_available ||
(offboard_up_velocity_combo && offboard_up_velocity_combo == noclip_combo);
if (r.offboard_up_velocity_bind_latch.update(offboard_up_velocity_combo, controller, offboard_up_velocity_blocked)) {
std::lock_guard lock(r.mutex);
const dingosdk::overlay::DebugRequest request{dingosdk::overlay::DebugAction::add_offboard_up_velocity};
if (r.requests.enqueue(request, request_context(r), GetCurrentThreadId())) {
debug_request = r.requests.take<dingosdk::overlay::DebugRequest>(GetCurrentThreadId());
record("{\"event\":\"controller_binding_triggered\",\"action\":\"offboard_up_velocity\"}");
}
}
if (debug_request || r.debug_model.free_camera || r.debug_model.first_person || r.debug_model.drone_follow || r.debug_model.noclip ||
now >= r.next_debug) {
ScheduledWorkScope work{r, debug_request.has_value()};
r.next_debug = now + 100;
const bool debug_ready = !r.observer_failed && !debug_busy && (state == 13 || state == 21) && native_context_ready();
dingosdk::overlay::FlightInput flight_input;
DingoSDKOverlayReadFlightInput(&flight_input, (r.debug_model.free_camera || r.debug_model.noclip) && debug_ready,
r.debug_model.noclip || (r.debug_model.free_camera && freecam_controller));
const auto phase_issue = camera_phase_issue();
auto debug = dingosdk::on_client_debug_tick(r.base, client, debug_ready,
phase_issue == nullptr,
debug_request ? &*debug_request : nullptr, &flight_input);
// Input ownership must follow the live camera even with every overlay closed.
DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && (freecam_controller || dingosdk::drone::controller_capture()));
// A debug action, or a mode the tick ended by itself (host policy, lost
// camera), can have replaced the native camera: validate it again.
if (debug_request || debug.free_camera != r.debug_model.free_camera ||
debug.first_person != r.debug_model.first_person || debug.noclip != r.debug_model.noclip ||
debug.park_editor != r.debug_model.park_editor)
frame.camera_issue.reset();
if (debug_ready && phase_issue && !debug.camera_available) {
debug.camera_unavailable = phase_issue;
debug.noclip_unavailable = phase_issue;
}
// Ignore brief loading/ownership transitions. Report a persistent reason
// once, then report recovery; the menu always shows the current reason.
const std::string issue = debug_ready && debug.skater_position_valid && !debug.noclip_available
? debug.noclip_unavailable : std::string{};
if (issue != r.flight_unavailable_issue) {
r.flight_unavailable_issue = issue;
r.flight_unavailable_since = now;
}
if (!issue.empty() && now - r.flight_unavailable_since >= 2000 && issue != r.flight_unavailable_logged) {
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::skater,
"{} unavailable: {}", debug.camera_available ? "Noclip" : "Freecam and Noclip", issue);
r.flight_unavailable_logged = issue;
} else if (debug.noclip_available && !r.flight_unavailable_logged.empty()) {
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::skater,
"Freecam and Noclip are available again.");
r.flight_unavailable_logged.clear();
}
if (debug.noclip_velocity_updates && !r.debug_model.noclip_velocity_updates)
record("{\"event\":\"noclip_velocity_applied\",\"count\":" + std::to_string(debug.noclip_velocity_updates) + "}");
if (debug.noclip_motion_updates && !r.debug_model.noclip_motion_updates)
record("{\"event\":\"noclip_offboard_motion_applied\",\"count\":" + std::to_string(debug.noclip_motion_updates) + "}");
if (debug.forward_velocity_updates != r.debug_model.forward_velocity_updates)
record("{\"event\":\"forward_velocity_applied\",\"count\":" + std::to_string(debug.forward_velocity_updates) + "}");
if (debug.up_velocity_updates != r.debug_model.up_velocity_updates)
record("{\"event\":\"up_velocity_applied\",\"count\":" + std::to_string(debug.up_velocity_updates) + "}");
if (debug.offboard_up_velocity_updates != r.debug_model.offboard_up_velocity_updates)
record("{\"event\":\"offboard_up_velocity_applied\",\"count\":" + std::to_string(debug.offboard_up_velocity_updates) + "}");
if (r.debug_model.noclip && !debug.noclip && !debug_request) {
std::ostringstream stopped;
stopped << "{\"event\":\"noclip_stopped\",\"reason\":" << std::quoted(debug.status) << '}';
record(stopped.str());
}
if (debug_request) record("{\"event\":\"interactive_debug_result\",\"action\":" +
std::to_string(static_cast<int>(debug_request->action)) + ",\"free_camera\":" +
(debug.free_camera ? "true" : "false") + ",\"noclip\":" + (debug.noclip ? "true" : "false") + ",\"game_ui_hidden\":" +
(debug.game_ui_hidden ? "true" : "false") + ",\"no_bail\":" + (debug.no_bail ? "true" : "false") +
",\"no_bail_active\":" + (debug.no_bail_active ? "true" : "false") + ",\"speed\":" + std::to_string(debug.camera_speed) +
",\"velocity_updates\":" + std::to_string(debug.noclip_velocity_updates) + ",\"motion_updates\":" + std::to_string(debug.noclip_motion_updates) + "}");
if (debug_request) {
std::ostringstream message;
message << "Noclip " << (debug.noclip ? "on" : "off") << " | free camera " << (debug.free_camera ? "on" : "off")
<< " | No Bail " << (debug.no_bail_active ? "active" : debug.no_bail ? "waiting" : "off")
<< (debug.no_bail_active && !debug.no_bail ? " (noclip)" : "")
<< " | speed " << debug.camera_speed << " | game UI "
<< (debug.game_ui_hidden ? "hidden" : "visible");
if (!debug.status.empty()) message << " | " << debug.status;
activity_line(dingosdk::ConsoleSource::runtime, message.str());
}
std::lock_guard lock(r.mutex);
r.debug_model = std::move(debug);
++r.debug_revision;
}
std::optional<dingosdk::overlay::OfflineFeatureRequest> offline_request;
{
std::lock_guard lock(r.mutex);
offline_request = r.requests.take<dingosdk::overlay::OfflineFeatureRequest>(GetCurrentThreadId());
}
if (offline_request || now >= r.next_offline) {
ScheduledWorkScope work{r, offline_request.has_value()};
DINGO_PROFILE_ZONE("tick/update_model/features (500 ms)");
r.next_offline = now + 500;
auto offline = dingosdk::update_gameplay_settings_override(
offline_request ? &*offline_request : nullptr);
if (offline_request) {
using dingosdk::overlay::OfflineFeatureGroup;
if (offline_request->group == OfflineFeatureGroup::board_wear)
dingosdk::profile_runtime::set_local_preference("BoardWear", offline_request->enabled);
else if (offline_request->group == OfflineFeatureGroup::restore_all)
dingosdk::profile_runtime::set_local_preference("BoardWear", false);
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime,
"Offline feature %d -> %d applied: %s (board wear available %d, effective %d).",
static_cast<int>(offline_request->group), offline_request->enabled, offline.model.status.c_str(),
offline.model.board_wear.available, offline.model.board_wear.effective);
}
auto slot_action = dingosdk::SkaterSlotOverrideAction::tick;
if (offline_request &&
offline_request->group == dingosdk::overlay::OfflineFeatureGroup::restore_all)
slot_action = dingosdk::SkaterSlotOverrideAction::restore;
const auto profile_access = dingosdk::local_profile_access();
{
DINGO_PROFILE_ZONE("tick/update_model/features (500 ms)/local profile");
dingosdk::update_local_customization();
}
// Saved access owns these prerequisites. This runs on the recorded
// game-update thread and reacquires/validates each native settings object.
const auto enable_saved_feature = [&](const char* name, bool requested, const char* reapplied_log = nullptr) {
if (!requested) return;
const auto variable = std::find_if(offline.model.variables.begin(), offline.model.variables.end(),
[&](const auto& field) { return field.name == name; });
if (variable != offline.model.variables.end() && variable->available && !variable->value) {
offline = dingosdk::update_gameplay_engine_variable({name, dingosdk::EngineVariableAction::set, true});
if (reapplied_log)
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime,
"%s", reapplied_log);
}
};
enable_saved_feature("FastTravelPointsEnabled", profile_access.bus_stops);
enable_saved_feature("EnableNeighborhoodRank", profile_access.neighborhoods || profile_access.neighborhood_ranks);
enable_saved_feature("EnableCASArtistSandbox", profile_access.cosmetics);
enable_saved_feature("EnableMyStuffMenu", profile_access.cosmetics);
enable_saved_feature("BoardWearEnabled",
dingosdk::profile_runtime::local_preference("BoardWear").value_or(false),
"Re-applied saved board wear preference (BoardWearEnabled).");
if (profile_access.preset_slots) slot_action = dingosdk::SkaterSlotOverrideAction::enable;
const auto slots = dingosdk::update_skater_slot_override(slot_action, dingosdk::local_customization_selected_preset(),
dingosdk::local_customization_outfits_loadable());
if (slots.manager_available && slots.ui_ready)
dingosdk::observe_local_customization_selection(slots.selected_slot);
const bool missions_requested = offline.model.activities.available &&
offline.model.activities.effective &&
offline.model.activities.override_active;
if (missions_requested)
dingosdk::arm_main_mission_override(r.base, true);
else
dingosdk::restore_main_mission_override();
const auto missions = dingosdk::main_mission_override_observation();
const bool fixed_stop_requested = r.fixed_stop_entitlement_route_ready &&
(profile_access.bus_stops || (offline.model.fast_travel.available &&
offline.model.fast_travel.effective &&
offline.model.fast_travel.override_active));
dingosdk::set_fixed_stop_entitlement_provider_enabled(fixed_stop_requested);
const bool neighborhood_requested = profile_access.neighborhoods || (offline.model.progression.available &&
offline.model.progression.effective &&
offline.model.progression.override_active);
(void)dingosdk::set_neighborhood_unlock_override_enabled(neighborhood_requested);
const auto fixed_stops = dingosdk::fixed_stop_entitlement_provider_observation();
const auto neighborhoods = dingosdk::neighborhood_unlock_override_observation();
merge_skater_slot_observation(offline.model, slots);
merge_main_mission_observation(offline.model, missions);
merge_progression_provider_observations(offline.model, fixed_stops, neighborhoods,
r.fixed_stop_entitlement_route_ready);
if (offline.json != r.last_gameplay_settings_override_observation) {
record(offline.json);
r.last_gameplay_settings_override_observation = offline.json;
}
if (slots.json != r.last_skater_slot_override_observation) {
record(slots.json);
r.last_skater_slot_override_observation = slots.json;
}
if (missions.json != r.last_main_mission_override_observation) {
record(missions.json);
r.last_main_mission_override_observation = missions.json;
}
if (fixed_stops.json != r.last_fixed_stop_entitlement_provider_observation) {
record(fixed_stops.json);
r.last_fixed_stop_entitlement_provider_observation = fixed_stops.json;
}
if (neighborhoods.json != r.last_neighborhood_unlock_override_observation) {
record(neighborhoods.json);
r.last_neighborhood_unlock_override_observation = neighborhoods.json;
}
std::lock_guard lock(r.mutex);
r.offline_model = std::move(offline.model);
++r.offline_revision;
// Skater slots, teleports and customization ran: check the camera again.
frame.camera_issue.reset();
}
if (r.debug_model.park_editor && !r.observer_failed && !debug_busy && (state == 13 || state == 21) &&
native_context_ready()) {
{
DINGO_PROFILE_ZONE("tick/update_model/park editor");
dingosdk::tick_local_park_editor();
}
frame.camera_issue.reset();
auto editor = dingosdk::local_park_editor();
std::lock_guard lock(r.mutex);
r.model.editor = std::move(editor);
++r.model_revision;
} else {
// Other players' objects still to be created: queue the next as soon as the
// last one is in, not at the 500 ms customization pace.
DINGO_PROFILE_ZONE("tick/update_model/network objects");
dingosdk::tick_network_objects();
}
if (r.observer_failed) return; // Keep the bounded restore/telemetry path available after catalog failure.
dingosdk::hall_of_meat::on_client_tick();
dingosdk::road_rash::on_client_tick(client);
dingosdk::drone::outfit::on_client_tick();
dingosdk::board_break::on_client_tick(client);
dingosdk::grind_exit_control::on_client_tick(client);
if (!has_request && now < r.next_model && state == r.previous_state) return;
r.next_model = now + 500;
DINGO_PROFILE_ZONE("tick/update_model/world model (500 ms)");
auto description = dingosdk::read_world_description(GetCurrentProcess(), client + 0x198);
r.multiplayer_map = description.level.empty() ? std::string{} : description.level + "|" + description.lm_level;
const auto& current_level = description.lm_level.empty() ? description.level : description.lm_level;
dingosdk::live_mods::set_current_level(current_level);
dingosdk::hall_of_meat::set_level(current_level);
dingosdk::replay_drafts::on_client_tick(r.base, state);
// A live mod apply hands over the destinations its mods declare. A map
// being played that is no longer among them (its mod disabled or deleted)
// cannot stay loaded: the player goes to San Van.
bool left_map{}, manifests_applied{};
if (auto manifests = dingosdk::live_mods::take_level_manifests()) {
manifests_applied = true;
auto next = read_custom_levels(*manifests);
const auto listed = [&](const dingosdk::CustomLevelManifest& set) {
return std::ranges::any_of(set.levels, [&](const auto& level) { return equals(level.asset, description.lm_level); });
};
left_map = !description.lm_level.empty() && listed(r.custom_levels) && !listed(next);
r.custom_levels = std::move(next);
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level,
"Custom-level destinations after the live mod apply: {}.", r.custom_levels.levels.size());
}
// Handed out with the manifests: once the merge is in, load the level being
// played again ("Apply and reload level"), or San Van when that map is gone.
const bool reload = dingosdk::live_mods::take_reload();
if (left_map || reload) {
constexpr std::string_view san_van = "levels/game/BAM_LevelRoot/BAM_LevelRoot";
const std::string target = left_map ? std::string(san_van) : current_level;
std::lock_guard lock(r.mutex);
if (target.empty() || !r.requests.enqueue(ConsoleRequest{"load " + target}, request_context(r), GetCurrentThreadId()))
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::level,
"Mods applied, but the level could not be {}; load a level to use them.", left_map ? "left" : "reloaded");
else if (left_map)
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level,
"{} was removed by the mod apply; loading San Van.", description.lm_level);
else
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level,
"Reloading {} to use the applied mods.", target);
}
// The native level registry changes only with a mod apply or a level load,
// and the server's sublevels settle during loads: read both again then, and
// whenever this client is not in play; otherwise every 10 s and 2 s.
bool requests_loading{};
{ std::lock_guard lock(r.mutex); requests_loading = r.requests.loading(); }
const bool world_changed = manifests_applied || requests_loading || (state != 13 && state != 21) ||
state != r.previous_state || description.level != r.last_level || description.lm_level != r.catalog_level;
r.catalog_level = description.lm_level;
if (!r.native_catalog || !r.native_catalog->available || world_changed || now >= r.next_catalog_read) {
r.native_catalog = dingosdk::read_world_catalog(GetCurrentProcess(), r.base);
r.next_catalog_read = now + 10000;
}
const bool sublevels_read = !r.sublevel_catalog || !r.sublevel_catalog->available || world_changed ||
now >= r.next_sublevel_read;
if (sublevels_read) {
r.sublevel_catalog = dingosdk::read_server_sublevels(GetCurrentProcess(), r.base);
r.next_sublevel_read = now + 2000;
}
auto catalog = *r.native_catalog;
const auto native_level_count = catalog.levels.size();
catalog = dingosdk::merge_custom_levels(std::move(catalog), r.custom_levels);
const auto& sublevels = *r.sublevel_catalog;
const auto client_content = read_client_content(r.base, client, description.lm_level);
const bool context_ready = native_context_ready();
const bool manifest_custom_active = (state == 13 || state == 21) &&
context_ready && description.available &&
std::any_of(catalog.levels.begin(), catalog.levels.end(), [&](const auto& level) {
return level.manifest_only && equals(level.asset, description.lm_level);
});
// Schema 1 Studio maps are authored exclusively from BAM's bounded
// service/environment closure. This hint permits that reduced controller
// graph to identify its family without weakening native-map detection.
dingosdk::set_local_world_layer_map_hint(
manifest_custom_active ? dingosdk::WorldMap::bam : dingosdk::WorldMap::none);
dingosdk::ConsoleActivity activity;
activity.state = state;
activity.native_loading_logging = r.native_loading_logging;
activity.state_name = state_name(state);
activity.level = description.level;
activity.sublevel = description.lm_level;
for (const auto& level : sublevels.levels)
activity.sublevel_active |= sublevels.available && sublevels.active && level.state == 5 &&
equals(level.asset, description.lm_level);
activity.client_sublevel_present = client_content.sublevels_available &&
client_content.requested_sublevel_present;
activity.players_available = client_content.players_available;
activity.players = client_content.local_player_count;
activity.controllables = client_content.controllable_reference_count;
{
std::lock_guard lock(r.mutex);
if (r.debug_model.skater_position_valid) {
activity.skater = r.debug_model.skater_identity;
activity.position = r.debug_model.skater_position;
}
}
r.console_activity.update(activity, &activity_line);
const auto content_json = "{\"event\":\"client_content\",\"sublevels_available\":" +
std::string(client_content.sublevels_available ? "true" : "false") +
",\"sublevel_count\":" + std::to_string(client_content.sublevel_count) +
",\"requested_sublevel_present\":" + (client_content.requested_sublevel_present ? "true" : "false") +
",\"player_manager_present\":" + (client_content.player_manager_present ? "true" : "false") +
",\"players_available\":" + (client_content.players_available ? "true" : "false") +
",\"local_player_count\":" + std::to_string(client_content.local_player_count) +
",\"controllable_reference_count\":" + std::to_string(client_content.controllable_reference_count) + "}";
if (content_json != r.last_client_content) { record(content_json); r.last_client_content = content_json; }
if (sublevels_read) {
const auto sublevel_json = dingosdk::sublevel_catalog_json(sublevels);
if (sublevel_json != r.last_sublevels) {
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Context::server,
dingosdk::logging::Channel::level, "Server sublevel catalog: {} entries; available={}, root active={}.",
sublevels.levels.size(), sublevels.available, sublevels.active);
dingosdk::logging::event(dingosdk::logging::Context::server, dingosdk::logging::Channel::level,
"{\"event\":\"server_sublevels\",\"catalog\":" + sublevel_json + "}");
r.last_sublevels = sublevel_json;
}
}
if (state != r.previous_state || description.level != r.last_level) {
record("{\"event\":\"engine_state\",\"state\":" + std::to_string(state) +
",\"description\":" + dingosdk::world_description_json(description) + "}");
r.previous_state = state;
r.last_level = description.level;
}
if (!r.catalog_logged && catalog.available && native_level_count && !catalog.levels.empty()) {
record("{\"event\":\"level_catalog\",\"catalog\":" + dingosdk::world_catalog_json(catalog) + "}");
r.catalog_logged = true;
const auto manifest_count = static_cast<std::size_t>(std::count_if(
catalog.levels.begin(), catalog.levels.end(), [](const auto& level) { return level.manifest_only; }));
activity_line(dingosdk::ConsoleSource::level,
"Discovered " + std::to_string(catalog.levels.size()) + " levels (" +
std::to_string(native_level_count) + " native, " + std::to_string(manifest_count) +
" manifest). Ready for manual selection.");
}
dingosdk::overlay::Model model;
const auto missions_model = dingosdk::local_profile_missions();
model.progression = dingosdk::local_profile_progression();
model.player_card = dingosdk::local_profile_player_card();
model.hall_of_meat = {dingosdk::hall_of_meat::available(), dingosdk::hall_of_meat::enabled()};
model.road_rash = {dingosdk::road_rash::available(), dingosdk::road_rash::enabled(), dingosdk::road_rash::blood()};
const auto breakable_deck = dingosdk::board_break::deck();
model.board_break = {dingosdk::board_break::available(), dingosdk::board_break::enabled(), !breakable_deck.empty(),
dingosdk::board_break::deck_label(breakable_deck)};
{
const auto feel = dingosdk::board_break::settings();
auto& bb = model.board_break;
bb.always = feel.always;
bb.rails = feel.rails;
bb.wear = feel.wear;
bb.crack_sounds = feel.crack_sounds;
bb.body_sounds = feel.body_sounds;
bb.sensitivity = feel.sensitivity;
bb.joined = feel.joined;
bb.wear_rate = feel.wear_rate;
bb.worn = dingosdk::board_break::wear();
bb.condition = std::string(dingosdk::board_break::condition_name(bb.worn));
}
model.grind_exit = {dingosdk::grind_exit_control::available(), dingosdk::grind_exit_control::enabled(),
dingosdk::grind_exit_control::toe_heel(), dingosdk::grind_exit_control::stance(),
dingosdk::grind_exit_control::strength()};
model.bindings = dingosdk::local_profile_controller_bindings();
model.parks = dingosdk::local_profile_parks();
model.world = dingosdk::local_profile_world_layers();
model.missions_available = missions_model.available;
model.mission_feedback = missions_model.feedback;
for (const auto& row : missions_model.rows) model.missions.push_back({row.id, row.group, row.completed});
model.state = state_name(state);
model.detail = description.level.empty() ? "Waiting for a level description" : description.level;
if (!description.lm_level.empty()) {
bool active = false;
for (const auto& level : sublevels.levels)
active = active || (sublevels.available && sublevels.active && level.state == 5 && equals(level.asset, description.lm_level));
model.detail += "\nSublevel: " + description.lm_level + (active ? " (active on local server)" : " (not yet confirmed active)");
model.detail += "\nClient sublevel: " + std::string(!client_content.sublevels_available ? "unavailable" :
(client_content.requested_sublevel_present ? "present" : "not present"));
}
model.detail += client_content.players_available ? "\nLocal players: " + std::to_string(client_content.local_player_count) +
" | Controllable references: " + std::to_string(client_content.controllable_reference_count) : "\nLocal players: unavailable";
for (const auto& level : catalog.levels) {
dingosdk::overlay::Level choice;
choice.asset = level.asset;
choice.display_name = level.display_name;
choice.manifest_start_point = level.manifest_start_point;
choice.native_registered = !level.manifest_only;
choice.custom = level.custom;
for (const auto& point : level.start_points) choice.start_points.push_back(point.name);
choice.can_load = catalog.available && context_ready;
choice.load_block_reason = "Waiting for this process's native local world context.";
model.levels.push_back(std::move(choice));
}
const bool local = (state == 13 || state == 21) && context_ready && catalog.available && !catalog.levels.empty();
model.can_queue_load = local;
model.load_block_reason = local ? "" : "Waiting for an active local level and initialized level manager.";
if (!catalog.available) model.load_block_reason = catalog.issue;
const auto readiness = "{\"event\":\"load_readiness\",\"can_load\":" + std::string(local ? "true" : "false") +
",\"state\":" + std::to_string(state) +
",\"context_ready\":" + (context_ready ? "true" : "false") +
",\"game_type\":" + std::to_string(game_type) +
",\"catalog_available\":" + (catalog.available ? "true" : "false") +
",\"level_count\":" + std::to_string(catalog.levels.size()) + "}";
if (readiness != r.last_readiness) {
record(readiness);
r.last_readiness = readiness;
}
bool load_busy{};
{ std::lock_guard lock(r.mutex); load_busy = r.requests.loading(); }
dingosdk::update_local_world_controls(!r.observer_failed && !load_busy && local && context_ready &&
(state == 13 || state == 21) && model.world.ready ? model.world.map : dingosdk::WorldMap::none);
model.world_controls = dingosdk::local_profile_world_controls();
dingosdk::update_local_graphics_controls();
model.graphics = dingosdk::local_profile_graphics_controls();
model.object_persistence = dingosdk::local_profile_object_persistence();
// The park list is a disk walk of the Mods folder: only while the menu or the editor shows it.
dingosdk::set_park_mod_list_visible(r.debug_model.park_editor ||
GetTickCount64() < r.named_settings_wanted_until.load(std::memory_order_relaxed));
model.editor = dingosdk::local_park_editor();
// Spectating only needs one controllable local player; it works on every
// map, including custom mod levels.
r.spectate_ready = local && !load_busy && client_content.players_available &&
client_content.local_player_count == 1 && client_content.controllable_reference_count == 1;
r.spectate_client = client;
std::optional<Request> request;
{
std::lock_guard lock(r.mutex);
if (r.requests.inflight()) {
r.transition_seen = r.transition_seen || state != r.requested_from;
const bool manifest_sublevel = !r.last_lm_level.empty() &&
std::any_of(catalog.levels.begin(), catalog.levels.end(), [&](const auto& level) {
return level.manifest_only && equals(level.asset, r.last_lm_level);
});
bool sublevel_active = r.last_lm_level.empty();
for (const auto& level : sublevels.levels)
sublevel_active = sublevel_active || (sublevels.available && sublevels.active && level.state == 5 &&
equals(level.asset, r.last_lm_level));
const bool client_sublevel_present = r.last_lm_level.empty() ||
(client_content.sublevels_available && client_content.requested_sublevel_present);
// Native destinations are corroborated by both stock sublevel
// registries. A Studio manifest destination may not be published by
// one or both supplemental registries, so the engine's settled state
// plus its exact active WorldDescription is authoritative.
const bool manifest_description_active = manifest_sublevel && description.available &&
equals(description.level, r.last_request) &&
equals(description.lm_level, r.last_lm_level);
const bool content_confirmed = manifest_sublevel ? manifest_description_active :
(sublevel_active && client_sublevel_present);
const bool settled = r.transition_seen && (state == 13 || state == 21) && content_confirmed;
const bool timed_out = now - r.request_started >= 90000 && (state == 13 || state == 21) && local;
const bool cancelled = state == 2 || state == 24 || state == 25;
if (settled || timed_out || cancelled) {
const bool destination_matched = settled && equals(description.level, r.last_request) &&
(r.last_lm_level.empty() || equals(description.lm_level, r.last_lm_level));
const bool start_points_matched =
(r.last_start.empty() || equals(description.start_point, r.last_start)) &&
(r.last_lm_start.empty() || equals(description.lm_start_point, r.last_lm_start));
// A start point is a placement request, not part of a detached
// custom level's loaded identity. Preserve the comparison in
// diagnostics, but do not report a loaded custom world as failed
// if the engine normalizes or substitutes its default spawn.
const bool matched = destination_matched &&
(manifest_sublevel || start_points_matched);
r.load_result = matched ? (manifest_sublevel ?
"Requested custom destination is active; player control is not yet verified." :
"Requested root loaded and sublevel content is present; player control is not yet verified.") :
"The requested level/start point did not finish loading as expected.";
record("{\"event\":\"native_level_result\",\"matched\":" + std::string(matched ? "true" : "false") +
",\"timed_out\":" + (timed_out ? "true" : "false") +
",\"manifest_sublevel\":" + (manifest_sublevel ? "true" : "false") +
",\"manifest_description_active\":" + (manifest_description_active ? "true" : "false") +
",\"start_points_matched\":" + (start_points_matched ? "true" : "false") +
",\"server_sublevel_active\":" + (sublevel_active ? "true" : "false") +
",\"client_sublevel_present\":" + (client_sublevel_present ? "true" : "false") +
",\"description\":" + dingosdk::world_description_json(description) + "}");
activity_line(dingosdk::ConsoleSource::level,
matched ? "Level content loaded: " + r.last_request +
(r.last_lm_level.empty() ? "" : " + " + r.last_lm_level)
: (timed_out ? "Level load timed out: " : "Level load did not complete: ") + r.last_request,
matched ? dingosdk::ConsoleSeverity::success : dingosdk::ConsoleSeverity::warning);
r.requests.finish_load();
dingosdk::loading_screen::cancel();
}
}
if (!r.load_result.empty()) model.detail += "\n" + r.load_result;
if (r.requests.inflight()) {
model.can_queue_load = false;
model.load_block_reason = "Waiting for the requested level to finish loading.";
}
request = r.requests.take_load();
r.menu_splash_ready = local && description.available &&
equals(description.level, "levels/Game/DingoLevel_Splash/DingoLevel_Splash");
if (!r.menu_splash_ready || (!r.requests.loading() && !request)) r.menu_load_queued = false;
r.model = std::move(model);
++r.model_revision;
}
// Start San Van as soon as the splash world's local loader is ready. Use
// the normal request scheduler, preserving user/join requests and all
// transition cleanup. This is a once-per-process startup action.
if (!r.startup_load_queued) {
if (request || (local && description.available && !r.menu_splash_ready))
r.startup_load_queued = true;
else if (queue_menu_bam()) r.startup_load_queued = true;
}
if (!request) return;
const dingosdk::LevelInfo* selected_level = nullptr;
for (const auto& level : catalog.levels)
if (!level.manifest_only && equals(level.asset, request->asset)) selected_level = &level;
if (!local || !registered_request(catalog, *request) || !selected_level) {
activity_line(dingosdk::ConsoleSource::level,
"Load rejected: local context or selected level changed.", dingosdk::ConsoleSeverity::warning);
record("{\"event\":\"native_level_rejected\",\"reason\":\"context_or_selection_changed\"}");
std::lock_guard lock(r.mutex);
r.requests.finish_load();
r.model.load_block_reason = "Load rejected: the local context or registered selection changed.";
++r.model_revision;
return;
}
// Validated afresh for the restore, which may itself change the camera.
frame.camera_issue.reset();
const bool restored = dingosdk::restore_client_debug(r.base, client, camera_phase_ready());
DingoSDKOverlaySetFreecamInputCapture(false);
frame.camera_issue.reset();
if (!restored) {
std::lock_guard lock(r.mutex);
r.requests.finish_load();
r.load_result = "Load cancelled: restore the current debug camera/settings first, then retry.";
activity_line(dingosdk::ConsoleSource::level, r.load_result, dingosdk::ConsoleSeverity::warning);
return;
}
if (!dingosdk::multiplayer::prepare_native_menu_level_load(r.base)) {
std::lock_guard lock(r.mutex);
r.requests.finish_load();
r.load_result = "Load cancelled: menu cleanup could not finish. Close the pause menu and retry.";
activity_line(dingosdk::ConsoleSource::level, r.load_result, dingosdk::ConsoleSeverity::warning);
return;
}
{
std::lock_guard lock(r.mutex);
r.requests.submitted_load();
r.transition_seen = false;
r.requested_from = state;
r.request_started = now;
r.last_request = request->asset;
r.last_start = request->start;
r.last_lm_level = request->lm_level;
r.last_lm_start = request->lm_start;
r.load_result.clear();
r.model.can_queue_load = false;
r.model.load_block_reason = "Level request submitted; waiting for engine loading.";
++r.model_revision;
}
dingosdk::WorldDescription selected;
selected.available = true; selected.level = request->asset; selected.start_point = request->start;
selected.lm_level = request->lm_level; selected.lm_start_point = request->lm_start;
selected.flags[0] = selected_level->level_flag68;
const bool hosted = reinterpret_cast<HostedSelector>(r.base + rt::hosted_level_selector)(request->asset.c_str());
activity_line(dingosdk::ConsoleSource::level, "Loading " + request->asset +
(request->lm_level.empty() ? "" : " + " + request->lm_level) +
" (" + (hosted ? "local hosted server" : "single player") + ").");
record("{\"event\":\"native_level_request\",\"predicted_route\":\"" + std::string(hosted ? "HostedLocal" : "SinglePlayer") +
"\",\"selection\":" + dingosdk::world_description_json(selected) + "}");
dingosdk::multiplayer::host_map_change(request->asset + "|" + request->lm_level);
submit_local(r.base, client, *request, selected_level->level_flag68);
}
}
void tick(std::uintptr_t client, std::uintptr_t update) {
const auto incoming_error = GetLastError();
// The profiler's client update timing: the native update, and ReSkate's own work around it.
const auto tick_start = dingosdk::profiler::now_ns();
auto& r = runtime();
dingosdk::poll_level_loading(client);
dingosdk::camera_tick_enter(client);
struct CameraTickScope {
~CameraTickScope() {
const auto error = GetLastError();
dingosdk::camera_tick_leave();
SetLastError(error);
}
} camera_tick_scope;
SetLastError(incoming_error);
const auto native_start = dingosdk::profiler::now_ns();
r.original_tick(client, update);
const auto native_time = dingosdk::profiler::now_ns() - native_start;
struct RecordUpdate {
std::uint64_t start, native;
~RecordUpdate() { dingosdk::profiler::record_client_update(dingosdk::profiler::now_ns() - start - native); }
} record_update{tick_start, native_time};
struct PreserveTickError {
DWORD value{GetLastError()};
~PreserveTickError() { SetLastError(value); }
} preserve_tick_error;
dingosdk::camera_tick_native_return();
dingosdk::poll_level_loading(client);
try {
TickState tick_state;
{
DINGO_PROFILE_ZONE("tick/update_model");
update_model(client, tick_state);
}
if (r.engine_thread.load() != GetCurrentThreadId()) return;
DWORD state{},game_type{};
bool loading{};
{ std::lock_guard lock(r.mutex); loading=r.requests.loading(); }
// update_model's context check, when it made one for this same state and mode.
const auto context_ready = [&] {
return tick_state.valid && tick_state.context && tick_state.state == state && tick_state.game_type == game_type
? *tick_state.context : native_context(r.base, client, game_type);
};
const bool multiplayer_ready=!r.observer_failed && !loading &&
memory::peek(client+0xc4,state) && (state==13 || state==21) && memory::peek(client+0xc0,game_type) &&
context_ready();
{
DINGO_PROFILE_ZONE("tick/multiplayer");
dingosdk::multiplayer::tick(r.base,client,multiplayer_ready,r.multiplayer_map,load_multiplayer_map);
if (multiplayer_ready) {
dingosdk::multiplayer::tick_prop_test(r.base, client);
// Other players' drones as models in the world (DRONE page, LOBBY tab: show other players' drones).
dingosdk::multiplayer::tick_drone_models(r.base, client,
dingosdk::drone::knobs().value[dingosdk::drone::kShowBeacons].load(std::memory_order_relaxed) >= 0.5f);
}
// And heard where they fly (every frame, so their sounds stop on leaving a level).
dingosdk::multiplayer::tick_drone_sounds(r.base, client, multiplayer_ready,
dingosdk::drone::knobs().value[dingosdk::drone::kShowBeacons].load(std::memory_order_relaxed) >= 0.5f);
if (auto notice = dingosdk::multiplayer::take_leave_notice(); !notice.empty())
dingosdk::overlay::notify(dingosdk::overlay::NoticeLevel::warning, "Map not installed", std::move(notice));
// Kicked, banned or otherwise put out by the host or server: said in the middle of
// the screen, to be answered (message_card.cpp).
if (auto removed = dingosdk::multiplayer::take_removed_notice(); !removed.empty()) {
const char* title = removed.find("banned") != std::string::npos ? "YOU WERE BANNED"
: removed.find("kicked") != std::string::npos ? "YOU WERE KICKED" : "YOU WERE REMOVED";
dingosdk::overlay::show_message(title, std::move(removed));
}
// Or, when the host said which Thunderstore package its map is from, the offer to
// fetch it (map_download.h), which then applies it and joins the session again.
if (const auto need = dingosdk::multiplayer::take_map_need()) dingosdk::map_download::offer(*need);
dingosdk::map_download::tick();
dingosdk::map_icons::tick();
dingosdk::texture_handle_limit::tick();
dingosdk::report_native_render_resources();
// (Whether a menu of the game's is up, the music screen among them: a playlist changed then
// is the player's own choice, and one changed in play is a speaker's music being grabbed.)
{
const bool menu = dingosdk::multiplayer::sample_game_ui_state(r.base).in_menu;
static bool was{};
if (menu != was) {
was = menu;
try {
dingosdk::logging::log(dingosdk::logging::Level::debug, dingosdk::logging::Channel::music, "Trace: a menu of the game's is {}.", menu ? "up" : "no longer up");
} catch (...) {}
}
dingosdk::spotify_station::set_music_screen(menu);
}
dingosdk::spotify_station::tick();
dingosdk::music_inject::tick();
}
dingosdk::multiplayer::refresh_identity_lists();
// How much memory the game holds, beside what a session adds to it: said when it has
// moved by a quarter of a gigabyte or the players or skaters have changed much, at most
// every ten seconds. For telling what a full server's memory goes on (the map, every
// player, or only the skaters that are built).
if (static std::uint64_t next_memory{}; GetTickCount64() >= next_memory) {
next_memory = GetTickCount64() + 10000;
PROCESS_MEMORY_COUNTERS_EX counters{};
counters.cb = sizeof(counters);
if (GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters), sizeof(counters))) {
const auto mp = dingosdk::multiplayer::model();
const bool session = mp.active && !mp.echo && (mp.hosting || mp.connected);
const int players = session ? static_cast<int>(mp.roster.size()) : 0;
const int skaters = session ? static_cast<int>(std::ranges::count_if(mp.roster, [](const auto& player) { return player.visible; })) : 0;
static std::uint64_t said_private{};
static int said_players = -1, said_skaters = -1;
const auto moved = counters.PrivateUsage > said_private ? counters.PrivateUsage - said_private : said_private - counters.PrivateUsage;
// (And every two minutes whatever it has done: a slow leak moves it by less than
// that quarter for a long while.)
static std::uint64_t said_at{};
if (moved >= (256ull << 20) || std::abs(players - said_players) >= 5 || std::abs(skaters - said_skaters) >= 5 ||
GetTickCount64() - said_at >= 120000) {
said_at = GetTickCount64();
said_private = counters.PrivateUsage; said_players = players; said_skaters = skaters;
const auto gigabytes = [](std::uint64_t bytes) { return static_cast<double>(bytes) / (1024.0 * 1024.0 * 1024.0); };
// Handles grow with a leak of them (a leak looked for in the music player,
// 2026-10-13); counting them is cheap.
//
// The heaps' sizes and the threads were counted here as well, and that held the
// game's frame up: HeapSummary walks a whole heap with its lock held, which took
// the client thread from half a second to two seconds each time (a player's
// profile, 2026-10-15), every two minutes and at each change above. They are not
// counted any more. The GPU's share and the memory ledger (a DXGI factory, a lock)
// are written from a thread of their own, so the line costs the frame nothing.
DWORD handles{};
GetProcessHandleCount(GetCurrentProcess(), &handles);
auto line = std::format(
"Memory: {:.2f} GB committed, {:.2f} GB in RAM; {} player(s) in the session, {} built as skaters "
"(player distance {:.0f} m); map {}. {} handle(s).",
gigabytes(counters.PrivateUsage), gigabytes(counters.WorkingSetSize), players, skaters, mp.player_distance,
session && !mp.map.empty() ? mp.map : r.multiplayer_map, handles);
try {
std::thread([line = std::move(line)]() mutable {
try {
// The GPU's share (its system memory is not in the committed figure),
// and while the memory ledger is on, who holds the most (memory_ledger.h).
if (const auto gpu = dingosdk::memory_ledger::gpu_line(); !gpu.empty()) line += " " + gpu + ".";
if (const auto owners = dingosdk::memory_ledger::owners_line(); !owners.empty()) line += " " + owners + ".";
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::runtime, line);
} catch (...) {}
}).detach();
} catch (...) {}
}
}
}
// The player's Discord status (discord_presence.h): where they skate and with whom,
// looked at every couple of seconds.
if (static std::uint64_t next_presence{}; dingosdk::discord_presence::available() && GetTickCount64() >= next_presence) {
next_presence = GetTickCount64() + 2000;
const auto mp = dingosdk::multiplayer::model();
const bool session = mp.active && !mp.echo && (mp.hosting || mp.connected);
// A destination is "root|level": the level names the map. Maps ReSkate adds are "reskate <name>".
const std::string_view destination = session && !mp.map.empty() ? std::string_view(mp.map) : std::string_view(r.multiplayer_map);
auto map = dingosdk::world_level_name(destination.substr(destination.find_last_of('|') + 1));
if (map.size() > 8 && (map.starts_with("reskate ") || map.starts_with("ReSkate "))) map.erase(0, 8);
dingosdk::discord_presence::Presence presence;
presence.details = map.empty() ? std::string("Skating") : "On " + map;
if (!session) {
presence.state = "Solo skating";
} else {
// A session behind a password keeps its name to itself.
presence.state = mp.password_required ? std::string(mp.dedicated ? "Private server" : "Private lobby")
: mp.lobby_name.empty() ? std::string(mp.dedicated ? "On a server" : "In a lobby")
: (mp.dedicated ? "Server: " : "Lobby: ") + mp.lobby_name;
presence.party_size = mp.players;
presence.party_most = std::max(mp.capacity, mp.players);
presence.party = std::to_string(mp.host_id);
// Anyone may walk in: no password, and a server or a lobby that is publicly listed
// (one shared by code stays with those who were given the code).
if (!mp.password_required && (mp.dedicated || (mp.public_host && mp.lobby_listed) || mp.public_lobby))
presence.join = mp.join_code;
}
dingosdk::discord_presence::update(std::move(presence));
}
// A session the player chose to join from Discord, once the game can join one.
if (static std::string discord_join; dingosdk::discord_presence::available()) {
if (auto asked = dingosdk::discord_presence::take_join(); !asked.empty()) discord_join = std::move(asked);
if (!discord_join.empty() && multiplayer_ready && dingosdk::multiplayer::queue_command("join", discord_join, ""))
discord_join.clear();
}
dingosdk::tick_local_developer_hoodie(r.base, client, multiplayer_ready);
dingosdk::tick_local_developer_board(r.base, client, multiplayer_ready);
// The session spawns and places skaters and can teleport: check the camera again.
tick_state.camera_issue.reset();
// The spectate camera follows a moving skater, so it runs every client tick, after the
// session placed the remote skaters for this frame (and also out of play, to give up).
std::uintptr_t camera_context{};
const bool camera_phase = tick_state.camera_issue ? *tick_state.camera_issue == nullptr :
memory::peek(client + 8, camera_context) && !dingosdk::camera_probe_unavailable_reason(camera_context);
{
DINGO_PROFILE_ZONE("tick/native party");
dingosdk::multiplayer::tick_native_party_actions(r.base, client,
r.spectate_ready && r.spectate_client == client && (state == 13 || state == 21), camera_phase);
}
if (state == 13 || state == 21) {
DINGO_PROFILE_ZONE("tick/camera view");
// The camera ReSkate's nametags start from, and the spectate camera's lesson in how
// the game frames the local skater.
if (dingosdk::publish_local_camera_view(r.base, client))
if (const auto view = dingosdk::latest_game_view())
dingosdk::multiplayer::observe_gameplay_camera(r.base, *view);
}
// Same verified client scope in which remote skater actors are created.
{
DINGO_PROFILE_ZONE("tick/AI skaters");
dingosdk::ai_skaters::tick(r.base,client,multiplayer_ready);
}
{
DINGO_PROFILE_ZONE("tick/trainer");
// A custom map is a sublevel of the root level: that is the map the player means.
dingosdk::trainer::tick(r.base, client, multiplayer_ready, r.catalog_level.empty() ? r.last_level : r.catalog_level);
}
{
DINGO_PROFILE_ZONE("tick/Steam friend join");
dingosdk::multiplayer::tick_steam_friend_join();
}
// Model creation requires cleanup on every native unload, including
// transitions that bypass the ReSkate request scheduler.
if (r.native_loading_logging) {
DINGO_PROFILE_ZONE("tick/native menu");
dingosdk::multiplayer::tick_native_menu(r.base, loading);
}
}
catch (const std::exception&) {
if (!r.observer_failed.exchange(true)) activity_line(dingosdk::ConsoleSource::runtime,
"Debug controller stopped after an observation error. See the runtime log.", dingosdk::ConsoleSeverity::error);
std::lock_guard lock(r.mutex);
r.model.can_queue_load = false;
r.model.load_block_reason = "Debug controller stopped after a local observation error.";
++r.model_revision;
r.requests.fault();
}
}
}