#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 #include #include 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 void put(std::uintptr_t p, const T &value) { SIZE_T done{}; if (!WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(p), &value, sizeof(value), &done) || done != sizeof(value)) throw std::runtime_error("Cannot update the remote cosmetic component."); } } // namespace std::optional 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(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 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>( 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( 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(c, 0x13c) == 2 && memory.get(c, 0x10a) == 1 && memory.get(c, 0x10d) == 1 && memory.ptr(c, 0x158) != reinterpret_cast(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