Просмотр исходного кода

Merge pull request #112 from HiddenInvariant/feature/activity-launcher

Add runtime Activity Launcher to the HUD
stan 20 часов назад
Родитель
Сommit
66888a8684
35 измененных файлов с 3662 добавлено и 499 удалено
  1. 2 0
      .gitignore
  2. 25 4
      Sunrise/Sunrise.vcxproj
  3. 549 0
      Sunrise/src/client/activity/mission_launch.cpp
  4. 47 0
      Sunrise/src/client/activity/mission_launch.h
  5. 140 0
      Sunrise/src/client/activity/mission_launch_options.h
  6. 46 0
      Sunrise/src/client/content/activity/activity_catalog_build.cpp
  7. 15 0
      Sunrise/src/client/content/activity/activity_catalog_build.h
  8. 118 0
      Sunrise/src/client/content/activity/activity_menu_text_build.h
  9. 192 0
      Sunrise/src/client/content/activity/activity_presentation_build.h
  10. 7 1
      Sunrise/src/client/content/items/packages/package_item_build.cpp
  11. 4 1
      Sunrise/src/client/hooks/bootflow/world_step.cpp
  12. 2 0
      Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_frame.cpp
  13. 2 0
      Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_lifecycle.cpp
  14. 3 0
      Sunrise/src/client/runtime/client_hook_activation.cpp
  15. 2 0
      Sunrise/src/client/runtime/client_runtime_lifecycle.cpp
  16. 88 0
      Sunrise/src/client/ui/mission_launch/mission_launch_art.cpp
  17. 21 0
      Sunrise/src/client/ui/mission_launch/mission_launch_art.h
  18. 88 0
      Sunrise/src/client/ui/mission_launch/mission_launch_cards.h
  19. 56 0
      Sunrise/src/client/ui/mission_launch/mission_launch_detail_style.h
  20. 7 89
      Sunrise/src/client/ui/mission_launch/mission_launch_lists.cpp
  21. 3 31
      Sunrise/src/client/ui/mission_launch/mission_launch_lists.h
  22. 275 0
      Sunrise/src/client/ui/mission_launch/mission_launch_manual.h
  23. 322 0
      Sunrise/src/client/ui/mission_launch/mission_launch_model.h
  24. 507 0
      Sunrise/src/client/ui/mission_launch/mission_launch_panel.cpp
  25. 6 0
      Sunrise/src/client/ui/mission_launch/mission_launch_panel.h
  26. 9 2
      Sunrise/src/client/ui/runtime/client_ui_module_runtime.cpp
  27. 190 0
      Sunrise/src/middleware/content/packages/tables/activity_presentation_table.h
  28. 118 0
      Sunrise/src/middleware/content/packages/tables/activity_table.h
  29. 0 340
      Sunrise/src/server/ui/activity_override/activity_override_panel.cpp
  30. 0 8
      Sunrise/src/server/ui/activity_override/activity_override_panel.h
  31. 6 23
      Sunrise/src/server/ui/runtime/server_ui_module_runtime.cpp
  32. 22 0
      Sunrise/src/state/build_data/activities/activity_artwork.h
  33. 106 0
      Sunrise/src/state/build_data/activities/activity_catalog.cpp
  34. 61 0
      Sunrise/src/state/build_data/activities/activity_catalog.h
  35. 623 0
      Sunrise/src/state/build_data/activities/activity_releases.h

+ 2 - 0
.gitignore

@@ -15,3 +15,5 @@
 # CMake build files
 # CMake build files
 /build/
 /build/
 /out/
 /out/
+/docs/
+/Sunrise/docs/

+ 25 - 4
Sunrise/Sunrise.vcxproj

@@ -879,8 +879,6 @@
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_world_links.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_world_links.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.cpp" />
-    <ClCompile Include="src\server\ui\activity_override\activity_override_panel.cpp" />
-    <ClCompile Include="src\server\ui\activity_override\activity_override_lists.cpp" />
     <ClCompile Include="src\server\transport\bap_listener.cpp" />
     <ClCompile Include="src\server\transport\bap_listener.cpp" />
     <ClCompile Include="src\server\bap\bap_route.cpp" />
     <ClCompile Include="src\server\bap\bap_route.cpp" />
     <ClCompile Include="src\server\bap\bap_activity_link_view.cpp" />
     <ClCompile Include="src\server\bap\bap_activity_link_view.cpp" />
@@ -1599,8 +1597,6 @@
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_scriptable_world_links.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_scriptable_world_links.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.h" />
-    <ClInclude Include="src\server\ui\activity_override\activity_override_panel.h" />
-    <ClInclude Include="src\server\ui\activity_override\activity_override_lists.h" />
     <ClInclude Include="src\server\bap\runtime.h" />
     <ClInclude Include="src\server\bap\runtime.h" />
     <ClInclude Include="src\server\bap\activity_mission_seed_lease.h" />
     <ClInclude Include="src\server\bap\activity_mission_seed_lease.h" />
     <ClInclude Include="src\server\bap\internal.h" />
     <ClInclude Include="src\server\bap\internal.h" />
@@ -2156,5 +2152,30 @@
     <None Include="resources\database\account_settings_schema.sql" />
     <None Include="resources\database\account_settings_schema.sql" />
     <None Include="resources\database\account_settings_defaults.sql" />
     <None Include="resources\database\account_settings_defaults.sql" />
   </ItemGroup>
   </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="src\client\ui\mission_launch\mission_launch_panel.cpp" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_panel.h" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_model.h" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_cards.h" />
+    <ClCompile Include="src\client\ui\mission_launch\mission_launch_art.cpp" />
+    <ClCompile Include="src\client\ui\mission_launch\mission_launch_lists.cpp" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_lists.h" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_art.h" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_manual.h" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_detail_style.h" />
+    <ClCompile Include="src\client\activity\mission_launch.cpp" />
+    <ClInclude Include="src\client\activity\mission_launch.h" />
+    <ClInclude Include="src\client\activity\mission_launch_options.h" />
+    <ClCompile Include="src\client\content\activity\activity_catalog_build.cpp" />
+    <ClInclude Include="src\client\content\activity\activity_catalog_build.h" />
+    <ClInclude Include="src\client\content\activity\activity_presentation_build.h" />
+    <ClInclude Include="src\client\content\activity\activity_menu_text_build.h" />
+    <ClCompile Include="src\state\build_data\activities\activity_catalog.cpp" />
+    <ClInclude Include="src\state\build_data\activities\activity_catalog.h" />
+    <ClInclude Include="src\state\build_data\activities\activity_artwork.h" />
+    <ClInclude Include="src\state\build_data\activities\activity_releases.h" />
+    <ClInclude Include="src\middleware\content\packages\tables\activity_table.h" />
+    <ClInclude Include="src\middleware\content\packages\tables\activity_presentation_table.h" />
+  </ItemGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
 </Project>
 </Project>

+ 549 - 0
Sunrise/src/client/activity/mission_launch.cpp

@@ -0,0 +1,549 @@
+#include "mission_launch.h"
+
+#include <Windows.h>
+
+#include <array>
+#include <atomic>
+#include <cstddef>
+#include <cstdio>
+#include <cstring>
+#include <string_view>
+
+#include "../../core/logging/log.h"
+#include "../../state/activity/forced/activity_forced_destination.h"
+#include "../../state/activity/runtime.h"
+#include "../../state/build_data/activities/activity_catalog.h"
+#include "../../state/build_data/runtime.h"
+#include "../patterns/image_scan.h"
+#include "mission_launch_options.h"
+
+namespace sunrise::client::activity::mission_launch {
+namespace {
+
+using patterns::resolve_relative;
+using patterns::scan_main_image_unique;
+using patterns::signature;
+using patterns::signature_length;
+
+// --- Director selection entry points ---
+
+// Matches the member-record getter: the manager call, then the primary-session walk.
+constexpr std::string_view kMemberRecordText = "40 53 48 83 EC 20 8B D9 E8 ? ? ? ? 33 D2";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kMemberRecord = signature<signature_length(kMemberRecordText)>(kMemberRecordText);
+/** The record getter's first call is a near call to the session-manager getter. */
+constexpr std::size_t kManagerCallOffset = 8;
+/** A near call is its opcode and a 4-byte displacement. */
+constexpr std::size_t kNearCallBytes = 5;
+/** Intel's direct near-call opcode guards a decoded target against layout drift. */
+constexpr std::byte kNearCallOpcode{0xE8};
+
+// Matches the membership-revision test; the exact field offset is what makes it unique.
+constexpr std::string_view kSessionReadyText = "83 B9 6C 08 00 00 00 0F 95 C0 C3";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kSessionReady = signature<signature_length(kSessionReadyText)>(kSessionReadyText);
+
+// Matches the local-member readiness test by its two slot reads and their bound checks.
+constexpr std::string_view kMemberReadyText =
+    "4C 8B C1 48 63 89 3C E9 00 00 49 63 90 74 08 00 00 83 F9 1F 77 ? 83 FA 1F 77 ?";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kMemberReady = signature<signature_length(kMemberReadyText)>(kMemberReadyText);
+
+// Matches the selection constructor by its argument moves.
+constexpr std::string_view kConstructSelectionText =
+    "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 20 41 0F B7 D8 8B FA";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kConstructSelection =
+    signature<signature_length(kConstructSelectionText)>(kConstructSelectionText);
+
+// Matches the selection validator. Its field offsets and the no-name sentinel separate it
+// from its twin.
+constexpr std::string_view kSelectionValidText =
+    "0F B6 11 B0 01 80 FA FF 75 ? 66 83 79 02 FF 75 ? 83 79 08 FF 75 ? 66 83 79 04 FF 75 ? "
+    "48 8B 41 10 48 FF C8 48 83 F8 FD 76 ? 38 51 3C 75 ? 81 79 40 C5 9D 1C 81";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kSelectionValid =
+    signature<signature_length(kSelectionValidText)>(kSelectionValidText);
+
+// Matches the activity name getter. The record's name field at +0x68 separates it from its
+// twin.
+constexpr std::string_view kActivityNameText =
+    "48 89 5C 24 ? 57 48 83 EC 20 0F B7 F9 33 DB E8 ? ? ? ? 84 C0 74 ? E8 ? ? ? ? 0F B7 D7 "
+    "48 8B C8 4C 8B 00 41 FF 90 ? ? ? ? 48 8B C8 48 85 C0 74 ? 48 8B 40 68 48 85 C0 74 ? "
+    "48 8D 59 68";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kActivityName = signature<signature_length(kActivityNameText)>(kActivityNameText);
+
+// Matches one call site of the clear RPC. The RPC body is byte-identical to its twin.
+constexpr std::string_view kClearCallSiteText =
+    "E8 ? ? ? ? 48 8D 54 24 ? 33 C9 E8 ? ? ? ? 48 8B 9C 24";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kClearCallSite = signature<signature_length(kClearCallSiteText)>(kClearCallSiteText);
+
+// Matches the set-selection RPC by its 0x160-byte frame and stack cookie.
+constexpr std::string_view kSetSelectionText =
+    "48 89 5C 24 ? 48 89 74 24 ? 57 48 81 EC 60 01 00 00 48 8B 05 ? ? ? ? 48 33 C4 "
+    "48 89 84 24 ? ? ? ? 48 8B FA";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kSetSelection = signature<signature_length(kSetSelectionText)>(kSetSelectionText);
+
+// Matches the commit RPC through its four-argument dispatch and the block after its return.
+constexpr std::string_view kCommitSelectionText =
+    "89 4C 24 ? 48 83 EC 38 E8 ? ? ? ? F6 00 02 74 ? 48 8D 0D ? ? ? ? 41 B9 04 00 00 00 "
+    "48 89 4C 24 ? 4C 8D 44 24 ? 48 8B C8 48 8D 15 ? ? ? ? E8 ? ? ? ? 48 83 C4 38 C3 "
+    "48 8D 4C 24";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kCommitSelection =
+    signature<signature_length(kCommitSelectionText)>(kCommitSelectionText);
+
+// --- Native layouts, as far as the launch path reads them ---
+
+/** Local member slot of one group session. */
+constexpr std::size_t kSessionLocalMemberOffset = 0xE93C;
+/** Lifecycle state of one group session. */
+constexpr std::size_t kSessionStateOffset = 0x1AEF8;
+/** One group session's stride inside the manager. */
+constexpr std::size_t kSessionStride = 0x1C8A0;
+
+/** One group session. Only the two fields the launch reads are named. */
+struct GroupSession {
+    std::array<std::byte, kSessionLocalMemberOffset> opaque00{};
+    std::int32_t localMember{};
+    std::array<std::byte, kSessionStateOffset - kSessionLocalMemberOffset - sizeof(std::int32_t)>
+        opaque01{};
+    std::int32_t state{};
+    std::array<std::byte, kSessionStride - kSessionStateOffset - sizeof(std::int32_t)> opaque02{};
+};
+
+static_assert(offsetof(GroupSession, localMember) == kSessionLocalMemberOffset);
+static_assert(offsetof(GroupSession, state) == kSessionStateOffset);
+static_assert(sizeof(GroupSession) == kSessionStride);
+
+/** Index of the primary session inside the manager. */
+constexpr std::size_t kManagerPrimaryOffset = 0x10;
+/** First session slot inside the manager. */
+constexpr std::size_t kManagerSessionsOffset = 0x18;
+/** Session slots the manager carries. */
+constexpr std::size_t kSessionSlots = 4;
+
+/** The group session manager, as far as the launch reads it. */
+struct SessionManager {
+    std::array<std::byte, kManagerPrimaryOffset> opaque00{};
+    std::int32_t primary{};
+    std::uint32_t opaque01{};
+    std::array<GroupSession, kSessionSlots> sessions{};
+};
+
+static_assert(offsetof(SessionManager, primary) == kManagerPrimaryOffset);
+static_assert(offsetof(SessionManager, sessions) == kManagerSessionsOffset);
+
+/** Launch state byte of one member record. */
+constexpr std::size_t kMemberLaunchStateOffset = 0xA33;
+
+/** One session member record, as far as the launch reads it. */
+struct MemberRecord {
+    std::array<std::byte, kMemberLaunchStateOffset> opaque00{};
+    std::uint8_t launchState{};
+};
+
+static_assert(offsetof(MemberRecord, launchState) == kMemberLaunchStateOffset);
+
+/** Size of the selection the constructor fills and the RPCs consume. */
+constexpr std::size_t kSelectionBytes = 0x120;
+/** Selection fields past the destination that the launch never reads. */
+constexpr std::size_t kSelectionTailBytes = kSelectionBytes - 6;
+
+/** One activity selection. The constructor writes it whole; the launch reads the head. */
+struct alignas(16) Selection {
+    std::uint8_t kind{};
+    std::uint8_t opaque00{};
+    std::int16_t source{};
+    std::int16_t destination{};
+    std::array<std::byte, kSelectionTailBytes> opaque01{};
+};
+
+static_assert(offsetof(Selection, source) == 2);
+static_assert(offsetof(Selection, destination) == 4);
+static_assert(sizeof(Selection) == kSelectionBytes);
+
+// --- Launch policy ---
+
+/** Session states that accept a launch: joined through in-activity. */
+constexpr std::int32_t kSessionStateFirstReady = 4;
+constexpr std::int32_t kSessionStateLastReady = 9;
+/** Member slots one session addresses. */
+constexpr std::int32_t kMemberSlots = 12;
+/** Launch states from here on already hold a pending launch. */
+constexpr std::uint8_t kLaunchStatePending = 3;
+/** Selection kind the constructor writes for an activity pick. */
+constexpr std::uint8_t kSelectionKindActivity = 0;
+/** Reason the Director passes to the constructor for a plain activity pick. */
+constexpr std::uint32_t kSelectionReason = 0;
+/** Selection slot the Director fills for an orbit launch. */
+constexpr std::uint8_t kSelectionSlot = 0;
+/** Commit argument the Director sends for its own launch. */
+constexpr std::int32_t kCommitLaunch = 1;
+/** Boot-flow step `setup:orbit`. */
+constexpr std::int32_t kSetupOrbitStep = 29;
+/** Boot-flow step `activity:in_world`. */
+constexpr std::int32_t kInWorldStep = 38;
+/** An arrival not confirmed inside this window reports as timed out. */
+constexpr std::uint64_t kArrivalTimeoutMs = 120'000;
+
+using GetManager = SessionManager*(__fastcall*)() noexcept;
+using SessionTest = bool(__fastcall*)(const GroupSession*) noexcept;
+using GetMemberRecord = MemberRecord*(__fastcall*)(std::uint32_t) noexcept;
+using ConstructSelection = Selection*(__fastcall*)(Selection*,
+                                                   std::uint32_t,
+                                                   std::int16_t) noexcept;
+using SelectionValid = bool(__fastcall*)(const Selection*) noexcept;
+using ActivityName = const char*(__fastcall*)(std::int16_t) noexcept;
+using ClearSelections = void(__fastcall*)() noexcept;
+using SetSelection = void(__fastcall*)(std::uint8_t, const Selection*) noexcept;
+using CommitSelection = void(__fastcall*)(std::int32_t) noexcept;
+
+/** Unowned entry points of the Director's selection path. */
+struct Natives {
+    GetManager manager{};
+    SessionTest sessionReady{};
+    SessionTest memberReady{};
+    GetMemberRecord memberRecord{};
+    ConstructSelection construct{};
+    SelectionValid valid{};
+    ActivityName name{};
+    ClearSelections clear{};
+    SetSelection select{};
+    CommitSelection commit{};
+};
+
+Natives g_natives{};
+std::atomic_bool g_resolved{false};
+
+SRWLOCK g_lock{SRWLOCK_INIT};
+Snapshot g_state{};
+std::uint64_t g_requestedAt{};
+// Game-thread-only receipt state, scoped to a successfully submitted request.
+state::activity::SessionBinding g_previousSession{};
+bool g_leftOrbit{};
+ManualScratch g_manualScratch{}; // Game-frame owner only, outside the UI arena and native stack.
+
+/** @return The newest joined session binding, or an empty one when none is joined. */
+state::activity::SessionBinding newest_joined_session() noexcept {
+    std::array<state::activity::SessionRosterRow, state::activity::kSessionCapacity> rows{};
+    std::size_t count{};
+    state::activity::SessionBinding newest{};
+    if (!state::activity::snapshot_session_roster(rows, count)) {
+        return newest;
+    }
+    for (const auto& row : std::span(rows).first(count)) {
+        if (row.joined && row.binding.createdRevision > newest.createdRevision) {
+            newest = row.binding;
+        }
+    }
+    return newest;
+}
+
+/** Records the outcome of the pending request and logs it. @param status The outcome. */
+void finish(Status status) noexcept {
+    AcquireSRWLockExclusive(&g_lock);
+    g_state.status = status;
+    g_state.busy = status == Status::queued;
+    const auto state = g_state;
+    ReleaseSRWLockExclusive(&g_lock);
+    std::array<char, 256> line{};
+    const int size =
+        state.manual
+            ? std::snprintf(line.data(),
+                            line.size(),
+                            "ev=mission_launch activity=%u manual=1 destination=%.*s bubble=%u "
+                            "slice=%u spawn=0x%08X status=%u",
+                            state.index,
+                            static_cast<int>(destination_name(state.destination).size()),
+                            state.destination.packageName.data(),
+                            state.destination.bubble,
+                            state.destination.sliceSet,
+                            state.destination.hasSpawnSetHash ? state.destination.spawnSetHash
+                                                              : forced::kAbsentSpawnSetHash,
+                            static_cast<unsigned>(status))
+            : std::snprintf(line.data(),
+                            line.size(),
+                            "ev=mission_launch activity=%u manual=0 status=%u",
+                            state.index,
+                            static_cast<unsigned>(status));
+    if (size > 0 && static_cast<std::size_t>(size) < line.size()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(size)});
+    }
+}
+
+/**
+ * Confirms the client arrived in the requested destination.
+ * @param state The queued request.
+ * @param rows The published activity catalog.
+ * @return True when the newest joined session binds the requested destination.
+ */
+[[nodiscard]] bool
+arrived(const Snapshot& state,
+        std::span<const state::build_data::activities::Definition> rows) noexcept {
+    const auto session = newest_joined_session();
+    const auto& destination = session.destination;
+    if (session.sessionId == 0 || state::activity::same_binding(session, g_previousSession)
+        || state.index >= rows.size()
+        || destination.activityIndex != static_cast<std::int16_t>(state.index)) {
+        return false;
+    }
+    const std::string_view name =
+        state.manual ? destination_name(state.destination) : rows[state.index].name();
+    if (destination.packageNameLength != name.size()
+        || std::memcmp(destination.packageName.data(), name.data(), name.size()) != 0) {
+        return false;
+    }
+    if (!state.manual) {
+        return true;
+    }
+    // A bubble override always publishes a spawn override, the no-name sentinel when none was
+    // chosen, so the spawn check follows the bubble one.
+    const std::uint32_t spawn = state.destination.hasSpawnSetHash ? state.destination.spawnSetHash
+                                                                  : forced::kAbsentSpawnSetHash;
+    return (!state.destination.hasBubble
+            || (destination.hasArrivalBubbleOverride
+                && destination.arrivalBubbleOverride == state.destination.bubble))
+           && (!state.destination.hasSliceSet
+               || (destination.hasSliceSetOverride
+                   && destination.sliceSetOverride == state.destination.sliceSet))
+           && (!state.destination.hasBubble
+               || (destination.hasSpawnSetOverride && destination.spawnSetOverride == spawn));
+}
+
+/**
+ * Submits one validated request through the Director's own selection path.
+ * @param state The request to submit.
+ * @param rows The published activity catalog.
+ * @return The outcome; `queued` when the commit was sent.
+ */
+[[nodiscard]] Status
+submit(const Snapshot& state,
+       std::span<const state::build_data::activities::Definition> rows) noexcept {
+    const Natives natives = g_natives;
+    SessionManager* const manager = natives.manager();
+    if (manager == nullptr || manager->primary < 0
+        || manager->primary >= static_cast<std::int32_t>(kSessionSlots)) {
+        return Status::notReady;
+    }
+    const GroupSession& session = manager->sessions[static_cast<std::size_t>(manager->primary)];
+    if (session.state < kSessionStateFirstReady || session.state > kSessionStateLastReady
+        || !natives.sessionReady(&session) || !natives.memberReady(&session)
+        || session.localMember < 0 || session.localMember >= kMemberSlots) {
+        return Status::notReady;
+    }
+    const MemberRecord* const record =
+        natives.memberRecord(static_cast<std::uint32_t>(session.localMember));
+    if (record == nullptr || record->launchState >= kLaunchStatePending) {
+        return Status::notReady;
+    }
+    const auto index = static_cast<std::int16_t>(state.index);
+    const char* const package = natives.name(index);
+    const auto& expected = rows[state.index].package;
+    if (package == nullptr || std::strncmp(package, expected.data(), expected.size()) != 0) {
+        return Status::descriptorRejected;
+    }
+    Selection selection{};
+    if (natives.construct(&selection, kSelectionReason, index) != &selection
+        || selection.kind != kSelectionKindActivity || selection.source != index
+        || selection.destination != index || !natives.valid(&selection)) {
+        return Status::descriptorRejected;
+    }
+    g_previousSession = newest_joined_session();
+    g_leftOrbit = false;
+    // Reuse the standalone override service only after all native readiness/descriptor checks.
+    if (state.manual && !forced::publish(state.destination)) {
+        return Status::manualRejected;
+    }
+    natives.clear();
+    natives.select(kSelectionSlot, &selection);
+    natives.commit(kCommitLaunch);
+    return Status::queued;
+}
+
+} // namespace
+
+/** Finds every selection entry point, or none. */
+bool install() noexcept {
+    std::byte* const record = scan_main_image_unique(kMemberRecord, "mission_launch_member_record");
+    std::byte* const sessionReady =
+        scan_main_image_unique(kSessionReady, "mission_launch_session_ready");
+    std::byte* const memberReady =
+        scan_main_image_unique(kMemberReady, "mission_launch_member_ready");
+    std::byte* const construct =
+        scan_main_image_unique(kConstructSelection, "mission_launch_construct_selection");
+    std::byte* const valid =
+        scan_main_image_unique(kSelectionValid, "mission_launch_selection_valid");
+    std::byte* const name = scan_main_image_unique(kActivityName, "mission_launch_activity_name");
+    std::byte* const clearSite =
+        scan_main_image_unique(kClearCallSite, "mission_launch_clear_call_site");
+    std::byte* const select = scan_main_image_unique(kSetSelection, "mission_launch_set_selection");
+    std::byte* const commit =
+        scan_main_image_unique(kCommitSelection, "mission_launch_commit_selection");
+    if (record == nullptr || sessionReady == nullptr || memberReady == nullptr
+        || construct == nullptr || valid == nullptr || name == nullptr || clearSite == nullptr
+        || select == nullptr || commit == nullptr || record[kManagerCallOffset] != kNearCallOpcode
+        || clearSite[0] != kNearCallOpcode) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::warn,
+                         "ev=mission_launch stage=install result=fail reason=target");
+        return false;
+    }
+    Natives found{};
+    found.manager = reinterpret_cast<GetManager>(resolve_relative(
+        record + kManagerCallOffset + 1, record + kManagerCallOffset + kNearCallBytes));
+    found.sessionReady = reinterpret_cast<SessionTest>(sessionReady);
+    found.memberReady = reinterpret_cast<SessionTest>(memberReady);
+    found.memberRecord = reinterpret_cast<GetMemberRecord>(record);
+    found.construct = reinterpret_cast<ConstructSelection>(construct);
+    found.valid = reinterpret_cast<SelectionValid>(valid);
+    found.name = reinterpret_cast<ActivityName>(name);
+    found.clear = reinterpret_cast<ClearSelections>(
+        resolve_relative(clearSite + 1, clearSite + kNearCallBytes));
+    found.select = reinterpret_cast<SetSelection>(select);
+    found.commit = reinterpret_cast<CommitSelection>(commit);
+    g_natives = found;
+    g_resolved.store(true, std::memory_order_release);
+    core::log::write(core::log::Channel::client,
+                     core::log::Level::info,
+                     "ev=mission_launch stage=install result=ok");
+    return true;
+}
+
+void uninstall() noexcept {
+    g_resolved.store(false, std::memory_order_release);
+    g_natives = {};
+}
+
+Snapshot snapshot() noexcept {
+    AcquireSRWLockShared(&g_lock);
+    const auto result = g_state;
+    ReleaseSRWLockShared(&g_lock);
+    return result;
+}
+
+/** Queues an orbit launch of one catalog row. @return False while a request is pending. */
+bool request(std::uint16_t index) noexcept {
+    AcquireSRWLockExclusive(&g_lock);
+    if (g_state.busy) {
+        ReleaseSRWLockExclusive(&g_lock);
+        return false;
+    }
+    g_state = {Status::requested, index, true};
+    g_requestedAt = GetTickCount64();
+    ReleaseSRWLockExclusive(&g_lock);
+    return true;
+}
+
+/** Queues a launch with its arrival override. @return False while a request is pending. */
+bool request_manual(std::uint16_t index, const forced::ForcedDestination& destination) noexcept {
+    AcquireSRWLockExclusive(&g_lock);
+    if (g_state.busy) {
+        ReleaseSRWLockExclusive(&g_lock);
+        return false;
+    }
+    g_state = {Status::requested, index, true, true, destination};
+    g_requestedAt = GetTickCount64();
+    ReleaseSRWLockExclusive(&g_lock);
+    return true;
+}
+
+/** Validates the pending request against the catalog and the client, then submits it. */
+void poll(std::int32_t step) noexcept {
+    const auto state = snapshot();
+    if (!state.busy) {
+        return;
+    }
+    if (!g_resolved.load(std::memory_order_acquire)) {
+        finish(Status::nativeUnavailable);
+        return;
+    }
+    const auto rows = state::build_data::activities::entries();
+    if (state.status == Status::queued) {
+        // Requests are immutable until this owner completes them; the timestamp follows that lock.
+        AcquireSRWLockShared(&g_lock);
+        const auto started = g_requestedAt;
+        ReleaseSRWLockShared(&g_lock);
+        g_leftOrbit = g_leftOrbit || step != kSetupOrbitStep;
+        if (g_leftOrbit && step == kInWorldStep && arrived(state, rows)) {
+            finish(Status::arrived);
+        } else if (GetTickCount64() - started > kArrivalTimeoutMs) {
+            finish(Status::timedOut);
+        }
+        return;
+    }
+    if (rows.empty()) {
+        finish(Status::catalogUnavailable);
+        return;
+    }
+    if (state.index >= rows.size() || rows[state.index].name().empty()) {
+        finish(Status::entryUnavailable);
+        return;
+    }
+    state::build_data::scenarios::Definition layout{};
+    if (!state::build_data::find_scenario_layout(rows[state.index].name(), layout)) {
+        finish(Status::entryUnavailable);
+        return;
+    }
+    if (state.manual) {
+        if (!manual_transport_valid(state.index, state.destination, rows)
+            || validate_manual(state.destination, g_manualScratch) != ManualError::none) {
+            finish(Status::manualRejected);
+            return;
+        }
+    } else {
+        forced::ForcedDestination effective{};
+        forced::snapshot(effective);
+        if (forced::active(effective) || forced::override_active()) {
+            finish(Status::overrideActive);
+            return;
+        }
+    }
+    // A launch is only accepted from orbit. Leaving a world belongs to the native lifecycle.
+    if (step != kSetupOrbitStep) {
+        finish(Status::returnToOrbit);
+        return;
+    }
+    finish(submit(state, rows));
+}
+
+/** @return A static UI message for one status. */
+const char* description(Status status) noexcept {
+    switch (status) {
+    case Status::idle:
+        return "Choose an activity, return to orbit, then launch.";
+    case Status::requested:
+        return "Checking the native launch request...";
+    case Status::queued:
+        return "Submitted to the Director. Waiting for the native activity transition.";
+    case Status::arrived:
+        return "Native activity arrival confirmed.";
+    case Status::catalogUnavailable:
+        return "Activity catalog is still being extracted.";
+    case Status::entryUnavailable:
+        return "This entry has no available direct-launch scenario.";
+    case Status::overrideActive:
+        return "An Activity override is active. Disable it before launching this selection.";
+    case Status::returnToOrbit:
+        return "Return to orbit through the Director, then click Launch again.";
+    case Status::nativeUnavailable:
+        return "Native launch entry points are unavailable in this client build.";
+    case Status::notReady:
+        return "The fireteam is not ready to launch. Wait in orbit and try again.";
+    case Status::descriptorRejected:
+        return "The native client rejected this activity selection.";
+    case Status::timedOut:
+        return "No arrival confirmation was received. Check the Director before retrying.";
+    case Status::manualRejected:
+        return "The manual destination, bubble, slice, spawn or native launch route is no longer "
+               "valid. Review the selection.";
+    }
+    return "Launch status unavailable.";
+}
+
+} // namespace sunrise::client::activity::mission_launch

+ 47 - 0
Sunrise/src/client/activity/mission_launch.h

@@ -0,0 +1,47 @@
+#pragma once
+#include <cstdint>
+
+#include "../../state/activity/forced/definition.h"
+namespace sunrise::client::activity::mission_launch {
+enum class Status : std::uint8_t {
+    idle,
+    requested,
+    queued,
+    arrived,
+    catalogUnavailable,
+    entryUnavailable,
+    overrideActive,
+    returnToOrbit,
+    nativeUnavailable,
+    notReady,
+    descriptorRejected,
+    timedOut,
+    manualRejected
+};
+struct Snapshot {
+    Status status{Status::idle};
+    std::uint16_t index{};
+    bool busy{};
+    bool manual{};
+    state::activity::forced::ForcedDestination destination{};
+};
+/** Finds the Director's selection entry points. @return True when every one was found. */
+[[nodiscard]] bool install() noexcept;
+/** Forgets the entry points, so a pending request fails instead of calling into a gone image. */
+void uninstall() noexcept;
+/** Render thread: enqueue one immutable public activity ordinal. No native calls here. */
+[[nodiscard]] bool request(std::uint16_t index) noexcept;
+/** Enqueues a copied draft; the game-frame owner validates and publishes it before launch. */
+[[nodiscard]] bool
+request_manual(std::uint16_t index,
+               const state::activity::forced::ForcedDestination& destination) noexcept;
+/** @return A synchronized copy of the latest request and its native transition status. */
+[[nodiscard]] Snapshot snapshot() noexcept;
+/**
+ * Advances the pending request. Game thread only, once per frame.
+ * @param step The client's current boot-flow step.
+ */
+void poll(std::int32_t step) noexcept;
+/** @return A static UI message explaining the request result or next required action. */
+[[nodiscard]] const char* description(Status status) noexcept;
+} // namespace sunrise::client::activity::mission_launch

+ 140 - 0
Sunrise/src/client/activity/mission_launch_options.h

@@ -0,0 +1,140 @@
+#pragma once
+
+#include <array>
+#include <span>
+#include <string_view>
+
+#include "../../middleware/content/packages/tables/component_container_reader.h"
+#include "../../middleware/content/packages/tables/region_reader.h"
+#include "../../state/activity/forced/definition.h"
+#include "../../state/build_data/activities/activity_catalog.h"
+#include "../../state/build_data/runtime.h"
+
+namespace sunrise::client::activity::mission_launch {
+namespace forced = state::activity::forced;
+namespace layouts = state::build_data::scenarios;
+namespace spawns = state::build_data::spawn_sets;
+namespace tables = middleware::content::packages::tables;
+
+enum class ManualError : std::uint8_t { none, activity, bubble, slice, spawn };
+
+[[nodiscard]] inline std::string_view
+destination_name(const forced::ForcedDestination& value) noexcept {
+    return value.packageNameLength <= value.packageName.size()
+               ? std::string_view(value.packageName.data(), value.packageNameLength)
+               : std::string_view{};
+}
+
+[[nodiscard]] inline bool
+manual_transport_valid(std::uint16_t index,
+                       const forced::ForcedDestination& value,
+                       std::span<const state::build_data::activities::Definition> rows) noexcept {
+    return index < rows.size() && !rows[index].name().empty()
+           && rows[index].name() == destination_name(value) && value.hasActivityIndex
+           && value.activityIndex == index;
+}
+
+/** Exact destination/bubble/package membership; map-wide candidates are not proven arrival points.
+ */
+[[nodiscard]] inline bool spawn_supported(const layouts::Definition& layout,
+                                          std::uint8_t bubble,
+                                          const spawns::NameHash& row) noexcept {
+    if (bubble >= layout.bubbleCount || bubble >= layout.bubbleMapIndices.size()
+        || row.pointCount == 0 || row.activityPackageOverflow != 0
+        || row.activityPackageCount > row.activityPackages.size()
+        || layout.packageCount > layout.packages.size()
+        || !tables::bubble_in_mask(row.bubbleMask, layout.bubbleMapIndices[bubble])) {
+        return false;
+    }
+    if (row.inMapPackage != 0) {
+        return true;
+    }
+    for (std::size_t i = 0; i < row.activityPackageCount; ++i) {
+        for (std::size_t j = 0; j < layout.packageCount; ++j) {
+            if (row.activityPackages[i] == layout.packages[j]) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+/** Pure launch argument check, shared by the draft UI and the game-frame request owner. */
+[[nodiscard]] inline ManualError
+validate_manual(const forced::ForcedDestination& value,
+                const layouts::Definition& layout,
+                std::span<const spawns::NameHash> spawnRows) noexcept {
+    const auto name = destination_name(value);
+    if (!value.enabled || name.empty() || layout.nameLength > layout.name.size()
+        || name != std::string_view(layout.name.data(), layout.nameLength) || layout.truncated != 0
+        || layout.bubbleCount > layout.bubbleStates.size()) {
+        return ManualError::activity;
+    }
+    if (!value.hasBubble || value.bubble >= layout.bubbleCount) {
+        return ManualError::bubble;
+    }
+    const auto states = layout.bubbleStateCounts[value.bubble];
+    const auto base = tables::region_index(value.bubble);
+    if (!value.hasSliceSet || states == 0 || states > tables::kSliceSetIndexFactor
+        || value.sliceSet < base || value.sliceSet >= base + states) {
+        return ManualError::slice;
+    }
+    if (value.hasSpawnSetHash) {
+        if (value.spawnSetHash == 0 || value.spawnSetHash == forced::kAbsentSpawnSetHash) {
+            return ManualError::spawn;
+        }
+        for (const auto& row : spawnRows) {
+            if (row.value == value.spawnSetHash && spawn_supported(layout, value.bubble, row)) {
+                return ManualError::none;
+            }
+        }
+        return ManualError::spawn;
+    }
+    return ManualError::none;
+}
+
+/** Caller-owned storage keeps independent render/game-thread reads outside the UI arena and stack.
+ */
+struct ManualScratch {
+    layouts::Definition layout{};
+    std::array<spawns::NameHash, spawns::kNameHashCapacity> spawns{};
+    std::size_t spawnCount{};
+};
+
+[[nodiscard]] inline ManualError validate_manual(const forced::ForcedDestination& value,
+                                                 ManualScratch& scratch) noexcept {
+    scratch.layout = {};
+    scratch.spawnCount = 0;
+    if (!state::build_data::find_scenario_layout(destination_name(value), scratch.layout)) {
+        return ManualError::activity;
+    }
+    if (value.hasSpawnSetHash
+        && scratch.layout.spawnStemLength <= scratch.layout.spawnStem.size()) {
+        const std::string_view stem(scratch.layout.spawnStem.data(),
+                                    scratch.layout.spawnStemLength);
+        if (!state::build_data::find_spawn_sets(stem, scratch.spawns, scratch.spawnCount)
+            || scratch.spawnCount > scratch.spawns.size()) {
+            scratch.spawnCount = 0;
+        }
+    }
+    return validate_manual(
+        value, scratch.layout, std::span(scratch.spawns).first(scratch.spawnCount));
+}
+
+[[nodiscard]] inline const char* manual_error(ManualError error) noexcept {
+    switch (error) {
+    case ManualError::none:
+        return "Ready to apply the selected arrival override.";
+    case ManualError::activity:
+        return "Select an available activity layout.";
+    case ManualError::bubble:
+        return "Select a bubble from this activity.";
+    case ManualError::slice:
+        return "Select an authored slice from the selected bubble.";
+    case ManualError::spawn:
+        return "This spawn is not proven to load in the selected activity and bubble. Choose "
+               "another or let the client pick.";
+    }
+    return "Manual selection unavailable.";
+}
+} // namespace sunrise::client::activity::mission_launch

+ 46 - 0
Sunrise/src/client/content/activity/activity_catalog_build.cpp

@@ -0,0 +1,46 @@
+#include "activity_catalog_build.h"
+
+#include "../../../middleware/content/packages/tables/activity_table.h"
+#include "../items/packages/internal.h"
+#include "activity_presentation_build.h"
+
+namespace sunrise::client::content::activity {
+namespace packages = middleware::content::packages;
+bool build_catalog(const packages::reader::Source& source,
+                   packages::reader::Scratch& scratch) noexcept {
+    namespace catalog = state::build_data::activities;
+    namespace tables = packages::tables;
+    if (catalog::ready()) {
+        return true;
+    }
+    if (catalog::extraction_failed()) {
+        return false;
+    }
+    std::array<std::uint32_t, items::packages::kContainerCandidates> candidates{};
+    std::size_t candidateCount{};
+    if (!items::packages::investment_globals_tags(candidates, candidateCount)) {
+        catalog::note_extraction_failure();
+        return false;
+    }
+    std::vector<std::byte> globals, root, table;
+    static std::array<catalog::Definition, catalog::kCapacity> rows{};
+    for (std::size_t i = 0; i < candidateCount; ++i) {
+        std::uint32_t rootTag{}, tableTag{}, rootClass{};
+        std::size_t count{};
+        if (packages::reader::read_tag(source, scratch, candidates[i], globals)
+            && tables::child_tag(globals, tables::kInvestmentRootChild, rootTag)
+            && packages::reader::read_tag(source, scratch, rootTag, root, rootClass)
+            && rootClass == tables::kInvestmentRootClass
+            && tables::slot_tag(root, tables::activities::kRootSlot, tableTag)
+            && packages::reader::read_tag(source, scratch, tableTag, table)
+            && tables::activities::decode(table, rows, count)) {
+            const auto found = std::span(rows).first(count);
+            build_presentations(source, scratch, globals, found);
+            build_artwork(source, scratch);
+            return catalog::publish(found);
+        }
+    }
+    catalog::note_extraction_failure();
+    return false;
+}
+} // namespace sunrise::client::content::activity

+ 15 - 0
Sunrise/src/client/content/activity/activity_catalog_build.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include "../../../middleware/content/packages/reader/reader.h"
+
+namespace sunrise::client::content::activity {
+
+/**
+ * Extracts and publishes optional launcher data once on the investment worker.
+ * @param source Installed package source; no file is created or modified.
+ * @param scratch Worker-owned reader storage, reused only by this caller.
+ * @return True when the immutable catalog is ready; failure settles the optional startup work.
+ */
+[[nodiscard]] bool build_catalog(const middleware::content::packages::reader::Source& source,
+                                 middleware::content::packages::reader::Scratch& scratch) noexcept;
+} // namespace sunrise::client::content::activity

+ 118 - 0
Sunrise/src/client/content/activity/activity_menu_text_build.h

@@ -0,0 +1,118 @@
+#pragma once
+#include "../../../middleware/content/packages/reader/reader.h"
+#include "../../../middleware/content/packages/tables/activity_presentation_table.h"
+
+namespace sunrise::client::content::activity {
+/** Runtime-only menu labels. Constants below are native localization identities, never text copies.
+ */
+inline void build_menu_text(
+    const middleware::content::packages::reader::Source& source,
+    middleware::content::packages::reader::Scratch& scratch,
+    std::span<const std::byte> globals,
+    std::span<const middleware::content::packages::tables::activities::DisplayRefs> activityRefs,
+    state::build_data::activities::MenuText& output) noexcept {
+    namespace packages = middleware::content::packages;
+    namespace catalog = state::build_data::activities;
+    namespace tables = packages::tables;
+    namespace parser = tables::activities;
+    output = {};
+    std::vector<std::byte> banks, container, language, bytes;
+    tables::Array bankRows{};
+    std::uint32_t tag{}, cls{};
+    if (!tables::child_tag(globals, 72, tag)
+        || !packages::reader::read_tag(source, scratch, tag, banks, cls) || cls != 0x80805F98
+        || !tables::find_array_at(banks, 8, bankRows) || bankRows.count > 8192
+        || bankRows.dataOffset > banks.size()
+        || bankRows.count > (banks.size() - bankRows.dataOffset) / 8) {
+        return;
+    }
+    const auto resolve = [&](parser::StringRef ref, catalog::NativeText& target) noexcept {
+        target.bank = ref.bank;
+        target.hash = ref.hash;
+        std::uint32_t root{}, english{}, actual{};
+        if (ref.bank >= bankRows.count
+            || !parser::read(banks, bankRows.dataOffset + ref.bank * 8 + 4, root)
+            || !packages::reader::read_tag(source, scratch, root, container, actual)
+            || actual != 0x80809A88 || !parser::read(container, 24, english)
+            || !packages::reader::read_tag(source, scratch, english, language, actual)
+            || actual != 0x80809A8A) {
+            return;
+        }
+        (void)parser::localized_string(container, language, ref.hash, target.text);
+    };
+    // Installed campaign-selection localization bank.
+    constexpr std::array<std::uint32_t, 5> campaignHashes{
+        0x269A3283, 0x5E6C45BA, 0x99807D0D, 0xA5E7229E, 0xD07640BC};
+    for (std::size_t i = 0; i < campaignHashes.size(); ++i) {
+        resolve({2417, campaignHashes[i]}, output.content[i + 1]);
+    }
+    if (activityRefs.size() > 1) {
+        resolve(activityRefs[1].title, output.content[6]);
+    }
+    // Seasons: the authored season row supplies its own bank/hash; no English matching or aliasing.
+    tables::Array seasons{};
+    if (tables::child_tag(globals, 54, tag)
+        && packages::reader::read_tag(source, scratch, tag, bytes, cls) && cls == 0x8080311B
+        && tables::find_array_at(bytes, 8, seasons) && seasons.count <= 64
+        && seasons.dataOffset <= bytes.size()
+        && seasons.count <= (bytes.size() - seasons.dataOffset) / 72) {
+        constexpr std::array<std::uint32_t, 7> identities{
+            0x854AC306, 0xAC5281E8, 0xFEDABB80, 0xD758957D, 0x77A58C71, 0xF088B659, 0x0ED0ED8B};
+        for (std::size_t i = 0; i < identities.size(); ++i) {
+            const auto row = i + 4;
+            std::uint32_t identity{};
+            parser::StringRef ref{};
+            if (row < seasons.count
+                && parser::read(bytes, seasons.dataOffset + row * 72 + 8, identity)
+                && identity == identities[i]
+                && parser::read(bytes, seasons.dataOffset + row * 72 + 36, ref)) {
+                resolve(ref, output.content[i + 7]);
+            }
+        }
+    }
+    struct Event {
+        std::size_t content;
+        std::uint32_t tag, cls;
+        std::size_t offset;
+    };
+    constexpr std::array<Event, 4> events{{{14, 0x8161353B, 0x80805CE1, 132},
+                                           {15, 0x816135B4, 0x80805CE1, 132},
+                                           {19, 0x816134AB, 0x80805CE1, 132},
+                                           {20, 0x81613CF8, 0x80802E11, 37676}}};
+    for (const auto& event : events) {
+        parser::StringRef ref{};
+        if (packages::reader::read_tag(source, scratch, event.tag, bytes, cls) && cls == event.cls
+            && parser::read(bytes, event.offset, ref)) {
+            resolve(ref, output.content[event.content]);
+        }
+    }
+    resolve({2095, 0xC019C7A5},
+            output.content[16]); // Installed event title, distinct from its intro quest title.
+    output.content[17] =
+        output.content[14]; // Same native recurring-event label; classification IDs stay distinct.
+    if (activityRefs.size() > 509) {
+        resolve(activityRefs[509].type, output.content[21]);
+    }
+    if (activityRefs.size() > 299) {
+        resolve(activityRefs[299].type, output.kinds[1]);
+        resolve(activityRefs[229].type, output.kinds[2]);
+    }
+    // Director style string containers carry the native type label under one shared key.
+    constexpr std::array<std::pair<std::size_t, std::uint32_t>, 4> styles{
+        {{3, 0x8161305A}, {4, 0x816131E5}, {5, 0x8161311D}, {6, 0x816132B5}}};
+    for (const auto [kind, style] : styles) {
+        std::uint32_t root{}, english{};
+        if (!packages::reader::read_tag(source, scratch, style, bytes, cls) || cls != 0x80802A38
+            || !parser::read(bytes, 16, root)
+            || !packages::reader::read_tag(source, scratch, root, container, cls)
+            || cls != 0x80809A88 || !parser::read(container, 24, english)
+            || !packages::reader::read_tag(source, scratch, english, language, cls)
+            || cls != 0x80809A8A) {
+            continue;
+        }
+        output.kinds[kind].bank = root;
+        output.kinds[kind].hash = 0x8648376F;
+        (void)parser::localized_string(container, language, 0x8648376F, output.kinds[kind].text);
+    }
+}
+} // namespace sunrise::client::content::activity

+ 192 - 0
Sunrise/src/client/content/activity/activity_presentation_build.h

@@ -0,0 +1,192 @@
+#pragma once
+
+#include "../../../middleware/content/packages/reader/reader.h"
+#include "../../../middleware/content/packages/tables/activity_presentation_table.h"
+#include "../../../state/build_data/activities/activity_artwork.h"
+#include "../../../state/build_data/activities/activity_releases.h"
+#include "activity_menu_text_build.h"
+
+namespace sunrise::client::content::activity {
+/** Native Director campaign/activity styles, verified against the installed style strings.
+ * +12 image set -> variant 2 (82px colored selection shield) -> image -> RGBA texture.
+ * These are artwork identities, never destination overrides or launch IDs. */
+inline void build_artwork(const middleware::content::packages::reader::Source& source,
+                          middleware::content::packages::reader::Scratch& scratch) noexcept {
+    namespace packages = middleware::content::packages;
+    namespace catalog = state::build_data::activities;
+    namespace parser = packages::tables::activities;
+    constexpr std::array<std::uint32_t, catalog::kIconCount> styles{
+        0,          0x816130A9, 0x8161308D, 0x816130C9, 0x8161309B, 0x816130B9, 0x8161319B,
+        0x8161331E, 0x816132CF, 0x8161311D, 0x816131E5, 0x81613302, 0x81613292, 0x81613133,
+        0x8161305A, 0x816131CF, 0x816132F1, 0x81613175, 0x81613076, 0x816131C0, 0x8161338B,
+        0x81613154, 0x81613206, 0x816131F0, 0x8161321C, 0x81613211, 0x816131FB, 0x816132B5};
+    std::array<catalog::Artwork, catalog::kIconCount> images{};
+    std::vector<std::byte> bytes, pixels;
+    const auto read = [&](std::uint32_t tag, std::uint32_t expected) noexcept {
+        std::uint32_t actual{};
+        return packages::reader::read_tag(source, scratch, tag, bytes, actual)
+               && actual == expected;
+    };
+    const auto texture = [&](std::size_t i, std::uint32_t tag) noexcept {
+        std::uint32_t format{}, buffer{}, large{};
+        std::uint16_t cafe{}, width{}, height{}, depth{}, layers{};
+        if (!packages::reader::read_tag(source, scratch, tag, bytes, buffer) || bytes.size() != 40
+            || !parser::read(bytes, 4, format) || format != 28 || !parser::read(bytes, 12, cafe)
+            || cafe != 0xCAFE || !parser::read(bytes, 14, width) || !parser::read(bytes, 16, height)
+            || !parser::read(bytes, 18, depth) || !parser::read(bytes, 20, layers) || width == 0
+            || height == 0 || width > 256 || height > 256 || depth != 1 || layers != 1
+            || !parser::read(bytes, 36, large) || large != 0xFFFFFFFFU) {
+            return;
+        }
+        const auto size = static_cast<std::size_t>(width) * height * 4;
+        if (!packages::reader::read_tag(source, scratch, buffer, pixels) || pixels.size() < size
+            || pixels.size() > 512 * 1024) {
+            return;
+        }
+        images[i].tag = tag;
+        images[i].width = width;
+        images[i].height = height;
+        images[i].pixels.assign(pixels.begin(), pixels.begin() + static_cast<std::ptrdiff_t>(size));
+    };
+    for (std::size_t i = 1; i < styles.size(); ++i) {
+        if (styles[i] == 0) {
+            continue;
+        }
+        std::uint32_t tag{};
+        packages::tables::Array variants{};
+        if (!read(styles[i], 0x80802A38) || !parser::read(bytes, 12, tag) || !read(tag, 0x80804A55)
+            || !packages::tables::find_array_at(bytes, 8, variants) || variants.count < 3
+            || variants.count > 16 || !parser::read(bytes, variants.dataOffset + 2 * 112 + 4, tag)
+            || !read(tag, 0x80804A69) || !parser::read(bytes, 128, tag)) {
+            continue;
+        }
+        texture(i, tag);
+    }
+    // Authored icon indices from exact season/event/intro-quest records.
+    // The expected texture guards this build-specific join; no replacement artwork is synthesized.
+    struct NativeIcon {
+        catalog::Icon icon;
+        std::uint16_t index;
+        std::uint32_t expectedTexture;
+        std::uint8_t field{20}, variant{};
+    };
+    constexpr std::array<NativeIcon, 13> eventIcons{
+        {// The 32px season-table marks are smaller than the standalone 45px inventory marks.
+         // Variant 3 is the complete authored glyph; 96px variants are corner tiles with a smaller
+         // glyph.
+         {catalog::Icon{28}, 12109, 0x813217EC, 32, 3},
+         {catalog::Icon{29}, 13329, 0x81A2760C, 32, 3},
+         {catalog::Icon{30}, 13610, 0x81A27409, 32, 3},
+         {catalog::Icon{31}, 14737, 0x81A27487, 32, 3},
+         {catalog::Icon{32}, 10586, 0x8132D4B0},
+         {catalog::Icon{33}, 5973, 0x8132D6B0},
+         {catalog::Icon{34}, 5964, 0x8132D573},
+         {catalog::Icon{35}, 5980, 0x8132D4E6},
+         {catalog::Icon{36}, 11203, 0x8132D914},
+         {catalog::Icon{37}, 16, 0x81320DB2},
+         {catalog::Icon{38}, 10541, 0x813185CC, 32, 3},
+         {catalog::Icon{39}, 11305, 0x813217A8, 32, 3},
+         {catalog::Icon{40}, 11578, 0x813217CA, 32, 3}}};
+    packages::tables::Array iconRows{};
+    if (read(0x81A291C2, 0x80802951) && packages::tables::find_array_at(bytes, 8, iconRows)
+        && iconRows.count <= 20000 && iconRows.dataOffset <= bytes.size()
+        && iconRows.count <= (bytes.size() - iconRows.dataOffset) / 24) {
+        std::vector<std::byte> iconMap;
+        iconMap.swap(bytes);
+        for (const auto& icon : eventIcons) {
+            std::uint32_t tag{};
+            packages::tables::Array frames{}, textures{};
+            if (icon.index >= iconRows.count
+                || !parser::read(iconMap, iconRows.dataOffset + icon.index * 24 + 16, tag)
+                || !read(tag, 0x80804A53) || !parser::read(bytes, icon.field, tag)
+                || !read(tag, 0x80804A69) || !packages::tables::find_array_at(bytes, 32, frames)
+                || frames.count != 1
+                || !packages::tables::find_array_at(bytes, frames.dataOffset, textures)
+                || textures.count <= icon.variant || textures.count > 16
+                || textures.dataOffset > bytes.size()
+                || textures.count > (bytes.size() - textures.dataOffset) / 4
+                || !parser::read(bytes, textures.dataOffset + icon.variant * 4, tag)
+                || tag != icon.expectedTexture) {
+                continue;
+            }
+            texture(static_cast<std::size_t>(icon.icon), tag);
+        }
+    }
+    (void)catalog::publish_artwork(std::move(images));
+}
+
+inline void
+build_presentations(const middleware::content::packages::reader::Source& source,
+                    middleware::content::packages::reader::Scratch& scratch,
+                    std::span<const std::byte> globals,
+                    std::span<const state::build_data::activities::Definition> rows) noexcept {
+    namespace packages = middleware::content::packages;
+    namespace catalog = state::build_data::activities;
+    namespace tables = packages::tables;
+    namespace parser = tables::activities;
+    std::vector<std::byte> display, types, banks, container, language;
+    const auto child =
+        [&](std::size_t slot, std::uint32_t expected, std::vector<std::byte>& target) noexcept {
+            std::uint32_t tag{}, actual{};
+            return tables::child_tag(globals, slot, tag)
+                   && packages::reader::read_tag(source, scratch, tag, target, actual)
+                   && actual == expected;
+        };
+    static std::array<parser::DisplayRefs, catalog::kCapacity> refs{};
+    static std::array<catalog::Presentation, catalog::kCapacity> presentation{};
+    tables::Array bankRows{};
+    if (!child(3, 0x80805E0A, display) || !child(6, 0x80805F3C, types)
+        || !child(72, 0x80805F98, banks) || !tables::find_array_at(banks, 8, bankRows)
+        || bankRows.count > 8192 || bankRows.dataOffset > banks.size()
+        || bankRows.count > (banks.size() - bankRows.dataOffset) / 8
+        || !parser::display_refs(display, types, rows, refs)) {
+        return;
+    }
+    // Read each requested bank once; scratch block caching remains owned by the investment worker.
+    for (std::uint32_t bank = 0; bank < bankRows.count; ++bank) {
+        bool needed{};
+        for (std::size_t i = 0; i < rows.size() && !needed; ++i) {
+            needed = refs[i].title.bank == bank || refs[i].description.bank == bank
+                     || refs[i].type.bank == bank || refs[i].expansion.bank == bank;
+        }
+        if (!needed) {
+            continue;
+        }
+        std::uint32_t tag{}, actual{}, english{};
+        if (!parser::read(banks, bankRows.dataOffset + bank * 8 + 4, tag)
+            || !packages::reader::read_tag(source, scratch, tag, container, actual)
+            || actual != 0x80809A88 || !parser::read(container, 24, english)
+            || !packages::reader::read_tag(source, scratch, english, language, actual)
+            || actual != 0x80809A8A) {
+            continue;
+        }
+        for (std::size_t i = 0; i < rows.size(); ++i) {
+            const auto resolve = [&](parser::StringRef ref, std::span<char> text) noexcept {
+                if (ref.bank == bank) {
+                    (void)parser::localized_string(container, language, ref.hash, text);
+                }
+            };
+            resolve(refs[i].title, presentation[i].title);
+            resolve(refs[i].description, presentation[i].description);
+            resolve(refs[i].type, presentation[i].type);
+            resolve(refs[i].expansion, presentation[i].expansion);
+        }
+    }
+    for (std::size_t i = 0; i < rows.size(); ++i) {
+        auto& p = presentation[i];
+        p.nativeStyle = refs[i].style;
+        p.experience = rows[i].index;
+        p.contentGroup = static_cast<std::uint8_t>(catalog::releases::unresolved);
+        if (const auto* rule = catalog::releases::lookup(rows[i])) {
+            p.contentGroup = rule->release;
+            p.experience = rule->experience;
+            p.experienceKind = rule->kind;
+            p.classificationSource = 4;
+        }
+    }
+    (void)catalog::publish_presentations(std::span(presentation).first(rows.size()));
+    catalog::MenuText menuText{};
+    build_menu_text(source, scratch, globals, std::span(refs).first(rows.size()), menuText);
+    (void)catalog::publish_menu_text(menuText);
+}
+} // namespace sunrise::client::content::activity

+ 7 - 1
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -7,7 +7,9 @@
 #include "../../../../core/logging/log.h"
 #include "../../../../core/logging/log.h"
 #include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/reader/reader.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
 #include "../../../../middleware/content/packages/tables/definition_index_table.h"
+#include "../../../../state/build_data/activities/activity_catalog.h"
 #include "../../../../state/build_data/runtime.h"
 #include "../../../../state/build_data/runtime.h"
+#include "../../activity/activity_catalog_build.h"
 #include "../../activity/entity_position_profile_build.h"
 #include "../../activity/entity_position_profile_build.h"
 #include "../../hash_names/hash_name_build.h"
 #include "../../hash_names/hash_name_build.h"
 #include "../../scenarios/scenario_build.h"
 #include "../../scenarios/scenario_build.h"
@@ -47,7 +49,9 @@ bool ready() noexcept {
     return root_domains_ready() && state::build_data::scenario_layouts_ready()
     return root_domains_ready() && state::build_data::scenario_layouts_ready()
            && state::build_data::spawn_sets_ready() && state::build_data::hash_names_ready()
            && state::build_data::spawn_sets_ready() && state::build_data::hash_names_ready()
            && state::build_data::vendor_catalog_ready()
            && state::build_data::vendor_catalog_ready()
-           && content::activity::entity_position_profiles::ready();
+           && content::activity::entity_position_profiles::ready()
+           && (state::build_data::activities::ready()
+               || state::build_data::activities::extraction_failed());
 }
 }
 
 
 /**
 /**
@@ -73,6 +77,8 @@ bool build() noexcept {
     // storage. Both are independent of the item table, so a failure here leaves it alone.
     // storage. Both are independent of the item table, so a failure here leaves it alone.
     {
     {
         const reader::Source packageSource{directory.chars.data(), &keys};
         const reader::Source packageSource{directory.chars.data(), &keys};
+        // Process-local launcher data is extracted even when persistent build data was loaded.
+        (void)content::activity::build_catalog(packageSource, storage.scratch);
         (void)content::activity::entity_position_profiles::build(packageSource, storage.scratch);
         (void)content::activity::entity_position_profiles::build(packageSource, storage.scratch);
         (void)content::scenarios::build(packageSource, storage.scratch);
         (void)content::scenarios::build(packageSource, storage.scratch);
         (void)content::spawn_sets::build(packageSource, storage.scratch);
         (void)content::spawn_sets::build(packageSource, storage.scratch);

+ 4 - 1
Sunrise/src/client/hooks/bootflow/world_step.cpp

@@ -5,6 +5,7 @@
 #include <string_view>
 #include <string_view>
 
 
 #include "../../../core/logging/log.h"
 #include "../../../core/logging/log.h"
+#include "../../activity/mission_launch.h"
 #include "bootflow_hook_lifecycle.h"
 #include "bootflow_hook_lifecycle.h"
 #include "internal.h"
 #include "internal.h"
 #include "spawn/slice_set_sample.h"
 #include "spawn/slice_set_sample.h"
@@ -53,8 +54,10 @@ std::atomic_uint64_t g_publishedSliceSetTick{0};
 
 
 /** Publishes the client's own boot-flow step. */
 /** Publishes the client's own boot-flow step. */
 void poll_world_step() noexcept {
 void poll_world_step() noexcept {
-    g_publishedStep.store(read_step(), std::memory_order_relaxed);
+    const std::int32_t step = read_step();
+    g_publishedStep.store(step, std::memory_order_relaxed);
     g_publishedTick.store(GetTickCount64(), std::memory_order_release);
     g_publishedTick.store(GetTickCount64(), std::memory_order_release);
+    client::activity::mission_launch::poll(step);
 }
 }
 
 
 /** Publishes the client's current local slice-set index. */
 /** Publishes the client's current local slice-set index. */

+ 2 - 0
Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_frame.cpp

@@ -15,6 +15,7 @@
 #include "../../../../core/ui/theme/sunrise_ui_theme.h"
 #include "../../../../core/ui/theme/sunrise_ui_theme.h"
 #include "../../../ui/activity/authored_placement_marker.h"
 #include "../../../ui/activity/authored_placement_marker.h"
 #include "../../../ui/activity/authored_spatial_overlay.h"
 #include "../../../ui/activity/authored_spatial_overlay.h"
+#include "../../../ui/mission_launch/mission_launch_art.h"
 #include "../../teleport/runtime.h"
 #include "../../teleport/runtime.h"
 #include "../input/input.h"
 #include "../input/input.h"
 #include "graphics_renderer_report.h"
 #include "graphics_renderer_report.h"
@@ -141,6 +142,7 @@ void render_frame_locked() noexcept {
     ImGui_ImplDX11_NewFrame();
     ImGui_ImplDX11_NewFrame();
     ImGui_ImplWin32_NewFrame();
     ImGui_ImplWin32_NewFrame();
     ImGui::NewFrame();
     ImGui::NewFrame();
+    client::ui::mission_launch::art::prepare(g_resources.device);
     // A hidden surface still draws until its close animation ends, so the layout decides. The
     // A hidden surface still draws until its close animation ends, so the layout decides. The
     // HUD, running-work and notice overlays draw whether the surface is open or not. The HUD
     // HUD, running-work and notice overlays draw whether the surface is open or not. The HUD
     // goes first, so the surface stays above it when the two meet.
     // goes first, so the surface stays above it when the two meet.

+ 2 - 0
Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_lifecycle.cpp

@@ -6,6 +6,7 @@
 
 
 #include "../../../../core/ui/layout/layout.h"
 #include "../../../../core/ui/layout/layout.h"
 #include "../../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../../core/ui/runtime/ui_visibility_runtime.h"
+#include "../../../ui/mission_launch/mission_launch_art.h"
 #include "../../cursor/runtime.h"
 #include "../../cursor/runtime.h"
 #include "../../inactivity/inactivity_override.h"
 #include "../../inactivity/inactivity_override.h"
 #include "../../polled_input/runtime.h"
 #include "../../polled_input/runtime.h"
@@ -249,6 +250,7 @@ void release_render_target(Resources& resources) noexcept {
 
 
 /** @param resources SDK resources freed in an order that respects their dependencies. */
 /** @param resources SDK resources freed in an order that respects their dependencies. */
 void release_resources(Resources& resources) noexcept {
 void release_resources(Resources& resources) noexcept {
+    client::ui::mission_launch::art::release();
     world_lines::release();
     world_lines::release();
     release_render_target(resources);
     release_render_target(resources);
     textures::release_logo_sheet(resources.logoSheet);
     textures::release_logo_sheet(resources.logoSheet);

+ 3 - 0
Sunrise/src/client/runtime/client_hook_activation.cpp

@@ -10,6 +10,7 @@
 #include "../../core/ui/busy/busy.h"
 #include "../../core/ui/busy/busy.h"
 #include "../../core/ui/notice/ui_notice_overlay.h"
 #include "../../core/ui/notice/ui_notice_overlay.h"
 #include "../../server/bap/runtime.h"
 #include "../../server/bap/runtime.h"
+#include "../activity/mission_launch.h"
 #include "../content/activity/scriptable_catalog_worker.h"
 #include "../content/activity/scriptable_catalog_worker.h"
 #include "../content/bootstrap/bootstrap_token_publish.h"
 #include "../content/bootstrap/bootstrap_token_publish.h"
 #include "../content/investment/worker.h"
 #include "../content/investment/worker.h"
@@ -190,6 +191,8 @@ void clear_game_targets() noexcept {
     (void)hooks::config_getter::install();
     (void)hooks::config_getter::install();
     // Boot-step fixes scan for their own single-site targets; each reports its own outcome.
     // Boot-step fixes scan for their own single-site targets; each reports its own outcome.
     (void)hooks::bootflow::install();
     (void)hooks::bootflow::install();
+    // The launcher calls the Director's own selection entry points; nothing is detoured.
+    (void)activity::mission_launch::install();
     // The teleport hooks attach whether or not the feature is on, so the interface can enable it
     // The teleport hooks attach whether or not the feature is on, so the interface can enable it
     // without a restart. Both replacements return immediately while nothing is requested.
     // without a restart. Both replacements return immediately while nothing is requested.
     (void)hooks::teleport::install();
     (void)hooks::teleport::install();

+ 2 - 0
Sunrise/src/client/runtime/client_runtime_lifecycle.cpp

@@ -1,6 +1,7 @@
 #include "../../core/logging/log.h"
 #include "../../core/logging/log.h"
 #include "../../core/settings/settings.h"
 #include "../../core/settings/settings.h"
 #include "../../server/bap/runtime.h"
 #include "../../server/bap/runtime.h"
+#include "../activity/mission_launch.h"
 #include "../content/activity/activity_sdk_generation_worker.h"
 #include "../content/activity/activity_sdk_generation_worker.h"
 #include "../content/activity/scriptable_catalog_worker.h"
 #include "../content/activity/scriptable_catalog_worker.h"
 #include "../content/investment/worker.h"
 #include "../content/investment/worker.h"
@@ -137,6 +138,7 @@ bool shutdown() noexcept {
         return false;
         return false;
     }
     }
     hooks::bitmap::uninstall();
     hooks::bitmap::uninstall();
+    activity::mission_launch::uninstall();
     hooks::bootflow::uninstall();
     hooks::bootflow::uninstall();
     hooks::infinite_ammo::uninstall();
     hooks::infinite_ammo::uninstall();
     hooks::inactivity::uninstall();
     hooks::inactivity::uninstall();

+ 88 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_art.cpp

@@ -0,0 +1,88 @@
+#include "mission_launch_art.h"
+
+#include <algorithm>
+#include <cmath>
+#include <d3d11.h>
+
+namespace sunrise::client::ui::mission_launch::art {
+namespace {
+std::array<ID3D11ShaderResourceView*, state::build_data::activities::kIconCount> g_views{};
+ID3D11Device* g_device{};
+bool g_attempted{};
+} // namespace
+void release() noexcept {
+    for (auto*& view : g_views) {
+        if (view != nullptr) {
+            view->Release();
+            view = nullptr;
+        }
+    }
+    if (g_device != nullptr) {
+        g_device->Release();
+        g_device = nullptr;
+    }
+    g_attempted = false;
+}
+void prepare(ID3D11Device* device) noexcept {
+    if (device == nullptr) {
+        return;
+    }
+    if (device != g_device) {
+        release();
+        g_device = device;
+        g_device->AddRef();
+    }
+    const auto images = state::build_data::activities::artwork();
+    if (g_attempted || images.empty()) {
+        return;
+    }
+    g_attempted = true;
+    for (std::size_t i = 0; i < images.size(); ++i) {
+        const auto& image = images[i];
+        if (image.pixels.empty()) {
+            continue;
+        }
+        D3D11_TEXTURE2D_DESC desc{};
+        desc.Width = image.width;
+        desc.Height = image.height;
+        desc.MipLevels = 1;
+        desc.ArraySize = 1;
+        desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
+        desc.SampleDesc.Count = 1;
+        desc.Usage = D3D11_USAGE_IMMUTABLE;
+        desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
+        D3D11_SUBRESOURCE_DATA data{};
+        data.pSysMem = image.pixels.data();
+        data.SysMemPitch = static_cast<UINT>(image.width) * 4;
+        ID3D11Texture2D* texture{};
+        if (SUCCEEDED(device->CreateTexture2D(&desc, &data, &texture)) && texture != nullptr) {
+            (void)device->CreateShaderResourceView(texture, nullptr, &g_views[i]);
+            texture->Release();
+        }
+    }
+}
+std::uint64_t texture(state::build_data::activities::Icon icon) noexcept {
+    const auto index = static_cast<std::size_t>(icon);
+    return index < g_views.size() ? reinterpret_cast<std::uint64_t>(g_views[index]) : 0;
+}
+DisplaySize display_size(state::build_data::activities::Icon icon,
+                         float extent,
+                         float framebufferScale) noexcept {
+    namespace catalog = state::build_data::activities;
+    if (!std::isfinite(extent) || extent <= 0.0F) {
+        return {};
+    }
+    if (!std::isfinite(framebufferScale) || framebufferScale <= 0.0F) {
+        framebufferScale = 1.0F;
+    }
+    const auto images = catalog::artwork();
+    const auto index = static_cast<std::size_t>(icon);
+    if (index >= images.size() || images[index].width == 0 || images[index].height == 0) {
+        return {};
+    }
+    const auto& image = images[index];
+    float ratio = extent / static_cast<float>((std::max)(image.width, image.height));
+    ratio = (std::min)(ratio, 1.0F / framebufferScale);
+    return {static_cast<float>(image.width) * ratio, static_cast<float>(image.height) * ratio};
+}
+} // namespace sunrise::client::ui::mission_launch::art

+ 21 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_art.h

@@ -0,0 +1,21 @@
+#pragma once
+#include <cstdint>
+
+#include "../../../state/build_data/activities/activity_artwork.h"
+struct ID3D11Device;
+
+namespace sunrise::client::ui::mission_launch::art {
+/** Render-lock owned. Each device generation gets one bounded upload attempt per asset. */
+void prepare(ID3D11Device* device) noexcept;
+/** Releases the render-owner's texture views and device reference before renderer teardown. */
+void release() noexcept;
+/** @return A render-owned texture handle, or zero when this numeric slot is unavailable. */
+[[nodiscard]] std::uint64_t texture(state::build_data::activities::Icon icon) noexcept;
+struct DisplaySize {
+    float width{}, height{};
+};
+/** Preserve native aspect ratio; no icon upscales beyond one source pixel per framebuffer pixel. */
+[[nodiscard]] DisplaySize display_size(state::build_data::activities::Icon icon,
+                                       float extent,
+                                       float framebufferScale = 1.0F) noexcept;
+} // namespace sunrise::client::ui::mission_launch::art

+ 88 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_cards.h

@@ -0,0 +1,88 @@
+#pragma once
+#include <algorithm>
+#include <cmath>
+#include <imgui.h>
+
+#include "mission_launch_art.h"
+
+namespace sunrise::client::ui::mission_launch {
+[[nodiscard]] inline float card_scale() noexcept {
+    return ImGui::GetFontSize() / 16.0F;
+}
+[[nodiscard]] inline int grid_columns(float width) noexcept {
+    return (std::clamp)(static_cast<int>(width / (184.0F * card_scale())), 1, 5);
+}
+/** Draw within a caller-owned slot, keeping native pixels aligned and inheriting the menu fade. */
+[[nodiscard]] inline bool
+draw_icon(state::build_data::activities::Icon icon, ImVec2 origin, float extent) noexcept {
+    const auto texture = art::texture(icon);
+    if (texture == ImTextureID_Invalid) {
+        return false;
+    }
+    const auto framebuffer = ImGui::GetIO().DisplayFramebufferScale;
+    const float density = (std::max)(1.0F, (std::max)(framebuffer.x, framebuffer.y));
+    const auto image = art::display_size(icon, extent, density);
+    const float x = std::round((origin.x + (extent - image.width) * 0.5F) * density) / density;
+    const float y = std::round((origin.y + (extent - image.height) * 0.5F) * density) / density;
+    ImGui::GetWindowDrawList()->AddImage(texture,
+                                         {x, y},
+                                         {x + image.width, y + image.height},
+                                         {0, 0},
+                                         {1, 1},
+                                         ImGui::GetColorU32(ImVec4{1, 1, 1, 1}));
+    return true;
+}
+/** One complete layout item owns the card bounds. Text/images only draw inside those bounds.
+ * Never rewinds or extends a cursor, and does not allocate or resize a font. */
+[[nodiscard]] inline bool
+activity_card(const char* title,
+              const char* subtitle,
+              const char* footer,
+              state::build_data::activities::Icon icon,
+              bool selected,
+              bool available = true,
+              const char* artworkFallback = "Artwork unavailable") noexcept {
+    const float scale = card_scale();
+    const float width = (std::max)(1.0F, ImGui::GetContentRegionAvail().x);
+    const bool clicked = ImGui::InvisibleButton(
+        "##activity_card", {width, std::ceil(188.0F * scale)}, ImGuiButtonFlags_EnableNav);
+    const auto min = ImGui::GetItemRectMin(), max = ImGui::GetItemRectMax();
+    auto* draw = ImGui::GetWindowDrawList();
+    const bool hot = ImGui::IsItemHovered() || ImGui::IsItemFocused();
+    draw->AddRectFilled(min,
+                        max,
+                        ImGui::GetColorU32(hot ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg),
+                        7.0F * scale);
+    draw->AddRect(min,
+                  max,
+                  ImGui::GetColorU32(hot || selected ? ImGuiCol_CheckMark : ImGuiCol_Border),
+                  7.0F * scale,
+                  0,
+                  hot || selected ? 2.0F : 1.0F);
+    const float inset = 13.0F * scale;
+    const ImVec2 textMin{min.x + inset, min.y + 91.0F * scale};
+    const ImVec2 textMax{max.x - inset, max.y - inset};
+    draw->PushClipRect({min.x + inset, min.y + inset}, textMax, true);
+    if (!draw_icon(icon, {min.x + inset, min.y + 12.0F * scale}, 66.0F * scale)) {
+        draw->AddText({min.x + inset, min.y + 35.0F * scale},
+                      ImGui::GetColorU32(ImGuiCol_TextDisabled),
+                      artworkFallback);
+    }
+    draw->PushClipRect(textMin, {textMax.x, textMin.y + ImGui::GetTextLineHeight() * 2.0F}, true);
+    draw->AddText(ImGui::GetFont(),
+                  ImGui::GetFontSize(),
+                  textMin,
+                  ImGui::GetColorU32(available ? ImGuiCol_Text : ImGuiCol_TextDisabled),
+                  title,
+                  nullptr,
+                  (std::max)(1.0F, textMax.x - textMin.x));
+    draw->PopClipRect();
+    draw->AddText(
+        {textMin.x, min.y + 137.0F * scale}, ImGui::GetColorU32(ImGuiCol_TextDisabled), subtitle);
+    draw->AddText({textMin.x, min.y + 157.0F * scale},
+                  ImGui::GetColorU32(available ? ImGuiCol_TextDisabled : ImGuiCol_PlotHistogram),
+                  footer);
+    draw->PopClipRect();
+    return clicked;
+}
+} // namespace sunrise::client::ui::mission_launch

+ 56 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_detail_style.h

@@ -0,0 +1,56 @@
+#pragma once
+#include <imgui.h>
+
+namespace sunrise::client::ui::mission_launch::detail {
+/** Shared contrast for every Activity Launcher page; restores the surrounding overlay palette on
+ * exit. */
+struct Style final {
+    Style() noexcept {
+        ImGui::PushStyleColor(ImGuiCol_Text, {0.96F, 0.97F, 0.99F, 1});
+        ImGui::PushStyleColor(ImGuiCol_TextDisabled, {0.68F, 0.74F, 0.82F, 1});
+        ImGui::PushStyleColor(ImGuiCol_Border, {0.40F, 0.49F, 0.61F, 1});
+        ImGui::PushStyleColor(ImGuiCol_FrameBg, {0.13F, 0.18F, 0.24F, 1});
+        ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, {0.22F, 0.31F, 0.42F, 1});
+        ImGui::PushStyleColor(ImGuiCol_FrameBgActive, {0.28F, 0.40F, 0.54F, 1});
+        ImGui::PushStyleColor(ImGuiCol_Button, {0.17F, 0.23F, 0.31F, 1});
+        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, {0.27F, 0.36F, 0.47F, 1});
+        ImGui::PushStyleColor(ImGuiCol_ButtonActive, {0.34F, 0.44F, 0.57F, 1});
+        ImGui::PushStyleColor(ImGuiCol_Header, {0.20F, 0.33F, 0.48F, 1});
+        ImGui::PushStyleColor(ImGuiCol_HeaderHovered, {0.29F, 0.42F, 0.56F, 1});
+        ImGui::PushStyleColor(ImGuiCol_HeaderActive, {0.35F, 0.49F, 0.65F, 1});
+        ImGui::PushStyleColor(ImGuiCol_PopupBg, {0.07F, 0.09F, 0.13F, 1});
+        ImGui::PushStyleColor(ImGuiCol_NavCursor, {1.0F, 0.64F, 0.30F, 1});
+        ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0F);
+        ImGui::PushStyleVar(ImGuiStyleVar_DisabledAlpha, 0.60F);
+    }
+    ~Style() noexcept {
+        ImGui::PopStyleVar(2);
+        ImGui::PopStyleColor(14);
+    }
+    Style(const Style&) = delete;
+    Style& operator=(const Style&) = delete;
+};
+
+inline void heading(const char* text) noexcept {
+    // PushFont takes an unscaled base size; passing GetFontSize would double-apply DPI.
+    ImGui::PushFont(nullptr, ImGui::GetStyle().FontSizeBase * 1.375F);
+    ImGui::TextWrapped("%s", text);
+    ImGui::PopFont();
+}
+
+[[nodiscard]] inline bool launch_button(bool busy, bool disabled, float height) noexcept {
+    ImGui::PushStyleColor(ImGuiCol_Button,
+                          disabled ? ImVec4{0.19F, 0.24F, 0.30F, 1}
+                                   : ImVec4{0.97F, 0.51F, 0.25F, 1});
+    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, {1.0F, 0.64F, 0.37F, 1});
+    ImGui::PushStyleColor(ImGuiCol_ButtonActive, {0.86F, 0.40F, 0.17F, 1});
+    ImGui::PushStyleColor(ImGuiCol_Text,
+                          disabled ? ImVec4{0.91F, 0.94F, 0.98F, 1}
+                                   : ImVec4{0.035F, 0.045F, 0.065F, 1});
+    ImGui::BeginDisabled(disabled);
+    const bool clicked = ImGui::Button(busy ? "Launching..." : "Launch activity", {-1.0F, height});
+    ImGui::EndDisabled();
+    ImGui::PopStyleColor(4);
+    return clicked;
+}
+} // namespace sunrise::client::ui::mission_launch::detail

+ 7 - 89
Sunrise/src/server/ui/activity_override/activity_override_lists.cpp → Sunrise/src/client/ui/mission_launch/mission_launch_lists.cpp

@@ -1,4 +1,4 @@
-#include "activity_override_lists.h"
+#include "mission_launch_lists.h"
 
 
 #include <algorithm>
 #include <algorithm>
 #include <cstdio>
 #include <cstdio>
@@ -7,23 +7,17 @@
 #include "../../../middleware/content/packages/tables/component_container_reader.h"
 #include "../../../middleware/content/packages/tables/component_container_reader.h"
 #include "../../../middleware/content/packages/tables/region_reader.h"
 #include "../../../middleware/content/packages/tables/region_reader.h"
 #include "../../../middleware/content/packages/tables/scenario_reader.h"
 #include "../../../middleware/content/packages/tables/scenario_reader.h"
-#include "../../../middleware/content/packages/tables/spawn_reader.h"
-#include "../../../state/activity/forced/definition.h"
-#include "../../../state/activity_sdk/runtime.h"
 #include "../../../state/build_data/runtime.h"
 #include "../../../state/build_data/runtime.h"
 
 
-namespace sunrise::server::ui::activity_override {
+namespace sunrise::client::ui::mission_launch::lists {
 namespace {
 namespace {
 
 
 namespace layouts = state::build_data::scenarios;
 namespace layouts = state::build_data::scenarios;
 namespace tables = middleware::content::packages::tables;
 namespace tables = middleware::content::packages::tables;
-namespace sdk = state::activity_sdk;
 
 
 /** A bubble's slice sets are spaced by this factor, so it also caps how many it can declare. */
 /** A bubble's slice sets are spaced by this factor, so it also caps how many it can declare. */
 constexpr std::uint8_t kMaximumBubbleStates = tables::kSliceSetIndexFactor;
 constexpr std::uint8_t kMaximumBubbleStates = tables::kSliceSetIndexFactor;
 
 
-Lists g_lists{};
-
 /** @return The destination row's name as a bounded view. */
 /** @return The destination row's name as a bounded view. */
 [[nodiscard]] std::string_view name_of(const layouts::Definition& row) noexcept {
 [[nodiscard]] std::string_view name_of(const layouts::Definition& row) noexcept {
     return {row.name.data(), row.nameLength};
     return {row.name.data(), row.nameLength};
@@ -40,19 +34,6 @@ Lists g_lists{};
     return name_of(left) < name_of(right);
     return name_of(left) < name_of(right);
 }
 }
 
 
-/**
- * Names one spawn set.
- * The packages carry no name table for these. Only the two hashes the client itself defines are
- * named, and every other set is shown by hash.
- * @return The name, or an empty view.
- */
-[[nodiscard]] std::string_view spawn_name(std::uint32_t hash) noexcept {
-    if (hash == tables::kDefaultSpawnNameHash) {
-        return "default";
-    }
-    return hash == tables::kUnnamedSpawnNameHash ? std::string_view("unnamed") : std::string_view();
-}
-
 /**
 /**
  * Finds the internal name the packages give one hash.
  * Finds the internal name the packages give one hash.
  * @param hash Bubble or spawn-set name hash.
  * @param hash Bubble or spawn-set name hash.
@@ -102,9 +83,6 @@ void clear_spawns(Lists& rows) noexcept {
     rows.spawnHashes = {};
     rows.spawnHashes = {};
     rows.spawnCount = 0;
     rows.spawnCount = 0;
     rows.spawnUnavailable = false;
     rows.spawnUnavailable = false;
-    rows.spawnNarrowed = false;
-    rows.spawnHidden = 0;
-    rows.spawnForeign = 0;
 }
 }
 
 
 /** Clears every list that belongs to the selected destination. */
 /** Clears every list that belongs to the selected destination. */
@@ -189,39 +167,27 @@ void build_spawns(Lists& rows, std::string_view stem, std::uint16_t mapIndex) no
         return;
         return;
     }
     }
     const bool narrowing = mapIndex != tables::kAbsentMapBubbleIndex;
     const bool narrowing = mapIndex != tables::kAbsentMapBubbleIndex;
-    rows.spawnNarrowed = narrowing;
     for (std::size_t index = 0; index < count && rows.spawnCount < rows.spawns.size(); ++index) {
     for (std::size_t index = 0; index < count && rows.spawnCount < rows.spawns.size(); ++index) {
         const state::build_data::spawn_sets::NameHash& row = scratch[index];
         const state::build_data::spawn_sets::NameHash& row = scratch[index];
         const std::uint32_t hash = row.value;
         const std::uint32_t hash = row.value;
         const bool offered = !narrowing || tables::bubble_in_mask(row.bubbleMask, mapIndex);
         const bool offered = !narrowing || tables::bubble_in_mask(row.bubbleMask, mapIndex);
         if (!offered && row.unbound == 0) {
         if (!offered && row.unbound == 0) {
-            ++rows.spawnHidden;
             continue;
             continue;
         }
         }
-        // The hash always. Its name is one of the two the client itself defines, else the
-        // extracted one, else nothing.
-        std::string_view named = spawn_name(hash);
+        // The hash always, then the extracted name where the packages produce one.
         state::build_data::hash_names::Name storage{};
         state::build_data::hash_names::Name storage{};
-        if (named.empty()) {
-            named = resolve_name(hash, storage);
-        }
         Label name{};
         Label name{};
-        name_column(named, name);
+        name_column(resolve_name(hash, storage), name);
         // A set reaching this bubble only because nothing binds it is marked, not hidden.
         // A set reaching this bubble only because nothing binds it is marked, not hidden.
-        const char* const candidate = offered ? "" : "  (candidate)";
-        // A set this destination does not load is marked too. The bubble is right and the set
-        // still will not attach, which is what lands the player nowhere.
+        // A set this destination does not load is marked too: it will not attach.
         const bool loaded = loads_package(rows.selected, row);
         const bool loaded = loads_package(rows.selected, row);
-        if (!loaded) {
-            ++rows.spawnForeign;
-        }
         Label label{};
         Label label{};
         const int written = std::snprintf(label.data(),
         const int written = std::snprintf(label.data(),
                                           label.size(),
                                           label.size(),
                                           "0x%08X%s%s%s",
                                           "0x%08X%s%s%s",
                                           hash,
                                           hash,
                                           name.data(),
                                           name.data(),
-                                          candidate,
+                                          offered ? "" : "  (candidate)",
                                           loaded ? "" : "  (not loaded)");
                                           loaded ? "" : "  (not loaded)");
         if (written <= 0) {
         if (written <= 0) {
             continue;
             continue;
@@ -234,11 +200,6 @@ void build_spawns(Lists& rows, std::string_view stem, std::uint16_t mapIndex) no
 
 
 } // namespace
 } // namespace
 
 
-/** @return Process-lifetime picker rows, which no other module reads. */
-Lists& lists() noexcept {
-    return g_lists;
-}
-
 /** Rebuilds the destination rows when the published layout count has changed. */
 /** Rebuilds the destination rows when the published layout count has changed. */
 void refresh_activities(Lists& rows) noexcept {
 void refresh_activities(Lists& rows) noexcept {
     const std::size_t published = state::build_data::scenario_layout_count();
     const std::size_t published = state::build_data::scenario_layout_count();
@@ -265,49 +226,6 @@ void refresh_activities(Lists& rows) noexcept {
     }
     }
 }
 }
 
 
-/** Rebuilds the definition rows for one destination, keeping the hidden count. */
-void refresh_definitions(Lists& rows, std::string_view destination) noexcept {
-    rows.definitions = {};
-    rows.definitionIndices = {};
-    rows.definitionCount = 0;
-    rows.definitionsHidden = 0;
-    const sdk::Snapshot catalog = sdk::snapshot();
-    rows.definitionsUnavailable = catalog == nullptr;
-    if (rows.definitionsUnavailable || destination.empty()) {
-        return;
-    }
-    const auto activities = catalog->activities();
-    for (std::size_t row = 0; row < activities.size(); ++row) {
-        const sdk::format::Activity& activity = activities[row];
-        if (catalog->string(activity.internalName) != destination
-            || activity.activityIndex > state::activity::forced::kMaximumActivityIndex) {
-            continue;
-        }
-        if (rows.definitionCount == rows.definitions.size()) {
-            ++rows.definitionsHidden;
-            continue;
-        }
-        const bool exact =
-            (activity.flags & sdk::format::kActivityExactMask) == sdk::format::kActivityExactMask;
-        const std::string_view display = catalog->string(activity.displayName);
-        std::array<char, kLabelCapacity> line{};
-        const int written = std::snprintf(line.data(),
-                                          line.size(),
-                                          "%u  %.*s%s",
-                                          static_cast<unsigned>(activity.activityIndex),
-                                          static_cast<int>(display.size()),
-                                          display.data(),
-                                          exact ? "" : "  not exact");
-        if (written <= 0) {
-            continue;
-        }
-        assign(std::string_view(line.data()), rows.definitions[rows.definitionCount]);
-        rows.definitionIndices[rows.definitionCount] =
-            static_cast<std::uint16_t>(activity.activityIndex);
-        ++rows.definitionCount;
-    }
-}
-
 /** Rebuilds the bubble and spawn-set rows for one destination, and clears the slice-set rows. */
 /** Rebuilds the bubble and spawn-set rows for one destination, and clears the slice-set rows. */
 void refresh_destination(Lists& rows, std::string_view name) noexcept {
 void refresh_destination(Lists& rows, std::string_view name) noexcept {
     clear_destination(rows);
     clear_destination(rows);
@@ -347,4 +265,4 @@ void refresh_bubble(Lists& rows, std::uint8_t bubble) noexcept {
     }
     }
 }
 }
 
 
-} // namespace sunrise::server::ui::activity_override
+} // namespace sunrise::client::ui::mission_launch::lists

+ 3 - 31
Sunrise/src/server/ui/activity_override/activity_override_lists.h → Sunrise/src/client/ui/mission_launch/mission_launch_lists.h

@@ -7,7 +7,7 @@
 
 
 #include "../../../state/build_data/scenarios/definition.h"
 #include "../../../state/build_data/scenarios/definition.h"
 
 
-namespace sunrise::server::ui::activity_override {
+namespace sunrise::client::ui::mission_launch::lists {
 
 
 /** One row label and its null. The widest is a spawn row: a hash, a 48-byte name, 2 markers. */
 /** One row label and its null. The widest is a spawn row: a hash, a 48-byte name, 2 markers. */
 inline constexpr std::size_t kLabelCapacity = 96;
 inline constexpr std::size_t kLabelCapacity = 96;
@@ -15,15 +15,11 @@ inline constexpr std::size_t kLabelCapacity = 96;
 inline constexpr std::size_t kSliceCapacity = 8;
 inline constexpr std::size_t kSliceCapacity = 8;
 /** Spawn-set rows for one map stem. The widest installed stem declares 294. */
 /** Spawn-set rows for one map stem. The widest installed stem declares 294. */
 inline constexpr std::size_t kSpawnCapacity = 1'024;
 inline constexpr std::size_t kSpawnCapacity = 1'024;
-/** Index of no row, which is what every list starts on. */
-inline constexpr std::size_t kNoRow = static_cast<std::size_t>(-1);
-/** Definition rows for one destination name. The widest real destination declares twenty. */
-inline constexpr std::size_t kDefinitionCapacity = 64;
 
 
 /** One null-terminated row label. */
 /** One null-terminated row label. */
 using Label = std::array<char, kLabelCapacity>;
 using Label = std::array<char, kLabelCapacity>;
 
 
-/** Rows the four pickers draw, rebuilt from the catalogues rather than stored in State. */
+/** Rows the manual arrival pickers draw, rebuilt from the catalogues rather than stored. */
 struct Lists {
 struct Lists {
     std::array<Label, state::build_data::scenarios::kDefinitionCapacity> activities{};
     std::array<Label, state::build_data::scenarios::kDefinitionCapacity> activities{};
     std::size_t activityCount{};
     std::size_t activityCount{};
@@ -40,40 +36,16 @@ struct Lists {
     std::array<std::uint16_t, kSliceCapacity> sliceValues{};
     std::array<std::uint16_t, kSliceCapacity> sliceValues{};
     std::size_t sliceCount{};
     std::size_t sliceCount{};
 
 
-    /**
-     * Generated activity definitions whose internal name is the selected destination.
-     * A name maps to several, and only a named one lets the SDK and the mission script bind.
-     */
-    std::array<Label, kDefinitionCapacity> definitions{};
-    std::array<std::uint16_t, kDefinitionCapacity> definitionIndices{};
-    std::size_t definitionCount{};
-    /** Set when the generated estate is not loaded, so no definition can be listed. */
-    bool definitionsUnavailable{};
-    /** Rows this destination declares past the listed ones. */
-    std::size_t definitionsHidden{};
-
     std::array<Label, kSpawnCapacity> spawns{};
     std::array<Label, kSpawnCapacity> spawns{};
     std::array<std::uint32_t, kSpawnCapacity> spawnHashes{};
     std::array<std::uint32_t, kSpawnCapacity> spawnHashes{};
     std::size_t spawnCount{};
     std::size_t spawnCount{};
     /** Set when the destination has a map stem but its spawn sets could not be listed. */
     /** Set when the destination has a map stem but its spawn sets could not be listed. */
     bool spawnUnavailable{};
     bool spawnUnavailable{};
-    /** Set when the rows are narrowed to the selected bubble rather than the whole map. */
-    bool spawnNarrowed{};
-    /** Rows the map declares that the selected bubble does not offer. */
-    std::size_t spawnHidden{};
-    /** Rows whose package this destination does not load, which are shown and marked. */
-    std::size_t spawnForeign{};
 };
 };
 
 
-/** @return Process-lifetime picker rows, which no other module reads. */
-[[nodiscard]] Lists& lists() noexcept;
-
 /** Rebuilds the destination rows when the published layout count has changed. */
 /** Rebuilds the destination rows when the published layout count has changed. */
 void refresh_activities(Lists& rows) noexcept;
 void refresh_activities(Lists& rows) noexcept;
 
 
-/** Rebuilds the definition rows for one destination name from the generated estate. */
-void refresh_definitions(Lists& rows, std::string_view destination) noexcept;
-
 /**
 /**
  * Rebuilds the bubble and spawn-set rows for one destination, and clears the slice-set rows.
  * Rebuilds the bubble and spawn-set rows for one destination, and clears the slice-set rows.
  * @param name Destination package name, or empty to clear every dependent list.
  * @param name Destination package name, or empty to clear every dependent list.
@@ -87,4 +59,4 @@ void refresh_destination(Lists& rows, std::string_view name) noexcept;
  */
  */
 void refresh_bubble(Lists& rows, std::uint8_t bubble) noexcept;
 void refresh_bubble(Lists& rows, std::uint8_t bubble) noexcept;
 
 
-} // namespace sunrise::server::ui::activity_override
+} // namespace sunrise::client::ui::mission_launch::lists

+ 275 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_manual.h

@@ -0,0 +1,275 @@
+#pragma once
+
+#include <imgui.h>
+
+#include "../../activity/mission_launch_options.h"
+#include "mission_launch_lists.h"
+#include "mission_launch_model.h"
+
+namespace sunrise::client::ui::mission_launch::manual {
+namespace options = client::activity::mission_launch;
+namespace picker = lists;
+namespace forced = state::activity::forced;
+
+// A local draft, never the published override state.
+inline picker::Lists g_lists{};
+inline options::ManualScratch g_validation{};
+inline forced::ForcedDestination g_value{};
+inline bool g_enabled{};
+inline std::uint16_t g_detailIndex{0xFFFF}, g_transport{0xFFFF};
+inline std::size_t g_revision{};
+inline std::array<char, 96> g_activitySearch{};
+
+/** Restarts the draft on one destination and rebuilds its dependent rows. */
+inline void select_destination(std::string_view name) noexcept {
+    g_value = {};
+    if (name.size() > g_value.packageName.size()) {
+        name = {};
+    }
+    std::copy(name.begin(), name.end(), g_value.packageName.begin());
+    g_value.packageNameLength = static_cast<std::uint8_t>(name.size());
+    g_value.enabled = true;
+    picker::refresh_destination(g_lists, name);
+    g_revision = state::build_data::scenario_layout_count();
+}
+
+inline void select_transport(std::uint16_t index) noexcept {
+    g_transport = index;
+    const auto rows = state::build_data::activities::entries();
+    g_value.hasActivityIndex =
+        index < rows.size() && rows[index].name() == options::destination_name(g_value);
+    g_value.activityIndex = g_value.hasActivityIndex ? index : 0;
+}
+
+/** Picks a bubble, drops the dependent slice and spawn picks, and defaults the slice. */
+inline void select_bubble(std::uint8_t bubble) noexcept {
+    g_value.hasBubble = true;
+    g_value.bubble = bubble;
+    g_value.hasSliceSet = false;
+    g_value.sliceSet = 0;
+    g_value.hasSpawnSetHash = false;
+    g_value.spawnSetHash = 0;
+    picker::refresh_bubble(g_lists, bubble);
+    if (g_lists.sliceCount != 0) {
+        g_value.hasSliceSet = true;
+        g_value.sliceSet = g_lists.sliceValues[0];
+    }
+}
+
+inline void follow_activity(const Activity& row) noexcept {
+    if (g_detailIndex == row.index && g_revision == state::build_data::scenario_layout_count()) {
+        return;
+    }
+    g_enabled = false;
+    g_detailIndex = row.index;
+    g_transport = row.index;
+    select_destination(row.name());
+    select_transport(row.index);
+}
+
+/** Prefer an available named definition from the selected destination's own catalog rows. */
+inline std::uint16_t default_transport(std::string_view name) noexcept {
+    const auto rows = state::build_data::activities::entries();
+    std::uint16_t best = 0xFFFF;
+    for (const auto& row : rows) {
+        if (row.name() == name && content_visible(content_group(row))
+            && (best == 0xFFFF
+                || representative_score(row, true) > representative_score(rows[best], true))) {
+            best = row.index;
+        }
+    }
+    return best;
+}
+
+inline void start_custom() noexcept {
+    g_enabled = true;
+    g_detailIndex = 0xFFFF;
+    g_transport = 0xFFFF;
+    g_activitySearch.fill(0);
+    select_destination({});
+}
+
+/** Draws the destination picker of the custom launch page. */
+inline void custom_activity() noexcept {
+    picker::refresh_activities(g_lists);
+    if (g_revision != state::build_data::scenario_layout_count()) {
+        const auto previous = g_value.packageName;
+        const auto length = g_value.packageNameLength;
+        select_destination(
+            {previous.data(), length}); // Discard dependent values when source layouts change.
+        select_transport(default_transport(options::destination_name(g_value)));
+    }
+    ImGui::TextUnformatted("Activity");
+    ImGui::SetNextItemWidth(-1.0F);
+    const auto name = options::destination_name(g_value);
+    std::array<char, 41> preview{};
+    std::copy(name.begin(), name.end(), preview.begin());
+    if (ImGui::BeginCombo("##custom_activity",
+                          name.empty() ? "Choose an installed activity..." : preview.data())) {
+        ImGui::SetNextItemWidth(-1.0F);
+        (void)ImGui::InputTextWithHint("##custom_activity_filter",
+                                       "Search activity package...",
+                                       g_activitySearch.data(),
+                                       g_activitySearch.size());
+        for (std::size_t i = 0; i < g_lists.activityCount; ++i) {
+            const std::string_view candidate(g_lists.activities[i].data());
+            if (!contains(candidate, g_activitySearch.data())) {
+                continue;
+            }
+            if (ImGui::Selectable(g_lists.activities[i].data(), candidate == name)) {
+                select_destination(candidate);
+                select_transport(default_transport(candidate));
+            }
+        }
+        ImGui::EndCombo();
+    }
+}
+
+/** Draws the launch-route picker: the catalog rows whose package is the drafted destination. */
+inline void transport_picker(std::span<const Activity> rows,
+                             std::span<const bool> available) noexcept {
+    ImGui::TextUnformatted("Activity variant");
+    ImGui::SetNextItemWidth(-1.0F);
+    std::array<char, 240> preview{};
+    if (g_transport < rows.size()) {
+        (void)std::snprintf(preview.data(),
+                            preview.size(),
+                            "#%u | %s",
+                            g_transport,
+                            title(rows[g_transport]).data());
+    }
+    if (ImGui::BeginCombo("##custom_transport",
+                          preview[0] ? preview.data() : "Choose the native launch route...")) {
+        for (const auto& row : rows) {
+            if (row.index >= available.size() || !available[row.index]
+                || !content_visible(content_group(row))
+                || row.name() != options::destination_name(g_value)) {
+                continue;
+            }
+            std::array<char, 240> label{};
+            (void)std::snprintf(label.data(),
+                                label.size(),
+                                "#%u | %s | %s",
+                                row.index,
+                                title(row).data(),
+                                row.package.data());
+            if (ImGui::Selectable(label.data(), g_transport == row.index)) {
+                select_transport(row.index);
+            }
+        }
+        ImGui::EndCombo();
+    }
+}
+
+/** Draws the bubble, slice and spawn pickers for the drafted destination. */
+inline void arrival_fields() noexcept {
+    if (!g_enabled) {
+        return;
+    }
+    if (g_lists.selected.bubbleCount == 0) {
+        ImGui::TextWrapped(
+            "No manual arrival is available for this activity. Use its normal launch.");
+        return;
+    }
+    const int columns =
+        ImGui::GetContentRegionAvail().x >= 460.0F * ImGui::GetFontSize() / 16.0F ? 2 : 1;
+    const bool table =
+        ImGui::BeginTable("##manual_arrival", columns, ImGuiTableFlags_SizingStretchSame);
+    if (table) {
+        ImGui::TableNextColumn();
+    }
+    ImGui::AlignTextToFramePadding();
+    ImGui::TextUnformatted("Bubble");
+    ImGui::SetNextItemWidth(-1.0F);
+    const char* bubblePreview = "Choose a bubble...";
+    std::array<char, 48> bubbleValue{};
+    for (std::size_t i = 0; i < g_lists.bubbleCount; ++i) {
+        if (g_value.hasBubble && g_lists.bubbleOrdinals[i] == g_value.bubble) {
+            (void)std::snprintf(bubbleValue.data(), bubbleValue.size(), "%u", g_value.bubble);
+            bubblePreview = bubbleValue.data();
+        }
+    }
+    if (ImGui::BeginCombo("##manual_bubble", bubblePreview)) {
+        for (std::size_t i = 0; i < g_lists.bubbleCount; ++i) {
+            if (ImGui::Selectable(g_lists.bubbles[i].data(),
+                                  g_value.hasBubble
+                                      && g_value.bubble == g_lists.bubbleOrdinals[i])) {
+                select_bubble(g_lists.bubbleOrdinals[i]);
+            }
+        }
+        ImGui::EndCombo();
+    }
+    if (table) {
+        ImGui::TableNextColumn();
+    }
+    ImGui::AlignTextToFramePadding();
+    ImGui::TextUnformatted("Slice");
+    ImGui::SetNextItemWidth(-1.0F);
+    const char* slicePreview = "Choose a bubble first...";
+    std::array<char, 48> sliceValue{};
+    for (std::size_t i = 0; i < g_lists.sliceCount; ++i) {
+        if (g_value.hasSliceSet && g_value.sliceSet == g_lists.sliceValues[i]) {
+            (void)std::snprintf(sliceValue.data(), sliceValue.size(), "%u", g_value.sliceSet);
+            slicePreview = sliceValue.data();
+        }
+    }
+    if (ImGui::BeginCombo("##manual_slice", slicePreview)) {
+        for (std::size_t i = 0; i < g_lists.sliceCount; ++i) {
+            if (ImGui::Selectable(g_lists.slices[i].data(),
+                                  g_value.hasSliceSet
+                                      && g_value.sliceSet == g_lists.sliceValues[i])) {
+                g_value.hasSliceSet = true;
+                g_value.sliceSet = g_lists.sliceValues[i];
+                g_value.hasSpawnSetHash = false;
+                g_value.spawnSetHash = 0;
+            }
+        }
+        ImGui::EndCombo();
+    }
+    if (table) {
+        ImGui::EndTable();
+    }
+    ImGui::TextUnformatted("Spawn");
+    ImGui::SetNextItemWidth(-1.0F);
+    const char* spawnPreview = "Client picks";
+    for (std::size_t i = 0; i < g_lists.spawnCount; ++i) {
+        if (g_value.hasSpawnSetHash && g_value.spawnSetHash == g_lists.spawnHashes[i]) {
+            spawnPreview = g_lists.spawns[i].data();
+        }
+    }
+    if (ImGui::BeginCombo("##manual_spawn", spawnPreview)) {
+        if (ImGui::Selectable("Client picks (no forced spawn set)", !g_value.hasSpawnSetHash)) {
+            g_value.hasSpawnSetHash = false;
+            g_value.spawnSetHash = 0;
+        }
+        auto lookup = g_value;
+        lookup.hasSpawnSetHash = true;
+        (void)options::validate_manual(lookup,
+                                       g_validation); // One bounded catalog read per popup frame.
+        for (std::size_t i = 0; i < g_lists.spawnCount; ++i) {
+            auto candidate = g_value;
+            candidate.hasSpawnSetHash = true;
+            candidate.spawnSetHash = g_lists.spawnHashes[i];
+            const bool supported =
+                options::validate_manual(
+                    candidate,
+                    g_validation.layout,
+                    std::span(g_validation.spawns).first(g_validation.spawnCount))
+                == options::ManualError::none;
+            ImGui::BeginDisabled(!supported);
+            if (ImGui::Selectable(g_lists.spawns[i].data(),
+                                  g_value.hasSpawnSetHash
+                                      && g_value.spawnSetHash == g_lists.spawnHashes[i])) {
+                g_value.hasSpawnSetHash = true;
+                g_value.spawnSetHash = g_lists.spawnHashes[i];
+            }
+            ImGui::EndDisabled();
+        }
+        ImGui::EndCombo();
+    }
+    if (g_lists.spawnUnavailable) {
+        ImGui::TextWrapped(
+            "Spawn metadata is unavailable. The client can still choose its own spawn.");
+    }
+}
+} // namespace sunrise::client::ui::mission_launch::manual

+ 322 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_model.h

@@ -0,0 +1,322 @@
+#pragma once
+#include <array>
+#include <cctype>
+#include <cstdio>
+#include <string_view>
+
+#include "../../../state/build_data/activities/activity_artwork.h"
+#include "../../../state/build_data/activities/activity_catalog.h"
+#include "../../../state/build_data/activities/activity_releases.h"
+
+namespace sunrise::client::ui::mission_launch {
+using Activity = state::build_data::activities::Definition;
+namespace releases = state::build_data::activities::releases;
+struct ContentNames {
+    [[nodiscard]] static constexpr std::size_t size() noexcept {
+        return state::build_data::activities::kContentCount;
+    }
+    [[nodiscard]] const char* operator[](std::size_t content) const noexcept {
+        if (content == 0) {
+            return "All content";
+        }
+        if (content == releases::unresolved) {
+            return "Unclassified content";
+        }
+        const auto& names = state::build_data::activities::menu_text().content;
+        return content < names.size() && names[content].text[0] ? names[content].text.data()
+                                                                : "Content name unavailable";
+    }
+};
+inline constexpr ContentNames kContent{};
+inline constexpr auto kUnresolvedContent = releases::unresolved;
+/** Launcher presentation preference only; the native catalog and release evidence remain intact. */
+[[nodiscard]] inline bool content_visible(std::size_t content) noexcept {
+    return content != 18 && content != 21; // Hidden content IDs.
+}
+[[nodiscard]] inline bool library_card_visible(std::size_t content) noexcept {
+    return content_visible(content) && content != 17; // Content IDs 14 and 17 share one card.
+}
+[[nodiscard]] inline const char* library_name(std::size_t content) noexcept {
+    return kContent[content];
+}
+[[nodiscard]] inline unsigned library_kind(std::size_t content) noexcept {
+    if (content == kUnresolvedContent) {
+        return 0;
+    }
+    if ((content >= 14 && content <= 17) || (content >= 19 && content <= 21)) {
+        return 3;
+    }
+    return content >= 7 && content <= 13 ? 2 : 1;
+}
+/** Reviewed original-release evidence; runtime destination and variant subtitles do not identify
+ * release. */
+[[nodiscard]] inline std::size_t content_group(const Activity& activity) noexcept {
+    const auto group = state::build_data::activities::presentation(activity.index).contentGroup;
+    return group >= 1 && group < kContent.size() ? group : kUnresolvedContent;
+}
+[[nodiscard]] inline std::size_t library_group(const Activity& row) noexcept {
+    const auto content = content_group(row);
+    return content == 17
+               ? 14
+               : content; // Content IDs 14 and 17 share navigation, retaining their launch IDs.
+}
+[[nodiscard]] inline std::size_t activity_type(const Activity& activity) noexcept {
+    const auto kind = state::build_data::activities::presentation(activity.index).experienceKind;
+    if (kind == 3) {
+        return 3;
+    }
+    if (kind == 5) {
+        return 5;
+    }
+    switch (activity.nativeType) {
+    case 0:
+    case 1:
+    case 2:
+    case 11:
+    case 52:
+        return 1;
+    case 3:
+    case 4:
+    case 5:
+    case 12:
+    case 39:
+        return 2;
+    case 7:
+        return 4;
+    case 46:
+        return 5;
+    case 9:
+    case 20:
+        return 6;
+    case 14:
+    case 15:
+    case 16:
+    case 17:
+    case 19:
+    case 21:
+    case 22:
+    case 23:
+    case 24:
+    case 25:
+    case 26:
+    case 27:
+    case 28:
+    case 29:
+    case 30:
+    case 31:
+    case 32:
+    case 33:
+    case 34:
+    case 36:
+    case 38:
+    case 41:
+    case 42:
+    case 43:
+    case 47:
+    case 48:
+    case 49:
+    case 50:
+    case 51:
+        return 7;
+    default:
+        return 8;
+    }
+}
+[[nodiscard]] inline bool contains(std::string_view text, std::string_view query) noexcept {
+    if (query.empty()) {
+        return true;
+    }
+    if (query.size() > text.size()) {
+        return false;
+    }
+    for (std::size_t i = 0; i <= text.size() - query.size(); ++i) {
+        bool match = true;
+        for (std::size_t j = 0; match && j < query.size(); ++j) {
+            match = std::tolower(static_cast<unsigned char>(text[i + j]))
+                    == std::tolower(static_cast<unsigned char>(query[j]));
+        }
+        if (match) {
+            return true;
+        }
+    }
+    return false;
+}
+[[nodiscard]] inline std::array<char, 160> title(const Activity& activity) noexcept {
+    const auto& display = state::build_data::activities::presentation(activity.index);
+    if (display.title[0] != '\0') {
+        return display.title;
+    }
+    std::array<char, 160> result{};
+    const auto name = activity.name();
+    if (name.empty()) {
+        (void)std::snprintf(
+            result.data(), result.size(), "Activity %u - no direct destination", activity.index);
+        return result;
+    }
+    (void)std::snprintf(result.data(), result.size(), "%s", activity.package.data());
+    return result;
+}
+[[nodiscard]] inline state::build_data::activities::Icon
+activity_icon(const Activity& activity) noexcept {
+    using Icon = state::build_data::activities::Icon;
+    const auto& display = state::build_data::activities::presentation(activity.index);
+    switch (content_group(activity)) {
+    case 14:
+    case 17:
+        return Icon{34};
+    case 15:
+        return Icon{33};
+    case 16:
+        return Icon{36};
+    case 21:
+        return Icon{37};
+    default:
+        break;
+    }
+    switch (display.nativeStyle) {
+    case 0x9BD15777:
+        return Icon{14};
+    case 0x0ABD9396:
+        return Icon{22};
+    case 0xE72921B0:
+        return Icon{23};
+    case 0xCCA2A4D8:
+        return Icon{24};
+    case 0xAD35C5A9:
+        return Icon{25};
+    case 0xEC411540:
+        return Icon{26};
+    case 0x6B5D0665:
+        return Icon{27};
+    default:
+        break;
+    }
+    if (display.experienceKind == 3) {
+        return Icon{14};
+    }
+    if (display.experienceKind == 5) {
+        return Icon{9};
+    }
+    if (display.experienceKind == 6) {
+        return Icon{27};
+    }
+    switch (activity.nativeType) {
+    case 0:
+    case 1:
+    case 2:
+    case 11:
+    case 52:
+        return content_group(activity) <= 6 ? static_cast<Icon>(content_group(activity)) : Icon{0};
+    case 3:
+    case 4:
+    case 5:
+    case 12:
+    case 39:
+        return Icon{7};
+    case 7:
+        return Icon{8};
+    case 9:
+        return Icon{10};
+    case 20:
+        return Icon{11};
+    case 31:
+    case 32:
+    case 36:
+    case 38:
+        return Icon{13};
+    case 35:
+        return Icon{18};
+    case 37:
+        return Icon{21};
+    case 40:
+        return Icon{19};
+    case 8:
+    case 13:
+    case 44:
+        return Icon{16};
+    case 45:
+        return Icon{15};
+    case 46:
+        return Icon{9};
+    case 53:
+        return Icon{20};
+    default:
+        return activity_type(activity) == 7 ? Icon{12} : Icon{0};
+    }
+}
+/** Presentation grouping never changes the selected public ID or native launch context. */
+[[nodiscard]] inline std::uint16_t experience_id(const Activity& row) noexcept {
+    const auto id = state::build_data::activities::presentation(row.index).experience;
+    return id < state::build_data::activities::kCapacity ? id : row.index;
+}
+[[nodiscard]] inline std::string_view experience_type(const Activity& row) noexcept {
+    const auto& p = state::build_data::activities::presentation(row.index);
+    const auto& kinds = state::build_data::activities::menu_text().kinds;
+    if (p.experienceKind > 0 && p.experienceKind < kinds.size()) {
+        return kinds[p.experienceKind].text[0] ? kinds[p.experienceKind].text.data()
+                                               : "Type unavailable";
+    }
+    return p.type[0] ? p.type.data() : "Type unavailable";
+}
+[[nodiscard]] inline int representative_score(const Activity& row, bool available) noexcept {
+    const auto& p = state::build_data::activities::presentation(row.index);
+    const auto* rule = releases::lookup(row);
+    // Numeric variant preference is independent of localized display text.
+    return (available ? 1000 : 0) + (p.title[0] ? 100 : 0)
+           + (row.nativeType == 3 || row.nativeType == 0 ? 10 : 0)
+           - (rule != nullptr ? rule->preferencePenalty : 0);
+}
+[[nodiscard]] inline state::build_data::activities::Icon
+release_icon(std::size_t content) noexcept {
+    using Icon = state::build_data::activities::Icon;
+    if (content >= 1 && content <= 6) {
+        return static_cast<Icon>(content);
+    }
+    // Authored season marks and recurring event emblems, resolved from native definitions.
+    switch (content) {
+    case 7:
+        return Icon{38};
+    case 8:
+        return Icon{39};
+    case 9:
+        return Icon{40};
+    case 10:
+        return Icon{28};
+    case 11:
+        return Icon{29};
+    case 12:
+        return Icon{30};
+    case 13:
+        return Icon{31};
+    case 14:
+    case 17:
+        return Icon{34};
+    case 15:
+        return Icon{33};
+    case 16:
+        return Icon{36};
+    case 19:
+        return Icon{35};
+    case 20:
+        return Icon{32};
+    case 21:
+        return Icon{37};
+    default:
+        return Icon{0};
+    }
+}
+[[nodiscard]] inline bool matches(const Activity& activity,
+                                  std::size_t content,
+                                  std::size_t type,
+                                  std::string_view query) noexcept {
+    if ((content != 0 && library_group(activity) != content)
+        || (type != 0 && activity_type(activity) != type)) {
+        return false;
+    }
+    const auto name = title(activity);
+    std::array<char, 40> identity{};
+    (void)std::snprintf(identity.data(), identity.size(), "%u %08X", activity.index, activity.hash);
+    return contains(activity.name(), query) || contains(name.data(), query)
+           || contains(identity.data(), query);
+}
+} // namespace sunrise::client::ui::mission_launch

+ 507 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_panel.cpp

@@ -0,0 +1,507 @@
+#include "mission_launch_panel.h"
+
+#include "../../../state/activity/forced/activity_forced_destination.h"
+#include "../../../state/build_data/runtime.h"
+#include "../../activity/mission_launch.h"
+#include "mission_launch_art.h"
+#include "mission_launch_cards.h"
+#include "mission_launch_detail_style.h"
+#include "mission_launch_manual.h"
+#include "mission_launch_model.h"
+
+namespace sunrise::client::ui::mission_launch {
+namespace {
+std::size_t g_content{};
+unsigned g_libraryKind{};
+int g_selected{-1};
+bool g_custom{};
+std::array<char, 96> g_search{};
+std::array<char, 96> g_globalSearch{};
+std::array<char, 64> g_type{};
+bool g_showUnavailable{}, g_resetScroll{};
+std::array<bool, state::build_data::activities::kCapacity> g_available{};
+std::size_t g_layoutRevision{}, g_catalogRevision{};
+std::array<std::uint16_t, state::build_data::activities::kCapacity> g_representatives{},
+    g_variantCounts{};
+void rebuild_experiences(std::span<const Activity> rows) noexcept {
+    g_representatives.fill(0xFFFF);
+    g_variantCounts.fill(0);
+    for (const auto& row : rows) {
+        if (!content_visible(content_group(row))) {
+            continue;
+        }
+        const auto id = experience_id(row);
+        ++g_variantCounts[id];
+        auto& best = g_representatives[id];
+        if (best == 0xFFFF
+            || representative_score(row, g_available[row.index])
+                   > representative_score(rows[best], g_available[best])) {
+            best = row.index;
+        }
+    }
+}
+std::size_t visible_experiences(std::span<const Activity> rows,
+                                std::span<std::uint16_t> output) noexcept {
+    std::array<bool, state::build_data::activities::kCapacity> found{};
+    std::size_t count{};
+    for (const auto& row : rows) {
+        if (!content_visible(content_group(row))) {
+            continue;
+        }
+        const auto id = experience_id(row), representative = g_representatives[id];
+        if (representative == 0xFFFF || found[id]
+            || (!g_showUnavailable && !g_available[representative])) {
+            continue;
+        }
+        if (!matches(row, g_content, 0, g_content == 0 ? g_globalSearch.data() : g_search.data())
+            || (g_type[0] && experience_type(row) != g_type.data())) {
+            continue;
+        }
+        found[id] = true;
+        output[count++] = representative;
+    }
+    return count;
+}
+void navigate(std::size_t content, int selected = -1) noexcept {
+    g_content = content;
+    g_selected = selected;
+    g_custom = false;
+    g_resetScroll = true;
+}
+void reset_library() noexcept {
+    g_globalSearch.fill(0);
+    g_type.fill(0);
+    navigate(0);
+}
+void breadcrumbs() noexcept {
+    if (ImGui::Button("Content library")) {
+        reset_library();
+    }
+    if (g_custom) {
+        ImGui::TextDisabled("Custom Launch / Launch details");
+        return;
+    }
+    if (g_content == 0) {
+        if (g_globalSearch[0]) {
+            ImGui::TextDisabled(g_selected >= 0 ? "Search results / Launch details"
+                                                : "Search results across all content");
+        }
+        return;
+    }
+    if (ImGui::GetContentRegionAvail().x
+        > ImGui::CalcTextSize(library_name(g_content)).x + 75.0F * card_scale()) {
+        ImGui::SameLine();
+        ImGui::TextDisabled("/");
+        ImGui::SameLine();
+    }
+    if (ImGui::Button(library_name(g_content))) {
+        navigate(g_content);
+    }
+    if (g_selected >= 0) {
+        ImGui::TextDisabled("Launch details");
+    }
+}
+void launch_controls(std::uint16_t index, bool manualMode) noexcept {
+    namespace launch = client::activity::mission_launch;
+    const auto rows = state::build_data::activities::entries();
+    const auto status = launch::snapshot();
+    state::activity::forced::ForcedDestination effective{};
+    state::activity::forced::snapshot(effective);
+    const bool overrideActive =
+        state::activity::forced::active(effective) || state::activity::forced::override_active();
+    const auto error = manualMode ? launch::validate_manual(manual::g_value, manual::g_validation)
+                                  : launch::ManualError::none;
+    const bool routeValid =
+        index < rows.size() && g_available[index]
+        && (!manualMode || launch::manual_transport_valid(index, manual::g_value, rows));
+    const bool disabled = !routeValid || error != launch::ManualError::none || status.busy
+                          || (!manualMode && overrideActive);
+    if (manualMode) {
+        ImGui::TextWrapped("Applies on Launch; the override remains active until cleared.");
+    }
+    if (detail::launch_button(status.busy, disabled, 43.0F * card_scale())) {
+        if (manualMode) {
+            (void)launch::request_manual(index, manual::g_value);
+        } else {
+            (void)launch::request(index);
+        }
+    }
+    if (manualMode && error != launch::ManualError::none) {
+        ImGui::TextWrapped("%s", launch::manual_error(error));
+    } else if (!routeValid) {
+        ImGui::TextWrapped(manualMode ? "Choose an available native launch activity."
+                                      : "No direct-launch scenario is available for this variant.");
+    } else if (status.busy || (status.index == index && status.status != launch::Status::idle)) {
+        ImGui::TextWrapped("%s", launch::description(status.status));
+    } else if (!overrideActive || manualMode) {
+        ImGui::TextDisabled(manualMode ? "Launch from orbit."
+                                       : "Launch from orbit. Native arrival.");
+    }
+    state::activity::forced::ForcedDestination stored{};
+    state::activity::forced::snapshot(stored);
+    if (state::activity::forced::active(stored)) {
+        ImGui::TextWrapped("Override: %.*s",
+                           static_cast<int>(stored.packageNameLength),
+                           stored.packageName.data());
+        ImGui::BeginDisabled(status.busy);
+        if (ImGui::Button("Clear active override")) {
+            state::activity::forced::clear();
+        }
+        ImGui::EndDisabled();
+    }
+}
+void show_custom() noexcept {
+    if (ImGui::Button("< Back to library")) {
+        reset_library();
+        return;
+    }
+    ImGui::Spacing();
+    detail::heading("Custom Launch");
+    ImGui::TextWrapped("Choose an activity, its launch route and arrival.");
+    ImGui::Spacing();
+    manual::custom_activity();
+    manual::transport_picker(state::build_data::activities::entries(), g_available);
+    ImGui::Spacing();
+    manual::arrival_fields();
+    ImGui::Spacing();
+    launch_controls(manual::g_transport, true);
+}
+void show_detail(const Activity& row) noexcept {
+    const auto rows = state::build_data::activities::entries();
+    const auto id = experience_id(row);
+    if (ImGui::Button("< Back to activities")) {
+        navigate(g_content);
+        return;
+    }
+    if (ImGui::GetContentRegionAvail().x > 150.0F * card_scale()) {
+        ImGui::SameLine();
+    }
+    if (ImGui::Button("Content library")) {
+        reset_library();
+        return;
+    }
+    ImGui::Spacing();
+    const auto heading = title(row);
+    const auto& display = state::build_data::activities::presentation(row.index);
+    const auto icon = art::texture(activity_icon(row));
+    if (ImGui::BeginTable(
+            "##activity_heading", icon != 0 ? 2 : 1, ImGuiTableFlags_SizingStretchProp)) {
+        if (icon != 0) {
+            ImGui::TableSetupColumn(
+                "##icon", ImGuiTableColumnFlags_WidthFixed, 66.0F * card_scale());
+            ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthStretch);
+            ImGui::TableNextColumn();
+            const auto origin = ImGui::GetCursorScreenPos();
+            const float extent = 60.0F * card_scale();
+            ImGui::Dummy({extent, extent});
+            (void)draw_icon(activity_icon(row), origin, extent);
+        }
+        ImGui::TableNextColumn();
+        detail::heading(heading.data());
+        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
+        ImGui::TextWrapped(
+            "%s  /  %s", experience_type(row).data(), library_name(content_group(row)));
+        ImGui::PopStyleColor();
+        ImGui::EndTable();
+    }
+    ImGui::Spacing();
+    ImGui::TextWrapped(
+        "%s",
+        display.description[0]
+            ? display.description.data()
+            : "No description is available in this build's installed display metadata.");
+    ImGui::Spacing();
+    ImGui::Separator();
+    ImGui::Spacing();
+    manual::follow_activity(row);
+    if (ImGui::Checkbox("Manual Mode", &manual::g_enabled) && manual::g_enabled) {
+        manual::select_transport(row.index);
+    }
+    if (g_variantCounts[id] > 1) {
+        std::array<char, 240> current{};
+        const auto& p = state::build_data::activities::presentation(row.index);
+        (void)std::snprintf(
+            current.data(), current.size(), "Variant: %s (#%u)", p.type.data(), row.index);
+        if (ImGui::GetContentRegionAvail().x >= 430.0F * card_scale()) {
+            ImGui::SameLine(155.0F * card_scale());
+        }
+        ImGui::SetNextItemWidth(-1.0F);
+        if (ImGui::BeginCombo("##launch_variant", current.data())) {
+            for (const auto& variant : rows) {
+                if (experience_id(variant) != id || !content_visible(content_group(variant))) {
+                    continue;
+                }
+                const auto& vp = state::build_data::activities::presentation(variant.index);
+                std::array<char, 260> label{};
+                (void)std::snprintf(label.data(),
+                                    label.size(),
+                                    "#%u | %s | %s%s",
+                                    variant.index,
+                                    vp.type.data(),
+                                    title(variant).data(),
+                                    g_available[variant.index] ? "" : " (unavailable)");
+                if (ImGui::Selectable(label.data(), variant.index == row.index)) {
+                    g_selected = variant.index;
+                }
+            }
+            ImGui::EndCombo();
+        }
+        if (g_selected != row.index) {
+            return;
+        } // Never launch a stale selection in the same frame.
+    }
+    ImGui::Spacing();
+    manual::arrival_fields();
+    launch_controls(manual::g_enabled ? manual::g_transport : row.index, manual::g_enabled);
+    ImGui::Spacing();
+    if (ImGui::TreeNode("Activity information")) {
+        ImGui::TextWrapped("First released: %s", kContent[content_group(row)]);
+        ImGui::TextWrapped("%s", row.package[0] ? row.package.data() : "No direct destination");
+        ImGui::Text("Activity #%u  |  %08X", row.index, row.hash);
+        if (display.expansion[0]) {
+            ImGui::TextWrapped("Installed variant label: %s", display.expansion.data());
+        }
+        ImGui::TextWrapped("Separate activity IDs preserve authored variants. An extracted "
+                           "scenario does not guarantee complete mission scripting.");
+        if (!display.title[0]) {
+            ImGui::TextWrapped(
+                "The native display name is unavailable; the exact package identifier is shown.");
+        }
+        ImGui::TreePop();
+    }
+}
+} // namespace
+void draw() noexcept {
+    const detail::Style style{};
+    namespace catalog = state::build_data::activities;
+    const auto rows = catalog::entries();
+    const auto revision = state::build_data::scenario_layout_count();
+    if (revision != g_layoutRevision || rows.size() != g_catalogRevision) {
+        g_available.fill(false);
+        for (const auto& row : rows) {
+            state::build_data::scenarios::Definition layout{};
+            g_available[row.index] =
+                !row.name().empty() && state::build_data::find_scenario_layout(row.name(), layout);
+        }
+        rebuild_experiences(rows);
+        g_layoutRevision = revision;
+        g_catalogRevision = rows.size();
+    }
+    if (!rows.empty()) {
+        if (g_custom) {
+            show_custom();
+            return;
+        }
+        if (g_selected >= 0 && static_cast<std::size_t>(g_selected) < rows.size()) {
+            show_detail(rows[static_cast<std::size_t>(g_selected)]);
+            return;
+        }
+    }
+    ImGui::TextWrapped("%s",
+                       g_custom         ? "Choose a custom activity and manual arrival."
+                       : g_content == 0 ? "Explore expansions, seasons and recurring events."
+                       : g_selected < 0 ? "Choose an activity to review its details and launch."
+                                        : "Review your activity and launch from orbit.");
+    breadcrumbs();
+    ImGui::Spacing();
+    if (rows.empty()) {
+        ImGui::TextWrapped(catalog::extraction_failed()
+                               ? "The optional installed activity catalog could not be read. The "
+                                 "Director remains available."
+                               : "Reading installed Director metadata and artwork...");
+        return;
+    }
+    const float scale = card_scale();
+    if (g_content == 0) {
+        ImGui::SetNextItemWidth(-1.0F);
+        if (ImGui::InputTextWithHint("##all_content_search",
+                                     "Search activities across all content...",
+                                     g_globalSearch.data(),
+                                     g_globalSearch.size())) {
+            g_libraryKind = 0;
+            g_type.fill(0);
+            g_resetScroll = true;
+        }
+        if (g_globalSearch[0] && ImGui::Button("Clear search")) {
+            g_globalSearch.fill(0);
+            g_type.fill(0);
+            g_resetScroll = true;
+        }
+        if (!g_globalSearch[0]) {
+            ImGui::TextWrapped("Activities are grouped by their first release. Equivalent launch "
+                               "variants share one card; different activity types stay separate.");
+            ImGui::Spacing();
+            constexpr std::array<const char*, 4> kinds{
+                "All content", "Expansions", "Seasons", "Events"};
+            const auto kindColumns = static_cast<unsigned>(
+                (std::max)(1,
+                           static_cast<int>(ImGui::GetContentRegionAvail().x
+                                            / (100.0F * scale + ImGui::GetStyle().ItemSpacing.x))));
+            for (unsigned kind = 0; kind < kinds.size(); ++kind) {
+                if (kind % kindColumns != 0) {
+                    ImGui::SameLine();
+                }
+                if (ImGui::Selectable(kinds[kind], g_libraryKind == kind, 0, {100.0F * scale, 0})) {
+                    g_libraryKind = kind;
+                }
+            }
+            ImGui::Spacing();
+            ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {6.0F * scale, 6.0F * scale});
+            const int columns = grid_columns(ImGui::GetContentRegionAvail().x);
+            if (ImGui::BeginTable("##dlc_grid", columns, ImGuiTableFlags_SizingStretchSame)) {
+                if (g_libraryKind == 0) {
+                    ImGui::TableNextColumn();
+                    ImGui::PushID("custom_launch");
+                    if (activity_card("Custom Launch",
+                                      "Manual activity and arrival",
+                                      "Choose Bubble / Slice / Spawn",
+                                      state::build_data::activities::Icon{10},
+                                      false)) {
+                        manual::start_custom();
+                        g_custom = true;
+                    }
+                    ImGui::PopID();
+                }
+                for (std::size_t content = 1; content < kContent.size(); ++content) {
+                    if (!library_card_visible(content)
+                        || (g_libraryKind != 0 && library_kind(content) != g_libraryKind)) {
+                        continue;
+                    }
+                    ImGui::TableNextColumn();
+                    std::size_t count{};
+                    for (const auto index : g_representatives) {
+                        if (index != 0xFFFF && library_group(rows[index]) == content
+                            && g_available[index]) {
+                            ++count;
+                        }
+                    }
+                    std::array<char, 64> footer{};
+                    (void)std::snprintf(
+                        footer.data(), footer.size(), "%zu available activities", count);
+                    ImGui::PushID(static_cast<int>(content));
+                    if (activity_card(library_name(content),
+                                      content == kUnresolvedContent ? "Release not established"
+                                      : library_kind(content) == 3  ? "Recurring event"
+                                      : library_kind(content) == 2  ? "Content season"
+                                                                    : "Expansion / campaign",
+                                      footer.data(),
+                                      release_icon(content),
+                                      false)) {
+                        g_type.fill(0);
+                        g_search.fill(0);
+                        navigate(content);
+                    }
+                    ImGui::PopID();
+                }
+                ImGui::EndTable();
+            }
+            ImGui::PopStyleVar();
+            return;
+        }
+    }
+    if (g_content != 0) {
+        if (ImGui::Button("< Back to library")) {
+            reset_library();
+        }
+        ImGui::Spacing();
+        ImGui::SetNextItemWidth(-1.0F);
+        if (ImGui::InputTextWithHint("##mission_search",
+                                     "Search name, package, ID or hash...",
+                                     g_search.data(),
+                                     g_search.size())) {
+            g_resetScroll = true;
+        }
+    }
+    ImGui::SetNextItemWidth(-1.0F);
+    if (ImGui::BeginCombo("##mission_type", g_type[0] ? g_type.data() : "All activity types")) {
+        if (ImGui::Selectable("All activity types", !g_type[0])) {
+            g_type.fill(0);
+            g_resetScroll = true;
+        }
+        std::array<std::uint16_t, 256> unique{};
+        std::size_t count{};
+        for (const auto& row : rows) {
+            if (!content_visible(content_group(row))
+                || (g_content != 0 && library_group(row) != g_content)) {
+                continue;
+            }
+            const auto type = experience_type(row);
+            bool exists{};
+            for (std::size_t i = 0; i < count; ++i) {
+                exists |= experience_type(rows[unique[i]]) == type;
+            }
+            if (exists || count == unique.size()) {
+                continue;
+            }
+            unique[count++] = row.index;
+            if (ImGui::Selectable(type.data(), std::string_view(g_type.data()) == type)) {
+                (void)std::snprintf(g_type.data(), g_type.size(), "%s", type.data());
+                g_resetScroll = true;
+            }
+        }
+        ImGui::EndCombo();
+    }
+    if (ImGui::Checkbox("Include unavailable scenarios", &g_showUnavailable)) {
+        g_resetScroll = true;
+    }
+    std::array<std::uint16_t, catalog::kCapacity> visible{};
+    const auto shown = visible_experiences(rows, visible);
+    if (g_content == 20) {
+        ImGui::TextWrapped("This event is present in the native metadata. Its public activity "
+                           "catalog has no separate launch entry.");
+    }
+    ImGui::TextDisabled("%zu activities  |  Select a card for details", shown);
+    const float height =
+        (std::max)(150.0F * scale, ImGui::GetContentRegionAvail().y - 4.0F * scale);
+    ImGui::BeginChild("mission_activity_grid", {0, height}, ImGuiChildFlags_None);
+    if (g_resetScroll) {
+        ImGui::SetScrollY(0);
+        g_resetScroll = false;
+    }
+    ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {6.0F * scale, 6.0F * scale});
+    const int columns = grid_columns(ImGui::GetContentRegionAvail().x);
+    if (ImGui::BeginTable("##activities", columns, ImGuiTableFlags_SizingStretchSame)) {
+        ImGuiListClipper clipper;
+        clipper.Begin((static_cast<int>(shown) + columns - 1) / columns);
+        while (clipper.Step()) {
+            for (int y = clipper.DisplayStart; y < clipper.DisplayEnd; ++y) {
+                ImGui::TableNextRow();
+                for (int x = 0; x < columns; ++x) {
+                    const auto index = static_cast<std::size_t>(y * columns + x);
+                    if (index >= shown) {
+                        break;
+                    }
+                    ImGui::TableSetColumnIndex(x);
+                    const auto& row = rows[visible[index]];
+                    const auto heading = title(row);
+                    std::array<char, 64> footer{};
+                    (void)std::snprintf(footer.data(),
+                                        footer.size(),
+                                        "%u variant(s)  |  %s",
+                                        static_cast<unsigned>(g_variantCounts[experience_id(row)]),
+                                        g_available[row.index] ? "View launch details"
+                                                               : "Scenario unavailable");
+                    ImGui::PushID(row.index);
+                    if (activity_card(heading.data(),
+                                      experience_type(row).data(),
+                                      footer.data(),
+                                      activity_icon(row),
+                                      false,
+                                      g_available[row.index])) {
+                        navigate(g_content, row.index);
+                    }
+                    ImGui::PopID();
+                }
+            }
+        }
+        ImGui::EndTable();
+    }
+    ImGui::PopStyleVar();
+    if (shown == 0) {
+        ImGui::TextWrapped(
+            "No matching activities. Clear the search or choose another activity type. Unavailable "
+            "playlist entries can be shown with the checkbox above.");
+    }
+    ImGui::EndChild();
+}
+} // namespace sunrise::client::ui::mission_launch

+ 6 - 0
Sunrise/src/client/ui/mission_launch/mission_launch_panel.h

@@ -0,0 +1,6 @@
+#pragma once
+
+namespace sunrise::client::ui::mission_launch {
+/** Draws the launcher inside the active Core UI frame, without issuing native calls. */
+void draw() noexcept;
+} // namespace sunrise::client::ui::mission_launch

+ 9 - 2
Sunrise/src/client/ui/runtime/client_ui_module_runtime.cpp

@@ -4,6 +4,7 @@
 
 
 #include "../../../core/ui/modules/registry/ui_module_registry.h"
 #include "../../../core/ui/modules/registry/ui_module_registry.h"
 #include "../../../core/ui/modules/ui_module_descriptor.h"
 #include "../../../core/ui/modules/ui_module_descriptor.h"
+#include "../mission_launch/mission_launch_panel.h"
 #include "../movement/movement_panel.h"
 #include "../movement/movement_panel.h"
 #include "../player/player_panel.h"
 #include "../player/player_panel.h"
 
 
@@ -20,21 +21,27 @@ constexpr std::string_view kPlayerDisplayName = "Player";
 
 
 core::ui::modules::registry::PageRegistration g_movementPage;
 core::ui::modules::registry::PageRegistration g_movementPage;
 core::ui::modules::registry::PageRegistration g_playerPage;
 core::ui::modules::registry::PageRegistration g_playerPage;
+core::ui::modules::registry::PageRegistration g_activityLauncherPage;
 
 
 } // namespace
 } // namespace
 
 
-/** @return True when both Client modules own their Core UI registry slots. */
+/** @return True when the Client modules own their Core UI registry slots. */
 bool initialize() noexcept {
 bool initialize() noexcept {
     // Registered after movement, which is the order the menu lists them in.
     // Registered after movement, which is the order the menu lists them in.
     const bool movementOwned = g_movementPage.acquire(
     const bool movementOwned = g_movementPage.acquire(
         core::ui::modules::Owner::client, kMovementStableId, kMovementDisplayName, &movement::draw);
         core::ui::modules::Owner::client, kMovementStableId, kMovementDisplayName, &movement::draw);
     const bool playerOwned = g_playerPage.acquire(
     const bool playerOwned = g_playerPage.acquire(
         core::ui::modules::Owner::client, kPlayerStableId, kPlayerDisplayName, &player::draw);
         core::ui::modules::Owner::client, kPlayerStableId, kPlayerDisplayName, &player::draw);
-    return movementOwned && playerOwned;
+    const bool launcherOwned = g_activityLauncherPage.acquire(core::ui::modules::Owner::client,
+                                                              "client.mission_launch",
+                                                              "Activity Launcher",
+                                                              &mission_launch::draw);
+    return movementOwned && playerOwned && launcherOwned;
 }
 }
 
 
 /** Removes the Client modules from the Core UI registry. */
 /** Removes the Client modules from the Core UI registry. */
 void shutdown() noexcept {
 void shutdown() noexcept {
+    g_activityLauncherPage.release();
     g_playerPage.release();
     g_playerPage.release();
     g_movementPage.release();
     g_movementPage.release();
 }
 }

+ 190 - 0
Sunrise/src/middleware/content/packages/tables/activity_presentation_table.h

@@ -0,0 +1,190 @@
+#pragma once
+#include "activity_table.h"
+
+namespace sunrise::middleware::content::packages::tables::activities {
+/** Bank ordinal of a string reference that names no bank. */
+inline constexpr std::uint32_t kNoBank = 0xFFFFU;
+/** Category of a display row that names none. */
+inline constexpr std::uint16_t kNoCategory = 0xFFFFU;
+/** One display index row: the definition hash, then a self-relative pointer to the record. */
+inline constexpr std::size_t kDisplayRowStride = 16;
+inline constexpr std::size_t kDisplayRecordField = 8;
+/** The display record starts with a self-relative pointer to its text block. */
+inline constexpr std::size_t kDisplayTextField = 0;
+inline constexpr std::size_t kDisplayCategoryOffset = 8;
+/** String references inside the text block, and the Director style tag after them. */
+inline constexpr std::size_t kTextTitleOffset = 4;
+inline constexpr std::size_t kTextDescriptionOffset = 12;
+inline constexpr std::size_t kTextExpansionOffset = 20;
+inline constexpr std::size_t kTextStyleOffset = 32;
+/** Client activity-type rows, with the type name reference after the row hash. */
+inline constexpr std::size_t kTypeRowStride = 128;
+inline constexpr std::size_t kTypeNameOffset = 4;
+/** Rows the type table may declare; the type byte on an activity record is one of them. */
+inline constexpr std::size_t kTypeRowLimit = 256;
+
+/** Bank string hashes are packed u32 rows. */
+inline constexpr std::size_t kBankHashStride = 4;
+/** The language blob's combination array descriptor, and its rows. */
+inline constexpr std::size_t kLanguageCombinationDescriptor = 72;
+inline constexpr std::size_t kCombinationStride = 16;
+/** A combination starts with a self-relative pointer to its parts, then the part count. */
+inline constexpr std::size_t kCombinationPartsField = 0;
+inline constexpr std::size_t kCombinationCountOffset = 8;
+/** Parts one string may combine; the widest installed string is far below it. */
+inline constexpr std::int64_t kCombinationPartLimit = 32;
+/** A part: a self-relative pointer to its bytes, their count, and the per-byte shift. */
+inline constexpr std::size_t kPartStride = 32;
+inline constexpr std::size_t kPartDataField = 8;
+inline constexpr std::size_t kPartByteCountOffset = 20;
+inline constexpr std::size_t kPartShiftOffset = 24;
+/** UTF-8 lead-byte thresholds for two, three and four byte sequences, and the first invalid lead.
+ */
+inline constexpr std::uint8_t kUtf8TwoByteLead = 0xC0;
+inline constexpr std::uint8_t kUtf8ThreeByteLead = 0xE0;
+inline constexpr std::uint8_t kUtf8FourByteLead = 0xF0;
+inline constexpr std::uint8_t kUtf8InvalidLead = 0xF5;
+
+struct StringRef {
+    std::uint32_t bank{kNoBank}, hash{kNoPlugSource};
+};
+struct DisplayRefs {
+    StringRef title{}, description{}, type{}, expansion{};
+    std::uint16_t category{kNoCategory};
+    std::uint32_t style{kNoPlugSource};
+};
+/**
+ * Reads the string references of every activity's display row.
+ * Client slot 3 uses the exact same ordinal AND authored identity as public slot 4.
+ * @param display The client activity blob.
+ * @param types The client activity-type blob.
+ * @param rows The public activity rows, in ordinal order.
+ * @param output Receives one entry per row.
+ * @return False when a display row disagrees with its public row.
+ */
+[[nodiscard]] inline bool
+display_refs(std::span<const std::byte> display,
+             std::span<const std::byte> types,
+             std::span<const state::build_data::activities::Definition> rows,
+             std::span<DisplayRefs> output) noexcept {
+    Array a{}, t{};
+    if (!find_array_at(display, kTableArrayDescriptor, a) || a.count != rows.size()
+        || output.size() < rows.size() || !find_array_at(types, kTableArrayDescriptor, t)
+        || t.count > kTypeRowLimit || a.dataOffset > display.size()
+        || a.count > (display.size() - a.dataOffset) / kDisplayRowStride
+        || t.dataOffset > types.size()
+        || t.count > (types.size() - t.dataOffset) / kTypeRowStride) {
+        return false;
+    }
+    for (std::size_t i = 0; i < rows.size(); ++i) {
+        const auto entry = a.dataOffset + i * kDisplayRowStride;
+        std::uint32_t hash{};
+        std::size_t record{}, text{};
+        output[i] = {};
+        if (!read(display, entry, hash) || hash != rows[i].hash
+            || !relative(display, entry + kDisplayRecordField, record)
+            || !relative(display, record + kDisplayTextField, text)
+            || !read(display, text + kTextTitleOffset, output[i].title)
+            || !read(display, text + kTextDescriptionOffset, output[i].description)
+            || !read(display, text + kTextExpansionOffset, output[i].expansion)
+            || !read(display, record + kDisplayCategoryOffset, output[i].category)
+            || !read(display, text + kTextStyleOffset, output[i].style)) {
+            return false;
+        }
+        if (rows[i].nativeType < t.count
+            && !read(types,
+                     t.dataOffset + rows[i].nativeType * kTypeRowStride + kTypeNameOffset,
+                     output[i].type)) {
+            return false;
+        }
+    }
+    return true;
+}
+/**
+ * Decodes one bank-scoped English string.
+ * A hash never resolves against another bank. Oversize or malformed text is unavailable, not
+ * truncated.
+ * @param container The bank's string-hash blob.
+ * @param language The bank's English language blob.
+ * @param hash The string hash to decode.
+ * @param output Receives the text and its null; empty on failure.
+ * @return True when the whole string fit.
+ */
+[[nodiscard]] inline bool localized_string(std::span<const std::byte> container,
+                                           std::span<const std::byte> language,
+                                           std::uint32_t hash,
+                                           std::span<char> output) noexcept {
+    if (output.empty()) {
+        return false;
+    }
+    output[0] = '\0';
+    Array hashes{}, combinations{};
+    if (hash == kNoPlugSource || !find_array_at(container, kTableArrayDescriptor, hashes)
+        || !find_array_at(language, kLanguageCombinationDescriptor, combinations)
+        || hashes.count != combinations.count || hashes.dataOffset > container.size()
+        || hashes.count > (container.size() - hashes.dataOffset) / kBankHashStride
+        || combinations.dataOffset > language.size()
+        || combinations.count > (language.size() - combinations.dataOffset) / kCombinationStride) {
+        return false;
+    }
+    std::size_t ordinal = static_cast<std::size_t>(hashes.count);
+    for (std::size_t i = 0; i < hashes.count; ++i) {
+        std::uint32_t value{};
+        if (read(container, hashes.dataOffset + i * kBankHashStride, value) && value == hash) {
+            ordinal = i;
+            break;
+        }
+    }
+    if (ordinal == hashes.count) {
+        return false;
+    }
+    const auto combination = combinations.dataOffset + ordinal * kCombinationStride;
+    std::int64_t parts{};
+    std::size_t first{}, written{};
+    if (!read(language, combination + kCombinationCountOffset, parts) || parts < 0
+        || parts > kCombinationPartLimit) {
+        return false;
+    }
+    if (parts == 0) {
+        return true;
+    }
+    if (!relative(language, combination + kCombinationPartsField, first)
+        || static_cast<std::uint64_t>(parts) > (language.size() - first) / kPartStride) {
+        return false;
+    }
+    for (std::size_t p = 0; p < static_cast<std::size_t>(parts); ++p) {
+        const auto part = first + p * kPartStride;
+        std::size_t data{};
+        std::uint16_t bytes{}, shift{};
+        if (!relative(language, part + kPartDataField, data)
+            || !read(language, part + kPartByteCountOffset, bytes)
+            || !read(language, part + kPartShiftOffset, shift) || bytes > language.size() - data
+            || bytes >= output.size() - written) {
+            output[0] = '\0';
+            return false;
+        }
+        for (std::size_t i = 0; i < bytes;) {
+            const auto c = static_cast<std::uint8_t>(language[data + i]);
+            const std::size_t width = c < kUtf8TwoByteLead     ? 1
+                                      : c < kUtf8ThreeByteLead ? 2
+                                      : c < kUtf8FourByteLead  ? 3
+                                                               : 4;
+            if (width > bytes - i || c >= kUtf8InvalidLead) {
+                output[0] = '\0';
+                return false;
+            }
+            // The shift applies to the last byte of each sequence only.
+            for (std::size_t j = 0; j < width; ++j) {
+                auto decoded = static_cast<std::uint8_t>(language[data + i + j]);
+                if (j + 1 == width) {
+                    decoded = static_cast<std::uint8_t>(decoded + shift);
+                }
+                output[written++] = static_cast<char>(decoded);
+            }
+            i += width;
+        }
+    }
+    output[written] = '\0';
+    return true;
+}
+} // namespace sunrise::middleware::content::packages::tables::activities

+ 118 - 0
Sunrise/src/middleware/content/packages/tables/activity_table.h

@@ -0,0 +1,118 @@
+#pragma once
+#include <cstring>
+#include <limits>
+
+#include "../../../../state/build_data/activities/activity_catalog.h"
+#include "definition_index_table.h"
+
+namespace sunrise::middleware::content::packages::tables::activities {
+/** Public activity index, NOT the package-definition index in root slot 29. */
+inline constexpr std::size_t kRootSlot = 4;
+inline constexpr std::uint32_t kRowClass = 0x808076FCU;
+/** One index row: the definition hash, then a self-relative pointer to the record. */
+inline constexpr std::size_t kIndexRowStride = 16;
+inline constexpr std::size_t kIndexRecordField = 8;
+/** Self-relative pointer to the internal package name, or zero when the row has none. */
+inline constexpr std::size_t kRecordNameField = 0x68;
+/** Activity type, a row of the activity type table. */
+inline constexpr std::size_t kRecordTypeOffset = 0xDA;
+inline constexpr std::size_t kRecordGameplaySettingsOffset = 0xDC;
+/** Destination, a row of the destination table. */
+inline constexpr std::size_t kRecordDestinationOffset = 0xE0;
+/** Every field above sits inside this many bytes of a record. */
+inline constexpr std::size_t kRecordReadBytes = 0xE4;
+
+template <class T>
+[[nodiscard]] bool read(std::span<const std::byte> bytes, std::size_t offset, T& value) noexcept {
+    if (offset > bytes.size() || sizeof(T) > bytes.size() - offset) {
+        return false;
+    }
+    std::memcpy(&value, bytes.data() + offset, sizeof(T));
+    return true;
+}
+/**
+ * Resolves a self-relative field.
+ * @param field Offset of the 8-byte signed delta.
+ * @param target Receives the resolved offset.
+ * @return False when the delta is zero or the target falls outside the blob.
+ */
+[[nodiscard]] inline bool
+relative(std::span<const std::byte> bytes, std::size_t field, std::size_t& target) noexcept {
+    std::int64_t delta{};
+    if (!read(bytes, field, delta) || delta == 0
+        || field > static_cast<std::size_t>((std::numeric_limits<std::int64_t>::max)())) {
+        return false;
+    }
+    const auto base = static_cast<std::int64_t>(field);
+    if (delta < -base || delta > (std::numeric_limits<std::int64_t>::max)() - base) {
+        return false;
+    }
+    target = static_cast<std::size_t>(base + delta);
+    return target < bytes.size();
+}
+/**
+ * Parses the dense public activity table.
+ * @param bytes The table blob.
+ * @param output Receives one row per ordinal.
+ * @param count Receives the row count.
+ * @return False when a row fails its identity check or a name is malformed.
+ */
+[[nodiscard]] inline bool decode(std::span<const std::byte> bytes,
+                                 std::span<state::build_data::activities::Definition> output,
+                                 std::size_t& count) noexcept {
+    count = 0;
+    Array array{};
+    if (!find_array_at(bytes, kTableArrayDescriptor, array) || array.elementClass != kRowClass
+        || array.count > output.size() || array.count > state::build_data::activities::kCapacity
+        || array.dataOffset > bytes.size()
+        || array.count > (bytes.size() - array.dataOffset) / kIndexRowStride) {
+        return false;
+    }
+    for (std::size_t i = 0; i < array.count; ++i) {
+        state::build_data::activities::Definition row{};
+        row.index = static_cast<std::uint16_t>(i);
+        const auto entry = array.dataOffset + i * kIndexRowStride;
+        std::size_t record{};
+        std::uint32_t identity{};
+        // The record repeats the index row's hash; both copies must agree.
+        if (!read(bytes, entry, row.hash) || row.hash == 0
+            || !relative(bytes, entry + kIndexRecordField, record)
+            || bytes.size() - record < kRecordReadBytes || !read(bytes, record, identity)
+            || identity != row.hash
+            || !read(bytes, record + kRecordGameplaySettingsOffset, row.gameplaySettingsHash)
+            || !read(bytes, record + kRecordTypeOffset, row.nativeType)
+            || !read(bytes, record + kRecordDestinationOffset, row.destination)) {
+            return false;
+        }
+        std::int64_t nameDelta{};
+        if (!read(bytes, record + kRecordNameField, nameDelta)) {
+            return false;
+        }
+        if (nameDelta != 0) {
+            std::size_t name{};
+            if (!relative(bytes, record + kRecordNameField, name)) {
+                return false;
+            }
+            bool terminated = false;
+            for (std::size_t n = 0; n < row.package.size() && n < bytes.size() - name; ++n) {
+                const auto c = static_cast<char>(bytes[name + n]);
+                if (c == '\0') {
+                    terminated = true;
+                    break;
+                }
+                if (n + 1 == row.package.size()
+                    || (c != '_' && (c < 'a' || c > 'z') && (c < '0' || c > '9'))) {
+                    return false;
+                }
+                row.package[n] = c;
+            }
+            if (!terminated) {
+                return false;
+            }
+        }
+        output[i] = row;
+    }
+    count = static_cast<std::size_t>(array.count);
+    return count != 0;
+}
+} // namespace sunrise::middleware::content::packages::tables::activities

+ 0 - 340
Sunrise/src/server/ui/activity_override/activity_override_panel.cpp

@@ -1,340 +0,0 @@
-/**
- * The activity override module's interface. Nothing here is saved. The process starts with an
- * empty selection and the switch off, and the clear action puts it back.
- */
-
-#include "activity_override_panel.h"
-
-#include <algorithm>
-#include <array>
-#include <cstdio>
-#include <imgui.h>
-#include <span>
-#include <string_view>
-
-#include "../../../core/ui/components/label/ui_label_component.h"
-#include "../../../core/ui/components/picker/ui_picker_component.h"
-#include "../../../core/ui/components/section/ui_section_component.h"
-#include "../../../core/ui/components/toggle/ui_toggle_component.h"
-#include "../../../state/activity/forced/activity_forced_destination.h"
-#include "activity_override_lists.h"
-
-namespace sunrise::server::ui::activity_override {
-namespace {
-
-namespace forced = state::activity::forced;
-namespace label = core::ui::components::label;
-namespace picker = core::ui::components::picker;
-
-/** Preview shown by a picker with nothing chosen. */
-constexpr char kUnset[] = "none";
-/** Preview shown by a picker whose list is empty. */
-constexpr char kEmptyList[] = "nothing to pick";
-/** Spawn row that names no set, which forces the default one. */
-constexpr char kDefaultSpawnRow[] = "none  (client picks)";
-/** Room for the longest status line this module builds. */
-constexpr std::size_t kStatusCapacity = 64;
-/** Widest picker list this module draws, so no list is silently cut short. */
-constexpr std::size_t kItemCapacity =
-    (std::max)(state::build_data::scenarios::kDefinitionCapacity, kSpawnCapacity + 1);
-
-/** Row each picker is highlighting. These follow the published selection, not the other way. */
-std::size_t g_activityRow{kNoRow};
-std::size_t g_definitionRow{kNoRow};
-std::size_t g_bubbleRow{kNoRow};
-std::size_t g_sliceRow{kNoRow};
-std::size_t g_spawnRow{kNoRow};
-
-/** @return The published selection's destination name as a bounded view. */
-[[nodiscard]] std::string_view name_of(const forced::ForcedDestination& value) noexcept {
-    return {value.packageName.data(), value.packageNameLength};
-}
-
-/**
- * Draws one labelled picker over a list of row labels.
- * @param id Stable Dear ImGui scope ID.
- * @param count Rows in use.
- * @param preview Text shown while the popup is closed.
- * @param row Highlighted row, updated on a pick.
- * @return True when a row was picked this frame.
- */
-[[nodiscard]] bool row_picker(const char* id,
-                              const char* caption,
-                              const Label* labels,
-                              std::size_t count,
-                              const char* preview,
-                              std::size_t& row) noexcept {
-    label::align();
-    ImGui::TextUnformatted(caption);
-    std::array<picker::Item, kItemCapacity> items{};
-    const std::size_t shown = (std::min)(count, items.size());
-    for (std::size_t index = 0; index < shown; ++index) {
-        items[index].label = labels[index].data();
-    }
-    return picker::control(
-        id, shown != 0 ? preview : kEmptyList, std::span(items).first(shown), row);
-}
-
-/** Rebuilds every derived list. @param value Published selection. */
-void follow_destination(const forced::ForcedDestination& value, Lists& rows) noexcept {
-    refresh_destination(rows, name_of(value));
-    refresh_definitions(rows, name_of(value));
-    g_bubbleRow = kNoRow;
-    g_sliceRow = kNoRow;
-    g_spawnRow = kNoRow;
-    g_definitionRow = kNoRow;
-}
-
-/**
- * Draws the destination picker.
- * @param value Published selection, updated on a pick.
- * @return True when the selection changed.
- */
-[[nodiscard]] bool draw_activity(forced::ForcedDestination& value, Lists& rows) noexcept {
-    std::array<char, kLabelCapacity> preview{};
-    const std::string_view name = name_of(value);
-    (void)std::snprintf(
-        preview.data(), preview.size(), "%.*s", static_cast<int>(name.size()), name.data());
-    if (!row_picker("activity",
-                    "Activity",
-                    rows.activities.data(),
-                    rows.activityCount,
-                    name.empty() ? kUnset : preview.data(),
-                    g_activityRow)) {
-        return false;
-    }
-    const Label& picked = rows.activities[g_activityRow];
-    const std::string_view text(picked.data());
-    value.packageName = {};
-    std::copy_n(
-        text.begin(), (std::min)(text.size(), value.packageName.size()), value.packageName.begin());
-    value.packageNameLength =
-        static_cast<std::uint8_t>((std::min)(text.size(), value.packageName.size()));
-    // The three lists below name rows of this destination only, so they go with it.
-    value.hasBubble = false;
-    value.bubble = 0;
-    value.hasSliceSet = false;
-    value.sliceSet = 0;
-    value.hasSpawnSetHash = false;
-    value.spawnSetHash = 0;
-    value.hasActivityIndex = false;
-    value.activityIndex = 0;
-    follow_destination(value, rows);
-    // One definition needs no choice, and leaving it unpicked is what stops the SDK binding.
-    if (rows.definitionCount == 1) {
-        g_definitionRow = 0;
-        value.activityIndex = rows.definitionIndices[0];
-        value.hasActivityIndex = true;
-    }
-    return true;
-}
-
-/**
- * Draws the definition picker, which names which of the destination's activities to bind.
- * @param value Published selection, updated on a pick.
- * @return True when the selection changed.
- */
-[[nodiscard]] bool draw_definition(forced::ForcedDestination& value, Lists& rows) noexcept {
-    const char* const preview = value.hasActivityIndex && g_definitionRow < rows.definitionCount
-                                    ? rows.definitions[g_definitionRow].data()
-                                    : kUnset;
-    if (!row_picker("definition",
-                    "Activity",
-                    rows.definitions.data(),
-                    rows.definitionCount,
-                    preview,
-                    g_definitionRow)) {
-        return false;
-    }
-    value.activityIndex = rows.definitionIndices[g_definitionRow];
-    value.hasActivityIndex = true;
-    return true;
-}
-
-/**
- * Draws the bubble picker, which also seeds the slice set that bubble starts on.
- * @param value Published selection, updated on a pick.
- * @return True when the selection changed.
- */
-[[nodiscard]] bool draw_bubble(forced::ForcedDestination& value, Lists& rows) noexcept {
-    const char* const preview = value.hasBubble && g_bubbleRow < rows.bubbleCount
-                                    ? rows.bubbles[g_bubbleRow].data()
-                                    : kUnset;
-    if (!row_picker(
-            "bubble", "Bubble", rows.bubbles.data(), rows.bubbleCount, preview, g_bubbleRow)) {
-        return false;
-    }
-    value.bubble = rows.bubbleOrdinals[g_bubbleRow];
-    value.hasBubble = true;
-    // The slice sets belong to the bubble, so the list is rebuilt for the one just picked.
-    refresh_bubble(rows, value.bubble);
-    // A bubble's first slice set is the one an ordinary arrival there uses, so picking a bubble
-    // fills the slice set in instead of leaving the selection unfinished.
-    g_sliceRow = kNoRow;
-    value.hasSliceSet = false;
-    value.sliceSet = 0;
-    if (rows.sliceCount != 0) {
-        g_sliceRow = 0;
-        value.sliceSet = rows.sliceValues[0];
-        value.hasSliceSet = true;
-    }
-    g_spawnRow = kNoRow;
-    value.hasSpawnSetHash = false;
-    value.spawnSetHash = 0;
-    return true;
-}
-
-/**
- * Draws the slice-set picker.
- * @param value Published selection, updated on a pick.
- * @return True when the selection changed.
- */
-[[nodiscard]] bool draw_slice(forced::ForcedDestination& value, Lists& rows) noexcept {
-    const char* const preview =
-        value.hasSliceSet && g_sliceRow < rows.sliceCount ? rows.slices[g_sliceRow].data() : kUnset;
-    if (!row_picker(
-            "slice", "Slice set", rows.slices.data(), rows.sliceCount, preview, g_sliceRow)) {
-        return false;
-    }
-    value.sliceSet = rows.sliceValues[g_sliceRow];
-    value.hasSliceSet = true;
-    return true;
-}
-
-/**
- * Draws the spawn-set picker, whose first row names no set.
- * @param value Published selection, updated on a pick.
- * @return True when the selection changed.
- */
-[[nodiscard]] bool draw_spawn(forced::ForcedDestination& value, Lists& rows) noexcept {
-    // The leading row is what makes the spawn set optional. It forces the default set instead.
-    static std::array<Label, kSpawnCapacity + 1> labels{};
-    labels[0] = {};
-    std::copy_n(
-        std::string_view(kDefaultSpawnRow).begin(), sizeof kDefaultSpawnRow - 1, labels[0].begin());
-    const std::size_t count = (std::min)(rows.spawnCount, labels.size() - 1) + 1;
-    for (std::size_t index = 1; index < count; ++index) {
-        labels[index] = rows.spawns[index - 1];
-    }
-    const char* const preview =
-        value.hasSpawnSetHash && g_spawnRow < count ? labels[g_spawnRow].data() : labels[0].data();
-    if (!row_picker("spawn", "Spawn set", labels.data(), count, preview, g_spawnRow)) {
-        return false;
-    }
-    value.hasSpawnSetHash = g_spawnRow != 0;
-    value.spawnSetHash = value.hasSpawnSetHash ? rows.spawnHashes[g_spawnRow - 1] : 0;
-    return true;
-}
-
-/** Shows what the selection still needs. */
-void draw_status(const forced::ForcedDestination& value, const Lists& rows) noexcept {
-    if (rows.activityCount == 0) {
-        ImGui::TextUnformatted("waiting for the destination layouts to extract");
-        return;
-    }
-    if (!value.enabled) {
-        ImGui::TextDisabled("disabled, activities load normall");
-        return;
-    }
-    if (!forced::active(value)) {
-        ImGui::TextUnformatted("incomplete, select valid activity");
-        return;
-    }
-    ImGui::TextUnformatted(value.hasSpawnSetHash ? "active, forcing the chosen spawn set"
-                                                 : "active, the client picks its own spawn");
-    // Without an activity the load still works and the SDK cannot bind, so it is worth saying.
-    if (rows.definitionsUnavailable) {
-        ImGui::TextDisabled("no generated estate, so no activity can be named");
-    } else if (!value.hasActivityIndex) {
-        ImGui::TextDisabled("no activity named, so the SDK and scripts will not load");
-    }
-    if (rows.definitionsHidden != 0) {
-        std::array<char, kStatusCapacity> hidden{};
-        const int count = std::snprintf(hidden.data(),
-                                        hidden.size(),
-                                        "%zu more activities than the list holds",
-                                        rows.definitionsHidden);
-        if (count > 0) {
-            ImGui::TextDisabled("%s", hidden.data());
-        }
-    }
-    if (rows.spawnUnavailable) {
-        ImGui::TextDisabled("this destination's spawn sets could not be listed");
-    }
-    if (!rows.spawnNarrowed) {
-        ImGui::TextDisabled("currently showing spawn sets for all bubbles");
-        return;
-    }
-    std::array<char, kStatusCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "spawn sets of this bubble, %zu of the map's hidden",
-                                      rows.spawnHidden);
-    if (written > 0) {
-        ImGui::TextDisabled("%s", line.data());
-    }
-    // A marked row is the failure the operator cannot see otherwise. The bubble is right, the
-    // package is not loaded, and the client then spawns nothing at all.
-    if (rows.spawnForeign != 0) {
-        std::array<char, kStatusCapacity> foreign{};
-        const int count = std::snprintf(foreign.data(),
-                                        foreign.size(),
-                                        "%zu in a package this destination does not load",
-                                        rows.spawnForeign);
-        if (count > 0) {
-            ImGui::TextDisabled("%s", foreign.data());
-        }
-    }
-}
-
-} // namespace
-
-/** Draws the activity override module inside the active Core UI frame. */
-void draw() noexcept {
-    Lists& rows = lists();
-    refresh_activities(rows);
-    forced::ForcedDestination value{};
-    forced::snapshot(value);
-    // The rows follow whatever State holds, so a selection cleared elsewhere clears the lists.
-    if (name_of(value).empty() && rows.bubbleCount != 0) {
-        follow_destination(value, rows);
-        g_activityRow = kNoRow;
-    }
-
-    core::ui::components::section::header("Activity override",
-                                          "Forces every load to redirect to these values.");
-
-    bool changed = core::ui::components::toggle::control("Enabled", value.enabled);
-    ImGui::SameLine();
-    if (ImGui::Button("Clear")) {
-        forced::clear();
-        value = {};
-        g_activityRow = kNoRow;
-        follow_destination(value, rows);
-        changed = false;
-    }
-    // The estate can finish generating after a pick, so an empty list is retried while it stands.
-    if (rows.definitionCount == 0 && !name_of(value).empty()) {
-        refresh_definitions(rows, name_of(value));
-    }
-
-    ImGui::Spacing();
-    changed = draw_activity(value, rows) || changed;
-    ImGui::Spacing();
-    changed = draw_definition(value, rows) || changed;
-    ImGui::Spacing();
-    changed = draw_bubble(value, rows) || changed;
-    ImGui::Spacing();
-    changed = draw_slice(value, rows) || changed;
-    ImGui::Spacing();
-    changed = draw_spawn(value, rows) || changed;
-
-    ImGui::Spacing();
-    ImGui::Separator();
-    draw_status(value, rows);
-    if (changed && !forced::publish(value)) {
-        ImGui::TextUnformatted("value out of range, not applied");
-    }
-}
-
-} // namespace sunrise::server::ui::activity_override

+ 0 - 8
Sunrise/src/server/ui/activity_override/activity_override_panel.h

@@ -1,8 +0,0 @@
-#pragma once
-
-namespace sunrise::server::ui::activity_override {
-
-/** Draws the activity override module inside the active Core UI frame. */
-void draw() noexcept;
-
-} // namespace sunrise::server::ui::activity_override

+ 6 - 23
Sunrise/src/server/ui/runtime/server_ui_module_runtime.cpp

@@ -5,49 +5,32 @@
 #include "../../../core/ui/modules/registry/ui_module_registry.h"
 #include "../../../core/ui/modules/registry/ui_module_registry.h"
 #include "../../../core/ui/modules/ui_module_descriptor.h"
 #include "../../../core/ui/modules/ui_module_descriptor.h"
 #include "../activity_host/activity_host_panel.h"
 #include "../activity_host/activity_host_panel.h"
-#include "../activity_override/activity_override_panel.h"
 
 
 namespace sunrise::server::ui::runtime {
 namespace sunrise::server::ui::runtime {
 namespace {
 namespace {
 
 
-/** A namespaced stable ID keeps Server modules from clashing with Client modules. */
-constexpr std::string_view kOverrideStableId = "server.activity_override";
-/** Short menu label for the activity override page. */
-constexpr std::string_view kOverrideDisplayName = "Activity";
 /** A namespaced stable ID for the Activity Host page. */
 /** A namespaced stable ID for the Activity Host page. */
 constexpr std::string_view kHostStableId = "server.activity_host";
 constexpr std::string_view kHostStableId = "server.activity_host";
 /** Short menu label for the Activity Host page. */
 /** Short menu label for the Activity Host page. */
 constexpr std::string_view kHostDisplayName = "Activity Host";
 constexpr std::string_view kHostDisplayName = "Activity Host";
 
 
-core::ui::modules::registry::PageRegistration g_overridePage;
 core::ui::modules::registry::PageRegistration g_hostPage;
 core::ui::modules::registry::PageRegistration g_hostPage;
 
 
 } // namespace
 } // namespace
 
 
 /** @return True when the Server module owns its Core UI registry slot. */
 /** @return True when the Server module owns its Core UI registry slot. */
 bool initialize() noexcept {
 bool initialize() noexcept {
-    if (!g_overridePage.acquire(core::ui::modules::Owner::server,
-                                kOverrideStableId,
-                                kOverrideDisplayName,
-                                &activity_override::draw)) {
-        return false;
-    }
-    if (!g_hostPage.acquire(core::ui::modules::Owner::server,
-                            kHostStableId,
-                            kHostDisplayName,
-                            &activity_host::draw,
-                            nullptr,
-                            &activity_host::draw_windows)) {
-        g_overridePage.release();
-        return false;
-    }
-    return true;
+    return g_hostPage.acquire(core::ui::modules::Owner::server,
+                              kHostStableId,
+                              kHostDisplayName,
+                              &activity_host::draw,
+                              nullptr,
+                              &activity_host::draw_windows);
 }
 }
 
 
 /** Removes the Server module from the Core UI registry. */
 /** Removes the Server module from the Core UI registry. */
 void shutdown() noexcept {
 void shutdown() noexcept {
     g_hostPage.release();
     g_hostPage.release();
-    g_overridePage.release();
 }
 }
 
 
 } // namespace sunrise::server::ui::runtime
 } // namespace sunrise::server::ui::runtime

+ 22 - 0
Sunrise/src/state/build_data/activities/activity_artwork.h

@@ -0,0 +1,22 @@
+#pragma once
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+#include <vector>
+
+namespace sunrise::state::build_data::activities {
+/** Numeric slots into the extracted artwork table; zero has no image. */
+enum class Icon : std::uint8_t {};
+inline constexpr std::size_t kIconCount = 41;
+struct Artwork {
+    std::uint32_t tag{};
+    std::uint16_t width{}, height{};
+    /** Native RGBA8 pixels. No game-owned texture/resource pointers cross this boundary. */
+    std::vector<std::byte> pixels{};
+};
+/** Publishes validated pixel buffers once from the investment worker, transferring ownership. */
+[[nodiscard]] bool publish_artwork(std::array<Artwork, kIconCount>&& rows) noexcept;
+/** @return Immutable process-lifetime buffers, or an empty span before publication. */
+[[nodiscard]] std::span<const Artwork> artwork() noexcept;
+} // namespace sunrise::state::build_data::activities

+ 106 - 0
Sunrise/src/state/build_data/activities/activity_catalog.cpp

@@ -0,0 +1,106 @@
+#include "activity_catalog.h"
+
+#include <algorithm>
+#include <atomic>
+
+#include "activity_artwork.h"
+
+namespace sunrise::state::build_data::activities {
+namespace {
+std::array<Definition, kCapacity> g_rows{};
+std::atomic_size_t g_count{};
+std::atomic_bool g_failed{};
+std::array<Presentation, kCapacity> g_presentations{};
+std::atomic_size_t g_presentationCount{};
+std::array<Artwork, kIconCount> g_artwork{};
+std::atomic_bool g_artReady{};
+MenuText g_menuText{};
+std::atomic_bool g_menuTextReady{};
+} // namespace
+bool publish_menu_text(const MenuText& value) noexcept {
+    if (g_menuTextReady.load(std::memory_order_acquire)) {
+        return false;
+    }
+    for (const auto& row : value.content) {
+        if (row.text.back() != '\0') {
+            return false;
+        }
+    }
+    for (const auto& row : value.kinds) {
+        if (row.text.back() != '\0') {
+            return false;
+        }
+    }
+    g_menuText = value;
+    g_menuTextReady.store(true, std::memory_order_release);
+    return true;
+}
+const MenuText& menu_text() noexcept {
+    static const MenuText absent{};
+    return g_menuTextReady.load(std::memory_order_acquire) ? g_menuText : absent;
+}
+bool ready() noexcept {
+    return g_count.load(std::memory_order_acquire) != 0;
+}
+bool publish_artwork(std::array<Artwork, kIconCount>&& rows) noexcept {
+    if (g_artReady.load(std::memory_order_acquire)) {
+        return false;
+    }
+    for (const auto& row : rows) {
+        if (!row.pixels.empty()
+            && (row.width == 0 || row.height == 0 || row.width > 256 || row.height > 256
+                || row.pixels.size() != static_cast<std::size_t>(row.width) * row.height * 4)) {
+            return false;
+        }
+    }
+    g_artwork = std::move(rows);
+    g_artReady.store(true, std::memory_order_release);
+    return true;
+}
+std::span<const Artwork> artwork() noexcept {
+    return g_artReady.load(std::memory_order_acquire) ? std::span<const Artwork>(g_artwork)
+                                                      : std::span<const Artwork>{};
+}
+bool publish_presentations(std::span<const Presentation> rows) noexcept {
+    if (g_presentationCount.load(std::memory_order_acquire) != 0 || rows.empty()
+        || rows.size() > kCapacity) {
+        return false;
+    }
+    for (const auto& row : rows) {
+        if (row.title.back() != '\0' || row.description.back() != '\0' || row.type.back() != '\0') {
+            return false;
+        }
+    }
+    std::copy(rows.begin(), rows.end(), g_presentations.begin());
+    g_presentationCount.store(rows.size(), std::memory_order_release);
+    return true;
+}
+const Presentation& presentation(std::uint16_t index) noexcept {
+    static const Presentation absent{};
+    return index < g_presentationCount.load(std::memory_order_acquire) ? g_presentations[index]
+                                                                       : absent;
+}
+bool extraction_failed() noexcept {
+    return !ready() && g_failed.load(std::memory_order_acquire);
+}
+void note_extraction_failure() noexcept {
+    g_failed.store(true, std::memory_order_release);
+}
+bool publish(std::span<const Definition> rows) noexcept {
+    // The cooperative investment worker is the sole producer. Never replace published storage.
+    if (ready() || rows.empty() || rows.size() > g_rows.size()) {
+        return false;
+    }
+    for (std::size_t i = 0; i < rows.size(); ++i) {
+        if (rows[i].index != i || rows[i].hash == 0 || rows[i].package.back() != '\0') {
+            return false;
+        }
+    }
+    std::copy(rows.begin(), rows.end(), g_rows.begin());
+    g_count.store(rows.size(), std::memory_order_release);
+    return true;
+}
+std::span<const Definition> entries() noexcept {
+    return {g_rows.data(), g_count.load(std::memory_order_acquire)};
+}
+} // namespace sunrise::state::build_data::activities

+ 61 - 0
Sunrise/src/state/build_data/activities/activity_catalog.h

@@ -0,0 +1,61 @@
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <span>
+#include <string_view>
+
+namespace sunrise::state::build_data::activities {
+inline constexpr std::size_t kCapacity = 4095;
+inline constexpr std::size_t kContentCount = 23, kExperienceKindCount = 7;
+struct NativeText {
+    std::array<char, 96> text{};
+    // Bank ordinal, or direct native string-container tag for Director style labels.
+    std::uint32_t bank{0xFFFF}, hash{0x811C9DC5};
+};
+struct MenuText {
+    std::array<NativeText, kContentCount> content{};
+    std::array<NativeText, kExperienceKindCount> kinds{};
+};
+/** Optional runtime localization. Classification tables never supply display text. */
+[[nodiscard]] bool publish_menu_text(const MenuText& value) noexcept;
+/** @return Published labels, or an empty immutable value while extraction is pending. */
+[[nodiscard]] const MenuText& menu_text() noexcept;
+struct Definition {
+    std::uint16_t index{};
+    std::uint32_t hash{};
+    std::uint32_t gameplaySettingsHash{};
+    std::uint8_t nativeType{};
+    std::uint8_t destination{};
+    std::array<char, 40> package{};
+    [[nodiscard]] std::string_view name() const noexcept {
+        return package.data();
+    }
+};
+struct Presentation {
+    std::array<char, 160> title{};
+    std::array<char, 1024> description{};
+    std::array<char, 64> type{};
+    std::array<char, 64> expansion{};
+    std::uint32_t nativeStyle{};
+    std::uint16_t experience{0xFFFF};
+    std::uint8_t experienceKind{};
+    /** Original release index; releases::unresolved when not established. */
+    std::uint8_t contentGroup{};
+    std::uint8_t classificationSource{}; // 4 explicit local activity-hash release map
+};
+/** Optional, immutable Director text. Kept outside the persistent scenario cache. */
+[[nodiscard]] bool publish_presentations(std::span<const Presentation> rows) noexcept;
+/** @return The indexed presentation, or an empty immutable value when unavailable. */
+[[nodiscard]] const Presentation& presentation(std::uint16_t index) noexcept;
+/** Immutable process-local public activity table, independent of the persistent scenario cache. */
+[[nodiscard]] bool ready() noexcept;
+/** @return True when extraction failed and no catalog was published during this process. */
+[[nodiscard]] bool extraction_failed() noexcept;
+/** Optional menu data must not keep core investment startup waiting. */
+void note_extraction_failure() noexcept;
+/** Publishes validated dense identities once from the investment worker. Never replaces rows. */
+[[nodiscard]] bool publish(std::span<const Definition> rows) noexcept;
+/** @return Immutable process-lifetime identities, or an empty span before publication. */
+[[nodiscard]] std::span<const Definition> entries() noexcept;
+} // namespace sunrise::state::build_data::activities

+ 623 - 0
Sunrise/src/state/build_data/activities/activity_releases.h

@@ -0,0 +1,623 @@
+// Numeric release and equivalent-activity assignments keyed by the installed activity hash.
+// Display text and artwork are read from the game's packages each process launch.
+#pragma once
+#include <array>
+#include <cstdint>
+
+#include "activity_catalog.h"
+
+namespace sunrise::state::build_data::activities::releases {
+inline constexpr std::size_t contentCount = 23;
+inline constexpr auto unresolved = contentCount - 1;
+static_assert(contentCount == kContentCount);
+struct Rule {
+    std::uint32_t hash;
+    std::uint16_t nativeIndex, experience;
+    std::uint8_t release, kind, preferencePenalty{};
+};
+inline constexpr std::array<Rule, 1170> rules{{
+    {0x00A64D9CU, 104, 104, 7, 0},    {0x00AAE268U, 98, 98, 7, 1},
+    {0x00D2C692U, 819, 232, 3, 2},    {0x011D555BU, 680, 232, 3, 2},
+    {0x0130E9C0U, 985, 985, 1, 4},    {0x01C5971CU, 501, 501, 3, 0},
+    {0x01CD6EA0U, 891, 891, 1, 4},    {0x02355769U, 890, 890, 1, 4},
+    {0x026268A2U, 472, 443, 4, 0},    {0x02750356U, 860, 860, 12, 0},
+    {0x02B05FEDU, 250, 250, 12, 3},   {0x02DDCBE1U, 673, 225, 1, 2},
+    {0x03106D0DU, 1045, 1045, 1, 4},  {0x0337467EU, 977, 384, 1, 3},
+    {0x0346A3E5U, 847, 226, 1, 2},    {0x0367538CU, 959, 959, 1, 4},
+    {0x0416ED40U, 844, 226, 1, 2},    {0x0416ED41U, 843, 226, 1, 2},
+    {0x0416ED42U, 842, 226, 1, 2},    {0x0416ED46U, 846, 226, 1, 2},
+    {0x0416ED47U, 845, 226, 1, 2},    {0x0470CDA4U, 31, 31, 3, 1},
+    {0x0477BF1AU, 786, 784, 8, 0},    {0x04B074E8U, 1005, 400, 1, 3},
+    {0x04CFCB63U, 337, 337, 1, 6},    {0x04F12A8AU, 0, 0, 22, 0},
+    {0x0559226FU, 568, 565, 1, 0},    {0x05633DA4U, 1102, 1102, 4, 4},
+    {0x05BF9A25U, 383, 383, 1, 3, 5}, {0x05D9144DU, 1133, 1133, 4, 4},
+    {0x06382358U, 955, 955, 1, 4},    {0x066DC1D2U, 211, 211, 22, 1},
+    {0x06EF641DU, 1048, 1048, 1, 4},  {0x07001999U, 737, 229, 2, 2},
+    {0x07263408U, 539, 539, 3, 0},    {0x0754A871U, 968, 383, 1, 3},
+    {0x07BE651FU, 74, 74, 1, 1, 55},  {0x07CC70B9U, 1137, 1137, 4, 4},
+    {0x08142329U, 525, 525, 1, 0},    {0x08194028U, 850, 237, 4, 2},
+    {0x08194029U, 849, 237, 4, 2},    {0x0819402AU, 848, 237, 4, 2},
+    {0x0819402EU, 852, 237, 4, 2},    {0x0819402FU, 851, 237, 4, 2},
+    {0x088FE2F1U, 888, 888, 1, 4},    {0x08A69C31U, 1073, 1073, 2, 4},
+    {0x08A92488U, 558, 227, 1, 2},    {0x08E09C03U, 478, 449, 2, 0},
+    {0x0926CD66U, 330, 330, 1, 3},    {0x096B3CDBU, 145, 145, 17, 1},
+    {0x098E369CU, 368, 368, 4, 3},    {0x0A776A51U, 949, 949, 1, 4},
+    {0x0B0EBE1FU, 376, 376, 1, 3, 5}, {0x0B163C3CU, 1144, 1142, 4, 3},
+    {0x0B48DCF9U, 928, 928, 1, 4},    {0x0BC83821U, 938, 938, 1, 4},
+    {0x0C2D5761U, 886, 886, 1, 4},    {0x0D5E7AE3U, 235, 235, 4, 2},
+    {0x0D9FF5E0U, 132, 127, 9, 0},    {0x0D9FF5E1U, 133, 127, 9, 0},
+    {0x0D9FF5E2U, 130, 127, 9, 0},    {0x0D9FF5E3U, 131, 127, 9, 0},
+    {0x0D9FF5E4U, 128, 127, 9, 0},    {0x0D9FF5E5U, 129, 127, 9, 0},
+    {0x0D9FF5E7U, 127, 127, 9, 0},    {0x0D9FF5EEU, 134, 127, 9, 0},
+    {0x0D9FF5EFU, 135, 127, 9, 0},    {0x0DF38E06U, 35, 35, 14, 1},
+    {0x0E8DAE4DU, 763, 761, 4, 0},    {0x0E9E1F38U, 869, 235, 4, 2},
+    {0x0E9E1F39U, 868, 235, 4, 2},    {0x0E9E1F3AU, 867, 235, 4, 2},
+    {0x0E9E1F3EU, 871, 235, 4, 2},    {0x0E9E1F3FU, 870, 235, 4, 2},
+    {0x0EC931E2U, 1032, 1032, 1, 4},  {0x0EE17367U, 780, 767, 4, 0},
+    {0x0F93DEABU, 894, 894, 1, 4},    {0x0FCBF3D8U, 246, 246, 12, 3},
+    {0x0FCBF3DBU, 245, 245, 12, 3},   {0x1035D21FU, 63, 63, 1, 1, 55},
+    {0x104365E2U, 666, 233, 3, 2},    {0x10DBDC88U, 1167, 1167, 5, 4},
+    {0x1110F6DEU, 956, 956, 1, 4},    {0x11149801U, 630, 225, 1, 2},
+    {0x11252DB2U, 566, 565, 1, 0},    {0x1183EB40U, 926, 926, 1, 4},
+    {0x11882989U, 255, 255, 22, 0},   {0x11CE8609U, 937, 937, 1, 4},
+    {0x11CF3864U, 986, 986, 1, 4},    {0x12B0BD5AU, 17, 11, 1, 1},
+    {0x13073868U, 402, 335, 1, 3, 5}, {0x131D3042U, 969, 393, 1, 3},
+    {0x133591C2U, 674, 226, 1, 2},    {0x13B3BA49U, 24, 22, 1, 0},
+    {0x13CD7D86U, 442, 442, 1, 0},    {0x142F9486U, 618, 225, 1, 2},
+    {0x14440E47U, 1091, 359, 3, 3},   {0x14A4CF14U, 161, 161, 5, 2},
+    {0x15054978U, 125, 125, 9, 0},    {0x1537E366U, 951, 951, 1, 4},
+    {0x156D5CD2U, 1060, 380, 1, 3},   {0x1642C720U, 593, 229, 2, 2},
+    {0x169C1E2CU, 12, 11, 1, 1},      {0x16B4EEF0U, 857, 234, 4, 2},
+    {0x16B4EEF1U, 858, 234, 4, 2},    {0x16B4EEF5U, 854, 234, 4, 2},
+    {0x16B4EEF6U, 855, 234, 4, 2},    {0x16B4EEF7U, 856, 234, 4, 2},
+    {0x16EF5EFCU, 86, 86, 7, 1},      {0x17108A3BU, 1143, 1142, 4, 3},
+    {0x1713C46CU, 895, 895, 1, 4},    {0x1716D7F0U, 639, 639, 2, 0, 5},
+    {0x1716D7F3U, 640, 640, 2, 0, 5}, {0x1724D3A3U, 1023, 1023, 1, 4},
+    {0x17CFFAC7U, 435, 435, 1, 0},    {0x188A726AU, 677, 229, 2, 2},
+    {0x18C252E3U, 1034, 1034, 1, 4},  {0x18DE7108U, 583, 565, 1, 0},
+    {0x18E575BDU, 1158, 1158, 5, 4},  {0x194FF64DU, 3, 3, 6, 1},
+    {0x19978953U, 880, 880, 1, 4},    {0x19E55DBDU, 638, 638, 2, 0, 5},
+    {0x19FCABA9U, 999, 999, 1, 4},    {0x1A62A2A2U, 632, 227, 1, 2},
+    {0x1A783C74U, 933, 933, 1, 4},    {0x1A8C8680U, 415, 415, 22, 1},
+    {0x1AD153E3U, 398, 398, 1, 3, 5}, {0x1AE849DDU, 454, 424, 3, 0},
+    {0x1B6A5B96U, 945, 945, 1, 4},    {0x1B7D6817U, 931, 931, 1, 4},
+    {0x1BCD6E50U, 151, 151, 5, 1},    {0x1BD9EC6CU, 733, 225, 1, 2},
+    {0x1C1DFE9EU, 164, 159, 5, 1},    {0x1C439D4FU, 1109, 1109, 4, 4},
+    {0x1C4679A9U, 1039, 1039, 1, 4},  {0x1D60987FU, 634, 634, 1, 0},
+    {0x1D7B8B38U, 1148, 1148, 4, 3},  {0x1DA883C6U, 1142, 1142, 4, 3},
+    {0x1DB23C2CU, 1122, 1122, 4, 4},  {0x1DE7617AU, 320, 320, 8, 1},
+    {0x1DF7D42BU, 1159, 1159, 5, 4},  {0x1E1137DCU, 486, 486, 2, 0},
+    {0x1E53B599U, 582, 565, 1, 0},    {0x1F13FA86U, 308, 61, 4, 1},
+    {0x1F21F35FU, 665, 232, 3, 2},    {0x1FA226ABU, 310, 66, 4, 1},
+    {0x1FBB88AEU, 439, 439, 2, 0},    {0x202E2CE5U, 965, 391, 1, 3},
+    {0x202F474CU, 315, 48, 4, 1},     {0x207FB562U, 1055, 1055, 1, 4},
+    {0x20B54310U, 547, 547, 7, 0},    {0x20BAF477U, 883, 883, 1, 4},
+    {0x20FE51B3U, 282, 65, 1, 1},     {0x21757424U, 708, 230, 2, 2},
+    {0x21757425U, 693, 230, 2, 2},    {0x21757427U, 723, 230, 2, 2},
+    {0x2180A14EU, 672, 27, 1, 2},     {0x21955673U, 33, 33, 14, 1},
+    {0x21AB21ACU, 285, 285, 1, 1},    {0x2209AAD8U, 179, 175, 5, 0},
+    {0x2209AAD9U, 178, 175, 5, 0},    {0x2209AADEU, 177, 175, 5, 0},
+    {0x2209AADFU, 176, 175, 5, 0},    {0x22A64A1AU, 988, 988, 1, 4},
+    {0x22DF7B52U, 564, 228, 1, 2},    {0x22E35108U, 505, 505, 22, 6},
+    {0x231DD291U, 54, 54, 1, 1, 55},  {0x23DAC862U, 560, 227, 1, 2},
+    {0x2426388EU, 90, 90, 7, 1},      {0x24597C6BU, 431, 431, 11, 0},
+    {0x24EA187CU, 99, 99, 7, 6},      {0x2520A335U, 902, 396, 1, 3},
+    {0x25434698U, 405, 365, 4, 3, 5}, {0x254DA5E2U, 5, 5, 12, 0},
+    {0x25860F77U, 239, 221, 5, 2},    {0x25B79F76U, 908, 395, 1, 3},
+    {0x26480DF5U, 556, 226, 1, 2},    {0x26486061U, 550, 27, 1, 2},
+    {0x26B8D9D7U, 655, 228, 1, 2},    {0x27248014U, 626, 229, 2, 2},
+    {0x276B05F2U, 158, 158, 5, 1},    {0x27731DC1U, 628, 223, 1, 2},
+    {0x27B35349U, 307, 61, 4, 1},     {0x27BE1B72U, 470, 441, 1, 0},
+    {0x280C72ADU, 994, 994, 1, 4},    {0x281DFBE3U, 756, 755, 8, 0},
+    {0x28DD4614U, 241, 221, 5, 2},    {0x291F76C6U, 624, 229, 2, 2},
+    {0x292CB5B4U, 1166, 1166, 5, 4},  {0x29EBAF60U, 19, 11, 1, 1},
+    {0x2A30935AU, 79, 78, 15, 1},     {0x2A4F8673U, 96, 96, 7, 1},
+    {0x2A70C143U, 787, 787, 8, 0},    {0x2AA2B77DU, 1030, 1030, 1, 4},
+    {0x2B165174U, 1083, 1083, 3, 4},  {0x2B234B12U, 354, 346, 2, 3, 5},
+    {0x2B34E609U, 1087, 1087, 3, 4},  {0x2C21F2BFU, 532, 532, 1, 0},
+    {0x2C291AC1U, 526, 526, 5, 0},    {0x2C4ACEDDU, 922, 922, 1, 4},
+    {0x2C57FA9EU, 740, 232, 3, 2},    {0x2CB41E8BU, 441, 441, 1, 0},
+    {0x2CBDFF96U, 1019, 1019, 1, 4},  {0x2D20AF96U, 587, 565, 1, 0},
+    {0x2D72A969U, 452, 421, 12, 0},   {0x2DAA02E0U, 877, 221, 5, 2},
+    {0x2DAA02E1U, 876, 221, 5, 2},    {0x2DAA02E4U, 873, 221, 5, 2},
+    {0x2DAA02E6U, 875, 221, 5, 2},    {0x2DAA02E7U, 874, 221, 5, 2},
+    {0x2DE845C3U, 735, 227, 1, 2},    {0x2DECFCBDU, 148, 148, 17, 1},
+    {0x2DF70AAAU, 580, 565, 1, 0},    {0x2E23E003U, 1168, 1168, 5, 4},
+    {0x2E591EF7U, 445, 445, 1, 0},    {0x2E637920U, 437, 437, 1, 0},
+    {0x2E67813EU, 160, 160, 5, 1},    {0x2E988C2FU, 499, 499, 3, 0},
+    {0x2E9F0BA9U, 1053, 1053, 1, 4},  {0x2EA0CFB5U, 1024, 1024, 1, 4},
+    {0x2ED770DDU, 379, 379, 1, 3, 5}, {0x2F0C4294U, 1001, 400, 1, 3},
+    {0x2F92B2D0U, 947, 947, 1, 4},    {0x2FC54B33U, 399, 399, 1, 3, 5},
+    {0x300C079EU, 446, 446, 1, 0},    {0x303751EEU, 909, 398, 1, 3},
+    {0x303AF7C6U, 538, 536, 2, 0},    {0x31EF431FU, 1012, 1012, 1, 4},
+    {0x3218852BU, 1130, 1130, 4, 4},  {0x321BBAE1U, 625, 230, 2, 2},
+    {0x329E69D7U, 991, 991, 1, 4},    {0x32C4ECB3U, 39, 39, 14, 1},
+    {0x32C612F9U, 312, 72, 4, 1},     {0x32EA3C16U, 784, 784, 8, 0},
+    {0x33055E15U, 93, 93, 7, 1},      {0x330AC130U, 381, 381, 1, 3, 5},
+    {0x335B93E1U, 725, 232, 3, 2},    {0x335B93E2U, 710, 232, 3, 2},
+    {0x335B93E3U, 695, 232, 3, 2},    {0x3421737AU, 1077, 344, 2, 3},
+    {0x3452A6CBU, 37, 37, 14, 1},     {0x347DE4AAU, 924, 924, 1, 4},
+    {0x3495E502U, 650, 223, 1, 2},    {0x34B43AB2U, 44, 44, 4, 0},
+    {0x34E13D59U, 823, 161, 5, 2},    {0x34E13D5CU, 820, 161, 5, 2},
+    {0x34E13D5EU, 822, 161, 5, 2},    {0x34E13D5FU, 821, 161, 5, 2},
+    {0x357B4A2EU, 1114, 1114, 4, 4},  {0x35DB9C95U, 487, 487, 2, 0},
+    {0x367CCB47U, 534, 534, 2, 0},    {0x36B53145U, 126, 126, 9, 0},
+    {0x36B5DAE7U, 489, 489, 1, 0},    {0x36CAACBAU, 984, 385, 1, 3},
+    {0x36E2B21BU, 533, 533, 2, 0},    {0x36F2BFDFU, 252, 252, 13, 1},
+    {0x3731D3CBU, 929, 929, 1, 4},    {0x373FFF22U, 549, 27, 1, 2},
+    {0x37449A6CU, 46, 46, 4, 6},      {0x3746EADAU, 679, 231, 3, 2},
+    {0x375EB7BAU, 421, 421, 12, 0},   {0x3787A3A5U, 468, 439, 2, 0},
+    {0x37CEEED6U, 662, 229, 2, 2},    {0x37F09185U, 279, 67, 1, 1},
+    {0x380D494FU, 622, 622, 1, 6},    {0x38C97115U, 769, 767, 4, 0},
+    {0x38CC2C83U, 522, 522, 10, 0},   {0x38F926B2U, 281, 54, 1, 1},
+    {0x38F97B56U, 236, 236, 4, 2},    {0x3915C42BU, 107, 107, 7, 0},
+    {0x3922BEA4U, 609, 233, 3, 2},    {0x393B1CC5U, 124, 123, 9, 0},
+    {0x395F5047U, 758, 758, 4, 0},    {0x39748A73U, 1062, 381, 1, 3},
+    {0x397879C3U, 987, 987, 1, 4},    {0x3991B25EU, 604, 604, 3, 6},
+    {0x39C7A803U, 333, 333, 1, 6},    {0x3A7EB820U, 691, 228, 1, 2},
+    {0x3A7EB821U, 706, 228, 1, 2},    {0x3A7EB822U, 721, 228, 1, 2},
+    {0x3AF76E4FU, 596, 230, 2, 2},    {0x3B113A91U, 458, 429, 1, 0},
+    {0x3B324D69U, 881, 881, 1, 4},    {0x3B3DCCD8U, 393, 393, 1, 3, 5},
+    {0x3B660B3CU, 1076, 342, 2, 3},   {0x3B677AF9U, 1134, 1134, 4, 4},
+    {0x3B9A600DU, 386, 386, 1, 3, 5}, {0x3BBB85F8U, 296, 296, 2, 1},
+    {0x3BC629F7U, 813, 229, 2, 2},    {0x3BD62431U, 475, 446, 1, 0},
+    {0x3BD72350U, 1145, 1145, 4, 3},  {0x3C474735U, 36, 36, 14, 1},
+    {0x3CAE11D7U, 387, 387, 1, 3, 5}, {0x3CB355D6U, 385, 385, 1, 3, 5},
+    {0x3CCB3384U, 101, 101, 7, 0},    {0x3CE2C83AU, 301, 60, 3, 1},
+    {0x3D087DB6U, 71, 71, 1, 1, 55},  {0x3D802A02U, 762, 761, 4, 0},
+    {0x3DA1A4BEU, 664, 231, 3, 2},    {0x3DB2E849U, 617, 27, 1, 2},
+    {0x3DBDD455U, 221, 221, 5, 2},    {0x3DD06F19U, 765, 764, 4, 0},
+    {0x3DE97694U, 899, 899, 1, 4},    {0x3E395D81U, 754, 752, 8, 0},
+    {0x3E9433BDU, 294, 75, 2, 1},     {0x3EC5AA6FU, 1052, 1052, 1, 4},
+    {0x3F00CF4BU, 291, 291, 2, 1},    {0x3F368C8EU, 180, 180, 5, 0},
+    {0x3F6AE1D0U, 1061, 376, 1, 3},   {0x3F81832FU, 779, 767, 4, 0},
+    {0x3FC7A9CFU, 66, 66, 4, 1, 55},  {0x3FF918C6U, 29, 29, 2, 0},
+    {0x401339E8U, 1135, 1135, 4, 4},  {0x401587ADU, 284, 284, 1, 1},
+    {0x402F74E7U, 1011, 1011, 1, 4},  {0x408675C8U, 264, 264, 18, 1},
+    {0x40B3C80AU, 1033, 1033, 1, 4},  {0x40FC2A33U, 1017, 1017, 1, 4},
+    {0x4183D457U, 1124, 1124, 4, 4},  {0x4189E121U, 32, 31, 3, 1, 5},
+    {0x41AC0431U, 621, 228, 1, 2},    {0x41B4F83EU, 523, 523, 2, 0},
+    {0x41BBC49BU, 1027, 1027, 1, 4},  {0x41E02EF4U, 901, 901, 1, 4},
+    {0x41FEA9C4U, 1067, 380, 1, 3},   {0x4202B38EU, 652, 225, 1, 2},
+    {0x423A0ECBU, 1150, 1148, 4, 3},  {0x423B34F3U, 83, 83, 7, 1},
+    {0x42CAC3F3U, 1075, 1075, 2, 4},  {0x434C31E0U, 551, 27, 1, 2},
+    {0x437D66E5U, 68, 68, 4, 1, 55},  {0x439E4994U, 234, 234, 4, 2},
+    {0x43A00DE7U, 88, 88, 7, 1},      {0x449FEDADU, 515, 515, 3, 0},
+    {0x44BFA063U, 438, 438, 4, 0},    {0x44D2BD32U, 1107, 1107, 4, 4},
+    {0x4519BAEFU, 328, 328, 1, 3},    {0x456D0393U, 1020, 1020, 1, 4},
+    {0x45835F90U, 303, 51, 3, 1},     {0x458D915AU, 4, 4, 12, 0},
+    {0x4614D477U, 290, 50, 2, 1},     {0x467BC98DU, 745, 745, 1, 0},
+    {0x468601B7U, 790, 790, 8, 0},    {0x468BEF7EU, 546, 546, 22, 1},
+    {0x46D4FCA2U, 209, 205, 5, 0},    {0x46D4FCA3U, 208, 205, 5, 0},
+    {0x46D4FCA4U, 207, 205, 5, 0},    {0x46D4FCA5U, 206, 205, 5, 0},
+    {0x47066385U, 212, 212, 22, 1},   {0x4720B2CFU, 262, 262, 22, 1},
+    {0x473A0B82U, 300, 60, 3, 1},     {0x476B53ADU, 741, 233, 3, 2},
+    {0x477ECFA6U, 147, 147, 17, 1},   {0x47AA0866U, 363, 60, 3, 1},
+    {0x47AA0867U, 362, 60, 3, 1},     {0x47E47377U, 1051, 1051, 1, 4},
+    {0x47F913A4U, 561, 561, 1, 6},    {0x48993C02U, 496, 496, 3, 0},
+    {0x48A9BD06U, 520, 520, 3, 0},    {0x4912D1B2U, 1064, 382, 1, 3},
+    {0x4936CAA2U, 963, 384, 1, 3},    {0x493929ABU, 778, 764, 4, 0},
+    {0x49494CCAU, 1129, 1129, 4, 4},  {0x4971A229U, 364, 364, 4, 3},
+    {0x4989CDE3U, 30, 30, 3, 0},      {0x4A15CF97U, 422, 422, 12, 0},
+    {0x4A1CA2E6U, 280, 76, 1, 1},     {0x4A80F657U, 1057, 1057, 1, 4},
+    {0x4A87FEC4U, 1036, 1036, 1, 4},  {0x4ABE9634U, 1154, 1154, 5, 4},
+    {0x4ACDA170U, 967, 394, 1, 3},    {0x4B16811BU, 75, 75, 2, 1, 55},
+    {0x4B559B9AU, 649, 230, 2, 2},    {0x4B6C531EU, 976, 328, 1, 3},
+    {0x4B8D6286U, 1103, 1103, 4, 4},  {0x4C0789C0U, 400, 400, 1, 3, 5},
+    {0x4C197F36U, 513, 513, 4, 0},    {0x4C36870FU, 298, 298, 2, 1},
+    {0x4C4925D5U, 344, 344, 2, 3},    {0x4C60AF4CU, 413, 413, 8, 0},
+    {0x4C774BB6U, 659, 226, 1, 2},    {0x4CCE82FBU, 242, 242, 12, 3},
+    {0x4CE1CFB4U, 1063, 379, 1, 3},   {0x4CEF34A6U, 26, 22, 1, 0},
+    {0x4D0474EDU, 752, 752, 8, 0},    {0x4D285A72U, 1090, 360, 3, 3},
+    {0x4D32C7A1U, 739, 231, 3, 2},    {0x4D405AA0U, 214, 214, 5, 6},
+    {0x4DA19F29U, 389, 331, 1, 3, 5}, {0x4DA8D072U, 864, 223, 1, 2},
+    {0x4DA8D073U, 865, 223, 1, 2},    {0x4DA8D074U, 862, 223, 1, 2},
+    {0x4DA8D075U, 863, 223, 1, 2},    {0x4DA8D077U, 861, 223, 1, 2},
+    {0x4E0FA012U, 22, 22, 1, 0},      {0x4E220E95U, 488, 488, 2, 0},
+    {0x4E3F8A09U, 511, 511, 2, 0},    {0x4E4CAEE0U, 62, 62, 1, 1, 55},
+    {0x4E4E10E6U, 373, 373, 4, 1},    {0x4E87586FU, 233, 233, 3, 2},
+    {0x4F10B9BDU, 165, 160, 5, 1},    {0x4F598A2DU, 989, 989, 1, 4},
+    {0x4F6D5C48U, 717, 27, 1, 2},     {0x4F6D5C4AU, 687, 27, 1, 2},
+    {0x4F6D5C4BU, 702, 27, 1, 2},     {0x4F7D749FU, 388, 388, 1, 3, 5},
+    {0x4FC7B3D4U, 316, 316, 4, 1},    {0x5004D002U, 188, 185, 5, 0},
+    {0x5004D003U, 189, 185, 5, 0},    {0x5004D004U, 186, 185, 5, 0},
+    {0x5004D005U, 187, 185, 5, 0},    {0x50A4250CU, 248, 248, 12, 3},
+    {0x50E27926U, 555, 226, 1, 2},    {0x50F55559U, 516, 516, 19, 0},
+    {0x50F74138U, 824, 227, 1, 2},    {0x50F7413AU, 826, 227, 1, 2},
+    {0x50F7413BU, 825, 227, 1, 2},    {0x50F7413CU, 828, 227, 1, 2},
+    {0x50F7413DU, 827, 227, 1, 2},    {0x5115D6E4U, 712, 234, 4, 2},
+    {0x5115D6E5U, 697, 234, 4, 2},    {0x5115D6E7U, 727, 234, 4, 2},
+    {0x517E0D39U, 97, 97, 7, 1},      {0x51F633D5U, 166, 166, 5, 0},
+    {0x5201A370U, 1038, 1038, 1, 4},  {0x52F4E3BEU, 670, 237, 4, 2},
+    {0x53423C1AU, 215, 215, 5, 6},    {0x5368E987U, 1169, 1169, 5, 4},
+    {0x54255751U, 249, 249, 12, 3},   {0x542B96C5U, 286, 286, 1, 1},
+    {0x546F8EAEU, 973, 393, 1, 3},    {0x548846E7U, 347, 342, 2, 3, 5},
+    {0x549B3BB1U, 304, 57, 3, 1},     {0x550500EEU, 295, 77, 2, 1},
+    {0x551E53BBU, 339, 339, 1, 6},    {0x5550A6A3U, 94, 94, 7, 6},
+    {0x557A367CU, 105, 105, 7, 0},    {0x55893300U, 474, 445, 1, 0},
+    {0x56397530U, 412, 410, 8, 0},    {0x56555F61U, 466, 437, 1, 0},
+    {0x568AF3C7U, 914, 914, 1, 4},    {0x56B7B6A5U, 78, 78, 15, 1},
+    {0x56D928B2U, 530, 530, 3, 0},    {0x5738B93CU, 111, 111, 7, 3},
+    {0x5738B93FU, 110, 110, 7, 3},    {0x576074B9U, 684, 236, 4, 2},
+    {0x5769C881U, 978, 388, 1, 3},    {0x57F2F0C0U, 714, 236, 4, 2},
+    {0x57F2F0C1U, 699, 236, 4, 2},    {0x57F2F0C3U, 729, 236, 4, 2},
+    {0x57FCC9BBU, 367, 367, 4, 3},    {0x581B1BDCU, 527, 527, 5, 0},
+    {0x5858AD22U, 467, 438, 4, 0},    {0x586787C1U, 102, 102, 7, 0},
+    {0x58CAB374U, 464, 435, 1, 0},    {0x58DC8B41U, 273, 62, 1, 1},
+    {0x58DF3107U, 1066, 378, 1, 3},   {0x592B0BCEU, 616, 223, 1, 2},
+    {0x5950C791U, 957, 957, 1, 4},    {0x59905E37U, 84, 20, 1, 0},
+    {0x599BB525U, 1093, 359, 3, 3},   {0x599D3401U, 671, 223, 1, 2},
+    {0x59C4F745U, 106, 106, 7, 0},    {0x59D01A95U, 136, 127, 9, 0},
+    {0x5A2E3FF4U, 292, 50, 2, 1},     {0x5A34706AU, 61, 61, 4, 1, 55},
+    {0x5AE7FBE6U, 1105, 1105, 4, 4},  {0x5AEF52A6U, 927, 927, 1, 4},
+    {0x5B619E48U, 114, 114, 7, 3},    {0x5B619E4BU, 115, 115, 7, 3},
+    {0x5B699ADEU, 258, 258, 22, 1},   {0x5B70DEA2U, 67, 67, 1, 1, 55},
+    {0x5B9929A5U, 917, 917, 1, 4},    {0x5BF26109U, 228, 228, 1, 2},
+    {0x5C5D3DB4U, 631, 226, 1, 2},    {0x5C871792U, 247, 247, 12, 3},
+    {0x5CD7B821U, 896, 896, 1, 4},    {0x5D2F7EF7U, 619, 226, 1, 2},
+    {0x5D813A4CU, 23, 22, 1, 0},      {0x5D9D6D69U, 346, 346, 2, 3},
+    {0x5E3F7DB3U, 1016, 1016, 1, 4},  {0x5E5E8D43U, 436, 436, 1, 0},
+    {0x5F3C8457U, 267, 267, 1, 1},    {0x5F8196AFU, 52, 52, 1, 1, 55},
+    {0x5F918C20U, 629, 27, 1, 2},     {0x60220C8BU, 81, 81, 15, 1},
+    {0x603E3ED9U, 935, 935, 1, 4},    {0x61CF02EFU, 371, 371, 4, 1},
+    {0x61EF4136U, 1095, 358, 3, 3},   {0x62271A18U, 603, 603, 22, 6},
+    {0x62773362U, 904, 398, 1, 3},    {0x62C93F4DU, 361, 357, 3, 3, 5},
+    {0x62D85FB3U, 266, 266, 1, 1},    {0x630C0C9EU, 545, 542, 4, 0},
+    {0x639D0874U, 377, 377, 1, 3, 5}, {0x63B9B3E3U, 440, 440, 2, 0},
+    {0x641A6E6BU, 1121, 1121, 4, 4},  {0x641B69A9U, 227, 227, 1, 2},
+    {0x6441D6F5U, 360, 360, 3, 3, 5}, {0x64700DC9U, 585, 565, 1, 0},
+    {0x6472AF91U, 919, 919, 1, 4},    {0x64B99B1DU, 1127, 1127, 4, 4},
+    {0x64CB7C1EU, 1123, 1123, 4, 4},  {0x64FF599CU, 45, 45, 4, 1},
+    {0x6506AEA8U, 493, 493, 3, 0},    {0x65532813U, 576, 565, 1, 0},
+    {0x657265CEU, 602, 602, 22, 6},   {0x657C5D71U, 471, 442, 1, 0},
+    {0x65AA9203U, 1101, 1101, 4, 4},  {0x65EB301FU, 420, 420, 3, 0},
+    {0x6605414BU, 447, 447, 1, 0},    {0x66D6053FU, 342, 342, 2, 3},
+    {0x674B961AU, 476, 447, 1, 0},    {0x67BB0655U, 353, 344, 2, 3, 5},
+    {0x67EBF930U, 703, 225, 1, 2},    {0x67EBF931U, 688, 225, 1, 2},
+    {0x67EBF933U, 718, 225, 1, 2},    {0x68145723U, 512, 512, 1, 0},
+    {0x68A28F6BU, 335, 335, 1, 3},    {0x68AB47FAU, 1044, 1044, 1, 4},
+    {0x68EE801FU, 607, 56, 1, 1},     {0x69B3F24EU, 1068, 376, 1, 3},
+    {0x69D86F40U, 220, 161, 5, 2},    {0x6A010122U, 900, 900, 1, 4},
+    {0x6A022B4CU, 730, 237, 4, 2},    {0x6A022B4EU, 700, 237, 4, 2},
+    {0x6A022B4FU, 715, 237, 4, 2},    {0x6A547DADU, 358, 358, 3, 3, 5},
+    {0x6A75085FU, 321, 321, 1, 6},    {0x6AB679A7U, 11, 11, 1, 1},
+    {0x6ADEC874U, 557, 226, 1, 2},    {0x6AEE6309U, 614, 232, 3, 2},
+    {0x6B405C8BU, 82, 82, 15, 1},     {0x6B519593U, 578, 565, 1, 0},
+    {0x6B554092U, 406, 366, 4, 3, 5}, {0x6B655768U, 801, 225, 1, 2},
+    {0x6B655769U, 802, 225, 1, 2},    {0x6B65576BU, 800, 225, 1, 2},
+    {0x6B65576EU, 803, 225, 1, 2},    {0x6B83E576U, 47, 47, 22, 1},
+    {0x6BF52632U, 608, 357, 3, 3, 5}, {0x6C33E433U, 448, 448, 4, 0},
+    {0x6CB6D5F3U, 343, 342, 2, 3},    {0x6CB90F30U, 906, 397, 1, 3},
+    {0x6D118B7DU, 971, 387, 1, 3},    {0x6D2A0665U, 141, 127, 9, 0},
+    {0x6E2B6B54U, 1088, 1088, 3, 4},  {0x6ED5D40CU, 498, 498, 3, 0},
+    {0x6F103C0FU, 563, 228, 1, 2},    {0x6FA0DF38U, 77, 77, 2, 1, 55},
+    {0x6FBBCDC8U, 352, 344, 2, 3, 5}, {0x6FCD5266U, 574, 565, 1, 0},
+    {0x6FF9660EU, 103, 103, 7, 0},    {0x7030FF48U, 50, 50, 2, 1, 55},
+    {0x707F616AU, 494, 494, 3, 0},    {0x70B9BCE5U, 644, 233, 3, 2},
+    {0x70D5CA4CU, 375, 370, 4, 5},    {0x70FC4FEDU, 744, 744, 1, 0},
+    {0x70FC4FEEU, 743, 743, 1, 0},    {0x70FC4FEFU, 742, 742, 1, 0},
+    {0x713088B0U, 760, 758, 4, 0},    {0x717A163AU, 1117, 1117, 4, 4},
+    {0x71A31BA8U, 65, 65, 1, 1, 55},  {0x71B20AF8U, 198, 195, 5, 0},
+    {0x71B20AF9U, 199, 195, 5, 0},    {0x71B20AFEU, 196, 195, 5, 0},
+    {0x71B20AFFU, 197, 195, 5, 0},    {0x72CEFA28U, 305, 305, 3, 1},
+    {0x72F9A7C0U, 41, 41, 4, 0},      {0x72F9A7C1U, 40, 40, 4, 0},
+    {0x730B3ED4U, 719, 226, 1, 2},    {0x730B3ED6U, 689, 226, 1, 2},
+    {0x730B3ED7U, 704, 226, 1, 2},    {0x7433B76CU, 941, 941, 1, 4},
+    {0x74611D89U, 14, 11, 1, 1},      {0x749E72DBU, 975, 327, 1, 3},
+    {0x74FACA07U, 1157, 1157, 5, 4},  {0x753E6CD5U, 302, 60, 3, 1},
+    {0x7540B569U, 154, 154, 10, 0},   {0x7568C4FBU, 907, 396, 1, 3},
+    {0x757D6EC5U, 964, 388, 1, 3},    {0x75880083U, 369, 369, 4, 3},
+    {0x75B918B8U, 562, 228, 1, 2},    {0x760F2C48U, 1081, 1081, 3, 4},
+    {0x76181371U, 1059, 378, 1, 3},   {0x76463F8AU, 615, 615, 1, 0},
+    {0x7678BCFCU, 350, 342, 2, 3, 5}, {0x76C649E0U, 1110, 1110, 4, 4},
+    {0x76FC5066U, 1138, 1138, 4, 4},  {0x7724F671U, 792, 792, 8, 3},
+    {0x77386707U, 56, 56, 1, 1, 55},  {0x774B738CU, 998, 998, 1, 4},
+    {0x7813A18AU, 591, 591, 21, 0},   {0x7882E199U, 500, 500, 3, 0},
+    {0x7891AA1CU, 788, 787, 8, 0},    {0x789EB910U, 835, 230, 2, 2},
+    {0x78B3EB47U, 887, 887, 1, 4},    {0x78C2E0A4U, 623, 623, 2, 0, 5},
+    {0x7926024AU, 370, 370, 4, 5},    {0x79E26E49U, 1079, 1079, 3, 4},
+    {0x79F88E78U, 595, 230, 2, 2},    {0x7A0F25FFU, 42, 42, 4, 0},
+    {0x7A35ADBAU, 427, 427, 12, 0},   {0x7A5372B5U, 269, 53, 1, 1},
+    {0x7A8C9B8AU, 28, 28, 2, 1},      {0x7AEFEF40U, 414, 414, 8, 0},
+    {0x7AFFAB48U, 510, 510, 2, 0},    {0x7B377C3BU, 341, 341, 2, 6},
+    {0x7B4DB3C9U, 318, 318, 4, 1},    {0x7B4F29BBU, 783, 783, 4, 0},
+    {0x7B54A1BBU, 382, 382, 1, 3, 5}, {0x7BFA334AU, 995, 995, 1, 4},
+    {0x7BFE7C01U, 620, 227, 1, 2},    {0x7C63AC39U, 770, 758, 4, 0},
+    {0x7C679701U, 508, 508, 2, 0},    {0x7C68CA15U, 365, 365, 4, 3},
+    {0x7CA0A7B3U, 155, 154, 10, 0},   {0x7CAD4BC9U, 355, 355, 3, 6},
+    {0x7DFCAFFCU, 275, 58, 1, 1},     {0x7E7FEEA8U, 542, 542, 4, 0},
+    {0x7E9FE403U, 646, 231, 3, 2},    {0x7F36B149U, 1084, 1084, 3, 4},
+    {0x7F94943CU, 774, 761, 4, 0},    {0x7FF847B8U, 73, 73, 4, 1, 55},
+    {0x8039D64CU, 953, 953, 1, 4},    {0x80B311B1U, 1097, 1097, 4, 4},
+    {0x81029D0AU, 537, 536, 2, 0},    {0x814627BFU, 799, 228, 1, 2},
+    {0x819D1B25U, 970, 331, 1, 3},    {0x81CF9C32U, 213, 213, 22, 1},
+    {0x81E9919DU, 612, 232, 3, 2},    {0x823A6C97U, 1029, 1029, 1, 4},
+    {0x825C16EDU, 1015, 1015, 1, 4},  {0x83211FEDU, 297, 69, 2, 1},
+    {0x83795946U, 753, 752, 8, 0},    {0x838CB8D8U, 1120, 1120, 4, 4},
+    {0x84003D1CU, 544, 542, 4, 0},    {0x84003D1DU, 543, 542, 4, 0},
+    {0x8405B64EU, 256, 256, 22, 1},   {0x84435BBDU, 396, 396, 1, 3, 5},
+    {0x84E745B2U, 598, 27, 1, 2},     {0x84EEB434U, 722, 229, 2, 2},
+    {0x84EEB436U, 692, 229, 2, 2},    {0x84EEB437U, 707, 229, 2, 2},
+    {0x85072D53U, 1007, 336, 1, 3},   {0x85230952U, 433, 433, 1, 0},
+    {0x855BF70DU, 152, 152, 10, 0},   {0x85B69A55U, 1014, 1014, 1, 4},
+    {0x85D311CAU, 384, 384, 1, 3, 5}, {0x86024A04U, 232, 232, 3, 2},
+    {0x86184C02U, 319, 319, 8, 1},    {0x862A8B7EU, 1006, 335, 1, 3},
+    {0x869A292CU, 675, 227, 1, 2},    {0x86A0B90DU, 408, 368, 4, 3, 5},
+    {0x86B1FAFBU, 13, 11, 1, 1},      {0x86CC2D8EU, 1115, 1115, 4, 4},
+    {0x86DEEF5DU, 432, 432, 3, 0},    {0x873B6389U, 456, 427, 12, 0},
+    {0x87693AD2U, 479, 450, 19, 0},   {0x8771D33AU, 1128, 1128, 4, 4},
+    {0x878D2015U, 506, 506, 1, 0},    {0x87AADB60U, 457, 428, 4, 0},
+    {0x87AC2003U, 299, 70, 2, 1},     {0x87CD3AE9U, 374, 374, 4, 1},
+    {0x87D9CA16U, 293, 55, 2, 1},     {0x87DACAC4U, 200, 200, 5, 0},
+    {0x88543EC3U, 1022, 1022, 1, 4},  {0x88641726U, 952, 952, 1, 4},
+    {0x8866FE3EU, 601, 601, 22, 6},   {0x88964714U, 324, 324, 1, 6},
+    {0x88F32E08U, 276, 56, 1, 1},     {0x892C6F4BU, 936, 936, 1, 4},
+    {0x894011D3U, 1092, 357, 3, 3},   {0x8953265EU, 518, 518, 1, 0},
+    {0x895ECE9BU, 524, 524, 5, 0},    {0x89764CC7U, 163, 157, 5, 1},
+    {0x8983669AU, 340, 340, 2, 6},    {0x89BA222FU, 390, 327, 1, 3, 5},
+    {0x8A02EBCAU, 1163, 1163, 5, 4},  {0x8A364583U, 27, 27, 1, 2},
+    {0x8A3A22B4U, 589, 589, 21, 0},   {0x8A738F8FU, 592, 229, 2, 2},
+    {0x8ABD3A1DU, 1104, 1104, 4, 4},  {0x8ABD416AU, 1108, 1108, 4, 4},
+    {0x8B4BC3D5U, 323, 323, 1, 6},    {0x8B856B03U, 392, 328, 1, 3, 5},
+    {0x8BD1E4C9U, 311, 68, 4, 1},     {0x8C2CCA33U, 289, 50, 2, 1},
+    {0x8CA48F33U, 736, 228, 1, 2},    {0x8D9D878BU, 95, 95, 7, 6},
+    {0x8DC85F30U, 690, 227, 1, 2},    {0x8DC85F31U, 705, 227, 1, 2},
+    {0x8DC85F32U, 720, 227, 1, 2},    {0x8DD433DFU, 1058, 1058, 1, 4},
+    {0x8E16CD2EU, 611, 233, 3, 2},    {0x8EEBDAACU, 757, 755, 8, 0},
+    {0x8F18CD83U, 1156, 1156, 5, 4},  {0x8F52275DU, 490, 490, 3, 0},
+    {0x90099292U, 597, 230, 2, 2},    {0x9041EE89U, 1049, 1049, 1, 4},
+    {0x9058D723U, 791, 791, 8, 6},    {0x90B8E716U, 451, 451, 5, 0},
+    {0x90BFDA9FU, 759, 758, 4, 0},    {0x90C357E4U, 1100, 1100, 4, 4},
+    {0x90CD93D8U, 149, 149, 17, 1},   {0x90F2E74EU, 1140, 1139, 4, 3},
+    {0x913AA612U, 761, 761, 4, 0},    {0x91B1EB20U, 460, 431, 11, 0},
+    {0x91BA11ADU, 485, 485, 19, 0},   {0x91BE3F03U, 961, 961, 1, 4},
+    {0x91D8D9D0U, 157, 157, 5, 1},    {0x9203AEF2U, 579, 565, 1, 0},
+    {0x920AA36AU, 183, 180, 5, 0},    {0x920AA36BU, 184, 180, 5, 0},
+    {0x920AA36CU, 181, 180, 5, 0},    {0x920AA36DU, 182, 180, 5, 0},
+    {0x9296B792U, 504, 504, 3, 0},    {0x92BD716AU, 751, 751, 3, 0},
+    {0x92F7A144U, 260, 260, 22, 1},   {0x931DC615U, 18, 11, 1, 1},
+    {0x935AF40DU, 605, 605, 22, 0},   {0x937502FCU, 430, 430, 3, 0},
+    {0x93BF386DU, 287, 287, 1, 1},    {0x93C68C6DU, 653, 226, 1, 2},
+    {0x9485B68DU, 682, 234, 4, 2},    {0x94872416U, 502, 502, 3, 0},
+    {0x953F1741U, 169, 169, 5, 0},    {0x95819CD5U, 205, 205, 5, 0},
+    {0x95948A18U, 138, 127, 9, 0},    {0x95948A19U, 139, 127, 9, 0},
+    {0x95948A1BU, 137, 127, 9, 0},    {0x960E9DECU, 981, 394, 1, 3},
+    {0x962D807AU, 892, 892, 1, 4},    {0x9676D102U, 122, 121, 16, 1},
+    {0x96FD9EF8U, 808, 229, 2, 2},    {0x96FD9EFAU, 810, 229, 2, 2},
+    {0x96FD9EFBU, 809, 229, 2, 2},    {0x96FD9EFCU, 812, 229, 2, 2},
+    {0x96FD9EFDU, 811, 229, 2, 2},    {0x972FCA73U, 960, 960, 1, 4},
+    {0x98861F2AU, 889, 889, 1, 4},    {0x988CF420U, 167, 166, 5, 0},
+    {0x988CF427U, 168, 166, 5, 0},    {0x988F1948U, 272, 63, 1, 1},
+    {0x98AA75EFU, 1046, 1046, 1, 4},  {0x991D77B6U, 57, 57, 3, 1, 55},
+    {0x996793A9U, 1018, 1018, 1, 4},  {0x997565B7U, 1008, 403, 1, 3},
+    {0x998A7E91U, 1028, 1028, 1, 4},  {0x99922208U, 1155, 1155, 5, 4},
+    {0x99BDAB3DU, 229, 229, 2, 2},    {0x99D28E2FU, 916, 916, 1, 4},
+    {0x9A451D42U, 1054, 1054, 1, 4},  {0x9A7AC553U, 444, 444, 4, 0},
+    {0x9AE00587U, 418, 418, 3, 0},    {0x9B3A9298U, 89, 89, 7, 1},
+    {0x9B9327ECU, 954, 954, 1, 4},    {0x9C1E5F83U, 939, 939, 1, 4},
+    {0x9C4F1FD6U, 185, 185, 5, 0},    {0x9C719F24U, 750, 750, 3, 0},
+    {0x9CC23CA3U, 879, 879, 1, 4},    {0x9CC298B8U, 913, 913, 1, 4},
+    {0x9CC40470U, 701, 223, 1, 2},    {0x9CC40471U, 686, 223, 1, 2},
+    {0x9CC40473U, 716, 223, 1, 2},    {0x9CD47828U, 6, 6, 12, 0},
+    {0x9D4B4BA5U, 1164, 1164, 5, 4},  {0x9D4CA314U, 87, 87, 7, 1},
+    {0x9D56A468U, 171, 170, 5, 0},    {0x9D56A469U, 172, 170, 5, 0},
+    {0x9D56A46EU, 173, 170, 5, 0},    {0x9D56A46FU, 174, 170, 5, 0},
+    {0x9D857512U, 274, 52, 1, 1},     {0x9E100A4DU, 962, 962, 1, 4},
+    {0x9E27FBA8U, 755, 755, 8, 0},    {0x9E5DCDF4U, 108, 108, 7, 0},
+    {0x9E88273CU, 216, 216, 5, 0},    {0x9E9A0AB4U, 60, 60, 3, 1, 55},
+    {0x9E9A9772U, 833, 230, 2, 2},    {0x9E9A9773U, 834, 230, 2, 2},
+    {0x9E9A9774U, 831, 230, 2, 2},    {0x9E9A9775U, 832, 230, 2, 2},
+    {0x9E9A9777U, 830, 230, 2, 2},    {0x9EDAB8F3U, 1065, 377, 1, 3},
+    {0x9EF38C06U, 434, 434, 1, 0},    {0x9F77E6E4U, 345, 344, 2, 3},
+    {0x9FA6D32DU, 627, 230, 2, 2},    {0x9FFC7326U, 230, 230, 2, 2},
+    {0xA001E806U, 934, 934, 1, 4},    {0xA03890CFU, 594, 229, 2, 2},
+    {0xA08600D8U, 567, 565, 1, 0},    {0xA08600D9U, 565, 565, 1, 0},
+    {0xA08600DAU, 571, 565, 1, 0},    {0xA08600DBU, 569, 565, 1, 0},
+    {0xA08600DCU, 575, 565, 1, 0},    {0xA08600DDU, 573, 565, 1, 0},
+    {0xA09BFA73U, 841, 231, 3, 2},    {0xA0A7BF4DU, 1116, 1116, 4, 4},
+    {0xA135209FU, 651, 27, 1, 2},     {0xA1A563A3U, 647, 231, 3, 2},
+    {0xA20C03EBU, 306, 306, 3, 1},    {0xA267B737U, 654, 227, 1, 2},
+    {0xA2CAEFDAU, 120, 119, 8, 1, 5}, {0xA30CDD67U, 1118, 1118, 4, 4},
+    {0xA32E952DU, 1069, 381, 1, 3},   {0xA3D4D0E6U, 449, 449, 2, 0},
+    {0xA41388D3U, 359, 359, 3, 3, 5}, {0xA41A31EEU, 225, 225, 1, 2},
+    {0xA4745341U, 338, 338, 1, 6},    {0xA501A0E3U, 990, 990, 1, 4},
+    {0xA514D630U, 210, 210, 22, 1},   {0xA54702EFU, 76, 76, 1, 1, 55},
+    {0xA54C03D1U, 920, 920, 1, 4},    {0xA578C313U, 58, 58, 1, 1, 55},
+    {0xA5849691U, 325, 325, 1, 6},    {0xA5D6899AU, 915, 915, 1, 4},
+    {0xA5FCBB00U, 940, 940, 1, 4},    {0xA666E81DU, 2, 2, 6, 1},
+    {0xA6D329A6U, 1040, 1040, 1, 4},  {0xA6E2207DU, 732, 27, 1, 2},
+    {0xA6F2A2CEU, 465, 436, 1, 0},    {0xA77FD210U, 424, 424, 3, 0},
+    {0xA7A3BA07U, 548, 547, 7, 0},    {0xA7C07E84U, 1098, 1098, 4, 4},
+    {0xA7C41991U, 43, 43, 4, 0},      {0xA8B24CD5U, 771, 758, 4, 0},
+    {0xA8C77605U, 391, 391, 1, 3, 5}, {0xA92ABFADU, 747, 747, 1, 0},
+    {0xA92ABFAEU, 746, 746, 1, 0},    {0xA9AE579DU, 1021, 1021, 1, 4},
+    {0xAAE75822U, 336, 336, 1, 3},    {0xAC0AE255U, 600, 600, 22, 6},
+    {0xAC5ED9FCU, 259, 259, 22, 1},   {0xAC6C3F8FU, 1111, 1111, 4, 4},
+    {0xACA0B0E4U, 1086, 1086, 3, 4},  {0xAD15B067U, 495, 495, 3, 0},
+    {0xAD21C9CFU, 789, 787, 8, 0},    {0xAD2D4EC5U, 1106, 1106, 4, 4},
+    {0xAD58F15DU, 996, 996, 1, 4},    {0xADF9F027U, 175, 175, 5, 0},
+    {0xAE72EF09U, 278, 71, 1, 1},     {0xAEE2DD8AU, 331, 331, 1, 3},
+    {0xAFA952AFU, 507, 507, 1, 0},    {0xAFC17283U, 118, 118, 7, 1},
+    {0xAFD48A4AU, 905, 399, 1, 3},    {0xB08EA38AU, 64, 64, 4, 6, 55},
+    {0xB0D667EAU, 1089, 358, 3, 3},   {0xB0FD7ECDU, 884, 884, 1, 4},
+    {0xB11AFBDFU, 226, 226, 1, 2},    {0xB14CE23FU, 317, 317, 4, 1},
+    {0xB18307CEU, 477, 448, 4, 0},    {0xB25E023CU, 643, 232, 3, 2},
+    {0xB2CF049FU, 153, 152, 10, 0},   {0xB2F6AFAEU, 972, 385, 1, 3},
+    {0xB316A4BEU, 540, 539, 3, 0},    {0xB3547A81U, 51, 51, 3, 1, 55},
+    {0xB37D2AC9U, 528, 528, 4, 0},    {0xB39DD6F2U, 1078, 346, 2, 3},
+    {0xB3BA8A13U, 403, 403, 1, 3, 5}, {0xB466D0DAU, 1009, 1009, 1, 4},
+    {0xB4CA37CDU, 401, 336, 1, 3, 5}, {0xB4E40828U, 668, 235, 4, 2},
+    {0xB51ECD6BU, 1035, 1035, 1, 4},  {0xB552F329U, 380, 380, 1, 3, 5},
+    {0xB5BDE950U, 1000, 1000, 1, 4},  {0xB5D269C1U, 554, 225, 1, 2},
+    {0xB5FB2EA2U, 162, 162, 5, 1},    {0xB6856D70U, 484, 484, 1, 0},
+    {0xB6F2427CU, 404, 364, 4, 3, 5}, {0xB7776C2CU, 1136, 1136, 4, 4},
+    {0xB7CF5C7AU, 253, 253, 13, 1},   {0xB8218A8CU, 536, 536, 2, 0},
+    {0xB8430548U, 112, 112, 7, 3},    {0xB843054BU, 113, 113, 7, 3},
+    {0xB86C9943U, 1099, 1099, 4, 4},  {0xB913ED3FU, 288, 288, 1, 1},
+    {0xB93D3358U, 140, 127, 9, 0},    {0xB94490E1U, 1113, 1113, 4, 4},
+    {0xB94CB8D1U, 667, 234, 4, 2},    {0xB9776015U, 1165, 1165, 5, 4},
+    {0xB9B2129EU, 142, 127, 9, 0},    {0xBA771EB7U, 781, 767, 4, 0},
+    {0xBAAE92BBU, 610, 233, 3, 2},    {0xBADDDBBDU, 419, 419, 3, 0},
+    {0xBB309B7EU, 1070, 379, 1, 3},   {0xBB606EB4U, 410, 410, 8, 0},
+    {0xBB628EA4U, 8, 8, 1, 0},        {0xBB7973D8U, 660, 227, 1, 2},
+    {0xBB8616D3U, 775, 761, 4, 0},    {0xBB9E35A6U, 271, 59, 1, 1},
+    {0xBBA93FF9U, 983, 387, 1, 3},    {0xBBB9C13EU, 48, 48, 4, 1},
+    {0xBBC387EFU, 334, 334, 1, 6},    {0xBBDC7FA5U, 49, 49, 4, 6},
+    {0xBCA4C639U, 426, 426, 1, 0},    {0xBD167A99U, 16, 11, 1, 1},
+    {0xBD22224AU, 992, 992, 1, 4},    {0xBD563A0CU, 517, 517, 1, 0},
+    {0xBE34AF92U, 633, 228, 1, 2},    {0xBEBDC610U, 840, 231, 3, 2},
+    {0xBEBDC611U, 839, 231, 3, 2},    {0xBEBDC614U, 836, 231, 3, 2},
+    {0xBEBDC616U, 838, 231, 3, 2},    {0xBEBDC617U, 837, 231, 3, 2},
+    {0xBF000366U, 356, 356, 3, 6},    {0xBF0C4B38U, 193, 190, 5, 0},
+    {0xBF0C4B39U, 194, 190, 5, 0},    {0xBF0C4B3EU, 191, 190, 5, 0},
+    {0xBF0C4B3FU, 192, 190, 5, 0},    {0xBF10C5BFU, 59, 59, 1, 1, 55},
+    {0xBF42175CU, 958, 958, 1, 4},    {0xBF689BF4U, 882, 882, 1, 4},
+    {0xBF6CA4FFU, 1003, 336, 1, 3},   {0xBF788BD4U, 473, 444, 4, 0},
+    {0xBF8AFAE2U, 541, 539, 3, 0},    {0xC04971D7U, 309, 66, 4, 1},
+    {0xC09624EFU, 1037, 1037, 1, 4},  {0xC0AC2439U, 119, 119, 8, 1},
+    {0xC0C0AD82U, 455, 426, 1, 0},    {0xC111B900U, 531, 531, 3, 0},
+    {0xC14EB626U, 483, 483, 1, 0},    {0xC14FF9EBU, 514, 514, 3, 0},
+    {0xC19B7F62U, 351, 342, 2, 3, 5}, {0xC20B5DDBU, 395, 395, 1, 3, 5},
+    {0xC2945556U, 411, 410, 8, 0},    {0xC2A365E8U, 804, 27, 1, 2},
+    {0xC2A365EAU, 806, 27, 1, 2},     {0xC2A365EBU, 805, 27, 1, 2},
+    {0xC2A365EDU, 807, 27, 1, 2},     {0xC2D4299EU, 948, 948, 1, 4},
+    {0xC3034C38U, 53, 53, 1, 1, 55},  {0xC306CC98U, 270, 74, 1, 1},
+    {0xC35AD812U, 1002, 335, 1, 3},   {0xC3846768U, 661, 228, 1, 2},
+    {0xC3BAAB29U, 974, 331, 1, 3},    {0xC41325D2U, 657, 27, 1, 2},
+    {0xC4148F00U, 1071, 382, 1, 3},   {0xC44603C9U, 428, 428, 4, 0},
+    {0xC49F5016U, 918, 918, 1, 4},    {0xC4B53AF1U, 257, 257, 22, 1},
+    {0xC4EA3DBAU, 943, 943, 1, 4},    {0xC4FBC913U, 327, 327, 1, 3},
+    {0xC647ACF5U, 678, 230, 2, 2},    {0xC6AC2BB6U, 123, 123, 9, 0},
+    {0xC6F1BE1CU, 912, 912, 1, 4},    {0xC758212EU, 503, 503, 3, 0},
+    {0xC7752C91U, 326, 326, 1, 6},    {0xC77A4C89U, 1119, 1119, 4, 4},
+    {0xC7A17330U, 461, 432, 3, 0},    {0xC7D1F4EEU, 793, 793, 8, 3},
+    {0xC7EB95DDU, 872, 235, 4, 2},    {0xC83D621AU, 268, 268, 1, 1},
+    {0xC8C30EE5U, 552, 225, 1, 2},    {0xC8E6294DU, 349, 346, 2, 3, 5},
+    {0xC8EC8969U, 768, 767, 4, 0},    {0xC8F95BFFU, 1080, 1080, 3, 4},
+    {0xC8FF1515U, 656, 223, 1, 2},    {0xC91E4B40U, 698, 235, 4, 2},
+    {0xC91E4B41U, 713, 235, 4, 2},    {0xC91E4B42U, 728, 235, 4, 2},
+    {0xC946EF89U, 1074, 1074, 2, 4},  {0xC94DFD32U, 1082, 1082, 3, 4},
+    {0xC9680E99U, 1025, 1025, 1, 4},  {0xC9BA010DU, 1094, 357, 3, 3},
+    {0xC9F80518U, 749, 749, 4, 6},    {0xCAEE6E8BU, 443, 443, 4, 0},
+    {0xCB05778AU, 637, 637, 1, 0},    {0xCB441A24U, 34, 34, 14, 1},
+    {0xCB4891D9U, 322, 322, 1, 6},    {0xCC0779F3U, 463, 434, 1, 0},
+    {0xCCB64E74U, 85, 85, 7, 1},      {0xCD6E0B2BU, 581, 565, 1, 0},
+    {0xCD7AC5D4U, 683, 235, 4, 2},    {0xCDEA681BU, 423, 423, 5, 0},
+    {0xCDF57243U, 878, 221, 5, 2},    {0xCE31F1F0U, 190, 190, 5, 0},
+    {0xCEA7516BU, 1131, 1131, 4, 4},  {0xCF65A15AU, 923, 923, 1, 4},
+    {0xCF7CBBB2U, 1010, 1010, 1, 4},  {0xCFC64104U, 911, 397, 1, 3},
+    {0xCFE0315EU, 1041, 1041, 1, 4},  {0xD0007C09U, 897, 897, 1, 4},
+    {0xD0A9D56AU, 903, 395, 1, 3},    {0xD13703C1U, 645, 233, 3, 2},
+    {0xD18E6767U, 946, 946, 1, 4},    {0xD2447052U, 1161, 1161, 5, 4},
+    {0xD2B59C79U, 1126, 1126, 4, 4},  {0xD2BD51DCU, 1112, 1112, 4, 4},
+    {0xD2CD058DU, 144, 144, 9, 6},    {0xD3384763U, 606, 305, 3, 1},
+    {0xD41AAC10U, 261, 261, 22, 1},   {0xD42C2175U, 313, 313, 4, 1},
+    {0xD53DF3D8U, 782, 782, 4, 0},    {0xD5BDAC49U, 767, 767, 4, 0},
+    {0xD6633DC8U, 222, 221, 5, 2},    {0xD66B8E0AU, 859, 234, 4, 2},
+    {0xD6A63D88U, 407, 367, 4, 3, 5}, {0xD6AB6B3AU, 1132, 1132, 4, 4},
+    {0xD7560F7FU, 1056, 1056, 1, 4},  {0xD7764614U, 92, 92, 7, 1},
+    {0xD7932A94U, 590, 590, 21, 0},   {0xD79B251DU, 417, 417, 3, 0},
+    {0xD81C9798U, 240, 161, 5, 2},    {0xD83110A6U, 1043, 1043, 1, 4},
+    {0xD842D200U, 100, 100, 7, 6},    {0xD873EF56U, 25, 22, 1, 0},
+    {0xD8986999U, 766, 764, 4, 0},    {0xD899253FU, 535, 535, 19, 0},
+    {0xD8A01826U, 1125, 1125, 4, 4},  {0xD906D0A6U, 91, 91, 7, 1},
+    {0xD93652B9U, 397, 397, 1, 3, 5}, {0xD9441623U, 1004, 403, 1, 3},
+    {0xD952C11CU, 529, 529, 1, 0},    {0xD9B5344AU, 15, 11, 1, 1},
+    {0xD9C272CBU, 777, 764, 4, 0},    {0xD9DB1B30U, 195, 195, 5, 0},
+    {0xDA4795C8U, 898, 898, 1, 4},    {0xDA6F567AU, 378, 378, 1, 3, 5},
+    {0xDA722C2DU, 982, 383, 1, 3},    {0xDA992F91U, 366, 366, 4, 3},
+    {0xDA9B1588U, 109, 109, 7, 0},    {0xDAB15376U, 1050, 1050, 1, 4},
+    {0xDB1BC2B7U, 648, 229, 2, 2},    {0xDB1D7EC8U, 1141, 1139, 4, 3},
+    {0xDB42F138U, 599, 599, 22, 6},   {0xDB577408U, 38, 37, 14, 1},
+    {0xDB5785CBU, 1149, 1148, 4, 3},  {0xDBD9819AU, 329, 329, 1, 6},
+    {0xDC0C3CAAU, 394, 394, 1, 3, 5}, {0xDC1737CAU, 685, 237, 4, 2},
+    {0xDC948CB1U, 1072, 377, 1, 3},   {0xDC9ACCA5U, 669, 236, 4, 2},
+    {0xDCA90485U, 332, 332, 1, 6},    {0xDCDBAB0EU, 772, 758, 4, 0},
+    {0xDD3AF12CU, 731, 223, 1, 2},    {0xDDA13731U, 663, 230, 2, 2},
+    {0xDE200437U, 829, 227, 1, 2},    {0xDE9B5A6FU, 462, 433, 1, 0},
+    {0xDEA1EB6CU, 724, 231, 3, 2},    {0xDEA1EB6EU, 694, 231, 3, 2},
+    {0xDEA1EB6FU, 709, 231, 3, 2},    {0xDEE2DFE6U, 254, 254, 22, 0},
+    {0xDF0B8288U, 1160, 1160, 5, 4},  {0xDF53CF75U, 372, 372, 4, 1},
+    {0xDF9A2791U, 231, 231, 3, 2},    {0xDFA39471U, 979, 391, 1, 3},
+    {0xDFB9F3F5U, 509, 509, 21, 0},   {0xDFD32DD4U, 480, 423, 5, 0},
+    {0xE08D5B0BU, 491, 491, 3, 0},    {0xE0FB6B14U, 1139, 1139, 4, 3},
+    {0xE14FAEC8U, 519, 519, 1, 0},    {0xE153A82DU, 966, 386, 1, 3},
+    {0xE154F3E2U, 1162, 1162, 5, 4},  {0xE1D15E7FU, 459, 430, 3, 0},
+    {0xE210B64DU, 1042, 1042, 1, 4},  {0xE211D26CU, 243, 243, 12, 3},
+    {0xE211D26FU, 244, 244, 12, 3},   {0xE29A7D1EU, 642, 232, 3, 2},
+    {0xE2D7949DU, 1153, 1153, 4, 3},  {0xE2E3DB19U, 942, 942, 1, 4},
+    {0xE2F0F519U, 636, 636, 1, 0},    {0xE2F0F51AU, 635, 635, 1, 0},
+    {0xE3029D28U, 492, 492, 3, 0},    {0xE34F469FU, 237, 237, 4, 2},
+    {0xE36F6CB4U, 676, 228, 1, 2},    {0xE42D38D7U, 283, 283, 1, 1},
+    {0xE48FF60CU, 932, 932, 1, 4},    {0xE497C16DU, 121, 121, 16, 1},
+    {0xE4A6080EU, 910, 399, 1, 3},    {0xE4E3C83FU, 146, 146, 17, 1},
+    {0xE53D42B3U, 217, 216, 5, 0},    {0xE575BA40U, 794, 228, 1, 2},
+    {0xE575BA42U, 796, 228, 1, 2},    {0xE575BA43U, 795, 228, 1, 2},
+    {0xE575BA44U, 798, 228, 1, 2},    {0xE575BA45U, 797, 228, 1, 2},
+    {0xE5773D00U, 429, 429, 1, 0},    {0xE5DC8E6FU, 238, 161, 5, 2},
+    {0xE5EA506EU, 588, 565, 1, 0},    {0xE5FBF1EFU, 143, 143, 9, 0},
+    {0xE6D20C29U, 980, 386, 1, 3},    {0xE6ED0216U, 80, 80, 15, 1},
+    {0xE737078DU, 219, 161, 5, 2},    {0xE741F9B5U, 577, 565, 1, 0},
+    {0xE743550DU, 853, 237, 4, 2},    {0xE77F40F8U, 817, 232, 3, 2},
+    {0xE77F40F9U, 818, 232, 3, 2},    {0xE77F40FDU, 814, 232, 3, 2},
+    {0xE77F40FEU, 815, 232, 3, 2},    {0xE77F40FFU, 816, 232, 3, 2},
+    {0xE84A8867U, 277, 277, 1, 1},    {0xE84C458EU, 469, 440, 2, 0},
+    {0xE8ABA41AU, 21, 21, 1, 0},      {0xE8ABA41BU, 20, 20, 1, 0},
+    {0xE8E73B66U, 681, 233, 3, 2},    {0xE90B732FU, 357, 357, 3, 3, 5},
+    {0xE9330DF7U, 586, 565, 1, 0},    {0xE954E549U, 1085, 1085, 3, 4},
+    {0xE96EA8D9U, 572, 565, 1, 0},    {0xE97E76D5U, 425, 425, 5, 0},
+    {0xE99B1210U, 866, 223, 1, 2},    {0xE9AF246DU, 348, 344, 2, 3, 5},
+    {0xE9B0449FU, 559, 227, 1, 2},    {0xE9E309B3U, 776, 764, 4, 0},
+    {0xEB1D0378U, 726, 233, 3, 2},    {0xEB1D037AU, 696, 233, 3, 2},
+    {0xEB1D037BU, 711, 233, 3, 2},    {0xEB327418U, 481, 425, 5, 0},
+    {0xEBE8E818U, 1147, 1145, 4, 3},  {0xEBF065A0U, 950, 950, 1, 4},
+    {0xEC70749BU, 9, 9, 1, 0},        {0xED20EAF0U, 1013, 1013, 1, 4},
+    {0xED5A458AU, 1, 1, 6, 1},        {0xEE303C2BU, 785, 784, 8, 0},
+    {0xEE94EB98U, 773, 761, 4, 0},    {0xEEA2009AU, 170, 170, 5, 0},
+    {0xEEFE7CA5U, 72, 72, 4, 1, 55},  {0xEF303B37U, 450, 450, 19, 0},
+    {0xEF68D192U, 156, 156, 22, 1},   {0xF07A75D3U, 265, 266, 1, 1},
+    {0xF0C316CCU, 570, 565, 1, 0},    {0xF1062EBDU, 925, 925, 1, 4},
+    {0xF1410711U, 893, 893, 1, 4},    {0xF1739DB6U, 930, 930, 1, 4},
+    {0xF18EDA50U, 641, 641, 2, 0, 5}, {0xF1B1E98EU, 553, 225, 1, 2},
+    {0xF1BA1726U, 416, 416, 22, 1},   {0xF2747E0DU, 150, 150, 22, 1},
+    {0xF383AD20U, 738, 230, 2, 2},    {0xF391F135U, 117, 115, 7, 3},
+    {0xF391F136U, 116, 114, 7, 3},    {0xF40B8FF6U, 1096, 360, 3, 3},
+    {0xF417E6CDU, 944, 944, 1, 4},    {0xF44B0312U, 201, 200, 5, 0},
+    {0xF44B0313U, 202, 200, 5, 0},    {0xF44B0314U, 203, 200, 5, 0},
+    {0xF44B0315U, 204, 200, 5, 0},    {0xF49BB0E5U, 997, 997, 1, 4},
+    {0xF5F25C0DU, 482, 451, 5, 0},    {0xF6744966U, 223, 223, 1, 2},
+    {0xF74050EEU, 251, 251, 13, 0},   {0xF764CA58U, 1152, 1151, 4, 3},
+    {0xF7E8ADC5U, 7, 7, 1, 0},        {0xF82C2085U, 764, 764, 4, 0},
+    {0xF858B379U, 10, 10, 1, 0},      {0xF86D716FU, 159, 159, 5, 1},
+    {0xF87556AAU, 453, 422, 12, 0},   {0xF8CBC2C6U, 1026, 1026, 1, 4},
+    {0xFA488C32U, 1151, 1151, 4, 3},  {0xFAA0E92CU, 314, 73, 4, 1},
+    {0xFAB45AD0U, 584, 565, 1, 0},    {0xFAE3B2C9U, 521, 521, 22, 0},
+    {0xFAEC10CAU, 921, 921, 1, 4},    {0xFB59329AU, 993, 993, 1, 4},
+    {0xFC383D2FU, 1047, 1047, 1, 4},  {0xFC62B530U, 55, 55, 2, 1, 55},
+    {0xFC8BA26FU, 70, 70, 2, 1, 55},  {0xFC9F78DEU, 409, 369, 4, 3, 5},
+    {0xFCDFCCCCU, 497, 497, 3, 0},    {0xFCFD6503U, 69, 69, 2, 1, 55},
+    {0xFD77546CU, 218, 218, 1, 0},    {0xFD9ABCB5U, 1031, 1031, 1, 4},
+    {0xFDE6EB35U, 658, 225, 1, 2},    {0xFDEF1D39U, 734, 226, 1, 2},
+    {0xFDFF0F71U, 224, 27, 1, 2},     {0xFE33B43CU, 263, 263, 22, 1},
+    {0xFE77744DU, 748, 748, 1, 0},    {0xFED660A7U, 1146, 1145, 4, 3},
+    {0xFF14DBA6U, 613, 232, 3, 2},    {0xFF534D45U, 885, 885, 1, 4},
+}};
+[[nodiscard]] inline const Rule* lookup_hash(std::uint32_t hash) noexcept {
+    std::size_t first{}, last{rules.size()};
+    while (first < last) {
+        const auto middle = first + (last - first) / 2;
+        if (rules[middle].hash < hash) {
+            first = middle + 1;
+        } else {
+            last = middle;
+        }
+    }
+    return first < rules.size() && rules[first].hash == hash ? &rules[first] : nullptr;
+}
+[[nodiscard]] inline const Rule* lookup(const Definition& row) noexcept {
+    const auto* rule = lookup_hash(row.hash);
+    // Experience anchors use native catalog ordinals, so a different catalog layout must be
+    // reviewed.
+    return rule != nullptr && rule->nativeIndex == row.index ? rule : nullptr;
+}
+} // namespace sunrise::state::build_data::activities::releases