From 1456c1fa5147b3551bbe01b1bf7e01c98a51bb38 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sat, 17 Oct 2026 00:48:40 -0500 Subject: [PATCH] Customization: a preset with a parameter type the game cannot read is left out, not applied Scrolling shirts onto Yudi-Yuddispack's philippleinshirt_ap crashed the game (Skate.exe+0x3dc07ed): one of the preset's set parameters has a type of 0x0c07010304040502 (null in another crash). The game type-checks every set parameter when it next applies a preset to a material, reading the type unchecked, and catching that crash inside addEsShaderPreset was too late: the preset was on the material already and the next update crashed on it. addEsShaderPreset's guard now checks the preset first, as that type check reads it: the TypeInfo, its data, a word at +4. A preset with a parameter whose type cannot be read is left out as a preset that is not loaded is (the item shows without it), and named in the log once with that parameter's key. Many of the game's own types are built at run time, so the test is that the type reads, not that it lies in the game's image. --- Engine/Game/Build/20260929/native_cosmetics.h | 7 +++ .../Customization/preset_lookup_guard.cpp | 55 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Engine/Game/Build/20260929/native_cosmetics.h b/Engine/Game/Build/20260929/native_cosmetics.h index c169c63..5293a91 100644 --- a/Engine/Game/Build/20260929/native_cosmetics.h +++ b/Engine/Game/Build/20260929/native_cosmetics.h @@ -47,6 +47,13 @@ inline constexpr std::array add_shader_preset_prefix{ inline constexpr std::uintptr_t script_preset_reference = 0x336f060; inline constexpr std::array script_preset_reference_prefix{0x4c,0x8d,0x05,0x81,0xa2,0x0d,0x03,0xe9}; inline constexpr std::uintptr_t referenced_preset = 0x16fe9c0; +// An AppearanceShaderExpressionPreset as loaded (its EBX layout): ParamInfo, the array of its +// AppearanceExpressionParamInfo at +0x20, 40 bytes each: the value's boxed {TypeInfo*, data*} at +// +0, its Parameter at +0x10, its ParamKey at +0x18. Applying a preset to a material (0x141314340, +// under 0x141330210) type-checks every entry whose data is set, reading the TypeInfo unchecked. +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; 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 51eb239..eef45aa 100644 --- a/Extension/Customization/preset_lookup_guard.cpp +++ b/Extension/Customization/preset_lookup_guard.cpp @@ -39,10 +39,61 @@ void **lookup(void **out, const char *name, void *table, std::uint8_t flag, void using ScriptFunction = int (*)(void *state); std::atomic original_add_preset{}; std::uintptr_t image{}; +// A preset whose parameters the game can apply: every value that is set has a type the game can +// read, as its type check (Skate.exe+0x3dc07e0) reads it: the TypeInfo, its data, a word at +4. A +// preset with one that is null or not an address at all crashed the game when the material was +// next applied, e.g. scrolling shirts onto a mod's (2026-10-17: Yudi-Yuddispack's +// philippleinshirt_ap, a type of 0x0c07010304040502). Catching that crash in this call is not +// enough: the preset is already on the material, and the game reads it again on its next update. +// (Not "a type in the game's image": many of the game's own presets have types built at run time, +// and leaving those out turned every item white.) `bad` is the key of the first parameter that fails. +bool preset_parameters_sound(void *preset, std::uint64_t &bad) { + const auto at = reinterpret_cast(preset); + std::uintptr_t parameters{}; + if (!memory::read_bytes(at + native::preset_parameters, ¶meters, sizeof(parameters))) return false; + if (!parameters) return true; + std::uint32_t count{}; + if (!memory::read_bytes(parameters - 4, &count, sizeof(count))) return false; + count &= 0x7fffffff; + if (count > 4096) return false; + for (std::uint32_t i = 0; i < count; ++i) { + const auto entry = parameters + i * native::preset_parameter_size; + std::array value{}; // the boxed value: TypeInfo*, data* + if (!memory::read_bytes(entry, value.data(), sizeof(value))) return false; + if (!value[1]) continue; // the game passes over an entry with no data + std::uintptr_t type_data{}; + std::uint16_t flags{}; + if (!value[0] || !memory::read_bytes(value[0], &type_data, sizeof(type_data)) || !type_data || + !memory::read_bytes(type_data + 4, &flags, sizeof(flags))) { + memory::read_bytes(entry + native::preset_parameter_key, &bad, sizeof(bad)); + return false; + } + } + return true; +} +std::string preset_name(void *preset) { + std::uintptr_t text{}; + if (!memory::read_bytes(reinterpret_cast(preset) + 0x18, &text, sizeof(text)) || !text) return {}; + char buffer[200]{}; + return memory::peek_cstring(text, buffer, sizeof(buffer) - 1) > 0 ? std::string(buffer) : std::string{}; +} int add_preset(void *state) { const auto reference = reinterpret_cast(image + native::script_preset_reference)(state, 3); - if (reference && reinterpret_cast(image + native::referenced_preset)(reference, 0)) - return original_add_preset.load()(state); + 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); + // Left out, as a preset that is not there: the item shows without it. + static std::mutex refused_mutex; + static std::set refused; + const auto name = preset_name(preset); + std::lock_guard lock(refused_mutex); + if (refused.size() < 64 && refused.insert(name).second) + logging::log(logging::Level::warning, logging::Channel::customization, + "The shader preset {} is left out: its parameter 0x{:016x} has a type the game cannot read " + "(a mod's preset that is broken or made for another build). The item is shown without it.", + name.empty() ? "(unnamed)" : name, bad); + return 0; + } static std::atomic seen{}; const auto count = seen.fetch_add(1, std::memory_order_relaxed) + 1; if (count <= 5 || count % 500 == 0)