#include "native_menu_internal.h" #include "Engine/Core/Log/logging.h" #include #include #include #include // The pause menu's Hub page lists live-service tiles in two native TileLists. // ReSkate has no store, skate.Pass, bounties, missions or crew bounties, so // those tiles go; News and Rip Score (profile) stay. The Hub resolves a press // by the tile's position, so a tile with a kept tile after it is hidden in // place (an empty gap with no action) rather than removed; trailing ones are // removed. The game rebuilds the Hub now and then; this is re-applied then. namespace dingosdk::multiplayer { using namespace menu_data; using namespace native_menu_detail; namespace { constexpr std::uint32_t tile_list_schema = 0x74b1ea1d, tile_items = 0x67223da7; // TileListItem -> presenter -> {button, content widget}; a tile's data names its title. constexpr std::uint32_t tile_presenter = 0x214d4984, tile_button = 0x0fb0d794, tile_content = 0x214d4984; constexpr std::uint32_t tile_title = 0x77b8ed0b, navigation = 0xb1e14cd6, delegate = 0x9c76b86c; constexpr std::uint32_t action_active = 0x4b3146e9, limited_interactivity = 0x67212c85, layer_blueprint = 0x8d1441c7; constexpr std::uint32_t default_index = 0x1600d48e; // Tiles navigate by name (bounties, missions, crew bounties), or open the // Store and skate.Pass through their own actions, known by their titles. constexpr std::array removed_navigation{"HUB_Nav_Events", "HUB_Nav_Quests", "HUB_Nav_CrewProgression"}; constexpr std::array removed_titles{"ID_MENU_STORE", "ID_MENU_SKATEPASS"}; Value interact(const Context &c, Value item) { return c.path(item, {tile_presenter, tile_button, 0xa704272a, 0xc52416ef, 0xa704272a}); } std::string navigation_name(const Context &c, Value item) { return c.text(c.path(interact(c, item), {0xa2b71c93, navigation}), 64); } // The tile's background comes from its button style, which every Hub tile // shares. A hidden tile gets its own copy with every fill (idle, focus and // the rest) set to the game's empty fill, so nothing of it is drawn. constexpr Schema button_style{0x3d13ff54, 120}; constexpr std::uint32_t button_style_ref = 0x8cc042ef, state_fills = 0x2d393a3a, focus_fills = 0x343abac3; constexpr std::array state_fill_fields{0xb6c6aace, 0xe8f882b7, 0x69cf50ca, 0x182004ff, 0xfadedb6a, 0x24996c65, 0xb0db14be}; constexpr std::array focus_fill_fields{0xf622ea66, 0x943abf2d, 0x287f4815}; constexpr std::uint64_t empty_style_id = 0x52534d5048554253ULL; // "RSMPHUBS" Value empty_style; Address empty_style_manager{}; Value empty_tile_style(const Context &c, Value style_ref) { auto &style = empty_style; auto &manager = empty_style_manager; if (style.handle && manager == c.manager && c.type_of(style.handle) == style.type) return style; // Copy the tile's own style so everything but the fills stays as authored. const auto current = read(c.address(style_ref)); const Value source{current.handle, current.handle ? c.type_of(current.handle) : 0}; require(source.handle && source.type == c.type(button_style), "Hub tile style differs."); // The authored tile style already uses the empty fill for its disabled state. const auto empty = read
(c.address(c.path(source, {state_fills, 0x69cf50ca}))); require(empty && string(read
(empty + 0x18), 96) == "UI/Foundations/Styles/FIll/Common/FillStyle_Empty", "Hub empty fill is unavailable."); style = c.create(button_style, empty_style_id); manager = c.manager; c.copy(style, c.address(source)); for (const auto field : state_fill_fields) c.set(c.path(style, {state_fills, field}), empty); for (const auto field : focus_fill_fields) c.set(c.path(style, {focus_fills, field}), empty); return style; } // A tile hidden earlier has lost its title and action, so it is known by the // empty style it was given. bool already_hidden(const Context &c, Value item) { if (!empty_style.handle || empty_style_manager != c.manager) return false; return read(c.address(c.path(item, {tile_presenter, tile_button, button_style_ref}))).handle == empty_style.handle; } // Keeps the tile's slot, size included, so the tiles after it stay where they // were; it just shows and does nothing. Runs every frame, so each field is // written only when it isn't hidden already. void hide_in_place(const Context &c, Value item) { const auto style_ref = c.path(item, {tile_presenter, tile_button, button_style_ref}); const auto style = empty_tile_style(c, style_ref); if (read(c.address(style_ref)).handle != style.handle) c.set(style_ref, Ref{0, style.handle}); const auto tile_widget = c.path(item, {tile_presenter, tile_content}); const auto shown = read(c.address(tile_widget)); if (shown.blueprint || shown.data.record || shown.data.handle) c.set(tile_widget, Widget{}); // An inactive tile draws the "limited interactivity" layer (a gray panel // with a scribble, LimitedInteractivity_Default_Rough); without its widget // blueprint nothing is drawn. const auto limited_layer = c.path(item, {tile_presenter, limited_interactivity, layer_blueprint}); if (read
(c.address(limited_layer))) c.set(limited_layer, Address{}); const auto handler = interact(c, item); const auto action = c.field(handler, 0xa2b71c93); if (!c.text(c.field(action, navigation), 64).empty()) c.text(c.field(action, navigation), ""); if (read
(c.address(c.field(action, delegate)))) c.set(c.field(action, delegate), Address{}); if (read(c.address(c.field(handler, action_active)))) c.set(c.field(handler, action_active), false); } std::string title_key(const Context &c, Value item) { const auto widget = read(c.address(c.path(item, {tile_presenter, tile_content}))); const auto type = widget.data.handle ? c.type_of(widget.data.handle) : 0; if (!type) return {}; try { return c.text(c.path(Value{widget.data.handle, type}, {tile_title, text_field}), 64); } catch (const std::exception &) { return {}; // a tile whose data has no title } } template bool listed(const std::array &names, const std::string &name) { return std::find(names.begin(), names.end(), name) != names.end(); } void remove_hub_tiles(const Context &c) { for (const auto &root : c.roots({tile_list_schema})) { const auto list = c.field(root.model, tile_items); unsigned count{}, stride{}; const auto bytes = c.array(list, 32, count, stride); std::vector unwanted(count); unsigned last_kept = 0; // one past the last tile that stays for (unsigned i = 0; i < count; ++i) { const auto item = c.element(list, i); unwanted[i] = already_hidden(c, item) || listed(removed_navigation, navigation_name(c, item)) || listed(removed_titles, title_key(c, item)); if (!unwanted[i]) last_kept = i + 1; } // Hide the ones before a kept tile; drop the trailing ones. for (unsigned i = 0; i < last_kept; ++i) if (unwanted[i]) hide_in_place(c, c.element(list, i)); // Focus entering the list starts on its first shown tile, not a hidden one. const auto first_kept = static_cast(std::find(unwanted.begin(), unwanted.end(), false) - unwanted.begin()); const auto entry = c.field(root.model, default_index); const auto current_entry = read(c.address(entry)); if (first_kept < static_cast(last_kept) && current_entry >= 0 && current_entry < static_cast(count) && unwanted[static_cast(current_entry)]) c.set(entry, first_kept); if (last_kept < count && std::any_of(unwanted.begin() + last_kept, unwanted.end(), [](bool u) { return u; })) { std::vector kept(bytes.begin(), bytes.begin() + std::size_t{last_kept} * stride); c.array(list, kept, last_kept); } } } // With every tile of the left list gone, the Hub has nothing to focus when the // pause menu opens: its partition (HorizontalPartition_LinearFocus_Widget) // always starts on the left side, and an empty side isn't focusable. So the // right list (News, Profile) moves into the left side and the right side is // left empty. constexpr std::uint32_t partition_schema = 0xf0b42cc6, partition_left = 0xf750f0a2, partition_right = 0x29023866; constexpr std::string_view hub_list_record = "HUB_Base_ContentResources/HUB_TileList"; // Record names elsewhere can be long; only the prefix is compared. bool named_like(Address record, std::string_view prefix) { const auto name = read
(record + 0x28); if (!name) return false; for (std::size_t i = 0; i < prefix.size(); ++i) if (read(name + i) != prefix[i]) return false; return true; } // The tile count of a Hub tile list shown in a partition side, or nothing. std::optional hub_list_tiles(const Context &c, const Widget &widget, Address list_type) { if (!widget.blueprint || !widget.data.record || !widget.data.handle) return std::nullopt; if (!named_like(widget.data.record, hub_list_record) || c.type_of(widget.data.handle) != list_type) return std::nullopt; unsigned count{}, stride{}; c.array(c.field(Value{widget.data.handle, list_type}, tile_items), 32, count, stride); return count; } void move_hub_column(const Context &c) { const auto list_type = c.type({tile_list_schema, 392}); for (const auto &root : c.roots({partition_schema})) { const auto left = c.field(root.model, partition_left), right = c.field(root.model, partition_right); const auto right_widget = read(c.address(right)); const auto right_tiles = hub_list_tiles(c, right_widget, list_type); if (!right_tiles || !*right_tiles) continue; const auto left_tiles = hub_list_tiles(c, read(c.address(left)), list_type); if (left_tiles && *left_tiles) continue; // the left list still has tiles of its own c.set(left, right_widget); c.set(right, Widget{}); } } // Each Hub list is anchored toward the centre split: the right list with // ListAnchorPoint 1, the left one with 0. The moved list takes the left one's. constexpr std::uint32_t list_anchor_point = 0xb9eae335; constexpr std::int32_t left_list_anchor = 0; void align_hub_column(const Context &c) { const auto list_type = c.type({tile_list_schema, 392}); for (const auto &root : c.roots({partition_schema})) { const auto left = read(c.address(c.field(root.model, partition_left))); const auto left_tiles = hub_list_tiles(c, left, list_type); if (!left_tiles || !*left_tiles) continue; if (read(c.address(c.field(root.model, partition_right))).blueprint) continue; // not the moved column const auto anchor = c.field(Value{left.data.handle, list_type}, list_anchor_point); if (read(c.address(anchor)) != left_list_anchor) c.set(anchor, left_list_anchor); } } } // namespace // Runs every frame while the pause menu is open, so the removed tiles never // get a frame on screen when the Hub is rebuilt. Never lets the Hub disturb // ReSkate's own tabs: a failure is logged once and retried later. void update_hub(const Context &c) { static std::uint64_t next_update{}; static std::string last_error; const auto now = GetTickCount64(); if (now < next_update) return; try { remove_hub_tiles(c); move_hub_column(c); align_hub_column(c); last_error.clear(); } catch (const std::exception &e) { next_update = now + 2000; if (last_error != e.what()) { last_error = e.what(); logging::log(logging::Level::warning, logging::Channel::ui, "Hub tiles: {}", e.what()); } } } } // namespace dingosdk::multiplayer