mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 20:57:08 +02:00
Trim per-frame multiplayer client work; size mesh pool to the GPU
Mesh streaming pool (fixes loading crashes on older GPUs) - 1.0.0 forced MeshStreaming.PoolSize to 3.5 GiB on every card. An RX 580 crashed while loading with DXGI_ERROR_DEVICE_RESET, and the oversized pool is the likely cause. The pool is now a quarter of the largest hardware adapter's dedicated video memory, capped at 3.5 GiB; cards of 6 GB or less keep the game's own 520 MB. The card, its memory and the chosen size are logged once at startup. Remote players (busy servers) - Transform interpolate is inline plain arithmetic: 22 -> 5.8 ns per joint. It was 4% of a busy server's client frame, since every player within 200 m has the whole skeleton interpolated every frame. - Players out of the camera's view and more than 8 m away are sampled at 20 Hz, like far players. Their position still updates every frame for collision, sound and nametags, and a player coming into view is sampled that frame. Expression hooks (run for every expression the game executes) - No profiler zones in the expression hooks: taking a sample switched them on, inflating the very samples that measured the hooks. - The throwdown lab pump does one sorted lookup of the running graph and skips everything else for graphs it doesn't watch (graph_throttle gets throttled_graph for this). Settings and memory reads - Script gameplay-setting and native-setting gets are served from small per-thread caches, keyed on the asset/key address, its first 8 name bytes and a new profile::Store::changes() counter, instead of a store lookup each call. - Per-frame reads in the client tick, native settings and population controls use memory::peek instead of ReadProcessMemory. - Script error logging probes each argument pointer once.
This commit is contained in:
1 parent
f7fc909ca7
commit
b763631b77
24 files changed
+320
-151
No files matched your search
@@ -965,24 +965,6 @@ std::uint64_t map_hash(std::string_view value) noexcept {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Transform interpolate(const Transform &a, const Transform &b, float t) {
|
||||
Transform out;
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
out.position[i] = std::lerp(a.position[i], b.position[i], t);
|
||||
out.scale[i] = std::lerp(a.scale[i], b.scale[i], t);
|
||||
}
|
||||
float dot{}, norm{};
|
||||
for (unsigned i = 0; i < 4; ++i)
|
||||
dot += a.rotation[i] * b.rotation[i];
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
out.rotation[i] = std::lerp(a.rotation[i], dot < 0 ? -b.rotation[i] : b.rotation[i], t);
|
||||
norm += out.rotation[i] * out.rotation[i];
|
||||
}
|
||||
for (float &v : out.rotation)
|
||||
v /= std::sqrt(norm);
|
||||
return out;
|
||||
}
|
||||
std::array<float, 16> to_matrix(const Transform &t) {
|
||||
const auto q = interpolate(t, t, 0).rotation;
|
||||
const float x = q[0], y = q[1], z = q[2], w = q[3];
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "Engine/Game/World/park_rotation.h"
|
||||
#include "Engine/Game/World/world_layers.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
@@ -213,7 +214,29 @@ std::optional<Packet> decode(std::span<const std::uint8_t>) noexcept;
|
||||
bool newer_sequence(std::uint32_t candidate, std::uint32_t previous) noexcept;
|
||||
std::uint64_t map_hash(std::string_view) noexcept;
|
||||
bool valid_map_destination(std::string_view) noexcept;
|
||||
Transform interpolate(const Transform &, const Transform &, float);
|
||||
// Normalized lerp between two transforms. Runs for every joint of every remote player each
|
||||
// frame, so it is inline and plain arithmetic (std::lerp's edge-case branches and a divide
|
||||
// per component were ~1% of a multiplayer client frame, profiled 2026-10-02).
|
||||
inline Transform interpolate(const Transform &a, const Transform &b, float t) {
|
||||
Transform out;
|
||||
t = t < 0.0f ? 0.0f : t > 1.0f ? 1.0f : t;
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
out.position[i] = a.position[i] + (b.position[i] - a.position[i]) * t;
|
||||
out.scale[i] = a.scale[i] + (b.scale[i] - a.scale[i]) * t;
|
||||
}
|
||||
const float dot = a.rotation[0] * b.rotation[0] + a.rotation[1] * b.rotation[1] +
|
||||
a.rotation[2] * b.rotation[2] + a.rotation[3] * b.rotation[3];
|
||||
const float sign = dot < 0 ? -1.0f : 1.0f;
|
||||
float norm{};
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
out.rotation[i] = a.rotation[i] + (sign * b.rotation[i] - a.rotation[i]) * t;
|
||||
norm += out.rotation[i] * out.rotation[i];
|
||||
}
|
||||
const float inverse = 1.0f / std::sqrt(norm);
|
||||
for (float &v : out.rotation)
|
||||
v *= inverse;
|
||||
return out;
|
||||
}
|
||||
void offset_pose(Pose &, const std::array<float, 3> &);
|
||||
std::array<float, 16> to_matrix(const Transform &);
|
||||
Transform from_matrix(const std::array<float, 16> &);
|
||||
|
||||
@@ -342,6 +342,11 @@ Pose out_of_sight(Pose pose) {
|
||||
// every frame again once back inside 190 m. Between samples the skater keeps its pose, which
|
||||
// saves the interpolation, the pose copy and the skeleton write for players too far away to see.
|
||||
constexpr int far_full_rate_return = 190, far_half_rate_start = 200, far_low_rate_start = 350;
|
||||
// Out of the camera's view, players past this many metres are sampled at 20 Hz too: the whole-
|
||||
// skeleton interpolation grows with the player count and was most of ReSkate's client-tick cost
|
||||
// on busy servers (profiled 2026-10-03). Nearer ones stay every frame for collision and close
|
||||
// shadows; one coming into view is sampled that frame.
|
||||
constexpr float out_of_view_full_rate = 8.0f;
|
||||
// Metres from a player to the nearer of the local skater and the camera (infinite if neither
|
||||
// is known).
|
||||
float nearest_distance(const Peer &p, const NativeFrame &local, const std::optional<GameView> &view) {
|
||||
@@ -355,6 +360,23 @@ float nearest_distance(const Peer &p, const NativeFrame &local, const std::optio
|
||||
if (view) consider(view->world[12], view->world[13], view->world[14]);
|
||||
return std::sqrt(nearest);
|
||||
}
|
||||
// Whether a player is certainly outside the camera's view: a 2 m sphere around their body past
|
||||
// the camera, or past a side, top or bottom of its view (taken as wide as a 21:9 screen).
|
||||
bool out_of_view(const Peer &p, const std::optional<GameView> &view) {
|
||||
if (!view || !(view->vertical_fov > 1 && view->vertical_fov < 175)) return false;
|
||||
const auto &m = view->world;
|
||||
const auto &at = p.render_pose.root.position;
|
||||
const float dx = at[0] - m[12], dy = at[1] + 1.0f - m[13], dz = at[2] - m[14];
|
||||
const float depth = -(dx * m[8] + dy * m[9] + dz * m[10]);
|
||||
const float side = dx * m[0] + dy * m[1] + dz * m[2], height = dx * m[4] + dy * m[5] + dz * m[6];
|
||||
constexpr float radius = 2.0f;
|
||||
if (depth < -radius) return true;
|
||||
const float tan_v = std::tan(view->vertical_fov * 3.14159265f / 360.0f), tan_h = tan_v * (21.0f / 9.0f);
|
||||
const auto outside = [&](float offset, float tangent) {
|
||||
return std::abs(offset) - depth * tangent > radius * std::sqrt(1.0f + tangent * tangent);
|
||||
};
|
||||
return outside(side, tan_h) || outside(height, tan_v);
|
||||
}
|
||||
std::uint64_t far_sample_interval(const Peer &p, float distance) {
|
||||
const auto beyond = [&](int metres) { return distance > static_cast<float>(metres); };
|
||||
if (!std::isfinite(distance) || !beyond(far_full_rate_return)) return 0;
|
||||
@@ -420,7 +442,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
|
||||
const bool was_visible = p.visible;
|
||||
// A far player between samples: their skater keeps its pose; sound, the label and
|
||||
// the spectate position are still refreshed (sound events are released one per frame).
|
||||
if (was_visible && local.ready && !p.render_failed && p.far_interval && now < p.next_far_sample) {
|
||||
if (was_visible && local.ready && !p.render_failed && p.far_interval && now < p.next_far_sample &&
|
||||
!(p.far_for_view && !out_of_view(p, view))) {
|
||||
present_audio(p);
|
||||
update_party_position(&p.render_pose);
|
||||
if (labels) label(p);
|
||||
@@ -447,6 +470,9 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
|
||||
p.native_status);
|
||||
const auto distance = nearest_distance(p, local, view);
|
||||
p.far_interval = p.visible && !hidden ? far_sample_interval(p, distance) : 0;
|
||||
p.far_for_view = p.visible && !hidden && !p.far_interval && std::isfinite(distance) &&
|
||||
distance > out_of_view_full_rate && s.mode != Mode::echo && out_of_view(p, view);
|
||||
if (p.far_for_view) p.far_interval = 50000;
|
||||
p.next_far_sample = now + p.far_interval;
|
||||
// The native work a far player's skater may skip (puppet_cost.cpp).
|
||||
if (p.visible) note_remote_distance(distance);
|
||||
|
||||
@@ -87,9 +87,11 @@ struct Peer {
|
||||
ReceiveBudget budget;
|
||||
PoseBuffer poses;
|
||||
Pose render_pose;
|
||||
// Far from the local skater and the camera, render_pose is sampled every
|
||||
// far_interval (0: every frame) and the skater keeps it in between.
|
||||
// Far from the local skater and the camera, or out of the camera's view, render_pose is
|
||||
// sampled every far_interval (0: every frame) and the skater keeps it in between.
|
||||
std::uint64_t far_interval{}, next_far_sample{};
|
||||
// far_interval is only for being out of view: coming into view samples at once.
|
||||
bool far_for_view{};
|
||||
AppearanceBuffer appearance;
|
||||
// Bumped per accepted outfit; render compares it with the one its actor wears
|
||||
// (0 after a spawn) to spread native recipe applies over frames.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <Windows.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
@@ -256,6 +257,9 @@ std::string with_cached_entitlements(std::string text) {
|
||||
std::string_view embedded_defaults() noexcept {
|
||||
return {reinterpret_cast<const char*>(embedded_profile_defaults), sizeof(embedded_profile_defaults)};
|
||||
}
|
||||
namespace {
|
||||
std::atomic<std::uint64_t> store_changes{1};
|
||||
}
|
||||
Store::Store(std::filesystem::path path, const std::filesystem::path& defaults) : path_(storage::database_path(std::move(path))),
|
||||
database_(std::make_unique<storage::SaveDatabase>(path_, database::profile_schema())) {
|
||||
const auto seed = decode(with_cached_entitlements(defaults.empty() || !std::filesystem::exists(defaults) ?
|
||||
@@ -283,8 +287,10 @@ Store::Store(std::filesystem::path path, const std::filesystem::path& defaults)
|
||||
if (next.extensions.contains("location_travel") && next.extensions.at("location_travel").is_object())
|
||||
for (const auto* key : {"destinations", "access_points"}) next.extensions.at("location_travel").erase(key);
|
||||
if (!database_->exists() || next != value_) commit(std::move(next));
|
||||
store_changes.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
Store::~Store() = default;
|
||||
std::uint64_t Store::changes() noexcept { return store_changes.load(std::memory_order_acquire); }
|
||||
Snapshot Store::snapshot() const { std::lock_guard lock(mutex_); return value_; }
|
||||
std::shared_ptr<const Snapshot> Store::shared_snapshot() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
@@ -452,6 +458,7 @@ void Store::commit(Snapshot next) {
|
||||
if (database_->exists()) database_->commit(std::move(tables));
|
||||
else database_->initialize(std::move(tables));
|
||||
value_ = std::move(next);
|
||||
store_changes.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -137,6 +137,9 @@ public:
|
||||
std::string export_json() const;
|
||||
void import_json(std::string_view);
|
||||
std::uint64_t revision() const;
|
||||
// Bumped whenever any Store's saved values may have changed (each commit, each Store made);
|
||||
// read without the lock, so caches on hot paths can tell whether a lookup is still current.
|
||||
static std::uint64_t changes() noexcept;
|
||||
// Same immutable object until a successful commit changes this Store's
|
||||
// revision. Retained readers remain valid after writes or Store destruction.
|
||||
std::shared_ptr<const MissionState> mission_state() const;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Profiling/profiler.h"
|
||||
#include "Extension/Customization/local_customization_runtime.h"
|
||||
#include "Extension/Profile/runtime_internal.h"
|
||||
#include "local_challenge_runtime.h"
|
||||
@@ -27,32 +26,28 @@ thread_local std::uintptr_t executing_expression{};
|
||||
|
||||
thread_local const ExpressionExecutionScope* expression_scope{};
|
||||
|
||||
// Both hooks run for every expression the game executes, thousands a frame on several threads:
|
||||
// no profiler zones here (each was two timestamp reads and shared atomic adds whenever the
|
||||
// profiler ran, which inflated the hooks in the very samples that measured them, 2026-10-03).
|
||||
// The sampler attributes their time.
|
||||
void execute_expression_hook(std::uintptr_t vm, std::uint32_t pc) {
|
||||
// The setup helper only prepares registers. Native calls actually execute
|
||||
// in 1416d0b90 / 1416d1a10, including direct ECS interpreter entry paths.
|
||||
// Keep the exact cursor and profiler context when entering either runner.
|
||||
ExpressionExecutionScope scope(vm);
|
||||
{
|
||||
DINGO_PROFILE_ZONE("hooks/expression start");
|
||||
initialize_starter_from_expression(vm, pc);
|
||||
}
|
||||
initialize_starter_from_expression(vm, pc);
|
||||
multiplayer::execute_native_party_expression(vm, pc, 0,
|
||||
[](std::uintptr_t expression, std::uint32_t cursor, std::uintptr_t) {
|
||||
local_runtime().execute_expression(expression, cursor);
|
||||
});
|
||||
DINGO_PROFILE_ZONE("hooks/expression end");
|
||||
deliver_local_challenge_completion(vm, pc);
|
||||
multiplayer::complete_native_throwdown_parameters(vm);
|
||||
}
|
||||
|
||||
void execute_profiled_expression_hook(std::uintptr_t vm, std::uint32_t pc, std::uintptr_t profiler) {
|
||||
ExpressionExecutionScope scope(vm);
|
||||
{
|
||||
DINGO_PROFILE_ZONE("hooks/expression start");
|
||||
initialize_starter_from_expression(vm, pc);
|
||||
}
|
||||
initialize_starter_from_expression(vm, pc);
|
||||
multiplayer::execute_native_party_expression(vm, pc, profiler, local_runtime().execute_profiled_expression);
|
||||
DINGO_PROFILE_ZONE("hooks/expression end");
|
||||
deliver_local_challenge_completion(vm, pc);
|
||||
multiplayer::complete_native_throwdown_parameters(vm);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ void script_error(std::uintptr_t, std::uintptr_t*, std::uintptr_t* arguments) {
|
||||
PreserveError preserve;
|
||||
// Authored graphs raise the same error every frame, from the same constant text: a text
|
||||
// pointer already looked at is not read again (each character was a system call).
|
||||
static std::mutex mutex; static std::set<std::string> seen; static std::set<std::uintptr_t> looked_at;
|
||||
static std::mutex mutex; static std::set<std::string> seen; static std::set<std::uintptr_t> looked_at, arguments_seen;
|
||||
std::lock_guard lock(mutex);
|
||||
try {
|
||||
// The message position is not known from the binding alone; take the
|
||||
@@ -35,6 +35,10 @@ void script_error(std::uintptr_t, std::uintptr_t*, std::uintptr_t* arguments) {
|
||||
for (std::size_t index = 0; arguments && index < 4 && message.empty(); ++index) {
|
||||
std::uintptr_t argument{}, inner{};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(arguments) + index * 8, argument) || argument < 0x10000) break;
|
||||
// The same arguments come back every frame: each is probed once (read, not peeked:
|
||||
// it is often not a pointer, and a failed peek costs an exception).
|
||||
if (arguments_seen.contains(argument)) continue;
|
||||
if (arguments_seen.size() < 4096) arguments_seen.insert(argument);
|
||||
if (read(argument, inner) && inner >= 0x10000) message = printable(inner);
|
||||
if (message.empty()) message = printable(argument);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "Extension/Progression/local_entitlement_trigger_runtime.h"
|
||||
#include "local_gameplay_settings.h"
|
||||
#include "local_user_settings.h"
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <array>
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
// Applied gameplay values live in an ECS cache even when the user-value
|
||||
@@ -48,6 +51,42 @@ bool gameplay_setting_key(const void* reference, std::string& key) {
|
||||
return asset && identifier(reinterpret_cast<const void*>(asset + 0x60), key);
|
||||
}
|
||||
|
||||
// Every script settings get the game makes (several a frame from its expressions) asks this. One
|
||||
// store lookup per setting asset and profile change, cached on the asking thread in a small
|
||||
// direct-mapped table: a hit is three peeks and compares, with no heap string, store lock or map
|
||||
// walk (~0.5% of a multiplayer client frame before, profiled 2026-10-02). A hit needs the same
|
||||
// asset, name address and first 8 name bytes, so a reused address cannot answer for another setting.
|
||||
const dingosdk::Json* saved_gameplay_json(const void* reference) {
|
||||
auto& s = local_runtime();
|
||||
if (!s.active.load(std::memory_order_acquire)) return nullptr;
|
||||
std::uintptr_t asset{}, name{};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(reference), asset)) return nullptr;
|
||||
asset &= ~std::uintptr_t{4};
|
||||
if (!asset || !memory::peek(asset + 0x60, name) || !name) return nullptr;
|
||||
std::uint64_t prefix{};
|
||||
(void)memory::peek(name, prefix); // a short name at the end of a page stays 0: a miss refills
|
||||
struct Entry { std::uintptr_t asset{}, name{}; std::uint64_t prefix{}, changes{}; std::optional<dingosdk::Json> value; };
|
||||
thread_local std::array<Entry, 256> cache;
|
||||
const auto changes = profile::Store::changes();
|
||||
auto& entry = cache[((asset >> 4) ^ (asset >> 12)) & 255];
|
||||
if (entry.asset != asset || entry.name != name || entry.prefix != prefix || entry.changes != changes) {
|
||||
char text[256];
|
||||
const auto length = memory::peek_cstring(name, text, sizeof(text));
|
||||
if (length <= 0) return nullptr;
|
||||
for (std::ptrdiff_t i = 0; i < length; ++i) {
|
||||
const auto c = static_cast<unsigned char>(text[i]);
|
||||
if (c < 32 || c == 127) return nullptr;
|
||||
}
|
||||
entry.changes = 0;
|
||||
entry.value = s.store->user_value(std::string_view(text, static_cast<std::size_t>(length)));
|
||||
entry.asset = asset;
|
||||
entry.name = name;
|
||||
entry.prefix = prefix;
|
||||
entry.changes = changes;
|
||||
}
|
||||
return entry.value ? &*entry.value : nullptr;
|
||||
}
|
||||
|
||||
void save_gameplay_setting(const void* reference, const dingosdk::Json& value) {
|
||||
auto& s = local_runtime();
|
||||
if (!s.active.load(std::memory_order_acquire)) return;
|
||||
|
||||
@@ -24,14 +24,14 @@ bool gameplay_user_edit();
|
||||
|
||||
bool gameplay_setting_key(const void* reference, std::string& key);
|
||||
|
||||
// The saved value for a script setting reference, or null: cached per setting asset and profile
|
||||
// change on the asking thread (valid until this thread's next call). May throw.
|
||||
const dingosdk::Json* saved_gameplay_json(const void* reference);
|
||||
|
||||
template<class T> std::optional<T> saved_gameplay_setting(const void* reference) {
|
||||
auto& s = local_runtime();
|
||||
if (!s.active.load(std::memory_order_acquire)) return {};
|
||||
PreserveError preserve;
|
||||
try {
|
||||
std::string key;
|
||||
if (!gameplay_setting_key(reference, key)) return {};
|
||||
const auto value = s.store->user_value(key);
|
||||
const auto* value = saved_gameplay_json(reference);
|
||||
if (!value) return {};
|
||||
if constexpr (std::is_same_v<T, bool>) { if (value->is_boolean()) return value->get<bool>(); }
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "Engine/Game/Build/20260929/local_native_settings.h"
|
||||
#include "Engine/Game/Build/20260929/named_settings.h"
|
||||
#include "local_user_settings.h"
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <array>
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
namespace native_types = addr::named_settings;
|
||||
@@ -30,32 +33,65 @@ unsigned native_settings_location(std::uintptr_t group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Runs on every settings get the game's scripts make: peeked, not read (one ReadProcessMemory
|
||||
// call per read, ~0.15% of a multiplayer client frame, profiled 2026-10-02).
|
||||
std::optional<dingosdk::Json> native_setting_json(const NativeSettingValue* input) {
|
||||
NativeSettingValue value{};
|
||||
if (!read(reinterpret_cast<std::uintptr_t>(input), value) || !value.data) return {};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(input), value) || !value.data) return {};
|
||||
const auto type = value.type - local_runtime().base;
|
||||
switch (type) {
|
||||
case native_types::native_bool: {
|
||||
std::uint8_t byte{};
|
||||
if (read(reinterpret_cast<std::uintptr_t>(value.data), byte) && byte <= 1) return byte != 0;
|
||||
if (memory::peek(reinterpret_cast<std::uintptr_t>(value.data), byte) && byte <= 1) return byte != 0;
|
||||
return {};
|
||||
}
|
||||
case native_types::native_uint32: return native_scalar<std::uint32_t>(value.data);
|
||||
case native_types::native_int32: return native_scalar<std::int32_t>(value.data);
|
||||
case native_types::native_float32: return native_scalar<float>(value.data);
|
||||
case native_types::native_cstring: {
|
||||
std::uintptr_t text{}; std::string result;
|
||||
if (!read(reinterpret_cast<std::uintptr_t>(value.data), text)) return {};
|
||||
for (std::size_t i = 0; i <= 4096; ++i) {
|
||||
char c{}; if (!read(text + i, c)) return {};
|
||||
if (!c) return result;
|
||||
result += c;
|
||||
}
|
||||
std::uintptr_t text{};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(value.data), text)) return {};
|
||||
char buffer[4097];
|
||||
const auto length = memory::peek_cstring(text, buffer, sizeof(buffer));
|
||||
if (length < 0) return {};
|
||||
return std::string(buffer, static_cast<std::size_t>(length));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The saved option for a native setting key, or null: one store lookup per key and profile change,
|
||||
// cached on the asking thread in a small direct-mapped table (valid until this thread's next call).
|
||||
// A hit needs the same key address, location and first 8 key bytes; the key text is read only on
|
||||
// a miss. May throw.
|
||||
const dingosdk::Json* saved_native_option(unsigned location, const char* key) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(key);
|
||||
std::uint64_t prefix{};
|
||||
(void)memory::peek(address, prefix); // a short key at the end of a page stays 0: a miss refills
|
||||
struct Entry { std::uintptr_t key{}; unsigned location{}; std::uint64_t prefix{}, changes{}; std::optional<dingosdk::Json> value; };
|
||||
thread_local std::array<Entry, 256> cache;
|
||||
const auto changes = profile::Store::changes();
|
||||
auto& entry = cache[((address >> 3) ^ (address >> 11) ^ location) & 255];
|
||||
if (entry.key != address || entry.location != location || entry.prefix != prefix || entry.changes != changes) {
|
||||
char text[256];
|
||||
const auto length = memory::peek_cstring(address, text, sizeof(text));
|
||||
if (length <= 0) return nullptr;
|
||||
for (std::ptrdiff_t i = 0; i < length; ++i) {
|
||||
const auto c = static_cast<unsigned char>(text[i]);
|
||||
if (c < 32 || c == 127) return nullptr;
|
||||
}
|
||||
entry.changes = 0;
|
||||
entry.value = local_runtime().store->native_profile_option(location, std::string_view(text, static_cast<std::size_t>(length)));
|
||||
entry.key = address;
|
||||
entry.location = location;
|
||||
entry.prefix = prefix;
|
||||
entry.changes = changes;
|
||||
}
|
||||
return entry.value ? &*entry.value : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool restore_native_setting(std::uintptr_t group, const char* key, std::uintptr_t type,
|
||||
const dingosdk::Json& saved) {
|
||||
const auto base = local_runtime().base;
|
||||
@@ -82,15 +118,18 @@ const NativeSettingValue* native_setting_get(std::uintptr_t group, const char* k
|
||||
auto* result = native_settings().get(group, key);
|
||||
PreserveError preserve;
|
||||
try {
|
||||
const auto location = native_settings_location(group); std::string id;
|
||||
if (!location || !identifier(&key, id)) return result;
|
||||
const auto saved = local_runtime().store->native_profile_option(location, id);
|
||||
const auto location = native_settings_location(group);
|
||||
const auto* saved = location ? saved_native_option(location, key) : nullptr;
|
||||
if (!saved) return result;
|
||||
const auto current = native_setting_json(result);
|
||||
if (!current || *current == *saved) return result;
|
||||
// Copied before the restore: a get it causes on this thread reuses the cache.
|
||||
const auto wanted = *saved;
|
||||
std::string name;
|
||||
(void)identifier(&key, name);
|
||||
NativeSettingValue native{};
|
||||
if (current && *current != *saved && read(reinterpret_cast<std::uintptr_t>(result), native) &&
|
||||
restore_native_setting(group, key, native.type, *saved))
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::settings, dingosdk::Json{{"event","local_native_setting_restored"},{"location",location},{"key",id}}.dump().c_str());
|
||||
if (memory::peek(reinterpret_cast<std::uintptr_t>(result), native) && restore_native_setting(group, key, native.type, wanted))
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::settings, dingosdk::Json{{"event","local_native_setting_restored"},{"location",location},{"key",name}}.dump().c_str());
|
||||
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::settings, "{\"event\":\"local_native_setting_restore_failed\"}"); }
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ unsigned native_settings_location(std::uintptr_t group);
|
||||
|
||||
template<class T> std::optional<dingosdk::Json> native_scalar(const void* data) {
|
||||
T value{};
|
||||
if (!read(reinterpret_cast<std::uintptr_t>(data), value)) return {};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(data), value)) return {};
|
||||
if constexpr (std::is_floating_point_v<T>) if (!std::isfinite(value)) return {};
|
||||
if constexpr (std::is_same_v<T, std::uint64_t>) if (value > INT64_MAX) return {};
|
||||
return dingosdk::Json(value);
|
||||
|
||||
@@ -162,6 +162,12 @@ void settle(Address base, Address resource, std::size_t index) noexcept {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool throttled_graph(std::uint32_t hash) noexcept {
|
||||
for (const auto& graph : graphs)
|
||||
if (graph.hash == hash) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void throttle_graph_ran(Address base, Address vm) noexcept {
|
||||
// The pump has just read these for the same run: the expression and its resource are alive.
|
||||
if (!vm) return;
|
||||
|
||||
@@ -21,4 +21,6 @@ namespace dingosdk::multiplayer {
|
||||
// Call after every graph run, any realm (the expression pump). Other graphs return after one
|
||||
// hash compare, lock free; each loaded copy of the seven above is checked and patched once.
|
||||
void throttle_graph_ran(std::uintptr_t base, std::uintptr_t vm) noexcept;
|
||||
// Whether the graph with this hash is one of the throttled ones (the pump skips the rest).
|
||||
bool throttled_graph(std::uint32_t hash) noexcept;
|
||||
} // namespace dingosdk::multiplayer
|
||||
@@ -1688,11 +1688,31 @@ void trace_expression(Address vm) {
|
||||
if (!seen.count++) seen.first_ms = now;
|
||||
seen.last_ms = now;
|
||||
}
|
||||
|
||||
// The pump runs after every expression the game executes, thousands a frame, and almost all
|
||||
// are none of the graphs it watches: one sorted lookup of the running graph decides
|
||||
// (~0.5% of a busy multiplayer client frame before, profiled 2026-10-03). Keep this list in
|
||||
// step with the graphs the pump's body compares. The handled graphs' zero padding stays in, as
|
||||
// the body's search also matched a graph of 0.
|
||||
bool lab_graph(Address vm) noexcept {
|
||||
static const auto watched = [] {
|
||||
std::vector<std::uint32_t> v{graph_celebration, graph_criteria_attempt, graph_queue_filled, graph_round_end_update,
|
||||
graph_turn_start, graph_skate_timer, graph_turn_end_begin,
|
||||
0xc33322b2, 0xc4a37359, 0xcc6467cf, 0xf073dd02};
|
||||
for (const auto& h : handled_events) v.insert(v.end(), h.graphs.begin(), h.graphs.end());
|
||||
std::ranges::sort(v);
|
||||
v.erase(std::unique(v.begin(), v.end()), v.end());
|
||||
return v;
|
||||
}();
|
||||
const auto resource = *reinterpret_cast<const Address*>(vm + 0x38);
|
||||
const auto graph = resource ? *reinterpret_cast<const std::uint32_t*>(resource + 0x10) : 0U;
|
||||
return std::ranges::binary_search(watched, graph) || (resource && throttled_graph(graph));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void pump_throwdown_lab(Address vm) noexcept {
|
||||
auto& l = lab();
|
||||
if (vm && l.installed.load(std::memory_order_acquire)) {
|
||||
if (vm && l.installed.load(std::memory_order_acquire) && lab_graph(vm)) {
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
const auto resource = *reinterpret_cast<const Address*>(vm + 0x38);
|
||||
|
||||
@@ -21,13 +21,14 @@ bool population_config_identity(std::uintptr_t config) {
|
||||
// The validated manager supplies a TrafficSystemConfig, but the asset name
|
||||
// varies by map (BAM uses Gameplay/AI/BAM_TrafficSystemConfig). The exact
|
||||
// native class identifies the field layout; the default asset name does not.
|
||||
return read(config + 8, type) && type == local_runtime().base + addr::population::traffic_system_config_type &&
|
||||
return memory::peek(config + 8, type) && type == local_runtime().base + addr::population::traffic_system_config_type &&
|
||||
identifier(reinterpret_cast<void*>(config + 0x18), name);
|
||||
}
|
||||
|
||||
// Every population tick, on the validated live config: peeked.
|
||||
bool population_value(std::uintptr_t config, unsigned i, float& out) {
|
||||
if (i == 0 || i == 3) return read(config + population_offsets[i], out) && std::isfinite(out) && out >= 0 && out <= 100;
|
||||
std::uint32_t v{}; if (!read(config + population_offsets[i], v) || v > 1000) return false; out = static_cast<float>(v); return true;
|
||||
if (i == 0 || i == 3) return memory::peek(config + population_offsets[i], out) && std::isfinite(out) && out >= 0 && out <= 100;
|
||||
std::uint32_t v{}; if (!memory::peek(config + population_offsets[i], v) || v > 1000) return false; out = static_cast<float>(v); return true;
|
||||
}
|
||||
|
||||
bool population_write(std::uintptr_t config, unsigned i, float value) {
|
||||
|
||||
@@ -68,7 +68,7 @@ template<unsigned Kind> void population_tick(std::uintptr_t manager) {
|
||||
const auto found = p.nodes.find(manager);
|
||||
if (found != p.nodes.end() && population_identity(manager, found->second.realm)) {
|
||||
auto& node = found->second; std::uintptr_t config{}; std::uint8_t running{};
|
||||
if (read(manager + 0x108, config) && read(manager + 0x152, running) && running == 1) {
|
||||
if (memory::peek(manager + 0x108, config) && memory::peek(manager + 0x152, running) && running == 1) {
|
||||
config &= ~std::uintptr_t{4};
|
||||
if (config != node.config) { population_release_config(manager); node.config = config; }
|
||||
if (population_config_identity(config)) {
|
||||
@@ -90,7 +90,7 @@ template<unsigned Kind> void population_tick(std::uintptr_t manager) {
|
||||
p.observed_at[Kind] = GetTickCount64(); r.model.population_ready[Kind] = ok;
|
||||
std::uintptr_t begin{}, end{};
|
||||
const unsigned offset = Kind == 0 ? 0xa8 : 0xe8;
|
||||
if (read(manager + offset, begin) && read(manager + offset + 8, end) && end >= begin && end - begin <= 8000)
|
||||
if (memory::peek(manager + offset, begin) && memory::peek(manager + offset + 8, end) && end >= begin && end - begin <= 8000)
|
||||
r.model.population[Kind] = static_cast<unsigned>((end - begin) / 8);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-51
@@ -172,23 +172,6 @@ void apply_style() {
|
||||
colours[ImGuiCol_NavHighlight] = ImVec4(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// True once `process` shows its startup splash (ReSkateStartupWindow) or, if the
|
||||
// splash is disabled, the game's own window.
|
||||
bool game_window_shown(DWORD process) {
|
||||
struct Search { DWORD process; bool found; } search{process, false};
|
||||
EnumWindows([](HWND window, LPARAM parameter) -> BOOL {
|
||||
auto& target = *reinterpret_cast<Search*>(parameter);
|
||||
DWORD owner{};
|
||||
GetWindowThreadProcessId(window, &owner);
|
||||
if (owner != target.process || !IsWindowVisible(window)) return TRUE;
|
||||
std::array<wchar_t, 64> name{};
|
||||
GetClassNameW(window, name.data(), static_cast<int>(name.size()));
|
||||
const std::wstring_view kind(name.data());
|
||||
if (kind == L"ReSkateStartupWindow" || kind == L"Skate") { target.found = true; return FALSE; }
|
||||
return TRUE;
|
||||
}, reinterpret_cast<LPARAM>(&search));
|
||||
return search.found;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace detail
|
||||
@@ -275,11 +258,6 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
auto& ui = *ui_storage;
|
||||
ModsPanel mods_panel;
|
||||
bool running = true;
|
||||
// The game this launcher started: hide when its splash opens, then exit
|
||||
// (Close the launcher when Skate starts) or wait and return when it closes.
|
||||
HANDLE game{};
|
||||
DWORD game_id{};
|
||||
bool hidden{};
|
||||
while (running) {
|
||||
MSG message;
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
@@ -292,34 +270,12 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
launcher.restart();
|
||||
break;
|
||||
}
|
||||
if (!game && launcher.game()) {
|
||||
game_id = launcher.game();
|
||||
game = OpenProcess(SYNCHRONIZE, FALSE, game_id);
|
||||
}
|
||||
if (game) {
|
||||
if (!hidden && game_window_shown(game_id)) {
|
||||
ShowWindow(window, SW_HIDE);
|
||||
hidden = true;
|
||||
}
|
||||
// Exit only after injection succeeded, so a failed launch can still report.
|
||||
if (hidden && launcher.launched() && launcher.settings().close_on_launch) break;
|
||||
if (hidden && !launcher.busy() && launcher.snapshot().phase == Phase::failed) {
|
||||
ShowWindow(window, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(window);
|
||||
hidden = false;
|
||||
}
|
||||
if (WaitForSingleObject(game, 0) == WAIT_OBJECT_0) {
|
||||
CloseHandle(game);
|
||||
game = nullptr;
|
||||
launcher.game_exited(hidden);
|
||||
if (hidden) {
|
||||
ShowWindow(window, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(window);
|
||||
hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hidden || IsIconic(window)) { Sleep(hidden ? 100 : 50); continue; }
|
||||
// The launcher is done the moment injection succeeded and Skate is running
|
||||
// on its own, so close immediately instead of staying beside the game. This
|
||||
// is the earliest safe point: a launch that fails never sets launched(), so
|
||||
// it still gets to report itself in the window.
|
||||
if (launcher.launched()) break;
|
||||
if (IsIconic(window)) { Sleep(50); continue; }
|
||||
ImGui_ImplDX12_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
@@ -328,7 +284,6 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
|
||||
renderer.render();
|
||||
}
|
||||
ShowWindow(window, SW_HIDE);
|
||||
if (game) CloseHandle(game);
|
||||
launcher.cancel();
|
||||
// Closing the window with Settings still open must not lose changes.
|
||||
try { launcher.save(); } catch (...) {}
|
||||
|
||||
+13
-5
@@ -313,7 +313,18 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
}
|
||||
g_dropped.clear();
|
||||
}
|
||||
const bool modal = ui.settings || ui.mods || ui.sign_in || qr_open || state.prompt.has_value();
|
||||
// The Steam name for the plate, and the one-time offline notice, both come
|
||||
// from the first Steam check; steam_offline_seen keeps it to one decision.
|
||||
if (time - ui.steam_checked > 5) {
|
||||
ui.steam_name = launcher_app::steam_persona_name();
|
||||
ui.steam_checked = time;
|
||||
if (!ui.steam_offline_seen) {
|
||||
ui.steam_offline_seen = true;
|
||||
ui.steam_offline = !launcher.settings().offline && !launcher_app::steam_signed_in();
|
||||
}
|
||||
}
|
||||
const bool modal = ui.settings || ui.mods || ui.sign_in || qr_open || state.prompt.has_value() ||
|
||||
ui.steam_offline;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowSize(size);
|
||||
@@ -329,10 +340,6 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
std::string version = "Development build";
|
||||
if (update::binary_updates_enabled() && state.config && !state.config->launcher.version.empty())
|
||||
version = "Launcher " + state.config->launcher.version;
|
||||
if (time - ui.steam_checked > 5) {
|
||||
ui.steam_name = launcher_app::steam_persona_name();
|
||||
ui.steam_checked = time;
|
||||
}
|
||||
const bool greet = !launcher.settings().offline && !ui.steam_name.empty();
|
||||
draw_plate(draw, fonts, ImVec2(S(50), S(40)), greet ? "Welcome, " + ui.steam_name : std::string("ReSkate"), version);
|
||||
draw_title(draw, fonts, ImVec2(S(52), S(88)), "RESKATE");
|
||||
@@ -417,6 +424,7 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
else if (ui.sign_in) sign_in_window(launcher, fonts, size, ui);
|
||||
else if (ui.settings) settings_window(launcher, fonts, size, ui);
|
||||
else if (ui.mods) mods_window(launcher, fonts, size, ui, mods_panel, window);
|
||||
else if (ui.steam_offline) steam_offline_window(fonts, size, ui);
|
||||
}
|
||||
|
||||
} // namespace dingosdk::launcher_gui::detail
|
||||
@@ -122,7 +122,6 @@ struct Settings {
|
||||
int console_key{static_cast<int>(launcher::default_console_key)};
|
||||
int log_level{2};
|
||||
std::string arguments;
|
||||
bool close_on_launch{true};
|
||||
// Off: never replace ReSkate.dll or the launcher (keeps a test build someone handed out).
|
||||
bool updates{true};
|
||||
// Off: a crash uploads nothing (RESKATE_CRASH_REPORTING=0 for the launcher and the game).
|
||||
@@ -158,12 +157,11 @@ public:
|
||||
State snapshot();
|
||||
bool busy() const { return busy_; }
|
||||
bool restart_requested() const { return restart_; }
|
||||
// Process id of the Skate this launcher started, until game_exited().
|
||||
// Process id of the Skate this launcher started, once it exists.
|
||||
DWORD game() const { return game_; }
|
||||
// Injection finished and the game is running on its own.
|
||||
// Injection finished and the game is running on its own; the launcher closes
|
||||
// as soon as this is true.
|
||||
bool launched() const { return launched_; }
|
||||
// Called once the started game has exited; `seen` if its splash screen ever opened.
|
||||
void game_exited(bool seen);
|
||||
Settings& settings() { return settings_; }
|
||||
const launcher_app::Session& session() const { return session_; }
|
||||
void save();
|
||||
@@ -236,6 +234,10 @@ struct Ui {
|
||||
// Steam display name for the name plate, re-read every few seconds.
|
||||
std::string steam_name;
|
||||
double steam_checked{-100};
|
||||
// One-time notice that Skate will start offline because Steam is not
|
||||
// running or not signed in. Decided on the first Steam check.
|
||||
bool steam_offline{};
|
||||
bool steam_offline_seen{};
|
||||
};
|
||||
|
||||
// The Thunderstore listing, fetched in the background when the launcher
|
||||
@@ -345,6 +347,7 @@ void open_sign_in(Launcher& launcher, Ui& ui, bool validate);
|
||||
void sign_in_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
void prompt_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, const update::Prompt& prompt, Ui& ui);
|
||||
void qr_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, const std::vector<std::string>& rows);
|
||||
void steam_offline_window(const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
void mods_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, ModsPanel& panel, HWND window);
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ Settings load_settings(const fs::path& path) {
|
||||
for (std::size_t index = 0; index < log_levels.size(); ++index)
|
||||
if (level == log_levels[index]) settings.log_level = static_cast<int>(index);
|
||||
settings.arguments = json.value("arguments", std::string());
|
||||
settings.close_on_launch = json.value("close_on_launch", settings.close_on_launch);
|
||||
settings.updates = json.value("updates", settings.updates);
|
||||
settings.crash_reports = json.value("crash_reports", settings.crash_reports);
|
||||
settings.steam_username = json.value("steam_username", std::string());
|
||||
@@ -80,7 +79,6 @@ void save_settings(const fs::path& path, const Settings& settings) {
|
||||
json["console_key"] = settings.console_key;
|
||||
json["log_level"] = log_levels[static_cast<std::size_t>(settings.log_level)];
|
||||
json["arguments"] = settings.arguments;
|
||||
json["close_on_launch"] = settings.close_on_launch;
|
||||
json["updates"] = settings.updates;
|
||||
json["crash_reports"] = settings.crash_reports;
|
||||
json["steam_username"] = settings.steam_username;
|
||||
@@ -133,14 +131,6 @@ State Launcher::snapshot() {
|
||||
return state_;
|
||||
}
|
||||
|
||||
void Launcher::game_exited(bool seen) {
|
||||
game_ = 0;
|
||||
launched_ = false;
|
||||
if (busy_ || snapshot().phase == Phase::failed) return; // the failed launch explains itself
|
||||
if (seen) check();
|
||||
else fail("Skate closed while it was starting.");
|
||||
}
|
||||
|
||||
void Launcher::save() {
|
||||
save_settings(settings_path(session_), settings_);
|
||||
// The game inherits the launcher's environment, so this reaches the next launch; the
|
||||
@@ -400,7 +390,7 @@ void Launcher::run_play() {
|
||||
launched_ = true;
|
||||
set(Phase::launching, "Skate is starting", options.offline || !launcher_app::steam_signed_in() ?
|
||||
"Offline mode: playing without Steam as Unknown Player; multiplayer is hidden." :
|
||||
"The launcher hides when the splash screen opens.");
|
||||
"The launcher closes now; Skate keeps starting on its own.");
|
||||
}
|
||||
|
||||
} // namespace dingosdk::launcher_gui::detail
|
||||
|
||||
@@ -183,9 +183,6 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
|
||||
break;
|
||||
default:
|
||||
section_caption(fonts, "LAUNCHER");
|
||||
setting_check("Close the launcher when Skate starts",
|
||||
"Otherwise the launcher hides while you play and comes back when the game closes.",
|
||||
settings.close_on_launch);
|
||||
setting_check("Install ReSkate updates",
|
||||
"Turn off to keep the ReSkate.dll and launcher you have, such as a test build someone gave you. "
|
||||
"Skate's own Steam updates are not affected.",
|
||||
|
||||
@@ -160,4 +160,21 @@ void qr_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, const std::v
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// Shown once when the launcher opens without Steam, so nobody reaches the main
|
||||
// menu as Unknown Player wondering why multiplayer is missing.
|
||||
void steam_offline_window(const Fonts& fonts, ImVec2 size, Ui& ui) {
|
||||
const auto panel = begin_panel("##steam_offline_panel", size, ImVec2(S(440), S(300)));
|
||||
panel_title(fonts, "STEAM IS NOT RUNNING");
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextDisabled("Steam is not running, or no account is signed in. PLAY still works, but Skate starts in "
|
||||
"offline mode: you play as Unknown Player and multiplayer is hidden.");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("To play as your own account, start Steam, sign in, and open ReSkate again.");
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::SetCursorPosY(panel.y - S(24) - ImGui::GetFrameHeight());
|
||||
ImGui::SetCursorPosX(panel.x - S(28) - S(110));
|
||||
if (ImGui::Button("OK", ImVec2(S(110), 0))) ui.steam_offline = false;
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace dingosdk::launcher_gui::detail
|
||||
+65
-15
@@ -24,6 +24,7 @@
|
||||
#include "Extension/Throwdowns/native_throwdowns.h"
|
||||
#include "Extension/World/level_loading.h"
|
||||
#include "Extension/World/loading_screen.h"
|
||||
#include <dxgi.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
@@ -122,18 +123,66 @@ void apply_performance_settings() {
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
// The mesh streaming pool holds every render mesh the world has loaded. 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. Match the 3.5 GiB PC presets instead of lowering them to 2 GiB.
|
||||
// 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.
|
||||
namespace mesh_pool {
|
||||
constexpr std::uint32_t stock_kb = 532960, most_kb = 3584u * 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;
|
||||
}
|
||||
// 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) {
|
||||
constexpr std::uint64_t six_gib = 6ull << 30, slack = 256ull << 20; // "6 GB" cards report a little under
|
||||
if (memory <= six_gib + slack) return 0;
|
||||
return static_cast<std::uint32_t>(std::clamp<std::uint64_t>(memory / 4 / 1024, stock_kb, most_kb));
|
||||
}
|
||||
}
|
||||
void apply_mesh_streaming_pool() {
|
||||
constexpr std::uint32_t pool_kb = 3584u * 1024u; // Exactly 3.5 GiB, expressed in KiB.
|
||||
static_assert((std::uint64_t{pool_kb} + 24576u) * 1024u <= 0xffffffffull);
|
||||
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();
|
||||
@@ -192,8 +241,9 @@ void update_model(std::uintptr_t client, TickState& frame) {
|
||||
auto& r = runtime();
|
||||
std::uintptr_t vtable{};
|
||||
DWORD state{}, game_type{};
|
||||
if (!read(client, vtable) || vtable != r.base + engine::client_vtable || client > highest - 0x2a0 ||
|
||||
!read(client + 0xc4, state) || state > 26 || !read(client + 0xc0, game_type) || game_type > 3) return;
|
||||
// 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;
|
||||
@@ -927,7 +977,7 @@ void tick(std::uintptr_t client, std::uintptr_t update) {
|
||||
? *tick_state.context : native_context(r.base, client, game_type);
|
||||
};
|
||||
const bool multiplayer_ready=!r.observer_failed && !loading &&
|
||||
read(client+0xc4,state) && (state==13 || state==21) && read(client+0xc0,game_type) &&
|
||||
memory::peek(client+0xc4,state) && (state==13 || state==21) && memory::peek(client+0xc0,game_type) &&
|
||||
context_ready();
|
||||
{
|
||||
DINGO_PROFILE_ZONE("tick/multiplayer");
|
||||
@@ -939,7 +989,7 @@ void tick(std::uintptr_t client, std::uintptr_t update) {
|
||||
// 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 :
|
||||
read(client + 8, camera_context) && !dingosdk::camera_probe_unavailable_reason(camera_context);
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user