Files
ReSkate/Extension/Multiplayer/Remote/native_cosmetics.cpp
T
2026-10-02 14:40:41 -05:00

107 lines
6.0 KiB
C++

#include "native_cosmetics.h"
#include "native_cosmetics_layout.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_cosmetics.h"
#include "Extension/Customization/local_customization_runtime.h"
#include "Extension/Customization/local_player_card_runtime.h"
#include <Windows.h>
#include <mutex>
#include <set>
namespace dingosdk::multiplayer {
namespace {
// Catalog, recipe and item records of loaded assets: a guarded copy, not a system
// call per read (a capture or apply reads a few hundred of them).
bool readable(std::uintptr_t p, void *out, std::size_t size) {
return size ? memory::peek_bytes(p, out, size) : p >= 0x10000;
}
template <class T> void put(std::uintptr_t p, const T &value) {
SIZE_T done{};
if (!WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void *>(p), &value, sizeof(value), &done) ||
done != sizeof(value))
throw std::runtime_error("Cannot update the remote cosmetic component.");
}
} // namespace
std::optional<Appearance> capture_cosmetics(std::uintptr_t base, const NativeFrame &local,
std::string &detail) {
try {
if (!local.ready || !local.board_entity)
throw std::runtime_error("Waiting for the local skater and skateboard.");
const CosmeticMemory memory{readable, base};
Appearance out{memory.capture(local.entity), memory.capture(local.board_entity)};
// Read the local profile's native UI record; never serialize model handles.
const auto &card = profile_runtime::player_card_runtime();
const auto handle = card.functions.get_local_info ? profile_runtime::local_player_info_hook() : 0;
if (handle && card.data_model) {
game::ModelWriteLock lock(card.data_model);
const auto value = game::native_data().models.value(card.data_model, handle, 0, 0);
if (value) {
readable(value + 0xd0, &out.card.background, sizeof(out.card.background));
readable(value + 0xc4, &out.card.emblem, sizeof(out.card.emblem));
readable(value + 0xd4, &out.card.title, sizeof(out.card.title));
}
}
if (!valid_appearance(out))
throw std::runtime_error("Local cosmetic recipe exceeds the multiplayer limits.");
detail.clear();
return out;
} catch (const std::exception &e) {
detail = std::string("Cosmetics: ") + e.what();
return {};
}
}
void apply_cosmetic_recipe(std::uintptr_t base, std::uintptr_t entity, std::uintptr_t local_entity,
const CosmeticRecipe &recipe) {
const CosmeticMemory memory{readable, base};
const auto c = memory.component(entity), local = memory.component(local_entity);
memory.check(entity != local_entity && c != local && memory.ptr(entity) == memory.ptr(local_entity) &&
memory.ptr(entity, 0x20) == memory.ptr(local_entity, 0x20),
"Remote cosmetic ownership differs.");
memory.check((recipe.key == skater_recipe_key &&
memory.ptr(entity) == base + addr::engine::skater_entity_vtable && !memory.ptr(entity, 0xf8)) ||
(recipe.key == board_recipe_key &&
memory.ptr(entity) == base + addr::engine::board_entity_vtable),
"Remote cosmetic actor type differs.");
memory.check(memory.get<std::uint32_t>(c, 0x130) < 3, "Cannot apply peer cosmetics to a local player.");
const auto resource = memory.resource(c);
memory.check(resource == memory.resource(local), "Remote cosmetic template ownership differs.");
// Items this PC does not have (a player's cosmetics mod) would reject the
// whole outfit: they become the matching default, or an empty slot.
auto usable = recipe;
const auto reserved = [](const std::string &asset) { return profile_runtime::reserved_cosmetic(asset); };
for (const auto &[missing, replacement] :
memory.substitute_missing(resource, usable, default_cosmetic_items, reserved)) {
static std::mutex logged_mutex;
static std::set<std::string> logged;
std::lock_guard lock(logged_mutex);
// Peers choose these names: remember (and log) a bounded number.
if (!reserved(missing) && logged.size() < 256 && logged.insert(missing).second)
logging::log(logging::Level::info, logging::Channel::runtime,
"Multiplayer: a player wears \"{}\", which is not installed here; showing {} instead.",
missing, replacement.empty() ? "nothing" : replacement);
}
memory.validate(resource, usable);
constexpr auto copy_prefix = addr::native_cosmetics::recipe_copy_prefix;
memory.check(memory.get<std::array<std::uint8_t, copy_prefix.size()>>(
base, addr::native_cosmetics::recipe_copy) == copy_prefix,
"Native cosmetic copy function differs.");
BorrowedCosmeticRecipe borrowed(usable);
put(c + 0x134, std::uint32_t{0}); // Explicit recipe; native mode update unsubscribes the local model.
reinterpret_cast<void (*)(std::uintptr_t, const NativeCosmeticRecipe *)>(
base + addr::native_cosmetics::recipe_copy)(c, &borrowed.value);
// The native copy owns strings and all arrays after this call. Its dirty
// flags schedule the normal mesh/appearance update, including mode cleanup.
memory.check(memory.get<std::uint32_t>(c, 0x13c) == 2 && memory.get<std::uint8_t>(c, 0x10a) == 1 &&
memory.get<std::uint8_t>(c, 0x10d) == 1 &&
memory.ptr(c, 0x158) != reinterpret_cast<std::uintptr_t>(borrowed.value.items),
"Native cosmetic copy did not take ownership.");
logging::log(
logging::Level::debug, logging::Channel::runtime,
"Multiplayer: peer cosmetics queued; entity={:#x}, component={:#x}, template={:#x}, slots={}.",
entity, c, usable.key, usable.items.size());
}
} // namespace dingosdk::multiplayer