diff --git a/Engine/Game/Build/20260929/native_cosmetics.h b/Engine/Game/Build/20260929/native_cosmetics.h index 5293a913..d4b922ee 100644 --- a/Engine/Game/Build/20260929/native_cosmetics.h +++ b/Engine/Game/Build/20260929/native_cosmetics.h @@ -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 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 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 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 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 material_preset_prefix{0x48,0x83,0xc1,0x08,0xe9,0x97,0x6c,0x01,0x00}; inline constexpr std::array referenced_preset_prefix{ 0x48,0x8b,0x09,0x48,0x85,0xc9,0x74,0x07,0x48,0x8b,0x01,0x48,0xff,0x60,0x18,0x33,0xc0}; } diff --git a/Extension/Customization/preset_lookup_guard.cpp b/Extension/Customization/preset_lookup_guard.cpp index eef45aa3..eb493729 100644 --- a/Extension/Customization/preset_lookup_guard.cpp +++ b/Extension/Customization/preset_lookup_guard.cpp @@ -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 #include #include @@ -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(image + native::script_userdata); + auto *const *item_block = static_cast(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(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(preset) + 0x18, &preset_name_field, sizeof(preset_name_field)); + reinterpret_cast(image + native::material_preset_name)(own, preset_name_field); + reinterpret_cast(image + native::material_item)(own, item); + reinterpret_cast(image + native::material_preset)(own, reference); + static std::mutex logged_mutex; + static std::set 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(image + native::script_preset_reference)(state, 3); if (const auto preset = reference ? reinterpret_cast(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 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(base + native::add_shader_preset); void *previous{}; if (hook_prepare(target, reinterpret_cast(&add_preset), &previous) != HookOk) return false; diff --git a/Extension/Skater/layer_materials.cpp b/Extension/Skater/layer_materials.cpp new file mode 100644 index 00000000..85e76861 --- /dev/null +++ b/Extension/Skater/layer_materials.cpp @@ -0,0 +1,84 @@ +#include "layer_materials.h" +#include +#include +#include + +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 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(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> 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> 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 diff --git a/Extension/Skater/layer_materials.h b/Extension/Skater/layer_materials.h new file mode 100644 index 00000000..c9c81341 --- /dev/null +++ b/Extension/Skater/layer_materials.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include + +// 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> 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(c); + hash = (hash * 0x21) ^ (byte + (byte - 'A' < 26u ? 0x20u : 0u)); + } + return hash; +} +} // namespace dingosdk::layer_materials diff --git a/Extension/Skater/outfit_overlay.cpp b/Extension/Skater/outfit_overlay.cpp index 3e9d28fe..00d8f541 100644 --- a/Extension/Skater/outfit_overlay.cpp +++ b/Extension/Skater/outfit_overlay.cpp @@ -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 #include #include @@ -63,7 +64,7 @@ struct State { std::set good, bad; // "slot|item" (lower case): loaded / did not load bool memory_read{}; std::map> 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 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>& 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(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 all_layers(const State& s) { std::vector 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> 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 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 (...) { } } diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index c877ab5f..d0661f6f 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -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