From 2b5772bdf591e42a36a1a8a6422ce254e0a6826a Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sat, 17 Oct 2026 20:32:28 -0500 Subject: [PATCH] Customization: a mod item's card shows the info sign instead of an empty icon The warning sign on a mod item's description notice was cleared by writing an empty IconOverride. The game resolves that override again later in its UI update and crashed on the empty one (Skate.exe+0x3e897fb, a lookup that finds nothing): about a quarter of 2.1.0's crash reports, all of them after the sign was cleared. The notice now gets the game's own info sign (img_generic_info_64), set the way the favorites star is. When that texture is not loaded the warning sign stays and it is looked for again two seconds later; an empty override is never written. Texture assets are matched by name ignoring case. --- Extension/Customization/item_browser.cpp | 32 +++++++++++++++----- Extension/UI/NativeMenu/native_menu_data.cpp | 3 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Extension/Customization/item_browser.cpp b/Extension/Customization/item_browser.cpp index 482a7bd8..e723c8dd 100644 --- a/Extension/Customization/item_browser.cpp +++ b/Extension/Customization/item_browser.cpp @@ -53,10 +53,13 @@ constexpr std::uint32_t rarity_icon = 0xd4f0f044; // (ui/features/mystuff/cas/actions/casgriditem_onfocus_inventorymanager) gives it a warning sign // (IconOverride: ui/textures/icons/generic/img_generic_warning_128) each time an item is // focused. A mod item's description is only room for the overlay's mod card -// (item_origins::reserved_description), so that notice loses the sign again. +// (item_origins::reserved_description), so that notice shows the game's info sign instead. Never +// none: the game resolves the override again later, and an empty one crashed it there +// (Skate.exe+0x3e897fb, a lookup that finds nothing; 2.1.0, 2026-10-18). constexpr std::uint32_t notice_view = 0x7290e514; // ui/foundations/components/notices/NoticeViewModel constexpr std::uint32_t notice_message = 0x94703efc, label_text = 0x4d8e01b9; // .Message (a label) .Text constexpr std::uint32_t notice_icon = 0x3514a765; // NoticeViewModel.IconOverride +constexpr char notice_info_texture[] = "ui/textures/icons/generic/img_generic_info_64"; // The game's own star (UI/Textures/Common/Shapes/Bespoke), as its asset is named. constexpr char star_texture[] = "UI/Textures/comMon/Shapes/Bespoke/img_Star_Fill_64"; @@ -156,7 +159,9 @@ struct State { ULONGLONG next_sync{}, next_scan{}, last_open{}; std::uint64_t active_list{}; // the grid list that had the focus at the last full pass std::vector notices; // the game's notice models, as of the last scan - bool notice_cleared_logged{}, notice_failed_logged{}; + std::uint64_t notice_info{}; // the info sign's texture, as last put on a notice + ULONGLONG notice_info_retry{}; // not loaded: when to look for it again + bool notice_cleared_logged{}, notice_failed_logged{}, notice_kept_logged{}; std::uint64_t serial{}; bool open{}; std::string last_error; @@ -599,12 +604,13 @@ void before_level_transition(unsigned next, std::uintptr_t base) noexcept { s.restyled.clear(); s.layout = {}; s.open = false; + s.notice_info = 0; // (its texture may go with the level) }); } catch (...) {} } -// A notice showing a mod item's reserved description loses its warning sign (see notice_view). -// Every tick while a grid is open: the game puts the sign back on each focus. +// A notice showing a mod item's reserved description has the info sign for the warning sign (see +// notice_view). Every tick while a grid is open: the game puts the warning back on each focus. void quiet_notices(State& s, const md::Context& context) { for (const auto handle : s.notices) { try { @@ -620,11 +626,21 @@ void quiet_notices(State& s, const md::Context& context) { std::string_view(start.data(), reserved.size()) != reserved) continue; const auto icon = context.field(notice, notice_icon); - std::vector now(md::size(icon.type)), none(now.size()); - if (now.empty() || !memory::peek_bytes(context.address(icon), now.data(), now.size()) || now == none) continue; - context.publish(icon, none.data()); + std::uint64_t now{}; + md::require(md::size(icon.type) == sizeof(now), "Native notice icon field differs."); + if (!memory::peek(context.address(icon), now) || !now || now == s.notice_info) continue; + // A texture the game has loaded, or the warning stays: never an empty override. + if (GetTickCount64() < s.notice_info_retry) continue; + if (!md::publish_texture(context, icon, notice_info_texture)) { + s.notice_info_retry = GetTickCount64() + 2000; + if (!std::exchange(s.notice_kept_logged, true)) + logging::log(Level::info, Channel::customization, "Item browser: the info sign is not loaded; a mod item's card keeps the warning sign."); + continue; + } + std::uint64_t info{}; + if (memory::peek(context.address(icon), info)) s.notice_info = info; if (!std::exchange(s.notice_cleared_logged, true)) - logging::log(Level::info, Channel::customization, "Item browser: a mod item's card shows no warning sign ({} byte icon cleared).", now.size()); + logging::log(Level::info, Channel::customization, "Item browser: a mod item's card shows the info sign for the warning sign."); } catch (const std::exception& error) { if (!std::exchange(s.notice_failed_logged, true)) logging::log(Level::warning, Channel::customization, "Item browser: a notice could not be checked for a mod item's warning sign: {}", error.what()); diff --git a/Extension/UI/NativeMenu/native_menu_data.cpp b/Extension/UI/NativeMenu/native_menu_data.cpp index 72cf05a9..f7061432 100644 --- a/Extension/UI/NativeMenu/native_menu_data.cpp +++ b/Extension/UI/NativeMenu/native_menu_data.cpp @@ -422,10 +422,11 @@ bool with_texture_assets(const Context& context, const std::array 65536 || count > 32768) return false; std::array assets{}; unsigned visited{}; + // (Ignoring case: assets keep the case they were authored with, which is not always lower.) const auto matches = [](Address text, std::string_view name) { std::array bytes{}; return name.size() < bytes.size() && memory::read_bytes(text, bytes.data(), name.size() + 1) && - bytes[name.size()] == '\0' && std::memcmp(bytes.data(), name.data(), name.size()) == 0; + bytes[name.size()] == '\0' && _strnicmp(bytes.data(), name.data(), name.size()) == 0; }; const auto complete = [&] { return std::all_of(assets.begin(), assets.end(), [](Address a) { return a != 0; }); }; for (unsigned i = 0; i < buckets && !complete(); ++i) {