#include "native_menu_data.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/engine.h" #include "Engine/Game/Build/20260929/native_menu.h" #include "Engine/Game/Build/20260929/native_party.h" #include #include #include namespace dingosdk::multiplayer::menu_data { namespace engine = addr::engine; namespace menu = addr::native_menu; void require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } unsigned kind(Address type) { return (read(read
(type) + 4) >> 5) & 31; } unsigned size(Address type) { return kind(type) == 3 ? 8 : read(read
(type) + 6); } std::string string(Address at, std::size_t maximum) { std::string result; if (!at) return result; for (std::size_t i = 0; i <= maximum; ++i) { const auto c = read(at + i); if (!c) return result; require(i < maximum, "Native menu text exceeds its limit."); result += c; } return result; } Address Context::type(Schema schema) const { if (const auto cached = types_.find(schema.hash); cached != types_.end()) { require(size(cached->second) == schema.size, "Native menu schema differs."); return cached->second; } const auto first = read
(manager + 0x600), last = read
(manager + 0x608); require(first && last >= first && (last - first) % 72 == 0 && last - first <= 65536 * 72, "Native menu type registry differs."); for (auto at = first; at < last; at += 72) { const auto begin = read
(at + 8), end = read
(at + 16); if (!begin || end <= begin) continue; const auto candidate = read
(begin), meta = read
(candidate); if (read(meta) != schema.hash) continue; require(kind(candidate) == 2 && size(candidate) == schema.size, "Native menu schema differs."); types_.emplace(schema.hash, candidate); return candidate; } throw std::runtime_error("Native menu schema has not loaded yet."); } Address Context::type_of(Handle handle) const { const auto record = reinterpret_cast
(base + addr::native_party::model_record)(manager, handle, 0); return record ? reinterpret_cast
(base + addr::native_party::model_type)(manager, handle, record, 0) : 0; } Address Context::address(Value value) const { require(value.handle && value.type && type_of(value.handle) == value.type, "Native menu model has expired."); // Return typed storage, including the address of a nullable asset pointer. // The unboxed form returns the asset itself (or null), which cannot be used // as a publication source and incorrectly rejects empty icon fields. const auto result = game::native_data().models.value(manager, value.handle, 0, 1); if (!result) throw std::runtime_error("Native menu model value is unavailable (handle " + std::to_string(value.handle) + ", schema " + std::to_string(read(read
(value.type))) + ")."); return result; } std::vector Context::roots(std::initializer_list hashes) const { const auto first = read
(manager + 0x98), last = read
(manager + 0xa0); require(first && last >= first && (last - first) % 40 == 0 && last - first <= 131072 * 40, "Native menu model registry differs."); std::vector result; const auto table = read
(manager + 0x600), table_end = read
(manager + 0x608); require(table && table_end >= table && (table_end - table) % 72 == 0 && table_end - table <= 65536 * 72, "Native menu type registry differs."); struct Record { Handle id; Address value; Handle root; std::uint16_t type, reserved; std::uint32_t parent, index, padding; }; static_assert(sizeof(Record) == 40); // The registry can hold a hundred thousand records, and the pause menu's page slots look // here every 250 ms until the menu opens: read it in chunks into one reused buffer (no // zero-filled copy of the whole thing), and decide each record type once per scan in a // flat table rather than a map (profiled 2026-10-01 at up to 7 ms a scan). constexpr std::size_t chunk = 4096; thread_local std::vector buffer(chunk); constexpr Address unknown = 1; std::vector
types(std::min((table_end - table) / 72, 65536), unknown); for (auto at = first; at < last; at += chunk * sizeof(Record)) { const auto count = std::min(chunk, (last - at) / sizeof(Record)); require(memory::peek_bytes(at, buffer.data(), count * sizeof(Record)), "Native menu model registry is unavailable."); for (std::size_t i = 0; i < count; ++i) { const auto& record = buffer[i]; if (record.parent != UINT32_MAX || !record.value || record.type >= types.size()) continue; auto& cached = types[record.type]; if (cached == unknown) { cached = 0; const auto entry = table + record.type * 72ULL; if (!read
(entry + 8)) continue; const auto candidate = read
(read
(entry + 8)); if (candidate && (!hashes.size() || std::find(hashes.begin(), hashes.end(), read(read
(candidate))) != hashes.end())) cached = candidate; } if (!cached) continue; const auto handle = record.root; if (type_of(handle) != cached) continue; result.push_back({{handle, cached}, game::native_data().models.value(manager, handle, 0, 0)}); } } return result; } Field Context::member(Address type, std::uint32_t hash) const { require(kind(type) == 2, "Native menu field is not a structure."); const auto meta = read
(type), fields = read
(meta + 0x60); const auto count = read(meta + 0x2a); require(count <= 128, "Native menu field table differs."); for (unsigned i = 0; i < count; ++i) { const auto entry = fields + i * 24; if (read(entry) != hash) continue; Field result{i, read(entry + 8), read
(entry + 16)}; require(result.offset + size(result.type) <= size(type), "Native menu field exceeds its structure."); return result; } throw std::runtime_error("Native menu field schema differs."); } Value Context::field(Value value, std::uint32_t hash) const { address(value); const auto f = member(value.type, hash); const auto handle = game::native_data().models.field(manager, value.handle, f.index, UINT32_MAX, false); require(handle && type_of(handle) == f.type, "Native menu field handle differs."); return {handle, f.type}; } Value Context::path(Value value, std::initializer_list hashes) const { for (const auto hash : hashes) value = field(value, hash); return value; } Value Context::element(Value array, unsigned index) const { require(kind(array.type) == 4, "Native menu value is not an array."); const auto pointer = read
(address(array)); require(pointer && index < (read(pointer - 4) & 0x7fffffff), "Native menu array index differs."); const auto type = read
(read
(array.type) + 0x30); const auto handle = game::native_data().models.field(manager, array.handle, UINT32_MAX, index, false); require(handle && type_of(handle) == type, "Native menu array element differs."); return {handle, type}; } Value Context::create(Schema schema, std::uint64_t id) const { const auto t = type(schema); const auto h = game::native_data().models.create(manager, t, id, game::native_name_hash("ReSkate.NativeMultiplayerMenu"), false, 2); Value result{h, t}; address(result); return result; } void Context::destroy(Value value) const { // The engine may already have removed a previous pause-menu generation. // Never resolve an expired handle to a replacement model or destroy a type. if (!value.handle || type_of(value.handle) != value.type) return; require(game::native_data().models.destroy != nullptr, "Native menu cleanup is unavailable."); std::array iterator{}; // Same non-forced, client-model removal used by native UI owners. It // invalidates bindings and runs typed destructors while assets are live. game::native_data().models.destroy(manager, iterator.data(), value.handle, 0, false, false); require(type_of(value.handle) != value.type, "Native menu model could not be released."); } void Context::publish(Value target, const void* bytes) const { address(target); game::native_data().models.publish(manager, target.handle, target.type, bytes); address(target); } void Context::copy(Value target, Address source) const { std::vector bytes(size(target.type)); require(memory::read_bytes(source, bytes.data(), bytes.size()), "Native menu template is unavailable."); publish(target, bytes.data()); } void Context::text(Value target, const std::string& text) const { require(kind(target.type) == 7 && text.size() <= 2048, "Native menu text schema differs."); Address native{}; game::native_data().values.assign(&native, text.c_str(), static_cast(text.size())); struct Release { Address base; Address& value; ~Release() { if (value) reinterpret_cast(base + engine::native_text_release)(&value); } } release{base, native}; set(target, native); } std::string Context::text(Value value, std::size_t maximum) const { require(kind(value.type) == 7, "Native menu text field differs."); return string(read
(address(value)), maximum); } std::vector Context::array(Value value, unsigned maximum, unsigned& count, unsigned& stride) const { require(kind(value.type) == 4, "Native menu list schema differs."); const auto at = read
(address(value)); count = at ? read(at - 4) & 0x7fffffff : 0; stride = size(read
(read
(value.type) + 0x30)); require(count <= maximum && stride && stride <= 8192, "Native menu list exceeds its limit."); std::vector result(static_cast(count) * stride); require(result.empty() || memory::peek_bytes(at, result.data(), result.size()), "Native menu list is unavailable."); return result; } void Context::array(Value target, std::span bytes, unsigned count) const { require(kind(target.type) == 4 && count <= 1024, "Native menu list is invalid."); const auto stride = size(read
(read
(target.type) + 0x30)); require(bytes.size() == static_cast(count) * stride, "Native menu list element size differs."); // The engine deep-copies strings, delegates, DataRefs and nested arrays // synchronously. No pointer into this temporary storage survives publish. std::vector storage((bytes.size() + 15) / 8); const std::array header{count, count}; std::memcpy(storage.data(), header.data(), 8); if (!bytes.empty()) std::memcpy(storage.data() + 1, bytes.data(), bytes.size()); const auto pointer = storage.data() + 1; set(target, pointer); } Address named_asset(const Context& context, Address anchor, const char* name) { const auto find = game::native_data().find_asset; if (!find || !anchor) return 0; // DataContainer stores its asset-domain ID at +16. A map's UI assets live // in that domain and its parents, not the boot-only domain 1. Match the // native parent traversal without searching unrelated worlds. auto domain = read(anchor + 0x16); std::set visited; while (domain < 0xbbf && visited.size() < 32 && visited.insert(domain).second) { const auto owner = read
(context.base + engine::domain_owners + domain * 8ULL); if (!owner) return 0; if (const auto asset = find(domain, name)) return asset; domain = read(owner + 0x42); } return 0; } namespace { constexpr std::array menu_assets{ "UI/Foundations/Templates/Page/Page_Widget", "UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget", "UI/Foundations/Components/Lists/Shared/AnchoredContentPresenterListItem_Widget", "UI/Foundations/Components/Buttons/LabelButton/LabelButton_Widget", "UI/Foundations/Components/Text/InputField/InputField_Widget", "UI/Foundations/Components/Text/Label/Widget/Label_Widget", "ButtonStyleList/TileButton.RoughPartial.Default", "ButtonStyleList/ButtonStyle.CTA.Hero", "UI/Foundations/Layers/Backgrounds/Background_Widget", "BackgroundStylesList/BackgroundStyle.Dark", "UI/Foundations/Templates/Layouts/Anchored/AnchoredContentPresenter_Widget", "UI/Foundations/Templates/Layouts/Partitions/Horizontal/HorizontalPartition_LinearFocus_Widget", "UI/Foundations/Templates/Layouts/Partitions/Vertical/VerticalPartition_LinearFocus_Widget", "BackgroundStylesList/BackgroundStyle.Black.Opaque"}; struct Walker { std::set> visited; std::map> assets; bool complete() const { return std::all_of(menu_assets.begin(), menu_assets.end(), [&](auto name) { return assets.contains(name); }); } void walk(Address type, Address value, unsigned depth = 0, bool object = false) { if (!value || depth > 18 || visited.size() >= 32768 || complete() || !visited.emplace(type, value).second) return; try { const auto meta = read
(type); const auto k = kind(type); if (k == 3 && !object) { const auto asset = read
(value); if (!asset) return; const auto actual = read
(asset + 8), am = read
(actual); if (kind(actual) != 3) return; const auto hash = read(am); if (hash == 0x5a080e4d || hash == 0x5366f94c || hash == 0xa70af1a2) assets.emplace(string(read
(asset + 0x18)), asset); if (hash == 0x7aab0d43) { assets.emplace(string(read
(asset + 0x28)), asset); walk(read
(asset + 0x18), read
(asset + 0x20), depth + 1); } else walk(actual, asset, depth + 1, true); } else if (k == 2 || object) { const auto count = read(meta + 0x2a); if (count > 128) return; const auto fields = read
(meta + (object ? 0x38 : 0x60)); for (unsigned i = 0; i < count; ++i) { const auto f = fields + i * 24; walk(read
(f + 16), value + read(f + 8), depth + 1); } } else if (k == 4) { const auto at = read
(value); if (!at) return; const auto count = read(at - 4) & 0x7fffffff; const auto element = read
(meta + 0x30); const auto stride = size(element); if (count > 4096 || !stride || stride > 8192) return; for (unsigned i = 0; i < std::min(count, 64U); ++i) walk(element, at + stride * i, depth + 1); } } catch (const std::exception&) { /* Optional asset branches may be unloaded. */ } } }; void foundation_widgets(const Context& context, Walker& walker, std::string_view anchor, Address radius = 4 * 1024 * 1024) { if (walker.complete()) return; // Some shared widgets (notably InputField) are resident in the Foundations // partition but have no live view-model reference until first opened. Limit // this fallback to 4 MiB either side of a known Foundations blueprint, // and accept only exact native headers and asset names. The allocator splits // these neighboring widgets across separate 2 MiB reservations. // This is a bounded, one-time lookup, never a whole-process string scan. const auto known = walker.assets.find(anchor); if (known == walker.assets.end()) return; const auto origin = read
(known->second + 8) == context.base + menu::asset_record_type ? read
(known->second + 0x20) : known->second; MEMORY_BASIC_INFORMATION region{}; if (!VirtualQuery(reinterpret_cast(origin), ®ion, sizeof(region)) || region.State != MEM_COMMIT || (region.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return; // VirtualQuery's BaseAddress starts at the queried page. It cannot be used // as the lower bound: Label_Widget precedes LabelButton in this allocation. const auto begin = origin - std::min(origin, radius); const auto end = origin + radius; if (end <= begin || end - begin > 16 * 1024 * 1024) return; const auto vtable = context.base + engine::blueprint_vtable, native_type = context.base + menu::widget_blueprint_type; constexpr std::array wanted{ "UI/Foundations/Templates/Page/Page_Widget", "UI/Foundations/Components/Lists/LinearList/Widget/LinearList_Widget", "UI/Foundations/Components/Lists/Shared/AnchoredContentPresenterListItem_Widget", "UI/Foundations/Components/Buttons/LabelButton/LabelButton_Widget", "UI/Foundations/Components/Text/InputField/InputField_Widget", "UI/Foundations/Components/Text/Label/Widget/Label_Widget", "ButtonStyleList/TileButton.RoughPartial.Default", "ButtonStyleList/ButtonStyle.CTA.Hero", "UI/Foundations/Layers/Backgrounds/Background_Widget", "UI/Foundations/Layers/Backgrounds/TextureBG_Widget", "BackgroundStylesList/BackgroundStyle.Dark", "UI/Foundations/Templates/Layouts/Anchored/AnchoredContentPresenter_Widget", "UI/Foundations/Templates/Layouts/Partitions/Horizontal/HorizontalPartition_GroupFocus_Widget", "UI/Foundations/Templates/Layouts/Partitions/Horizontal/HorizontalPartition_LinearFocus_Widget", "UI/Foundations/Templates/Layouts/Partitions/Vertical/VerticalPartition_GroupFocus_Widget", "UI/Foundations/Templates/Layouts/Partitions/Vertical/VerticalPartition_LinearFocus_Widget"}; for (auto at = begin; at < end;) { if (!VirtualQuery(reinterpret_cast(at), ®ion, sizeof(region))) break; const auto next = std::min(end, reinterpret_cast
(region.BaseAddress) + region.RegionSize); if (next <= at) break; if (region.State == MEM_COMMIT && !(region.Protect & (PAGE_GUARD | PAGE_NOACCESS))) { std::vector bytes(next - at); if (memory::read_bytes(at, bytes.data(), bytes.size())) { for (std::size_t off = (8 - (at & 7)) & 7; off + 48 <= bytes.size(); off += 8) { std::array header{}; std::memcpy(header.data(), bytes.data() + off, sizeof(header)); const bool widget = header[0] == vtable && header[1] == native_type; const bool texture = header[0] == context.base + menu::texture_asset_vtable && header[1] == context.base + menu::texture_asset_type; const bool image = header[0] == context.base + menu::image_asset_vtable && header[1] == context.base + menu::image_asset_type; const bool record = header[1] == context.base + menu::asset_record_type; if (!widget && !texture && !image && !record) continue; try { const auto name = string(header[record ? 5 : 3], 160); if (std::find(wanted.begin(), wanted.end(), name) != wanted.end()) walker.assets.emplace(name, at + off); } catch (const std::exception&) { } } } } at = next; } } } std::map> blueprints(const Context& context, Value core_value) { Walker walker; const auto social = context.field(context.element(context.path(core_value, {0x716496c8, 0x61742cb4}), 4), 0x716496c8); const auto anchor = read(context.address(social)).blueprint; // Native named lookup avoids walking every live UI model and scanning the // neighboring heaps on first pause. Fall back only for unindexed records. for (const auto name : menu_assets) { const auto asset = named_asset(context, anchor, name.data()); if (!asset) continue; const auto type = read
(asset + 8); if (type != context.base + menu::widget_blueprint_type && type != context.base + menu::asset_record_type) continue; const auto actual = string(read
(asset + (type == context.base + menu::asset_record_type ? 0x28 : 0x18)), 160); if (actual == name) walker.assets.emplace(actual, asset); } if (walker.complete()) return walker.assets; // Styles are exported records within these two small authored lists. Walk // those containers before considering unrelated live models or heaps. for (const auto* name : { "UI/Foundations/Styles/Lists/ButtonStyleList", "UI/Foundations/Layers/Backgrounds/BackgroundStylesList"}) { const auto asset = named_asset(context, anchor, name); if (!asset || read
(asset + 8) != context.base + menu::asset_list_type) continue; const auto records = read
(asset + 0x20); const auto count = records ? read(records - 4) & 0x7fffffff : 0; if (count > 128) continue; for (unsigned i = 0; i < count; ++i) { const auto record = read
(records + i * 8ULL); if (!record || read
(record + 8) != context.base + menu::asset_record_type) continue; const auto record_name = string(read
(record + 0x28), 160); if (std::find(menu_assets.begin(), menu_assets.end(), record_name) != menu_assets.end()) walker.assets.emplace(record_name, record); } } if (walker.complete()) return walker.assets; walker.walk(core_value.type, context.address(core_value)); if (walker.complete()) return walker.assets; // These roots contain the foundational button/list templates used by the // visible menu. // Take one registry snapshot, rather than scanning it again for each type. for (const auto& root : context.roots({page.hash, button.hash, linear_list.hash, presenter.hash, input.hash})) walker.walk(root.model.type, root.value); foundation_widgets(context, walker, "UI/Foundations/Components/Buttons/LabelButton/LabelButton_Widget"); foundation_widgets(context, walker, "UI/Foundations/Templates/Page/Page_Widget"); // Image resources in the small-object arena can straddle more reservations // on a fresh launch. Keep this lookup bounded around a resident style. foundation_widgets(context, walker, "ButtonStyleList/TileButton.RoughPartial.Default", 8 * 1024 * 1024); foundation_widgets(context, walker, "UI/Textures/Icons/Global/img_GlobalNav_Social_256"); // The resident settings layouts provide a bounded anchor for the fixed // partition widgets, which live outside the basic-controls asset arena. for (const auto& root : context.roots({0xf0b42cc6})) { for (const auto field : {0xf750f0a2U, 0x29023866U}) { // Preserve the authored asset reference as well as a mounted // widget's effective value, which may have been cleared on teardown. for (const auto address : {root.value + context.member(root.model.type, field).offset, context.address(context.field(root.model, field))}) { const auto widget = read(address); if (widget.blueprint && read
(widget.blueprint + 8) == context.base + menu::widget_blueprint_type) walker.assets.emplace(string(read
(widget.blueprint + 0x18)), widget.blueprint); } } } foundation_widgets(context, walker, "UI/Foundations/Templates/Layouts/Partitions/Horizontal/HorizontalPartition_LinearFocus_Widget"); foundation_widgets(context, walker, "UI/Foundations/Templates/Layouts/Partitions/Vertical/VerticalPartition_LinearFocus_Widget"); foundation_widgets(context, walker, "UI/Foundations/Templates/Layouts/Partitions/Vertical/VerticalPartition_GroupFocus_Widget"); foundation_widgets(context, walker, "UI/Foundations/Templates/Layouts/Partitions/Horizontal/HorizontalPartition_GroupFocus_Widget"); return walker.assets; } namespace { template bool with_texture_assets(const Context& context, const std::array& names, Publish&& publish) { // RimeTextureAsset registry, shared with native compass icons. Assets from // DingoLevel_Root may be far from the current map's widget allocations. // Native registry functions verify this table and critical-section layout. const auto manager = read
(context.base + menu::texture_registry); if (!manager) return false; auto* section = reinterpret_cast(manager + 0x188); EnterCriticalSection(section); struct Unlock { CRITICAL_SECTION* value; ~Unlock() { LeaveCriticalSection(value); } } unlock{section}; if (read
(context.base + menu::texture_registry) != manager) return false; const auto buckets = read(manager + 0xa0); const auto count = read(manager + 0xa4); const auto table = read
(manager + 0x98); if (!table || !buckets || buckets > 65536 || count > 32768) return false; std::array assets{}; unsigned visited{}; 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; }; const auto complete = [&] { return std::all_of(assets.begin(), assets.end(), [](Address a) { return a != 0; }); }; for (unsigned i = 0; i < buckets && !complete(); ++i) { for (auto node = read
(table + i * 8); node; node = read
(node + 16)) { if (++visited > count) return false; const auto asset = read
(node + 8); if (!asset) continue; const auto vtable = read
(asset), type = read
(asset + 8); const bool image = vtable == context.base + menu::image_asset_vtable && type == context.base + menu::image_asset_type; const bool texture = vtable == context.base + menu::texture_asset_vtable && type == context.base + menu::texture_asset_type; if (!image && !texture) continue; const auto name = read
(asset + 0x18); for (std::size_t n = 0; n < N; ++n) if (!assets[n] && matches(name, names[n])) assets[n] = asset; } } // Publish a complete pair, so missing art cannot mix unrelated idle/focus // images. No borrowed registry pointer survives this locked operation. if (!complete()) return false; publish(assets); return true; } } bool publish_tab_icons(const Context& context, Value tab, std::string_view idle, std::string_view focused) { return with_texture_assets(context, std::array{idle, focused}, [&](const auto& assets) { context.set(context.field(tab, 0xd4f0f044), assets[0]); // Icon. context.set(context.field(tab, 0xbc92d1fa), assets[1]); // FocusIcon. }); } } // namespace dingosdk::multiplayer::menu_data