mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 05:26:13 +02:00
Objects: other players' objects are ghosts by default, and their card is a visitor's
Objects other players place are spawned locally, so the game took every one for the local player's own: all were solid, and their card offered the owner's buttons and named the wrong player. - Other players' objects are ghosts the skater passes through, as in the game's own sessions. Each player's row in the session menu has an "Objects: solid / ghost" switch, remembered by Steam ID; a dedicated server's own objects (its parks) are solid unless switched off. `mp objects-solid <SteamID64|server> on|off` does the same. - The card's own Disable and Enable on one object stand over that player's setting until the setting is changed. - The card on an object someone else placed no longer offers Edit or Delete: the game's "is this mine" and "may this be edited" checks answer for who really placed it. It offers Disable instead. - The card names the owner of the object it is for, read from the card's own script, instead of the owner of the object nearest the skater. - A dedicated server has a player card of its own, "Server", shown on what it placed. Server: the Mods folder is kept when the configuration is reloaded. A park mod was reported as not being in the Mods folder after a reload.
This commit is contained in:
1 parent
30a320ff62
commit
28118efc27
20 files changed
+475
-78
No files matched your search
@@ -1,4 +1,9 @@
|
||||
#include "network_object_runtime.h"
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/local_placements.h"
|
||||
#include "Extension/Customization/local_customization_runtime.h"
|
||||
#include "Extension/Multiplayer/Session/object_state.h"
|
||||
#include "Extension/Multiplayer/Session/peer_slots.h"
|
||||
@@ -6,7 +11,14 @@
|
||||
#include "local_placements_runtime.h"
|
||||
#include "Extension/Objects/ParkEditor/park_editor_runtime.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <format>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
@@ -15,6 +27,9 @@ using Key = std::tuple<std::uint64_t, std::uint64_t, std::uint64_t>;
|
||||
struct Live {
|
||||
std::uint64_t entity{}, delete_sent{}, delete_started{}, refresh{};
|
||||
profile::PlacedObject applied;
|
||||
// Whether the native ghost is forced on it, and when that is next made sure of.
|
||||
bool ghosted{};
|
||||
std::uint64_t ghost_refresh{};
|
||||
};
|
||||
struct Create {
|
||||
Key key;
|
||||
@@ -40,6 +55,187 @@ State &state() {
|
||||
static State value;
|
||||
return value;
|
||||
}
|
||||
// The players whose objects are solid for the local player. Not part of State: it outlives
|
||||
// a change of world. Under the native lock, like State.
|
||||
std::set<std::uint64_t> &solid_owners() {
|
||||
static std::set<std::uint64_t> value;
|
||||
return value;
|
||||
}
|
||||
// How the client knows a placed object: the object's number, which is the index part of the
|
||||
// server's entity for it (bits 8 up; the low byte is that entity's generation). The client's
|
||||
// copy has an entity of its own, but its component (+0x24) and its record (+0x28) carry this
|
||||
// number (measured 2026-10-13: server entities 0x25'00000503 and 0x25'00001104 were objects
|
||||
// 5 and 0x11 on the client).
|
||||
std::uint32_t object_number(std::uint64_t server_entity) noexcept {
|
||||
return static_cast<std::uint32_t>(server_entity >> 8) & 0xffffff;
|
||||
}
|
||||
// The numbers of the objects other players placed, and who placed each, for the game's "is
|
||||
// this object mine?" (object_owned_hook): every copy has the local player as its native owner,
|
||||
// so the game would offer the owner's menu on all of them. The game asks as each object appears
|
||||
// on the client, so a number is here from the moment the server's entity is known. Written by
|
||||
// the object update, read by the client's scripts.
|
||||
std::mutex remote_objects_mutex;
|
||||
std::map<std::uint32_t, std::uint64_t> remote_objects;
|
||||
std::size_t published_objects{}; // how many objects there were when remote_objects was last rebuilt
|
||||
// What the local player chose for single objects with the game's own menu (Disable, and
|
||||
// enabling one again): that is the game's ghost native, called on the client. A choice stands
|
||||
// over the owner's setting until that setting is changed. Under remote_objects_mutex.
|
||||
std::map<std::uint32_t, bool> object_choices;
|
||||
void publish_remote_objects(const State &s) {
|
||||
std::map<std::uint32_t, std::uint64_t> numbers;
|
||||
for (const auto &[key, live] : s.live)
|
||||
if (std::get<0>(key)) numbers[object_number(live.entity)] = std::get<0>(key);
|
||||
std::lock_guard lock(remote_objects_mutex);
|
||||
remote_objects = std::move(numbers);
|
||||
std::erase_if(object_choices, [](const auto &choice) { return !remote_objects.contains(choice.first); });
|
||||
published_objects = s.live.size();
|
||||
}
|
||||
void add_remote_object(const Key &key, std::uint64_t entity) {
|
||||
if (!std::get<0>(key)) return;
|
||||
std::lock_guard lock(remote_objects_mutex);
|
||||
remote_objects[object_number(entity)] = std::get<0>(key);
|
||||
}
|
||||
// The number of the placed object an entity is part of, as the game's own checks find it.
|
||||
std::optional<std::uint32_t> client_object_number(std::uint64_t entity) noexcept {
|
||||
namespace placements = addr::local_placements;
|
||||
const auto base = local_runtime().base;
|
||||
std::array<std::uint64_t, 2> root{};
|
||||
reinterpret_cast<void (*)(std::uint64_t *, std::uint64_t, bool)>(base + placements::object_root)(root.data(), entity, true);
|
||||
std::array<std::uint64_t, 8> found{}; // [1] valid, [2] the component
|
||||
reinterpret_cast<void (*)(const std::uint64_t *, std::uint64_t *)>(base + placements::object_component)(root.data(), found.data());
|
||||
std::uint32_t number{};
|
||||
if (!found[1] || !found[2] || !memory::peek(static_cast<std::uintptr_t>(found[2]) + 0x24, number)) return std::nullopt;
|
||||
return number;
|
||||
}
|
||||
void (*object_ghost_original)(const std::uint64_t *, bool){};
|
||||
void object_ghost_hook(const std::uint64_t *entity, bool ghost) {
|
||||
object_ghost_original(entity, ghost);
|
||||
try {
|
||||
if (!entity) return;
|
||||
const auto number = client_object_number(*entity);
|
||||
if (!number) return;
|
||||
std::lock_guard lock(remote_objects_mutex);
|
||||
if (remote_objects.contains(*number)) object_choices[*number] = ghost;
|
||||
} catch (...) {}
|
||||
}
|
||||
// Another player's object is not the local player's to edit: the game's own check for the
|
||||
// card's Edit button does not ask whose the object is (every object is its owner's to see,
|
||||
// where the game runs its own service).
|
||||
bool (*objects_editable_original)(const std::uint64_t *const *){};
|
||||
bool objects_editable_hook(const std::uint64_t *const *entities) {
|
||||
const bool editable = objects_editable_original(entities);
|
||||
if (!editable || !entities) return editable;
|
||||
try {
|
||||
const std::uint64_t *first{};
|
||||
std::uint32_t count{};
|
||||
if (!memory::peek(reinterpret_cast<std::uintptr_t>(entities), first) || !first ||
|
||||
!memory::peek(reinterpret_cast<std::uintptr_t>(first) - 4, count)) return editable;
|
||||
count &= 0x7fffffff;
|
||||
for (std::uint32_t i = 0; i < count && i < 64; ++i) {
|
||||
const auto number = client_object_number(first[i]);
|
||||
if (!number) continue;
|
||||
std::unique_lock lock(remote_objects_mutex, std::try_to_lock);
|
||||
if (lock.owns_lock() && remote_objects.contains(*number)) return false;
|
||||
}
|
||||
} catch (...) {}
|
||||
return editable;
|
||||
}
|
||||
bool (*object_owned_original)(const std::uint64_t *){};
|
||||
bool object_owned_hook(const std::uint64_t *entity) {
|
||||
const bool owned = object_owned_original(entity);
|
||||
if (!owned || !entity) return owned;
|
||||
try {
|
||||
const auto number = client_object_number(*entity);
|
||||
if (!number) return owned;
|
||||
std::unique_lock lock(remote_objects_mutex, std::try_to_lock);
|
||||
return !(lock.owns_lock() && remote_objects.contains(*number));
|
||||
} catch (...) {
|
||||
return owned;
|
||||
}
|
||||
}
|
||||
void install_object_owned_hook() noexcept {
|
||||
static bool attempted{};
|
||||
if (std::exchange(attempted, true)) return;
|
||||
namespace placements = addr::local_placements;
|
||||
const auto base = local_runtime().base;
|
||||
std::array<unsigned char, 16> owned{}, root{}, component{};
|
||||
auto *const target = reinterpret_cast<void *>(base + placements::object_owned);
|
||||
if (!memory::read(base + placements::object_owned, owned) || owned != placements::object_owned_prefix ||
|
||||
!memory::read(base + placements::object_root, root) || root != placements::object_root_prefix ||
|
||||
!memory::read(base + placements::object_component, component) || component != placements::object_component_prefix ||
|
||||
hook_prepare(target, reinterpret_cast<void *>(&object_owned_hook), reinterpret_cast<void **>(&object_owned_original)) != HookOk ||
|
||||
hook_enable(target) != HookOk) {
|
||||
logging::write(logging::Level::warning, logging::Channel::runtime,
|
||||
"Object owners: the game's owner check could not be replaced; other players' objects offer the owner's menu.");
|
||||
return;
|
||||
}
|
||||
std::array<unsigned char, 16> editable{};
|
||||
auto *const editable_target = reinterpret_cast<void *>(base + placements::objects_editable);
|
||||
if (!memory::read(base + placements::objects_editable, editable) || editable != placements::objects_editable_prefix ||
|
||||
hook_prepare(editable_target, reinterpret_cast<void *>(&objects_editable_hook),
|
||||
reinterpret_cast<void **>(&objects_editable_original)) != HookOk ||
|
||||
hook_enable(editable_target) != HookOk)
|
||||
logging::write(logging::Level::warning, logging::Channel::runtime,
|
||||
"Object owners: the game's edit check could not be replaced; other players' objects offer Edit.");
|
||||
// The same functions tell which object the menu's Disable was pressed on.
|
||||
std::array<unsigned char, 16> ghost{};
|
||||
auto *const ghost_target = reinterpret_cast<void *>(base + placements::object_ghost);
|
||||
if (!memory::read(base + placements::object_ghost, ghost) || ghost != placements::object_ghost_prefix ||
|
||||
hook_prepare(ghost_target, reinterpret_cast<void *>(&object_ghost_hook), reinterpret_cast<void **>(&object_ghost_original)) != HookOk ||
|
||||
hook_enable(ghost_target) != HookOk)
|
||||
logging::write(logging::Level::warning, logging::Channel::runtime,
|
||||
"Object ghosting: the menu's own Disable could not be followed; an object disabled there turns solid again.");
|
||||
}
|
||||
// Makes placed object `number` a ghost for the local player, or lets it be solid again,
|
||||
// through the game's own record of it (local_placements.h). False when the client has no
|
||||
// record of it yet (it comes a moment after the object), or when this build's functions are
|
||||
// not the ones expected: objects then stay solid, as they always were.
|
||||
bool set_object_ghost(std::uint32_t number, bool ghost) noexcept {
|
||||
namespace placements = addr::local_placements;
|
||||
static const bool known = [] {
|
||||
std::array<unsigned char, 16> lookup{};
|
||||
const auto base = local_runtime().base;
|
||||
const bool same = memory::read(base + placements::realm_context, lookup) && lookup == placements::realm_context_prefix;
|
||||
if (!same)
|
||||
logging::write(logging::Level::warning, logging::Channel::runtime,
|
||||
"Object ghosting: the game's functions differ; other players' objects stay solid.");
|
||||
return same;
|
||||
}();
|
||||
if (!known) return false;
|
||||
const auto base = local_runtime().base;
|
||||
// The records are the client's, and this runs in the local server's context, where the
|
||||
// game's own function (which takes the calling thread's context) finds no record store.
|
||||
// So the client's context is asked for by name, the record is marked here, and the store
|
||||
// is told to decide every record again, which the client does on its next update.
|
||||
std::uintptr_t client{};
|
||||
reinterpret_cast<std::uintptr_t *(*)(std::uintptr_t *, std::uint32_t)>(base + placements::realm_context)(
|
||||
&client, placements::client_realm);
|
||||
std::uint32_t offset{};
|
||||
std::uintptr_t first{}, last{};
|
||||
std::uint8_t ready{};
|
||||
if (!client || !memory::peek(base + placements::client_ready_offset, offset) || offset > 0x1000000) return false;
|
||||
const auto store = client + offset;
|
||||
if (!memory::peek(store + 0x100, ready) || !ready || !memory::peek(store + 0x48, first) || !memory::peek(store + 0x50, last) ||
|
||||
!first || last < first || (last - first) % 0x50 || (last - first) / 0x50 > 65536)
|
||||
return false;
|
||||
// An object can have several records under its number; the active one carries the flags.
|
||||
std::uintptr_t record{};
|
||||
for (auto at = first; at != last && !record; at += 0x50) {
|
||||
std::uint32_t held{};
|
||||
std::uint8_t active{};
|
||||
if (memory::peek(at + 0x28, held) && held == number && memory::peek(at + 0x3a, active) && active) record = at;
|
||||
}
|
||||
if (!record) return false;
|
||||
// As the game's own native sets them: a forced ghost, or none (and solid forced only on a
|
||||
// record that was not a forced ghost, which ReSkate never asks for).
|
||||
auto *const bytes = reinterpret_cast<std::uint8_t *>(record);
|
||||
if ((bytes[0x3b] != 0) != ghost) {
|
||||
bytes[0x3c] = 0;
|
||||
bytes[0x3b] = ghost;
|
||||
*reinterpret_cast<std::uint8_t *>(store + 0xfb) = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Other players' objects are still being created: the client tick keeps calling
|
||||
// update_network_objects (tick_network_objects) instead of waiting for the 500 ms
|
||||
// customization update, which paced a big park at two objects a second.
|
||||
@@ -64,23 +260,16 @@ void refresh_budget() {
|
||||
extra_budget.store(static_cast<std::uint32_t>(count), std::memory_order_release);
|
||||
}
|
||||
} // namespace network_objects_detail
|
||||
std::uint64_t nearest_network_object_owner(const std::array<float, 3> &position, float reach) noexcept {
|
||||
std::uint64_t network_object_owner_of(std::uint64_t client_entity) noexcept {
|
||||
using namespace network_objects_detail;
|
||||
try {
|
||||
std::unique_lock lock(local_runtime().native_mutex, std::try_to_lock);
|
||||
if (!client_entity) return 0;
|
||||
const auto number = client_object_number(client_entity);
|
||||
if (!number) return 0;
|
||||
std::unique_lock lock(remote_objects_mutex, std::try_to_lock);
|
||||
if (!lock.owns_lock()) return 0;
|
||||
float best = reach * reach;
|
||||
std::uint64_t owner{};
|
||||
// Local objects are in `live` too, under owner 0, so the nearest being your own answers 0.
|
||||
for (const auto &[key, live] : network_objects_detail::state().live) {
|
||||
float squared{};
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
squared += (live.applied.position[i] - position[i]) * (live.applied.position[i] - position[i]);
|
||||
if (squared < best) {
|
||||
best = squared;
|
||||
owner = std::get<0>(key);
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
const auto remote = remote_objects.find(*number);
|
||||
return remote == remote_objects.end() ? 0 : remote->second;
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
@@ -130,6 +319,7 @@ bool network_object_observe(std::uint64_t entity, const profile::PlacedObject &o
|
||||
auto applied = object;
|
||||
applied.id = s.creating->object.id;
|
||||
s.live[s.creating->key] = {entity, 0, 0, 0, std::move(applied)};
|
||||
network_objects_detail::add_remote_object(s.creating->key, entity);
|
||||
s.dirty.insert(s.creating->key);
|
||||
s.creating.reset();
|
||||
network_objects_detail::refresh_budget();
|
||||
@@ -141,6 +331,7 @@ bool network_object_observe(std::uint64_t entity, const profile::PlacedObject &o
|
||||
auto applied = object;
|
||||
applied.id = late->second.object.id;
|
||||
s.live[late->second.key] = {entity, 0, 0, 0, std::move(applied)};
|
||||
network_objects_detail::add_remote_object(late->second.key, entity);
|
||||
s.dirty.insert(late->second.key);
|
||||
s.failed_creates.erase(late->second.key);
|
||||
s.late_creates.erase(late);
|
||||
@@ -271,6 +462,7 @@ void update_network_objects() {
|
||||
}
|
||||
void update_network_object_moves() {
|
||||
auto &s = network_objects_detail::state();
|
||||
if (s.live.empty() && network_objects_detail::published_objects) network_objects_detail::publish_remote_objects(s);
|
||||
if (s.map != placements_runtime().map || !network_object_native_ready() || s.live.empty())
|
||||
return;
|
||||
const auto now = GetTickCount64();
|
||||
@@ -278,9 +470,26 @@ void update_network_object_moves() {
|
||||
// rest are put back where their owner has them every 10 s (the game's own tools can
|
||||
// move a copy, and a move made before its physics body existed placed only the
|
||||
// model), not every second as before. A failed move retries a second later.
|
||||
network_objects_detail::install_object_owned_hook();
|
||||
const auto move = [&](auto it) {
|
||||
auto &live = it->second;
|
||||
const auto wanted = s.desired.find(it->first);
|
||||
// Another player's object is a ghost unless they were made solid. Set when that
|
||||
// changes, and a ghost again with each refresh: one forced before the game had its
|
||||
// record of the object did nothing.
|
||||
if (const auto owner = std::get<0>(it->first); owner && !live.delete_sent && now >= live.ghost_refresh) {
|
||||
const auto number = network_objects_detail::object_number(live.entity);
|
||||
bool ghost = !network_objects_detail::solid_owners().contains(owner);
|
||||
{
|
||||
std::lock_guard choices(network_objects_detail::remote_objects_mutex);
|
||||
if (const auto chosen = network_objects_detail::object_choices.find(number);
|
||||
chosen != network_objects_detail::object_choices.end()) ghost = chosen->second;
|
||||
}
|
||||
// The client's record of a new object comes a moment after the object: try again soon.
|
||||
const bool set = network_objects_detail::set_object_ghost(number, ghost);
|
||||
if (set) live.ghosted = ghost;
|
||||
live.ghost_refresh = now + (set ? 10000 : 1000);
|
||||
}
|
||||
if (!live.delete_sent && wanted != s.desired.end() && wanted->second.item == live.applied.item &&
|
||||
(wanted->second != live.applied || now >= live.refresh)) {
|
||||
try {
|
||||
@@ -319,6 +528,8 @@ void update_network_object_moves() {
|
||||
if (++it == s.live.end())
|
||||
it = s.live.begin();
|
||||
}
|
||||
// When an object went: its number must not name whatever takes it next.
|
||||
if (s.live.size() != network_objects_detail::published_objects) network_objects_detail::publish_remote_objects(s);
|
||||
}
|
||||
} // namespace dingosdk::profile_runtime
|
||||
|
||||
@@ -393,6 +604,24 @@ void clear_remote_network_objects() {
|
||||
s.dirty.clear();
|
||||
profile_runtime::network_objects_detail::refresh_budget();
|
||||
}
|
||||
void set_network_object_owner_solid(std::uint64_t owner, bool solid) {
|
||||
using namespace profile_runtime;
|
||||
if (!owner) return;
|
||||
std::lock_guard lock(local_runtime().native_mutex);
|
||||
auto &owners = network_objects_detail::solid_owners();
|
||||
if (solid == owners.contains(owner)) return;
|
||||
if (solid) owners.insert(owner);
|
||||
else owners.erase(owner);
|
||||
// Their objects go first in the next pass.
|
||||
auto &s = network_objects_detail::state();
|
||||
std::lock_guard choices(network_objects_detail::remote_objects_mutex);
|
||||
for (auto &[key, live] : s.live)
|
||||
if (std::get<0>(key) == owner) {
|
||||
network_objects_detail::object_choices.erase(network_objects_detail::object_number(live.entity));
|
||||
live.ghost_refresh = 0;
|
||||
s.dirty.insert(key);
|
||||
}
|
||||
}
|
||||
void remove_remote_network_objects(std::uint64_t owner, std::uint64_t epoch) {
|
||||
if (!owner || !epoch)
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user