mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-28 23:06:54 +01:00
Outfit layers: an item worn on top keeps its textures, and the log says what the layers do
The game's skater loader builds each recipe slot with that slot's own function, which puts the
item's appearance preset on a material named for the slot's kind ("Outfit_Top_mat"). An item worn
on top sits in another kind's slot, so its mesh found nothing under its own material ("Bottom_mat")
and drew white. The preset lookup guard's addEsShaderPreset hook now also puts the preset of an item
on top on its own kind's material (layer_materials.h: which presets those are, from each item's
catalog folder and AssetPaths; the five native functions checked by their first bytes).
The outfit service logs what goes on top and where, the slot trials, why an item is not worn and
what it waits for.
This commit is contained in:
1 parent
89a97caed7
commit
48d1e245db
6 files changed
+234
-2
No files matched your search
@@ -54,6 +54,30 @@ inline constexpr std::uintptr_t referenced_preset = 0x16fe9c0;
|
||||
inline constexpr std::size_t preset_parameters = 0x20;
|
||||
inline constexpr std::size_t preset_parameter_size = 40;
|
||||
inline constexpr std::size_t preset_parameter_key = 0x18;
|
||||
// What item:addEsShaderPreset does with the item, the material and the preset, besides keeping the
|
||||
// preset with the item (2026-10-21, for items worn on top: layer_materials.h):
|
||||
// - the Lua argument as a userdata of a type: (state, argument number, type name) -> its block, which
|
||||
// holds the native AppearanceItem; an AppearanceMaterial argument is the material itself;
|
||||
// - an AppearanceItem's material by name (AppearanceItem:addMaterial): (item, 0, has name, name hash,
|
||||
// name, variant 0xff) -> the material, made when the item has none of that name. The name is kept:
|
||||
// it must outlive the item;
|
||||
// - then on the material: (material, the preset's name), (material, item), (material, the preset's
|
||||
// Lua reference).
|
||||
inline constexpr std::uintptr_t script_userdata = 0x3362860;
|
||||
inline constexpr std::array<unsigned char, 16> script_userdata_prefix{
|
||||
0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x49};
|
||||
inline constexpr std::uintptr_t item_material = 0x131e5e0;
|
||||
inline constexpr std::array<unsigned char, 24> item_material_prefix{
|
||||
0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x6c,0x24,0x10,0x56,0x57,0x41,0x56,0x48,0x83,
|
||||
0xec,0x50,0x0f,0xb6,0xb4,0x24,0x98,0x00};
|
||||
inline constexpr std::uintptr_t material_preset_name = 0x1313150;
|
||||
inline constexpr std::array<unsigned char, 16> material_preset_name_prefix{
|
||||
0x48,0x89,0x54,0x24,0x10,0x48,0x83,0xec,0x48,0x48,0x81,0xc1,0x48,0x01,0x00,0x00};
|
||||
inline constexpr std::uintptr_t material_item = 0x1330480;
|
||||
inline constexpr std::array<unsigned char, 16> material_item_prefix{
|
||||
0x48,0x89,0x54,0x24,0x10,0x55,0x56,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x48};
|
||||
inline constexpr std::uintptr_t material_preset = 0x1312ce0;
|
||||
inline constexpr std::array<unsigned char, 9> material_preset_prefix{0x48,0x83,0xc1,0x08,0xe9,0x97,0x6c,0x01,0x00};
|
||||
inline constexpr std::array<unsigned char, 17> referenced_preset_prefix{
|
||||
0x48,0x8b,0x09,0x48,0x85,0xc9,0x74,0x07,0x48,0x8b,0x01,0x48,0xff,0x60,0x18,0x33,0xc0};
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/native_cosmetics.h"
|
||||
#include "Extension/Skater/layer_materials.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -77,11 +78,47 @@ std::string preset_name(void *preset) {
|
||||
char buffer[200]{};
|
||||
return memory::peek_cstring(text, buffer, sizeof(buffer) - 1) > 0 ? std::string(buffer) : std::string{};
|
||||
}
|
||||
// An item worn on top is built by another kind's slot, which puts its preset on that kind's
|
||||
// material ("Outfit_Top_mat"), one its mesh does not use: the mesh draws with no textures. So the
|
||||
// preset of such an item (layer_materials.h) goes on the item's own kind's material too ("Bottom_mat"),
|
||||
// as its own slot would have put it; for the item in its own slot that is the same material, and
|
||||
// nothing more is done.
|
||||
bool layers_ready{};
|
||||
void own_material(void *state, void *preset, void *reference) {
|
||||
const auto name = preset_name(preset);
|
||||
const char *material_name = name.empty() ? nullptr : layer_materials::material_for(name);
|
||||
if (!material_name) return;
|
||||
using Userdata = void *(*)(void *, int, const char *);
|
||||
const auto userdata = reinterpret_cast<Userdata>(image + native::script_userdata);
|
||||
auto *const *item_block = static_cast<void *const *>(userdata(state, 1, "AppearanceItem"));
|
||||
void *material = userdata(state, 2, "AppearanceMaterial");
|
||||
void *item = item_block ? *item_block : nullptr;
|
||||
if (!item || !material) return;
|
||||
using ItemMaterial = void *(*)(void *, int, bool, unsigned, const char *, std::uint8_t);
|
||||
void *own = reinterpret_cast<ItemMaterial>(image + native::item_material)(
|
||||
item, 0, true, layer_materials::name_hash(material_name), material_name, 0xff);
|
||||
if (!own || own == material) return;
|
||||
std::uintptr_t preset_name_field{};
|
||||
memory::read_bytes(reinterpret_cast<std::uintptr_t>(preset) + 0x18, &preset_name_field, sizeof(preset_name_field));
|
||||
reinterpret_cast<void (*)(void *, std::uintptr_t)>(image + native::material_preset_name)(own, preset_name_field);
|
||||
reinterpret_cast<void (*)(void *, void *)>(image + native::material_item)(own, item);
|
||||
reinterpret_cast<void (*)(void *, void *)>(image + native::material_preset)(own, reference);
|
||||
static std::mutex logged_mutex;
|
||||
static std::set<std::string> logged;
|
||||
std::lock_guard lock(logged_mutex);
|
||||
if (logged.size() < 64 && logged.insert(name).second)
|
||||
logging::log(logging::Level::info, logging::Channel::customization,
|
||||
"Outfit layers: {} is on its own material, {}, as well as its slot's.", name, material_name);
|
||||
}
|
||||
int add_preset(void *state) {
|
||||
const auto reference = reinterpret_cast<void *(*)(void *, int)>(image + native::script_preset_reference)(state, 3);
|
||||
if (const auto preset = reference ? reinterpret_cast<void *(*)(void *, int)>(image + native::referenced_preset)(reference, 0) : nullptr) {
|
||||
std::uint64_t bad{};
|
||||
if (preset_parameters_sound(preset, bad)) return original_add_preset.load()(state);
|
||||
if (preset_parameters_sound(preset, bad)) {
|
||||
const auto result = original_add_preset.load()(state);
|
||||
if (layers_ready) own_material(state, preset, reference);
|
||||
return result;
|
||||
}
|
||||
// Left out, as a preset that is not there: the item shows without it.
|
||||
static std::mutex refused_mutex;
|
||||
static std::set<std::string> refused;
|
||||
@@ -112,6 +149,14 @@ bool start_add_preset(std::uintptr_t base) {
|
||||
!matches(base + native::referenced_preset, native::referenced_preset_prefix))
|
||||
return false;
|
||||
image = base;
|
||||
layers_ready = matches(base + native::script_userdata, native::script_userdata_prefix) &&
|
||||
matches(base + native::item_material, native::item_material_prefix) &&
|
||||
matches(base + native::material_preset_name, native::material_preset_name_prefix) &&
|
||||
matches(base + native::material_item, native::material_item_prefix) &&
|
||||
matches(base + native::material_preset, native::material_preset_prefix);
|
||||
if (!layers_ready)
|
||||
logging::log(logging::Level::warning, logging::Channel::customization,
|
||||
"Items worn on top keep their slot's material: the game's material functions differ from this build's.");
|
||||
auto *target = reinterpret_cast<void *>(base + native::add_shader_preset);
|
||||
void *previous{};
|
||||
if (hook_prepare(target, reinterpret_cast<void *>(&add_preset), &previous) != HookOk) return false;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "layer_materials.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
|
||||
namespace dingosdk::layer_materials {
|
||||
namespace {
|
||||
struct Kind {
|
||||
std::string_view folder;
|
||||
int type;
|
||||
const char* material;
|
||||
};
|
||||
// From the loader's create...Item functions (2026-10-21).
|
||||
constexpr std::array<Kind, 20> kinds{{
|
||||
{"cust_tops", 3, "Top_mat"},
|
||||
{"cust_bottoms", 3, "Bottom_mat"},
|
||||
{"cust_socks", 3, "Sock_mat"},
|
||||
{"cust_shoes", 3, "Footwear_mat"},
|
||||
{"cust_hats", 3, "Headwear_mat"},
|
||||
{"cust_eyewear", 3, "Eyewear_mat"},
|
||||
{"cust_eyewear", 16, "EyewearTransparent_mat"},
|
||||
{"cust_handwear", 3, "Handwear_mat"},
|
||||
{"cust_earwear", 3, "Earwear_Left_mat"},
|
||||
{"cust_nosewear", 3, "Nosewear_Left_mat"},
|
||||
{"cust_outfit", 3, "Outfit_Top_mat"},
|
||||
{"cust_outfit", 16, "Outfit_Bottom_mat"},
|
||||
{"cust_fullbodycostume", 19, "FullbodyTop_mat"},
|
||||
{"cust_fullbodycostume", 20, "FullbodyBottom_mat"},
|
||||
{"cust_fullbodycostume", 21, "FullbodyFootwear_mat"},
|
||||
{"cust_fullbodycostume", 22, "FullbodyHeadwear_mat"},
|
||||
{"cust_fullbodycostume", 23, "FullbodyHandwear_mat"},
|
||||
{"cust_fullbodycostume", 24, "FullbodyAccessories_mat"},
|
||||
{"cust_fullbodycostume", 25, "FullbodyHardware_mat"},
|
||||
{"cust_fullbodycostume", 26, "FullbodyFX_mat"},
|
||||
}};
|
||||
std::string folded(std::string_view text) {
|
||||
std::string out(text);
|
||||
for (auto& c : out)
|
||||
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 0x20);
|
||||
return out;
|
||||
}
|
||||
// A preset's own name: what follows its last folder.
|
||||
std::string_view leaf(std::string_view name) {
|
||||
const auto slash = name.find_last_of("/\\");
|
||||
return slash == std::string_view::npos ? name : name.substr(slash + 1);
|
||||
}
|
||||
struct Registry {
|
||||
std::mutex mutex;
|
||||
std::vector<std::pair<std::string, const char*>> presets; // folded leaf names
|
||||
};
|
||||
Registry& registry() {
|
||||
static auto* value = new Registry; // process lifetime: the loader's threads read it
|
||||
return *value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const char* material_of(std::string_view folder, int type) noexcept {
|
||||
try {
|
||||
const auto name = folded(folder);
|
||||
for (const auto& kind : kinds)
|
||||
if (kind.folder == name && kind.type == type) return kind.material;
|
||||
} catch (...) {}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void set(std::vector<std::pair<std::string, const char*>> presets) {
|
||||
for (auto& [name, material] : presets) name = folded(leaf(name));
|
||||
auto& r = registry();
|
||||
std::lock_guard lock(r.mutex);
|
||||
r.presets = std::move(presets);
|
||||
}
|
||||
|
||||
const char* material_for(std::string_view preset) noexcept {
|
||||
try {
|
||||
auto& r = registry();
|
||||
std::lock_guard lock(r.mutex);
|
||||
if (r.presets.empty()) return nullptr;
|
||||
const auto name = folded(leaf(preset));
|
||||
for (const auto& [known, material] : r.presets)
|
||||
if (known == name) return material;
|
||||
} catch (...) {}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace dingosdk::layer_materials
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// The materials of items worn on top (outfit_overlay::set_layers). The game's skater loader
|
||||
// (characters/lua/skaterlualoaderasset) builds each recipe slot with that slot's own function, which
|
||||
// puts the item's appearance shader preset on a material named for the slot's kind: "Top_mat" for the
|
||||
// top, "Bottom_mat" for the bottoms, "Outfit_Top_mat" for the outfit. An item worn on top sits in
|
||||
// another kind's slot, so its preset lands on a material its mesh does not use, and the mesh draws
|
||||
// with no textures (white). The preset lookup guard (preset_lookup_guard.cpp) puts such a preset on
|
||||
// the item's own kind's material too; this says which presets those are, and that material's name.
|
||||
namespace dingosdk::layer_materials {
|
||||
// The material an item's own builder puts its preset of `type` on (AssetPathTypeEnum: 3
|
||||
// AppearanceShaderPreset, 16 AppearanceShaderPreset2, 19 to 26 a costume's), by the item's catalog
|
||||
// folder ("cust_bottoms"); null when there is none. The names live as long as the process.
|
||||
const char* material_of(std::string_view folder, int type) noexcept;
|
||||
// The presets of the items worn on top now, each with its material: replaced as a whole. Any thread.
|
||||
void set(std::vector<std::pair<std::string, const char*>> presets);
|
||||
// The material for a preset by its name (any case, its folders left out), or null. Any thread.
|
||||
const char* material_for(std::string_view preset) noexcept;
|
||||
// The name hash the game keys an item's materials by (AppearanceItem:addMaterial): djb2 over the
|
||||
// name with A-Z as a-z, each step xor.
|
||||
constexpr unsigned name_hash(std::string_view name) noexcept {
|
||||
unsigned hash = 0x1505;
|
||||
for (const char c : name) {
|
||||
const auto byte = static_cast<unsigned char>(c);
|
||||
hash = (hash * 0x21) ^ (byte + (byte - 'A' < 26u ? 0x20u : 0u));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
} // namespace dingosdk::layer_materials
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Vfs/content_catalogs.h"
|
||||
#include "Extension/World/load_error_guard.h"
|
||||
#include "Extension/Skater/layer_materials.h"
|
||||
#include <Windows.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -63,7 +64,7 @@ struct State {
|
||||
std::set<std::string> good, bad; // "slot|item" (lower case): loaded / did not load
|
||||
bool memory_read{};
|
||||
std::map<std::uintptr_t, std::vector<std::uintptr_t>> native_categories; // catalog item -> its own
|
||||
std::string layer_note;
|
||||
std::string layer_note, logged_note, logged_wait; // the notes last put in the log
|
||||
std::map<std::string, std::string> found; // installed items by ending
|
||||
bool own{}, dirty{}, worn{};
|
||||
std::uintptr_t skater{}, component{}, slots_of{};
|
||||
@@ -173,6 +174,24 @@ void add_category(const SkaterItems& m, std::uintptr_t asset, std::uintptr_t wan
|
||||
done == sizeof(elements),
|
||||
"Cannot write the item's categories.");
|
||||
}
|
||||
// The appearance presets of an installed item, each with the material its own builder puts it on
|
||||
// (layer_materials.h), from its catalog entry: the asset's name ("Items/cust_bottoms/Own_..."), its
|
||||
// item data (+0x28) and that data's AssetPaths (+0x98: {name, type} pairs of 16 bytes).
|
||||
void add_presets(const SkaterItems& m, const std::string& item, std::vector<std::pair<std::string, const char*>>& out) {
|
||||
const auto asset = catalog_asset(m, item);
|
||||
if (!asset) return;
|
||||
const auto name = m.text(m.ptr(asset, 0x18));
|
||||
const auto first = name.find('/'), second = first == std::string::npos ? first : name.find('/', first + 1);
|
||||
if (second == std::string::npos) return;
|
||||
const auto folder = std::string_view(name).substr(first + 1, second - first - 1);
|
||||
const auto data = m.ptr(asset, 0x28);
|
||||
if (!data) return;
|
||||
const auto paths = m.ptr(data, 0x98);
|
||||
const auto count = m.count(paths, 16, 32);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (const auto material = layer_materials::material_of(folder, m.get<std::int32_t>(paths, i * 16 + 8)))
|
||||
out.emplace_back(m.text(m.ptr(paths, i * 16)), material);
|
||||
}
|
||||
// Every owner's layers, in one list: owners by name, each in its own order, none twice.
|
||||
std::vector<std::string> all_layers(const State& s) {
|
||||
std::vector<std::string> out;
|
||||
@@ -293,15 +312,33 @@ void wear(State& s, const SkaterItems& m, std::uintptr_t skater, std::uint64_t n
|
||||
std::string note;
|
||||
for (const auto& one : left) note += (note.empty() ? "not worn: " : ", ") + one;
|
||||
if (waiting) note += std::format("{}{} more to try on", note.empty() ? "" : "; ", waiting);
|
||||
if (note != s.logged_note && !note.empty())
|
||||
logging::log(logging::Level::info, logging::Channel::skater, "Outfit layers: {}.", note);
|
||||
s.logged_note = note;
|
||||
s.layer_note = std::move(note);
|
||||
s.worn = worn || !s.placed.empty();
|
||||
if (!changed) return;
|
||||
// Before the skater is built again: which presets are of items on top, and their own materials.
|
||||
std::vector<std::pair<std::string, const char*>> presets;
|
||||
for (const auto& [i, item] : s.placed) {
|
||||
try {
|
||||
add_presets(m, item, presets);
|
||||
} catch (const std::exception&) {}
|
||||
}
|
||||
layer_materials::set(std::move(presets));
|
||||
const auto failed_loads = load_error_guard::described();
|
||||
skater_items::apply(m, component, recipe);
|
||||
s.own = true;
|
||||
s.component = component;
|
||||
s.applied = now;
|
||||
if (trial) s.trial = Trial{*trial, now, failed_loads};
|
||||
if (!wanted.empty() || !s.placed.empty()) {
|
||||
std::string where;
|
||||
for (const auto& [i, item] : s.placed)
|
||||
where += std::format("{}{} in {}", where.empty() ? "" : ", ", item, i < slots.size() ? slots[i].name : std::string("its slot"));
|
||||
logging::log(logging::Level::info, logging::Channel::skater, "Outfit layers: {} on top{}{}{}.", s.placed.size(), where.empty() ? "" : ": ", where,
|
||||
trial ? std::format(" ({} on trial in its slot)", trial->second) : std::string());
|
||||
}
|
||||
}
|
||||
// A layer on trial: once its time is up, it stays (it loaded) or comes off (the game described a
|
||||
// failed load meanwhile) and that slot is not tried for it again.
|
||||
@@ -311,6 +348,8 @@ void judge(State& s, const SkaterItems& m, std::uintptr_t skater, std::uint64_t
|
||||
const auto slots = slot_infos(m, m.resource(component));
|
||||
const auto key = pair_key(trial.placement.first < slots.size() ? slots[trial.placement.first].name : std::string{}, trial.placement.second);
|
||||
if (load_error_guard::described() == trial.failed_loads) {
|
||||
logging::log(logging::Level::info, logging::Channel::skater, "Outfit layers: {} loads from {}.", trial.placement.second,
|
||||
trial.placement.first < slots.size() ? slots[trial.placement.first].name : std::string("its slot"));
|
||||
remember(s, true, key);
|
||||
s.trial.reset();
|
||||
s.dirty = true;
|
||||
@@ -429,6 +468,7 @@ std::vector<Slot> slots() {
|
||||
void tick() noexcept {
|
||||
auto& s = state();
|
||||
if (s.wanted.empty() && s.layers.empty() && !s.own) {
|
||||
if (!s.placed.empty()) layer_materials::set({});
|
||||
s.placed.clear();
|
||||
s.layer_note.clear();
|
||||
return;
|
||||
@@ -479,6 +519,11 @@ void tick() noexcept {
|
||||
s.status = std::format("{} item(s) worn", s.wanted.size() + s.placed.size());
|
||||
} catch (const std::exception& error) {
|
||||
s.status = std::string("waiting: ") + error.what();
|
||||
// Once for each reason, while there are items on top: what keeps them off.
|
||||
if (!s.layers.empty() && s.logged_wait != error.what()) {
|
||||
s.logged_wait = error.what();
|
||||
logging::log(logging::Level::info, logging::Channel::skater, "Outfit layers: waiting ({}).", error.what());
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/Plugins/plugin_sound.cpp
|
||||
Extension/Skater/skater_events.cpp
|
||||
Extension/Skater/outfit_overlay.cpp
|
||||
Extension/Skater/layer_materials.cpp
|
||||
Extension/Skater/board_pose.cpp
|
||||
Extension/Skater/skater_physics.cpp
|
||||
Extension/Skater/xray.cpp
|
||||
|
||||
Reference in new issue
Block a user