Keep launcher alive through Skate startup; trim per-frame client work

Launcher
- "Close the launcher when Skate starts" now hides the launcher and exits
  when Skate.exe does, instead of exiting as the splash opens. Outside a
  Steam library, Skate's protection closed the game (TerminateProcess 0x32)
  when the launcher exited during startup; players confirmed turning the
  setting off avoided it.
- STATUS tile warns when Steam isn't open or signed in that PLAY starts
  offline as Unknown Player (hidden when Offline mode is chosen or while
  launching).
- Setting description updated to match.
Startup interventions (20260929)
- exit_return 0x87755f2 -> 0x87fe7fb: the last LEA RSP,[RSP]; JMP stub
  before the PUSHFQ / LOCK CMPXCHG routine, as 0x87367cf was in 20260908,
  and the return address player logs show. Only enforced once a frame site
  is armed; this build's protector has no equivalent rep stosb frame clear.
Client frame cost (profiled 2026-10-02)
- Transform interpolate is inline plain arithmetic (~1% of an MP frame).
- Script gameplay-setting and native-setting gets are served from small
  per-thread caches keyed on asset/key address, name prefix and a new
  profile::Store::changes() counter, instead of a store lookup each call.
- Hot-path reads in 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:
AyeZeeBB committed 2026-10-02 18:56:57 -05:00
1 parent ca1d82f636
commit b7932535e5
17 files changed
+177 -60

No files matched your search

@@ -9,9 +9,10 @@ inline constexpr std::array<std::uint8_t, 2> frame_instruction{0xf3, 0xaa};
// Handler and data expected in R9/R14 at the frame clear.
inline constexpr std::uintptr_t frame_handler = 0x0880d710;
inline constexpr std::uintptr_t frame_data = 0x086772fc;
// Exit thunk (JMP [rip+...]) and the return address it is expected to be called from.
// Exit thunk (JMP [rip+...]) and the return address it is expected to be called from:
// the last LEA RSP,[RSP]; JMP stub before the PUSHFQ / LOCK CMPXCHG routine, as in 20260908.
inline constexpr std::uintptr_t exit_thunk = 0x08a06ff3;
inline constexpr std::uintptr_t exit_return = 0x087755f2;
inline constexpr std::uintptr_t exit_return = 0x087fe7fb;
inline constexpr std::array<std::uint8_t, 6> exit_instruction{
0xff, 0x25, 0x7f, 0x43, 0x5e, 0xfd};
}
-18
View File
@@ -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];
+24 -1
View File
@@ -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> &);
+7
View File
@@ -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);
}
}
+3
View File
@@ -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;
+5 -1
View File
@@ -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;
+5 -5
View File
@@ -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>) {
+54 -15
View File
@@ -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;
}
+1 -1
View File
@@ -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);
@@ -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) {
+2 -2
View File
@@ -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);
}
}
+8 -3
View File
@@ -275,11 +275,14 @@ 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.
// The game this launcher started: hide when its splash opens, then wait for it
// to close and return, or exit (Close the launcher when Skate starts).
// Staying alive until then matters: outside a Steam library, Skate's protection
// closed the game (TerminateProcess 0x32) when the launcher exited during startup.
HANDLE game{};
DWORD game_id{};
bool hidden{};
bool exit_with_game{};
while (running) {
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
@@ -302,15 +305,17 @@ int run(const launcher_app::Session& session, const std::vector<std::wstring>& a
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.launched() && launcher.settings().close_on_launch) exit_with_game = true;
if (hidden && !launcher.busy() && launcher.snapshot().phase == Phase::failed) {
ShowWindow(window, SW_SHOWNORMAL);
SetForegroundWindow(window);
hidden = false;
exit_with_game = false;
}
if (WaitForSingleObject(game, 0) == WAIT_OBJECT_0) {
CloseHandle(game);
game = nullptr;
if (exit_with_game) break;
launcher.game_exited(hidden);
if (hidden) {
ShowWindow(window, SW_SHOWNORMAL);
+12 -3
View File
@@ -239,7 +239,9 @@ bool mods_tile(ImDrawList* draw, const Fonts& fonts, ImVec2 position, ImVec2 siz
}
// The STATUS tile, laid out like the HUB's BOUNTIES tile: a header and icon rows.
void status_tile(ImDrawList* draw, const Fonts& fonts, const State& state, ImVec2 position, float width, float time) {
// `steam_offline`: Steam isn't running or signed in, so PLAY starts offline.
void status_tile(ImDrawList* draw, const Fonts& fonts, const State& state, ImVec2 position, float width, float time,
bool steam_offline) {
struct Row { Icon icon; std::string text; std::string detail; };
std::vector<Row> rows;
Icon now = Icon::busy;
@@ -252,6 +254,11 @@ void status_tile(ImDrawList* draw, const Fonts& fonts, const State& state, ImVec
default: break;
}
rows.push_back({now, state.status, state.detail});
// While launching, the status detail already says the game is starting offline.
if (steam_offline && state.phase != Phase::launching)
rows.push_back({Icon::warning, "Steam isn't open or signed in",
"PLAY starts skate. in offline mode as Unknown Player; multiplayer is hidden. "
"Open Steam and sign in to play online."});
if (state.config) {
const auto build = state.config->game.build_id.empty() ? state.config->game.manifest_id : state.config->game.build_id;
const bool game_ok = state.phase != Phase::game_missing && state.phase != Phase::game_outdated &&
@@ -330,7 +337,8 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
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_signed_in = launcher_app::steam_signed_in();
ui.steam_name = ui.steam_signed_in ? launcher_app::steam_persona_name() : std::string();
ui.steam_checked = time;
}
const bool greet = !launcher.settings().offline && !ui.steam_name.empty();
@@ -406,7 +414,8 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
(ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) act();
ImGui::EndDisabled();
status_tile(draw, fonts, state, ImVec2(S(60), S(200)), S(360), time);
status_tile(draw, fonts, state, ImVec2(S(60), S(200)), S(360), time,
!ui.steam_signed_in && !launcher.settings().offline);
if (modal) draw->AddRectFilled(ImVec2(0, 0), size, rgba(4, 6, 9, 0.72f));
g_drag_allowed = !modal && !ImGui::IsAnyItemHovered();
+3 -1
View File
@@ -233,8 +233,10 @@ struct Ui {
// The MODS tile's "2 of 3 enabled", re-read every few seconds.
std::string mods_detail;
double mods_checked{-100};
// Steam display name for the name plate, re-read every few seconds.
// Steam display name for the name plate, and whether Steam is running and
// signed in (PLAY starts offline otherwise), re-read every few seconds.
std::string steam_name;
bool steam_signed_in{true};
double steam_checked{-100};
};
+2 -1
View File
@@ -184,7 +184,8 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
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.",
"The launcher hides while you play and exits when the game closes. "
"Turn off to have it come back instead.",
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. "
+5 -4
View File
@@ -192,8 +192,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 +928,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 +940,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,