mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 15:16:02 +02:00
228 lines
12 KiB
C++
228 lines
12 KiB
C++
#pragma once
|
|
#include "cosmetics.h"
|
|
#include "native_pose_layout.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include "Engine/Game/Build/20260929/native_cosmetics.h"
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <map>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
struct NativeCosmeticItem {
|
|
const char *asset{};
|
|
const std::uint32_t *parameters{};
|
|
std::uint32_t hash{}, slot{};
|
|
};
|
|
// Component+0x150 is the recipe value, without the resource pointer found in
|
|
// preset-manager records. The engine deep-copies this value (addr::native_cosmetics::recipe_copy).
|
|
struct NativeCosmeticRecipe {
|
|
const std::uint32_t *scalars{};
|
|
const NativeCosmeticItem *items{};
|
|
const std::uint32_t *mask{};
|
|
std::uint32_t word{};
|
|
std::uint8_t flag{};
|
|
std::array<std::byte, 3> padding{};
|
|
};
|
|
static_assert(sizeof(NativeCosmeticItem) == 0x18 && sizeof(NativeCosmeticRecipe) == 0x20);
|
|
static_assert(offsetof(NativeCosmeticRecipe, items) == 8 && offsetof(NativeCosmeticRecipe, flag) == 0x1c);
|
|
|
|
template <class T> struct CosmeticArray {
|
|
std::vector<std::uint64_t> storage;
|
|
explicit CosmeticArray(std::size_t count) : storage((8 + count * sizeof(T) + 7) / 8) {
|
|
const auto n = static_cast<std::uint32_t>(count);
|
|
std::memcpy(storage.data(), &n, 4);
|
|
std::memcpy(reinterpret_cast<std::byte *>(storage.data()) + 4, &n, 4);
|
|
}
|
|
T *data() { return reinterpret_cast<T *>(reinterpret_cast<std::byte *>(storage.data()) + 8); }
|
|
};
|
|
struct BorrowedCosmeticRecipe {
|
|
CosmeticArray<std::uint32_t> scalars, mask{0};
|
|
CosmeticArray<NativeCosmeticItem> items;
|
|
std::vector<CosmeticArray<std::uint32_t>> parameters;
|
|
NativeCosmeticRecipe value;
|
|
// Asset strings borrow the immutable packet only until the native copy returns.
|
|
explicit BorrowedCosmeticRecipe(const CosmeticRecipe &r)
|
|
: scalars(r.scalars.size()), items(r.items.size()) {
|
|
std::copy(r.scalars.begin(), r.scalars.end(), scalars.data());
|
|
parameters.reserve(r.items.size());
|
|
for (std::size_t i = 0; i < r.items.size(); ++i) {
|
|
const auto &item = r.items[i];
|
|
parameters.emplace_back(item.parameters.size());
|
|
std::copy(item.parameters.begin(), item.parameters.end(), parameters.back().data());
|
|
items.data()[i] = {item.asset.c_str(), parameters.back().data(), cosmetic_asset_hash(item.asset),
|
|
item.slot};
|
|
}
|
|
value = {scalars.data(), items.data(), mask.data()};
|
|
}
|
|
BorrowedCosmeticRecipe(const BorrowedCosmeticRecipe &) = delete;
|
|
BorrowedCosmeticRecipe &operator=(const BorrowedCosmeticRecipe &) = delete;
|
|
};
|
|
|
|
template <class Read> struct CosmeticMemory {
|
|
Read read;
|
|
std::uintptr_t base;
|
|
template <class T> T get(std::uintptr_t p, std::size_t off = 0) const {
|
|
return native_pose_detail::value<T>(read, native_pose_detail::add(p, off), "cosmetics");
|
|
}
|
|
std::uintptr_t ptr(std::uintptr_t p, std::size_t off = 0) const { return get<std::uintptr_t>(p, off); }
|
|
static void check(bool ok, const char *reason) { native_pose_detail::require(ok, reason); }
|
|
std::size_t count(std::uintptr_t p, std::size_t stride, std::size_t limit) const {
|
|
check(p >= 0x10008, "Cosmetic array is unavailable.");
|
|
const auto n = get<std::uint32_t>(p - 4) & 0x7fffffff;
|
|
check(n <= limit && p <= 0x00007fffffffffffULL - n * stride, "Cosmetic array exceeds bounds.");
|
|
return n;
|
|
}
|
|
template <class T> std::vector<T> array(std::uintptr_t p, std::size_t limit) const {
|
|
std::vector<T> out(count(p, sizeof(T), limit));
|
|
check(out.empty() || read(p, out.data(), out.size() * sizeof(T)), "Cosmetic array cannot be read.");
|
|
return out;
|
|
}
|
|
std::string text(std::uintptr_t p) const {
|
|
std::string out;
|
|
// One read per chunk, not per character. A chunk that runs past readable
|
|
// memory after a short name is read again byte by byte, up to its terminator.
|
|
std::array<unsigned char, 64> chunk{};
|
|
for (std::size_t at = 0; at <= max_cosmetic_asset; at += chunk.size()) {
|
|
const auto size = std::min(chunk.size(), max_cosmetic_asset + 1 - at);
|
|
const bool whole = read(native_pose_detail::add(p, static_cast<std::int64_t>(at)), chunk.data(), size);
|
|
for (std::size_t i = 0; i < size; ++i) {
|
|
const auto c = whole ? chunk[i] : get<unsigned char>(p, at + i);
|
|
if (!c)
|
|
return out;
|
|
check(c >= 32 && c != 127, "Cosmetic asset name is invalid.");
|
|
out += static_cast<char>(c);
|
|
}
|
|
}
|
|
throw std::runtime_error("Cosmetic asset name exceeds bounds.");
|
|
}
|
|
std::uintptr_t component(std::uintptr_t entity) const {
|
|
return read_native_component(read, entity, base + addr::native_cosmetics::cosmetic_component_vtable);
|
|
}
|
|
std::uintptr_t resource(std::uintptr_t component) const {
|
|
return ptr(ptr(component, 8), 0x90) & ~std::uintptr_t{4};
|
|
}
|
|
struct Item {
|
|
std::string name;
|
|
std::vector<std::uint32_t> categories;
|
|
};
|
|
// Catalog entries already found by this capture or apply: substitute_missing and
|
|
// validate check the same assets. Only for the lifetime of this object.
|
|
mutable std::map<std::uint32_t, Item> known{};
|
|
Item installed(std::uint32_t hash) const {
|
|
if (const auto found = known.find(hash); found != known.end())
|
|
return found->second;
|
|
auto item = lookup(hash);
|
|
known.emplace(hash, item);
|
|
return item;
|
|
}
|
|
Item lookup(std::uint32_t hash) const {
|
|
const auto manager = ptr(base, addr::engine::cosmetics_manager);
|
|
check(ptr(manager) == base + addr::engine::cosmetics_manager_vtable && get<std::uint8_t>(manager, 0xa8) == 1,
|
|
"Cosmetic catalog is not ready.");
|
|
const auto buckets = ptr(manager, 0x30);
|
|
const auto size = get<std::uint32_t>(manager, 0x38), total = get<std::uint32_t>(manager, 0x3c);
|
|
check(size && size <= 16384 && total && total <= 8192, "Cosmetic catalog exceeds bounds.");
|
|
auto node = ptr(buckets, (hash % size) * 8ULL);
|
|
for (std::uint32_t visited = 0; node && visited < total; ++visited, node = ptr(node, 0x10)) {
|
|
if (get<std::uint32_t>(node) != hash)
|
|
continue;
|
|
const auto asset = ptr(node, 8) & ~std::uintptr_t{4};
|
|
Item out{text(ptr(asset, 0x38)), {}};
|
|
check(!out.name.empty() && get<std::uint32_t>(asset, 0x48) == hash &&
|
|
cosmetic_asset_hash(out.name) == hash,
|
|
"Cosmetic catalog asset differs.");
|
|
for (auto category : array<std::uintptr_t>(ptr(asset, 0x20), 32))
|
|
out.categories.push_back(get<std::uint32_t>(category & ~std::uintptr_t{4}, 0x38));
|
|
return out;
|
|
}
|
|
throw std::runtime_error("Cosmetic asset is not installed.");
|
|
}
|
|
CosmeticRecipe capture(std::uintptr_t entity) const {
|
|
const auto c = component(entity), res = resource(c);
|
|
check(!get<std::uint8_t>(c, 0x10a) && !get<std::uint8_t>(c, 0x10d),
|
|
"Waiting for the local cosmetic recipe to finish updating.");
|
|
CosmeticRecipe out{get<std::uint32_t>(res, 0x38), get<std::uint32_t>(res, 0x3c)};
|
|
out.scalars = array<std::uint32_t>(ptr(c, 0x150), max_cosmetic_scalars);
|
|
for (const auto &item : array<NativeCosmeticItem>(ptr(c, 0x158), max_cosmetic_slots)) {
|
|
CosmeticSlot slot{item.slot, text(reinterpret_cast<std::uintptr_t>(item.asset)),
|
|
array<std::uint32_t>(reinterpret_cast<std::uintptr_t>(item.parameters),
|
|
max_cosmetic_parameters)};
|
|
// Active model recipes usually contain only hashes. Resolve those
|
|
// against the local catalog; never transmit addresses or empty aliases.
|
|
if (item.hash && slot.asset.empty())
|
|
slot.asset = installed(item.hash).name;
|
|
check(cosmetic_asset_hash(slot.asset) == item.hash, "Local cosmetic asset hash differs.");
|
|
out.items.push_back(std::move(slot));
|
|
}
|
|
validate(res, out);
|
|
return out;
|
|
}
|
|
// Whether this PC can show `item` in template slot `index`: its asset is in
|
|
// the local catalog and fits the slot. A player's cosmetics mod can add
|
|
// items other players do not have.
|
|
bool item_installed(std::uintptr_t res, std::size_t index, const CosmeticSlot &item) const {
|
|
if (item.asset.empty()) return true;
|
|
try {
|
|
const auto slots = ptr(res, 0x28);
|
|
if (index >= count(slots, 24, max_cosmetic_slots)) return false;
|
|
const auto slot = ptr(slots, index * 24 + 8) & ~std::uintptr_t{4};
|
|
const auto info = installed(cosmetic_asset_hash(item.asset));
|
|
const auto category = get<std::uint32_t>(ptr(slot, 0x30) & ~std::uintptr_t{4}, 0x38);
|
|
return info.name == item.asset &&
|
|
std::find(info.categories.begin(), info.categories.end(), category) != info.categories.end();
|
|
} catch (const std::exception &) {
|
|
return false;
|
|
}
|
|
}
|
|
// Swaps each item this PC does not have for the first of `fallbacks` that
|
|
// fits its slot, or leaves the slot empty, so the rest of a peer's outfit
|
|
// still applies. The slot keeps the sender's parameters: the slot, not the
|
|
// item, decides their layout. Returns each swap as {missing, replacement}.
|
|
template <class Fallbacks, class Blocked = bool (*)(const std::string &)>
|
|
std::vector<std::pair<std::string, std::string>> substitute_missing(
|
|
std::uintptr_t res, CosmeticRecipe &r, const Fallbacks &fallbacks,
|
|
const Blocked &blocked = [](const std::string &) { return false; }) const {
|
|
std::vector<std::pair<std::string, std::string>> swaps;
|
|
for (std::size_t i = 0; i < r.items.size(); ++i) {
|
|
auto &item = r.items[i];
|
|
if (item_installed(res, i, item) && (item.asset.empty() || !blocked(item.asset))) continue;
|
|
CosmeticSlot replacement{item.slot, {}, item.parameters};
|
|
for (const auto &candidate : fallbacks) {
|
|
CosmeticSlot fallback{item.slot, std::string(candidate), item.parameters};
|
|
if (item_installed(res, i, fallback) && !blocked(fallback.asset)) {
|
|
replacement = std::move(fallback);
|
|
break;
|
|
}
|
|
}
|
|
swaps.emplace_back(item.asset, replacement.asset);
|
|
item = std::move(replacement);
|
|
}
|
|
return swaps;
|
|
}
|
|
void validate(std::uintptr_t res, const CosmeticRecipe &r) const {
|
|
check(r.key == get<std::uint32_t>(res, 0x38) && r.version == get<std::uint32_t>(res, 0x3c),
|
|
"Peer cosmetic template differs from this game.");
|
|
const auto slots = ptr(res, 0x28);
|
|
check(r.scalars.size() == count(ptr(res, 0x30), 16, max_cosmetic_scalars) &&
|
|
r.items.size() == count(slots, 24, max_cosmetic_slots),
|
|
"Peer cosmetic recipe shape differs.");
|
|
for (std::size_t i = 0; i < r.items.size(); ++i) {
|
|
const auto slot = ptr(slots, i * 24 + 8) & ~std::uintptr_t{4};
|
|
const auto &item = r.items[i];
|
|
check(ptr(slot, 8) == base + addr::engine::cosmetic_slot_type &&
|
|
item.slot == get<std::uint32_t>(slot, 0x4c) &&
|
|
item.parameters.size() == count(ptr(slot, 0x18), 4, max_cosmetic_parameters),
|
|
"Peer cosmetic slot or parameter layout differs.");
|
|
if (item.asset.empty())
|
|
continue;
|
|
const auto info = installed(cosmetic_asset_hash(item.asset));
|
|
const auto category = get<std::uint32_t>(ptr(slot, 0x30) & ~std::uintptr_t{4}, 0x38);
|
|
check(info.name == item.asset && std::find(info.categories.begin(), info.categories.end(),
|
|
category) != info.categories.end(),
|
|
"Peer cosmetic item does not match its slot category.");
|
|
}
|
|
}
|
|
};
|
|
} // namespace dingosdk::multiplayer
|