mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 19:47:08 +02:00
712 lines
34 KiB
C++
712 lines
34 KiB
C++
#include "native_indicators.h"
|
|
#include "Extension/Multiplayer/Session/peer_slots.h"
|
|
#include "native_indicator_slots.h"
|
|
#include "native_nametag_context.h"
|
|
#include "native_party.h"
|
|
#include "Engine/Game/Abi/native_data.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/engine.h"
|
|
#include "Engine/Game/Build/20260929/native_indicators.h"
|
|
#include "Engine/Game/Build/20260929/native_menu.h"
|
|
#include "Engine/Game/Build/20260929/native_party.h"
|
|
#include <Windows.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <vector>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
namespace {
|
|
using Address = std::uintptr_t;
|
|
namespace engine = addr::engine;
|
|
namespace indicators = addr::native_indicators;
|
|
namespace party = addr::native_party;
|
|
// Presenter, registry, widget and texture records of the live HUD: guarded copies,
|
|
// not a system call per read (the presenter tick runs every frame).
|
|
template <class T> T read(Address address) {
|
|
T value{};
|
|
if (!memory::peek(address, value))
|
|
throw std::runtime_error("Native indicator memory unavailable.");
|
|
return value;
|
|
}
|
|
void require(bool value, const char *message) {
|
|
if (!value)
|
|
throw std::runtime_error(message);
|
|
}
|
|
bool read_bytes(Address address, void *value, std::size_t size) {
|
|
return memory::peek_bytes(address, value, size);
|
|
}
|
|
struct Provider;
|
|
struct ProviderMethods {
|
|
void (*destroy)(Provider *, unsigned);
|
|
bool (*transform)(Provider *, void *);
|
|
Address (*blueprint)(Provider *);
|
|
Address (*parent)(Provider *);
|
|
Address (*owner)(Provider *);
|
|
Address (*data)(Provider *);
|
|
bool (*enabled)(Provider *);
|
|
void (*unregister)(Provider *);
|
|
int (*sort)(Provider *);
|
|
};
|
|
// ID and channel vector are read directly by DingoIndicatorPresenter.
|
|
// The remaining fields are private to our position/data adapter. The game
|
|
// creates, draws, projects and destroys the actual Rime widgets.
|
|
struct Provider {
|
|
const ProviderMethods *methods{};
|
|
std::uint32_t id{}, padding{};
|
|
const std::uint32_t *begin{}, *end{}, *capacity{};
|
|
Address allocator{};
|
|
std::array<std::uint32_t, 8> channels{};
|
|
Address source{}, blueprint{}, parent{}, owner{}, data{};
|
|
std::array<float, 3> offset{};
|
|
std::array<float, 16> world{};
|
|
bool active{}, bound{}, failed{}, compass{};
|
|
Address widget_parent{};
|
|
std::size_t slot{};
|
|
};
|
|
static_assert(offsetof(Provider, id) == 8 && offsetof(Provider, begin) == 0x10 &&
|
|
offsetof(Provider, allocator) == 0x28 && sizeof(ProviderMethods) == 0x48);
|
|
struct Callback {
|
|
void *owner{};
|
|
void (*invoke)(void *, Address, Address){};
|
|
};
|
|
// The party compass icon found in the texture registry, under native_mutex.
|
|
// Reused while the registry still lists it in the same bucket (so it is still
|
|
// loaded); the whole registry is walked again only when it is not.
|
|
struct CompassTexture {
|
|
Address manager{}, table{}, asset{};
|
|
std::uint32_t buckets{}, bucket{};
|
|
// After a walk that found neither icon: no new walk for this registry
|
|
// size until retry_at, however many peers retry their compass.
|
|
std::uint32_t missing_count{};
|
|
ULONGLONG retry_at{};
|
|
};
|
|
struct Shared {
|
|
std::recursive_mutex native_mutex;
|
|
Address base{};
|
|
std::atomic<bool> installed{};
|
|
std::atomic<bool> nametags{true}, compass{true};
|
|
void (*tick)(Address){};
|
|
void (*remove)(Address, Provider *){};
|
|
Address (*widget)(Address, Address, int, Address, Address, const Callback *, const Callback *){};
|
|
bool attempted{};
|
|
CompassTexture compass_texture;
|
|
};
|
|
Shared &shared() {
|
|
static auto *s = new Shared;
|
|
return *s;
|
|
}
|
|
struct State {
|
|
std::mutex snapshot_mutex;
|
|
// Bumped by each client snapshot. A slot whose snapshot refresh() has already
|
|
// handled as absent (idle_generation) has nothing to do on presenter ticks.
|
|
std::atomic<std::uint64_t> generation{};
|
|
std::uint64_t idle_generation{}; // presenter tick only
|
|
bool visible{};
|
|
ULONGLONG sampled{}, next_discovery{}, next_refresh{};
|
|
std::array<float, 16> world{};
|
|
std::string name, published_name;
|
|
Address model_manager{}, context_type{};
|
|
std::uint64_t player_info{}, nametag_context{};
|
|
std::string status = "Native nametag/compass: waiting for a peer.";
|
|
Provider nametag, compass;
|
|
};
|
|
State &state() {
|
|
// DLL is pinned until process exit. Providers must outlive deferred native
|
|
// removals, including shutdown paths that no longer tick their presenter.
|
|
static auto *s = new PeerStorage<State>;
|
|
return s->current();
|
|
}
|
|
thread_local Address current_presenter{};
|
|
void status(std::string value) {
|
|
auto &s = state();
|
|
std::lock_guard lock(s.snapshot_mutex);
|
|
if (s.status != value) {
|
|
s.status = std::move(value);
|
|
logging::write(logging::Level::info, logging::Channel::ui, s.status);
|
|
}
|
|
}
|
|
void stop(Provider &provider);
|
|
void destroy_provider(Provider *p, unsigned) { stop(*p); }
|
|
bool provider_transform(Provider *p, void *out) {
|
|
if (!p->active || p->failed)
|
|
return false;
|
|
std::memcpy(out, p->world.data(), sizeof(p->world));
|
|
return true;
|
|
}
|
|
Address provider_blueprint(Provider *p) { return p->blueprint; }
|
|
Address provider_parent(Provider *p) { return p->parent; }
|
|
Address provider_owner(Provider *p) { return p->owner; }
|
|
Address provider_data(Provider *) {
|
|
// A source entity would connect the new widget to its existing bindings.
|
|
// Null creates independent widget properties, populated by our pre-init
|
|
// callback. Never write the session marker's/player's shared context.
|
|
return 0;
|
|
}
|
|
bool provider_enabled(Provider *p) { return p->active && !p->failed; }
|
|
void provider_unregister(Provider *p) { stop(*p); }
|
|
int provider_sort(Provider *) { return 0; }
|
|
const ProviderMethods methods{destroy_provider, provider_transform, provider_blueprint,
|
|
provider_parent, provider_owner, provider_data,
|
|
provider_enabled, provider_unregister, provider_sort};
|
|
Address registry() { return shared().base + indicators::presenter_registry; }
|
|
std::vector<Address> presenters() {
|
|
const auto r = registry();
|
|
const auto table = read<Address>(r + 0x20);
|
|
const auto buckets = read<std::uint32_t>(r + 0x28);
|
|
const auto count = read<std::uint32_t>(r + 0x2c);
|
|
require(buckets <= 4096 && count <= 32, "Native indicator presenter table exceeds bound.");
|
|
std::vector<Address> result;
|
|
for (unsigned i = 0; i < buckets && result.size() < count; ++i) {
|
|
for (auto node = read<Address>(table + i * 8); node;) {
|
|
require(result.size() < count, "Native indicator presenter chain is invalid.");
|
|
const auto presenter = read<Address>(node);
|
|
require(read<Address>(presenter) == shared().base + indicators::hud_presenter_vtable,
|
|
"Native indicator presenter type differs.");
|
|
result.push_back(presenter);
|
|
node = read<Address>(node + 8);
|
|
}
|
|
}
|
|
require(result.size() == count, "Native indicator presenter count differs.");
|
|
return result;
|
|
}
|
|
void stop(Provider &p) {
|
|
if (!p.active)
|
|
return;
|
|
p.active = false;
|
|
shared().remove(registry(), &p);
|
|
// Release widgets before their borrowed template parent/assets can unload.
|
|
// This is the game's pending-removal pass, on the native UI lifecycle path.
|
|
for (const auto presenter : presenters())
|
|
reinterpret_cast<void (*)(Address)>(shared().base + indicators::presenter_flush)(presenter);
|
|
p.source = p.blueprint = p.parent = p.owner = p.data = p.widget_parent = 0;
|
|
p.bound = p.failed = false;
|
|
}
|
|
void remove_hook(Address r, Provider *provider) {
|
|
std::lock_guard lock(shared().native_mutex);
|
|
// Idle slots (see tick_hook) have no registered provider.
|
|
if (shared().installed.load(std::memory_order_acquire) && r == registry()) each_peer([&] {
|
|
auto &s = state();
|
|
if (s.generation.load(std::memory_order_acquire) != s.idle_generation) {
|
|
for (auto *p : {&s.nametag, &s.compass}) {
|
|
if (p->active && p->source == reinterpret_cast<Address>(provider)) {
|
|
try {
|
|
stop(*p);
|
|
s.next_discovery = 0;
|
|
} catch (const std::exception &e) {
|
|
p->failed = true;
|
|
status(e.what());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
shared().remove(r, provider);
|
|
}
|
|
Address property(Address parent, Address data, std::uint32_t hash, bool output) {
|
|
return indicator_property(read_bytes, parent, data, hash, output);
|
|
}
|
|
void set_property(Provider &p, Address parent, std::uint32_t hash, Address type, const void *value) {
|
|
const auto interface_data = read<Address>(p.blueprint + 0x30);
|
|
const auto slot = property(parent, interface_data, hash, true);
|
|
require(slot && parent != p.parent, "Native indicator widget has no independent output property.");
|
|
const auto source_slot = property(p.parent, p.data, hash, false);
|
|
require(slot != source_slot, "Native indicator property aliases the template source.");
|
|
const auto actual_type = read<Address>(slot + 8);
|
|
require(!actual_type || actual_type == type, "Native indicator property type differs.");
|
|
Address initialized{};
|
|
reinterpret_cast<void (*)(Address, Address *, Address, std::uint32_t, Address, const void *)>(
|
|
shared().base + indicators::property_bind)(parent, &initialized, interface_data, hash, type, nullptr);
|
|
require(initialized == slot, "Native indicator output binding differs.");
|
|
reinterpret_cast<void (*)(const Address *, const void *, std::uint8_t)>(shared().base + engine::set_customization_flag)(
|
|
&initialized, value, 1);
|
|
}
|
|
void copy_compass_property(Provider &p, Address parent, std::uint32_t hash, Address type) {
|
|
const auto slot = property(p.parent, p.data, hash, false);
|
|
const auto argument = indicator_property_argument(read_bytes, shared().base, slot, type);
|
|
// The template parent remains live until remove_hook synchronously releases
|
|
// this widget; assets and models are borrowed read-only for that lifetime.
|
|
// The native setter retains asset references and copies ordinary values.
|
|
// Passing &asset here would retain a stack address as an engine asset.
|
|
set_property(p, parent, hash, type, reinterpret_cast<const void *>(argument));
|
|
}
|
|
void bind_party_compass_icon(Provider &p, Address parent) {
|
|
// The texture registry owns these assets. Hold its native lock until the
|
|
// widget setter has retained the selected asset; never cache a borrowed
|
|
// texture across map/asset unloading.
|
|
const auto manager = read<Address>(shared().base + addr::native_menu::texture_registry);
|
|
require(manager, "Native player compass texture registry unavailable.");
|
|
auto *section = reinterpret_cast<CRITICAL_SECTION *>(manager + 0x188);
|
|
EnterCriticalSection(section);
|
|
struct Unlock { CRITICAL_SECTION *value; ~Unlock() { LeaveCriticalSection(value); } } unlock{section};
|
|
require(read<Address>(shared().base + addr::native_menu::texture_registry) == manager,
|
|
"Native texture registry changed during compass binding.");
|
|
const auto buckets = read<std::uint32_t>(manager + 0xa0);
|
|
const auto count = read<std::uint32_t>(manager + 0xa4);
|
|
const auto table = read<Address>(manager + 0x98);
|
|
require(buckets && buckets <= 65536 && count <= 32768 && table,
|
|
"Native texture registry exceeds bound.");
|
|
const auto image = [](Address asset) {
|
|
return asset && read<Address>(asset) == shared().base + addr::native_menu::image_asset_vtable &&
|
|
read<Address>(asset + 8) == shared().base + addr::native_menu::image_asset_type;
|
|
};
|
|
constexpr const char *party_icon = "UI/Textures/Icons/Decorators/img_Decorator_PartyMember_64";
|
|
const auto named = [](Address asset, const char *expected) {
|
|
std::array<char, 128> value{};
|
|
const auto length = std::strlen(expected) + 1;
|
|
return length <= value.size() && memory::peek_bytes(read<Address>(asset + 0x18), value.data(), length) &&
|
|
std::memcmp(value.data(), expected, length) == 0;
|
|
};
|
|
// The icon found for an earlier widget, if the registry still lists it in the
|
|
// same bucket: its own chain proves the texture is still loaded.
|
|
auto &cached = shared().compass_texture;
|
|
if (cached.asset && cached.manager == manager && cached.table == table && cached.buckets == buckets &&
|
|
cached.bucket < buckets) {
|
|
unsigned visited{};
|
|
for (auto node = read<Address>(table + cached.bucket * 8ULL); node; node = read<Address>(node + 16)) {
|
|
require(++visited <= count, "Native texture registry chain differs.");
|
|
if (read<Address>(node + 8) == cached.asset && image(cached.asset) && named(cached.asset, party_icon)) {
|
|
set_property(p, parent, 0xbdd7e0da, shared().base + indicators::compass_icon_type,
|
|
reinterpret_cast<const void *>(cached.asset));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// After a walk that found neither icon, the next is due once the registry has
|
|
// grown or a second has passed, however many peers retry their compass.
|
|
const auto now = GetTickCount64();
|
|
require(cached.asset || cached.manager != manager || cached.table != table || cached.buckets != buckets ||
|
|
cached.missing_count != count || now >= cached.retry_at,
|
|
"Waiting for the native party/player compass texture.");
|
|
Address player{}, party{};
|
|
unsigned visited{}, party_bucket{};
|
|
for (unsigned i = 0; i < buckets && !party; ++i) {
|
|
for (auto node = read<Address>(table + i * 8ULL); node; node = read<Address>(node + 16)) {
|
|
require(++visited <= count, "Native texture registry chain differs.");
|
|
const auto asset = read<Address>(node + 8);
|
|
if (!image(asset)) continue;
|
|
if (named(asset, party_icon)) {
|
|
party = asset;
|
|
party_bucket = i;
|
|
}
|
|
if (named(asset, "UI/Textures/Icons/Map/img_Icon_Map_Player_128")) player = asset;
|
|
}
|
|
}
|
|
// Only the preferred icon is kept: with just the player icon, a later walk
|
|
// may still find the party one.
|
|
if (party)
|
|
cached = {manager, table, party, buckets, party_bucket};
|
|
else if (!player)
|
|
cached = {manager, table, 0, buckets, 0, count, now + 1000};
|
|
else
|
|
cached = {};
|
|
require(party || player, "Waiting for the native party/player compass texture.");
|
|
set_property(p, parent, 0xbdd7e0da, shared().base + indicators::compass_icon_type,
|
|
reinterpret_cast<const void *>(party ? party : player));
|
|
}
|
|
void bind_widget(void *context, Address, Address widget) {
|
|
auto &p = *static_cast<Provider *>(context);
|
|
const PeerScope scope(p.slot);
|
|
try {
|
|
auto &s = state();
|
|
const auto parent = read<Address>(widget + 0x30);
|
|
require(parent && read<Address>(parent) == shared().base + indicators::widget_parent_vtable,
|
|
"Native indicator widget parent differs.");
|
|
if (p.compass) {
|
|
copy_compass_property(p, parent, 0x02043b74, shared().base + engine::uint64_type);
|
|
bind_party_compass_icon(p, parent);
|
|
copy_compass_property(p, parent, 0x0ca8c5f8, shared().base + indicators::vec3_type);
|
|
} else {
|
|
require(s.nametag_context && s.nametag_context != s.player_info,
|
|
"Native nametag context unavailable.");
|
|
set_property(p, parent, 0x161286ea, shared().base + engine::uint64_type, &s.nametag_context);
|
|
}
|
|
p.widget_parent = parent;
|
|
p.bound = true;
|
|
logging::printf(logging::Level::debug, logging::Channel::ui,
|
|
"Native peer %s widget created: id=%u widget=%p parent=%p",
|
|
p.compass ? "compass" : "nametag", p.id, reinterpret_cast<void *>(widget),
|
|
reinterpret_cast<void *>(parent));
|
|
} catch (const std::exception &e) {
|
|
p.failed = true;
|
|
status(e.what());
|
|
}
|
|
}
|
|
Address widget_hook(Address cache, Address result, int id, Address list, Address info, const Callback *before,
|
|
const Callback *after) {
|
|
if (!current_presenter || cache != current_presenter + 0x268 || !info || !before || !after)
|
|
return shared().widget(cache, result, id, list, info, before, after);
|
|
std::lock_guard lock(shared().native_mutex);
|
|
if (shared().installed.load(std::memory_order_acquire)) {
|
|
for (std::size_t i = 0; i < max_remote_players; ++i) {
|
|
const PeerScope scope(i);
|
|
auto &s = state();
|
|
if (s.generation.load(std::memory_order_acquire) == s.idle_generation)
|
|
continue; // idle slots (see tick_hook) have no registered provider
|
|
for (auto *p : {&s.nametag, &s.compass}) {
|
|
if (!p->active || p->id != static_cast<unsigned>(id) ||
|
|
read<Address>(info + 0x30) != p->blueprint)
|
|
continue;
|
|
// Only the native indicator call uses empty callbacks. Preserve
|
|
// every other widget/cache request untouched.
|
|
if ((!before->owner && !before->invoke) && (!after->owner && !after->invoke)) {
|
|
const Callback binding{p, bind_widget};
|
|
return shared().widget(cache, result, id, list, info, &binding, after);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return shared().widget(cache, result, id, list, info, before, after);
|
|
}
|
|
// Native model type lookup, called only under the model manager's write lock.
|
|
Address model_type(Address manager, std::uint64_t handle) {
|
|
const auto base = shared().base;
|
|
const auto record = reinterpret_cast<Address (*)(Address, std::uint64_t, std::uint8_t)>(base + party::model_record)(
|
|
manager, handle, 0);
|
|
require(record, "Native nametag model record unavailable.");
|
|
return reinterpret_cast<Address (*)(Address, std::uint64_t, Address, std::uint8_t)>(base + party::model_type)(
|
|
manager, handle, record, 0);
|
|
}
|
|
void prepare_nametag_context(Provider &p);
|
|
void update_name(const std::string &name) {
|
|
auto &s = state();
|
|
auto &n = game::native_data().models;
|
|
require(n.create && n.value && n.field && n.publish, "Native player model API unavailable.");
|
|
const auto ui = read<Address>(shared().base + engine::ui_manager);
|
|
const auto manager = ui ? read<Address>(ui + 0x140) : 0;
|
|
require(manager, "Native player model manager unavailable.");
|
|
game::ModelWriteLock lock(manager);
|
|
if (s.model_manager != manager) {
|
|
// Old handles belong to the old manager and must never be dereferenced.
|
|
s.model_manager = manager;
|
|
s.player_info = s.nametag_context = s.context_type = 0;
|
|
s.published_name.clear();
|
|
}
|
|
if (const auto party_info = native_party_player_info(manager, peer_slot)) {
|
|
const bool changed = s.player_info != party_info;
|
|
s.player_info = party_info;
|
|
if (changed && s.nametag.active) prepare_nametag_context(s.nametag);
|
|
return;
|
|
}
|
|
if (!s.player_info || !n.value(manager, s.player_info, 0, 0)) {
|
|
s.published_name.clear();
|
|
s.player_info = n.create(
|
|
manager, shared().base + engine::player_info_type, 0,
|
|
game::native_name_hash(("ReSkate.Multiplayer.PlayerInfo." + std::to_string(peer_slot)).c_str()),
|
|
false, 2);
|
|
require(s.player_info, "Cannot allocate native peer name record.");
|
|
alignas(8) std::array<std::byte, 0x108> info{};
|
|
reinterpret_cast<void (*)(void *)>(shared().base + engine::card_info_construct)(info.data());
|
|
// publish also returns false when the initialized value is unchanged.
|
|
n.publish(manager, s.player_info, shared().base + engine::player_info_type, info.data());
|
|
require(n.value(manager, s.player_info, 0, 0), "Cannot initialize native peer name record.");
|
|
}
|
|
const std::string text = name.empty() ? "Remote skater" : name;
|
|
if (s.published_name == text)
|
|
return;
|
|
require(model_type(manager, s.player_info) == shared().base + engine::player_info_type,
|
|
"Native peer record type differs.");
|
|
const auto user = n.field(manager, s.player_info, 1, UINT32_MAX, false);
|
|
require(user && model_type(manager, user) == shared().base + indicators::user_info_type,
|
|
"Native peer user-info field differs.");
|
|
struct NativeString {
|
|
Address base, value{};
|
|
~NativeString() {
|
|
if (value)
|
|
reinterpret_cast<void (*)(Address *)>(base + engine::native_text_release)(&value);
|
|
}
|
|
} native{shared().base};
|
|
game::native_data().values.assign(&native.value, text.c_str(), static_cast<std::uint32_t>(text.size()));
|
|
for (const unsigned index : {0U, 4U}) {
|
|
const auto field = n.field(manager, user, index, UINT32_MAX, false);
|
|
require(field && model_type(manager, field) == shared().base + engine::string_type,
|
|
"Native peer display-name field differs.");
|
|
n.publish(manager, field, shared().base + engine::string_type, &native.value);
|
|
const auto stored = n.value(manager, field, 0, 0);
|
|
require(stored && read<Address>(stored), "Native peer display name is null.");
|
|
std::vector<char> published(text.size() + 1);
|
|
require(read_bytes(read<Address>(stored), published.data(), published.size()) &&
|
|
std::memcmp(published.data(), text.c_str(), published.size()) == 0,
|
|
"Native peer display name publication differs.");
|
|
}
|
|
s.published_name = text;
|
|
}
|
|
void prepare_nametag_context(Provider &p) {
|
|
auto &s = state();
|
|
auto &n = game::native_data().models;
|
|
game::ModelWriteLock lock(s.model_manager);
|
|
const auto slot = property(p.parent, p.data, 0x161286ea, false);
|
|
const auto argument =
|
|
indicator_property_argument(read_bytes, shared().base, slot, shared().base + engine::uint64_type);
|
|
const auto source = read<std::uint64_t>(argument);
|
|
const auto type = model_type(s.model_manager, source);
|
|
const auto value = n.value(s.model_manager, source, 0, 0);
|
|
alignas(8) const auto context =
|
|
nametag_context_copy(read_bytes, shared().base, type, value, s.player_info);
|
|
// Prove that the template's inherited field really is a player handle.
|
|
const auto original_player = read<std::uint64_t>(value);
|
|
require(model_type(s.model_manager, original_player) == shared().base + engine::player_info_type,
|
|
"Native nametag template does not reference player info.");
|
|
if (s.context_type != type || !s.nametag_context || !n.value(s.model_manager, s.nametag_context, 0, 0)) {
|
|
s.context_type = type;
|
|
s.nametag_context =
|
|
n.create(s.model_manager, type, 0,
|
|
game::native_name_hash(
|
|
("ReSkate.Multiplayer.NametagContext." + std::to_string(peer_slot)).c_str()),
|
|
false, 2);
|
|
}
|
|
require(s.nametag_context && s.nametag_context != source && s.nametag_context != s.player_info,
|
|
"Native nametag context must be independently owned.");
|
|
n.publish(s.model_manager, s.nametag_context, type, context.data());
|
|
const auto stored = n.value(s.model_manager, s.nametag_context, 0, 0);
|
|
require(model_type(s.model_manager, s.nametag_context) == type && stored &&
|
|
read<std::uint64_t>(stored) == s.player_info,
|
|
"Native nametag player-context binding differs.");
|
|
logging::printf(logging::Level::debug, logging::Channel::ui,
|
|
"Native nametag context: owned=%llx source=%llx player=%llx type=%p bytes=%zu",
|
|
s.nametag_context, source, s.player_info, reinterpret_cast<void *>(type), context.size());
|
|
}
|
|
bool find_template(Provider &target, const std::vector<Address> &sources, const char *name) {
|
|
for (const auto source : sources) {
|
|
if (!source || read<Address>(source) != shared().base + indicators::indicator_source_vtable)
|
|
continue;
|
|
const auto data = read<Address>(source - 8);
|
|
const auto blueprint = read<Address>(data + 0x70) & ~Address{4};
|
|
if (!blueprint)
|
|
continue;
|
|
std::array<char, 160> actual{};
|
|
if (!read_bytes(read<Address>(blueprint + 0x18), actual.data(), std::strlen(name) + 1) ||
|
|
std::strcmp(actual.data(), name) != 0)
|
|
continue;
|
|
const auto parent = read<Address>(source - 0x10);
|
|
require(parent && read<Address>(parent) == shared().base + indicators::widget_parent_vtable,
|
|
"Native indicator source parent differs.");
|
|
const auto begin = read<Address>(source + 0x10), end = read<Address>(source + 0x18);
|
|
require(end >= begin && (end - begin) % 4 == 0 && (end - begin) / 4 <= target.channels.size(),
|
|
"Native indicator channel list exceeds bound.");
|
|
target.methods = &methods;
|
|
target.slot = peer_slot;
|
|
target.source = source;
|
|
target.blueprint = blueprint;
|
|
target.parent = parent;
|
|
target.owner = read<Address>(parent + 8);
|
|
target.data = data;
|
|
target.offset = read<std::array<float, 3>>(data + 0xe0);
|
|
for (const auto f : target.offset)
|
|
require(std::isfinite(f) && std::abs(f) < 20, "Native indicator anchor offset differs.");
|
|
require(target.owner, "Native indicator template owner unavailable.");
|
|
const auto count = (end - begin) / 4;
|
|
if (count)
|
|
require(read_bytes(begin, target.channels.data(), end - begin),
|
|
"Native indicator channels unavailable.");
|
|
target.begin = target.channels.data();
|
|
target.end = target.capacity = target.begin + count;
|
|
target.bound = target.failed = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
void refresh() {
|
|
auto &s = state();
|
|
const auto now = GetTickCount64();
|
|
if (now < s.next_refresh)
|
|
return;
|
|
s.next_refresh = now + 50;
|
|
std::array<float, 16> world;
|
|
std::string name;
|
|
bool visible;
|
|
std::uint64_t generation{};
|
|
{
|
|
std::lock_guard lock(s.snapshot_mutex);
|
|
visible = s.visible && now - s.sampled < 1000;
|
|
world = s.world;
|
|
name = s.name;
|
|
generation = s.generation.load(std::memory_order_relaxed);
|
|
}
|
|
if (!visible) {
|
|
stop(s.nametag);
|
|
stop(s.compass);
|
|
s.next_discovery = 0;
|
|
status("Native nametag/compass: waiting for a peer.");
|
|
// Nothing more to do here until the client publishes this slot again.
|
|
s.idle_generation = generation;
|
|
return;
|
|
}
|
|
const bool nametags = shared().nametags.load(std::memory_order_relaxed);
|
|
const bool compass = shared().compass.load(std::memory_order_relaxed);
|
|
if (!nametags && (s.nametag.active || s.nametag.failed))
|
|
stop(s.nametag);
|
|
if (!compass && (s.compass.active || s.compass.failed))
|
|
stop(s.compass);
|
|
if (s.nametag.active)
|
|
update_name(name);
|
|
if (now >= s.next_discovery) {
|
|
s.next_discovery = now + 1000;
|
|
// Native icon textures may stream in after the widget blueprint.
|
|
// Retry failed providers through normal unregister/register ownership.
|
|
if (s.nametag.failed) stop(s.nametag);
|
|
if (s.compass.failed) stop(s.compass);
|
|
const auto begin = read<Address>(registry()), end = read<Address>(registry() + 8);
|
|
require(end >= begin && (end - begin) % 8 == 0 && (end - begin) / 8 <= 8192,
|
|
"Native indicator registry exceeds bound.");
|
|
std::vector<Address> sources((end - begin) / 8);
|
|
if (!sources.empty())
|
|
require(read_bytes(begin, sources.data(), end - begin),
|
|
"Native indicator registry unavailable.");
|
|
for (auto *p : {&s.nametag, &s.compass}) {
|
|
if (p->active || (p == &s.nametag && !nametags) || (p == &s.compass && !compass))
|
|
continue;
|
|
const char *asset =
|
|
p == &s.nametag
|
|
? "UI/Features/Social/InspectPlayer/Indicators/Widgets/PlayerNametag_Indicator_Widget"
|
|
: "UI/Features/HUD/Compass/CompassTagIndicator";
|
|
if (!find_template(*p, sources, asset))
|
|
continue;
|
|
p->compass = p == &s.compass;
|
|
if (!p->compass) {
|
|
update_name(name);
|
|
prepare_nametag_context(*p);
|
|
}
|
|
p->active = true;
|
|
reinterpret_cast<void (*)(Address, Provider *)>(shared().base + indicators::indicator_register)(registry(), p);
|
|
require(p->id, "Native indicator registration did not assign an ID.");
|
|
}
|
|
}
|
|
for (auto *p : {&s.nametag, &s.compass}) {
|
|
p->world = world;
|
|
for (unsigned i = 0; i < 3; ++i)
|
|
p->world[12 + i] += p->offset[i];
|
|
// Extra headroom for the peer's native nametag, above its authored
|
|
// anchor. This affects only the owned label provider's world position.
|
|
if (p == &s.nametag)
|
|
p->world[13] += 0.30f;
|
|
}
|
|
if (!s.nametag.failed && !s.compass.failed) {
|
|
const auto description = [](const Provider &p) {
|
|
return p.bound ? "bound" : p.active ? "registered" : "waiting for template";
|
|
};
|
|
status(std::string("Native nametag: ") + (nametags ? description(s.nametag) : "off") +
|
|
"; compass: " + (compass ? description(s.compass) : "off") + ".");
|
|
}
|
|
}
|
|
void tick_hook(Address presenter) {
|
|
std::lock_guard lock(shared().native_mutex);
|
|
if (shared().installed.load(std::memory_order_acquire)) {
|
|
// The presenter's type is the same for every slot: checked once per tick.
|
|
Address type{};
|
|
const bool presenter_ok =
|
|
memory::peek(presenter, type) && type == shared().base + indicators::hud_presenter_vtable;
|
|
each_peer([&] {
|
|
auto &s = state();
|
|
// Slots never published, or already returned to idle, are skipped.
|
|
if (s.generation.load(std::memory_order_acquire) == s.idle_generation)
|
|
return;
|
|
try {
|
|
require(presenter_ok, "Native HUD presenter type differs.");
|
|
refresh();
|
|
} catch (const std::exception &e) {
|
|
s.nametag.failed = s.compass.failed = true;
|
|
status(e.what());
|
|
}
|
|
});
|
|
}
|
|
const auto previous = current_presenter;
|
|
current_presenter = presenter;
|
|
shared().tick(presenter);
|
|
current_presenter = previous;
|
|
}
|
|
void install(Address base) {
|
|
shared().attempted = true;
|
|
shared().base = base;
|
|
auto prefix = [base](Address rva, const auto &bytes) {
|
|
std::array<std::uint8_t, 32> actual{};
|
|
require(bytes.size() <= actual.size() &&
|
|
memory::read_bytes(base + rva, actual.data(), bytes.size()) &&
|
|
std::equal(bytes.begin(), bytes.end(), actual.begin()),
|
|
"Native indicator function fingerprint differs.");
|
|
};
|
|
prefix(indicators::hud_tick, indicators::hud_tick_prefix);
|
|
prefix(indicators::provider_remove, indicators::provider_remove_prefix);
|
|
prefix(indicators::indicator_register, indicators::indicator_register_prefix);
|
|
prefix(indicators::presenter_flush, indicators::presenter_flush_prefix);
|
|
prefix(indicators::widget_create, indicators::widget_create_prefix);
|
|
prefix(indicators::property_bind, indicators::property_bind_prefix);
|
|
prefix(engine::set_customization_flag, indicators::set_customization_flag_prefix);
|
|
prefix(indicators::class_reference_getter, indicators::class_reference_getter_prefix);
|
|
prefix(party::model_record, indicators::model_record_prefix);
|
|
prefix(party::model_type, indicators::model_type_prefix);
|
|
prefix(engine::card_info_construct, indicators::card_info_construct_prefix);
|
|
prefix(engine::native_text_release, indicators::native_text_release_prefix);
|
|
struct Hook {
|
|
Address rva;
|
|
void *replacement;
|
|
void **original;
|
|
};
|
|
const std::array hooks{
|
|
Hook{indicators::hud_tick, reinterpret_cast<void *>(tick_hook), reinterpret_cast<void **>(&shared().tick)},
|
|
Hook{indicators::provider_remove, reinterpret_cast<void *>(remove_hook), reinterpret_cast<void **>(&shared().remove)},
|
|
Hook{indicators::widget_create, reinterpret_cast<void *>(widget_hook), reinterpret_cast<void **>(&shared().widget)}};
|
|
unsigned prepared{};
|
|
for (const auto &hook : hooks) {
|
|
if (hook_prepare(reinterpret_cast<void *>(base + hook.rva), hook.replacement, hook.original) !=
|
|
HookOk) {
|
|
while (prepared)
|
|
hook_remove(reinterpret_cast<void *>(base + hooks[--prepared].rva));
|
|
throw std::runtime_error("Cannot prepare native indicator hooks.");
|
|
}
|
|
++prepared;
|
|
}
|
|
// One transaction (one suspension of every game thread) for all of them.
|
|
for (const auto &hook : hooks)
|
|
require(hook_queue_enable(reinterpret_cast<void *>(base + hook.rva)) == HookOk,
|
|
"Cannot enable native indicator hooks; restart ReSkate.");
|
|
require(hook_apply_queued() == HookOk, "Cannot enable native indicator hooks; restart ReSkate.");
|
|
shared().installed.store(true, std::memory_order_release);
|
|
}
|
|
} // namespace
|
|
void update_native_indicators(Address base, const Pose *pose, const std::string &name) noexcept {
|
|
auto &s = state();
|
|
try {
|
|
{
|
|
std::lock_guard lock(s.snapshot_mutex);
|
|
// The indicator consumes only the root. PoseBuffer already
|
|
// validated the complete packet at admission.
|
|
s.visible = pose && valid_transform(pose->root);
|
|
s.sampled = GetTickCount64();
|
|
s.name = name;
|
|
if (s.visible)
|
|
s.world = to_matrix(pose->root);
|
|
s.generation.fetch_add(1, std::memory_order_release);
|
|
}
|
|
if (pose && !shared().attempted)
|
|
install(base);
|
|
} catch (const std::exception &e) {
|
|
status(e.what());
|
|
}
|
|
}
|
|
void set_native_nametags_enabled(bool enabled) noexcept {
|
|
shared().nametags.store(enabled, std::memory_order_relaxed);
|
|
}
|
|
void prepare_native_indicators(Address base) noexcept {
|
|
try {
|
|
if (!shared().attempted) install(base);
|
|
} catch (const std::exception &e) {
|
|
status(e.what());
|
|
}
|
|
}
|
|
void set_native_compass_enabled(bool enabled) noexcept {
|
|
shared().compass.store(enabled, std::memory_order_relaxed);
|
|
}
|
|
std::string native_indicators_status() {
|
|
std::lock_guard lock(state().snapshot_mutex);
|
|
return state().status;
|
|
}
|
|
} // namespace dingosdk::multiplayer
|