فهرست منبع

Add runtime Activity Launcher to the HUD

Add expansion, season and event browsing, search, activity details, and
validated manual arrival selection through native launch requests.
Extract labels, descriptions and artwork from installed packages on each
process launch; keep only numeric release and identity mappings compiled.

Integrate catalog startup, renderer lifetime and game-thread requests.
Hide the legacy Activity tab while retaining its override implementation.

Validation: Release x64 build; clang-format, clang-tidy and clangd;
8,319 UI frames, 32,823 argument checks and 1,170 unchanged numeric mappings.
Thomas Shields 1 روز پیش
والد
کامیت
2ea83da362
29فایلهای تغییر یافته به همراه3344 افزوده شده و 26 حذف شده
  1. 2 0
      .gitignore
  2. 23 0
      Sunrise/Sunrise.vcxproj
  3. 331 0
      Sunrise/src/client/activity/mission_launch.cpp
  4. 41 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. 2 0
      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. 88 0
      Sunrise/src/client/ui/mission_launch/mission_launch_art.cpp
  15. 21 0
      Sunrise/src/client/ui/mission_launch/mission_launch_art.h
  16. 88 0
      Sunrise/src/client/ui/mission_launch/mission_launch_cards.h
  17. 56 0
      Sunrise/src/client/ui/mission_launch/mission_launch_detail_style.h
  18. 295 0
      Sunrise/src/client/ui/mission_launch/mission_launch_manual.h
  19. 322 0
      Sunrise/src/client/ui/mission_launch/mission_launch_model.h
  20. 507 0
      Sunrise/src/client/ui/mission_launch/mission_launch_panel.cpp
  21. 6 0
      Sunrise/src/client/ui/mission_launch/mission_launch_panel.h
  22. 9 2
      Sunrise/src/client/ui/runtime/client_ui_module_runtime.cpp
  23. 121 0
      Sunrise/src/middleware/content/packages/tables/activity_presentation_table.h
  24. 91 0
      Sunrise/src/middleware/content/packages/tables/activity_table.h
  25. 7 23
      Sunrise/src/server/ui/runtime/server_ui_module_runtime.cpp
  26. 22 0
      Sunrise/src/state/build_data/activities/activity_artwork.h
  27. 106 0
      Sunrise/src/state/build_data/activities/activity_catalog.cpp
  28. 61 0
      Sunrise/src/state/build_data/activities/activity_catalog.h
  29. 623 0
      Sunrise/src/state/build_data/activities/activity_releases.h

+ 2 - 0
.gitignore

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

+ 23 - 0
Sunrise/Sunrise.vcxproj

@@ -2155,5 +2155,28 @@
     <None Include="resources\database\account_settings_schema.sql" />
     <None Include="resources\database\account_settings_defaults.sql" />
   </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" />
+    <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" />
 </Project>

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

@@ -0,0 +1,331 @@
+#include "mission_launch.h"
+
+#include <Windows.h>
+
+#include <array>
+#include <bit>
+#include <cstdio>
+#include <cstring>
+
+#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 "mission_launch_options.h"
+
+namespace sunrise::client::activity::mission_launch {
+namespace {
+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.
+using World = std::uintptr_t(__fastcall*)();
+using Ready = bool(__fastcall*)(std::uintptr_t);
+using Record = std::uintptr_t(__fastcall*)(std::uint32_t);
+using Construct = void*(__fastcall*)(void*, std::uint32_t, std::int16_t);
+using Valid = bool(__fastcall*)(const void*);
+using Name = const char*(__fastcall*)(std::int16_t);
+using Clear = void(__fastcall*)();
+using Select = void(__fastcall*)(std::uint8_t, const void*);
+using Commit = void(__fastcall*)(std::int32_t);
+using Step = std::int32_t(__fastcall*)();
+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;
+}
+template <class T> bool read(std::uintptr_t address, T& value) noexcept {
+    SIZE_T copied{};
+    return address != 0
+           && ReadProcessMemory(GetCurrentProcess(),
+                                std::bit_cast<const void*>(address),
+                                &value,
+                                sizeof(value),
+                                &copied)
+                  != FALSE
+           && copied == sizeof(value);
+}
+template <class F>
+F resolve(std::uintptr_t base, std::uintptr_t rva, std::array<unsigned char, 8> expected) noexcept {
+    std::array<unsigned char, 8> actual{};
+    return read(base + rva, actual) && actual == expected ? std::bit_cast<F>(base + rva) : nullptr;
+}
+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, 512> 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=%08X status=%u detail=%s",
+                            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),
+                            description(status))
+            : std::snprintf(line.data(),
+                            line.size(),
+                            "ev=mission_launch activity=%u status=%u detail=%s",
+                            state.index,
+                            static_cast<unsigned>(status),
+                            description(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)});
+    }
+}
+} // namespace
+Snapshot snapshot() noexcept {
+    AcquireSRWLockShared(&g_lock);
+    const auto result = g_state;
+    ReleaseSRWLockShared(&g_lock);
+    return result;
+}
+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;
+}
+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;
+}
+void poll() noexcept {
+    const auto state = snapshot();
+    if (!state.busy) {
+        return;
+    }
+    // Requests are immutable until this owner completes them; the timestamp follows that lock.
+    AcquireSRWLockShared(&g_lock);
+    const auto started = g_requestedAt;
+    ReleaseSRWLockShared(&g_lock);
+    const auto base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
+    const auto step =
+        resolve<Step>(base, 0xE2D510, {0x48, 0x83, 0xEC, 0x28, 0xE8, 0x07, 0x83, 0x00});
+    if (!step) {
+        finish(Status::nativeUnavailable);
+        return;
+    }
+    const auto currentStep = step();
+    const auto rows = state::build_data::activities::entries();
+    if (state.status == Status::queued) {
+        g_leftOrbit = g_leftOrbit || currentStep != 29;
+        const auto session = newest_joined_session();
+        const auto& destination = session.destination;
+        if (g_leftOrbit && currentStep == 38 && session.sessionId != 0
+            && !state::activity::same_binding(session, g_previousSession)
+            && state.index < rows.size()
+            && destination.activityIndex == static_cast<std::int16_t>(state.index)
+            && destination.packageNameLength
+                   == (state.manual ? destination_name(state.destination)
+                                    : rows[state.index].name())
+                          .size()
+            && std::memcmp(destination.packageName.data(),
+                           state.manual ? state.destination.packageName.data()
+                                        : rows[state.index].package.data(),
+                           destination.packageNameLength)
+                   == 0
+            && (!state.manual
+                || ((!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
+                                   == (state.destination.hasSpawnSetHash
+                                           ? state.destination.spawnSetHash
+                                           : forced::kAbsentSpawnSetHash)))))) {
+            finish(Status::arrived);
+            return;
+        }
+        if (GetTickCount64() - started > 120000) {
+            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;
+        }
+    }
+    // Captured retail setup:orbit is 29. In-world exit/reclassification belongs to the native
+    // activity lifecycle and is deliberately not synthesized by this UI request adapter.
+    if (currentStep != 29) {
+        finish(Status::returnToOrbit);
+        return;
+    }
+    const auto world =
+        resolve<World>(base, 0xC03430, {0x40, 0x56, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x8B});
+    const auto sessionReady =
+        resolve<Ready>(base, 0x1788810, {0x83, 0xB9, 0x6C, 0x08, 0x00, 0x00, 0x00, 0x0F});
+    const auto memberReady =
+        resolve<Ready>(base, 0x178D740, {0x4C, 0x8B, 0xC1, 0x48, 0x63, 0x89, 0x3C, 0xE9});
+    const auto record =
+        resolve<Record>(base, 0xBFA030, {0x40, 0x53, 0x48, 0x83, 0xEC, 0x20, 0x8B, 0xD9});
+    const auto construct =
+        resolve<Construct>(base, 0xC061D0, {0x48, 0x89, 0x5C, 0x24, 0x08, 0x48, 0x89, 0x74});
+    const auto valid =
+        resolve<Valid>(base, 0x4D5460, {0x0F, 0xB6, 0x11, 0xB0, 0x01, 0x80, 0xFA, 0xFF});
+    const auto name =
+        resolve<Name>(base, 0xDDECA0, {0x48, 0x89, 0x5C, 0x24, 0x08, 0x57, 0x48, 0x83});
+    const auto clear =
+        resolve<Clear>(base, 0xBF95D0, {0x48, 0x83, 0xEC, 0x38, 0xE8, 0xE7, 0xE0, 0x7F});
+    const auto select =
+        resolve<Select>(base, 0xBFB1F0, {0x48, 0x89, 0x5C, 0x24, 0x08, 0x48, 0x89, 0x74});
+    const auto commit =
+        resolve<Commit>(base, 0xBF97D0, {0x89, 0x4C, 0x24, 0x08, 0x48, 0x83, 0xEC, 0x38});
+    if (!world || !sessionReady || !memberReady || !record || !construct || !valid || !name
+        || !clear || !select || !commit) {
+        finish(Status::nativeUnavailable);
+        return;
+    }
+    const auto manager = world();
+    std::int32_t primary{}, sessionState{}, member{};
+    if (!manager || !read(manager + 0x10, primary) || primary < 0 || primary > 3) {
+        finish(Status::notReady);
+        return;
+    }
+    const auto session = manager + 0x18 + static_cast<std::uintptr_t>(primary) * 0x1C8A0;
+    if (!read(session + 0x1AEF8, sessionState) || sessionState < 4 || sessionState > 9
+        || !sessionReady(session) || !memberReady(session) || !read(session + 0xE93C, member)
+        || member < 0 || member >= 12) {
+        finish(Status::notReady);
+        return;
+    }
+    const auto current = record(static_cast<std::uint32_t>(member));
+    std::uint8_t launchState{};
+    if (!current || !read(current + 0xA33, launchState) || launchState >= 3) {
+        finish(Status::notReady);
+        return;
+    }
+    const auto index = static_cast<std::int16_t>(state.index);
+    std::array<char, 40> nativeName{};
+    const auto package = reinterpret_cast<std::uintptr_t>(name(index));
+    bool nameMatches = package != 0;
+    for (std::size_t i = 0; nameMatches && i <= rows[state.index].name().size(); ++i) {
+        nameMatches =
+            read(package + i, nativeName[i]) && nativeName[i] == rows[state.index].package[i];
+    }
+    if (!nameMatches) {
+        finish(Status::descriptorRejected);
+        return;
+    }
+    alignas(16) std::array<std::byte, 0x120> selection{};
+    if (construct(selection.data(), 0, index) != selection.data()) {
+        finish(Status::descriptorRejected);
+        return;
+    }
+    std::int16_t source{}, destination{};
+    std::memcpy(&source, selection.data() + 2, sizeof(source));
+    std::memcpy(&destination, selection.data() + 4, sizeof(destination));
+    if (source != index || destination != index || selection[0] != std::byte{}
+        || !valid(selection.data())) {
+        finish(Status::descriptorRejected);
+        return;
+    }
+    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)) {
+        finish(Status::manualRejected);
+        return;
+    }
+    clear();
+    select(0, selection.data());
+    commit(1);
+    finish(Status::queued);
+}
+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

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

@@ -0,0 +1,41 @@
+#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{};
+};
+/** 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 immediately 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;
+/** Existing game-frame owner only. Orbit launch uses retail selection wrappers unchanged. */
+void poll() 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 "../../../../middleware/content/packages/reader/reader.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 "../../activity/activity_catalog_build.h"
 #include "../../activity/entity_position_profile_build.h"
 #include "../../hash_names/hash_name_build.h"
 #include "../../scenarios/scenario_build.h"
@@ -47,7 +49,9 @@ bool ready() noexcept {
     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::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());
 }
 
 /** Publishes the dense item table from the installed packages, once. */
@@ -70,6 +74,8 @@ bool build() noexcept {
     // storage. Both are independent of the item table, so a failure here leaves it alone.
     {
         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::scenarios::build(packageSource, storage.scratch);
         (void)content::spawn_sets::build(packageSource, storage.scratch);

+ 2 - 0
Sunrise/src/client/hooks/bootflow/world_step.cpp

@@ -5,6 +5,7 @@
 #include <string_view>
 
 #include "../../../core/logging/log.h"
+#include "../../activity/mission_launch.h"
 #include "bootflow_hook_lifecycle.h"
 #include "internal.h"
 #include "spawn/slice_set_sample.h"
@@ -55,6 +56,7 @@ std::atomic_uint64_t g_publishedSliceSetTick{0};
 void poll_world_step() noexcept {
     g_publishedStep.store(read_step(), std::memory_order_relaxed);
     g_publishedTick.store(GetTickCount64(), std::memory_order_release);
+    client::activity::mission_launch::poll();
 }
 
 /** 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 "../../../ui/activity/authored_placement_marker.h"
 #include "../../../ui/activity/authored_spatial_overlay.h"
+#include "../../../ui/mission_launch/mission_launch_art.h"
 #include "../../teleport/runtime.h"
 #include "../input/input.h"
 #include "graphics_renderer_report.h"
@@ -141,6 +142,7 @@ void render_frame_locked() noexcept {
     ImGui_ImplDX11_NewFrame();
     ImGui_ImplWin32_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
     // 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.

+ 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/runtime/ui_visibility_runtime.h"
+#include "../../../ui/mission_launch/mission_launch_art.h"
 #include "../../cursor/runtime.h"
 #include "../../inactivity/inactivity_override.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. */
 void release_resources(Resources& resources) noexcept {
+    client::ui::mission_launch::art::release();
     world_lines::release();
     release_render_target(resources);
     textures::release_logo_sheet(resources.logoSheet);

+ 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

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

@@ -0,0 +1,295 @@
+#pragma once
+
+#include <imgui.h>
+
+#include "../../../server/ui/activity_override/activity_override_lists.h"
+#include "../../activity/mission_launch_options.h"
+#include "mission_launch_model.h"
+
+namespace sunrise::client::ui::mission_launch::manual {
+namespace options = client::activity::mission_launch;
+namespace picker = server::ui::activity_override;
+namespace forced = state::activity::forced;
+
+// A local draft, never the standalone override panel's published state or picker globals.
+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{};
+
+/** The shared picker has two compiled name aliases; this panel displays only extracted names. */
+inline picker::Label spawn_label(std::size_t index) noexcept {
+    picker::Label label{};
+    if (index >= g_lists.spawnCount) {
+        return label;
+    }
+    state::build_data::hash_names::Name extracted{};
+    const bool named = state::build_data::find_hash_name(g_lists.spawnHashes[index], extracted)
+                       && extracted.nameLength > 0 && extracted.nameLength <= extracted.name.size();
+    const std::string_view markers(g_lists.spawns[index].data());
+    (void)std::snprintf(label.data(),
+                        label.size(),
+                        "0x%08X%s%.*s%s%s",
+                        g_lists.spawnHashes[index],
+                        named ? "  " : "",
+                        named ? static_cast<int>(extracted.nameLength) : 0,
+                        extracted.name.data(),
+                        contains(markers, "(candidate)") ? "  (candidate)" : "",
+                        contains(markers, "(not loaded)") ? "  (not loaded)" : "");
+    return label;
+}
+
+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;
+}
+
+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({});
+}
+
+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();
+    }
+}
+
+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();
+    }
+}
+
+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";
+    picker::Label spawnValue{};
+    for (std::size_t i = 0; i < g_lists.spawnCount; ++i) {
+        if (g_value.hasSpawnSetHash && g_value.spawnSetHash == g_lists.spawnHashes[i]) {
+            spawnValue = spawn_label(i);
+            spawnPreview = spawnValue.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);
+            const auto label = spawn_label(i);
+            if (ImGui::Selectable(label.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/ui_module_descriptor.h"
+#include "../mission_launch/mission_launch_panel.h"
 #include "../movement/movement_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_playerPage;
+core::ui::modules::registry::PageRegistration g_activityLauncherPage;
 
 } // 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 {
     // Registered after movement, which is the order the menu lists them in.
     const bool movementOwned = g_movementPage.acquire(
         core::ui::modules::Owner::client, kMovementStableId, kMovementDisplayName, &movement::draw);
     const bool playerOwned = g_playerPage.acquire(
         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. */
 void shutdown() noexcept {
+    g_activityLauncherPage.release();
     g_playerPage.release();
     g_movementPage.release();
 }

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

@@ -0,0 +1,121 @@
+#pragma once
+#include "activity_table.h"
+
+namespace sunrise::middleware::content::packages::tables::activities {
+struct StringRef {
+    std::uint32_t bank{0xFFFFU}, hash{0x811C9DC5U};
+};
+struct DisplayRefs {
+    StringRef title{}, description{}, type{}, expansion{};
+    std::uint16_t category{0xFFFF};
+    std::uint32_t style{0x811C9DC5};
+};
+/** Client slot 3 uses the exact same ordinal AND authored identity as public slot 4. */
+[[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, 8, a) || a.count != rows.size() || output.size() < rows.size()
+        || !find_array_at(types, 8, t) || t.count > 256 || a.dataOffset > display.size()
+        || a.count > (display.size() - a.dataOffset) / 16 || t.dataOffset > types.size()
+        || t.count > (types.size() - t.dataOffset) / 128) {
+        return false;
+    }
+    for (std::size_t i = 0; i < rows.size(); ++i) {
+        const auto entry = a.dataOffset + i * 16;
+        std::uint32_t hash{};
+        std::size_t record{}, d{};
+        output[i] = {};
+        if (!read(display, entry, hash) || hash != rows[i].hash
+            || !relative(display, entry + 8, record) || !relative(display, record, d)
+            || !read(display, d + 4, output[i].title)
+            || !read(display, d + 12, output[i].description)
+            || !read(display, d + 20, output[i].expansion)
+            || !read(display, record + 8, output[i].category)
+            || !read(display, d + 32, output[i].style)) {
+            return false;
+        }
+        if (rows[i].nativeType < t.count
+            && !read(types, t.dataOffset + rows[i].nativeType * 128 + 4, output[i].type)) {
+            return false;
+        }
+    }
+    return true;
+}
+/** Decode one bank-scoped English string, with bounded combinations, parts and UTF-8 shifting.
+ * Never resolves a hash against another bank; oversize/malformed text is unavailable, not
+ * truncated. */
+[[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 == 0x811C9DC5U || !find_array_at(container, 8, hashes)
+        || !find_array_at(language, 72, combinations) || hashes.count != combinations.count
+        || hashes.dataOffset > container.size()
+        || hashes.count > (container.size() - hashes.dataOffset) / 4
+        || combinations.dataOffset > language.size()
+        || combinations.count > (language.size() - combinations.dataOffset) / 16) {
+        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 * 4, value) && value == hash) {
+            ordinal = i;
+            break;
+        }
+    }
+    if (ordinal == hashes.count) {
+        return false;
+    }
+    const auto combination = combinations.dataOffset + ordinal * 16;
+    std::int64_t parts{};
+    std::size_t first{}, written{};
+    if (!read(language, combination + 8, parts) || parts < 0 || parts > 32) {
+        return false;
+    }
+    if (parts == 0) {
+        return true;
+    }
+    if (!relative(language, combination, first)
+        || static_cast<std::uint64_t>(parts) > (language.size() - first) / 32) {
+        return false;
+    }
+    for (std::size_t p = 0; p < static_cast<std::size_t>(parts); ++p) {
+        const auto part = first + p * 32;
+        std::size_t data{};
+        std::uint16_t bytes{}, shift{};
+        if (!relative(language, part + 8, data) || !read(language, part + 20, bytes)
+            || !read(language, part + 24, 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 < 0xC0 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4;
+            if (width > bytes - i || c >= 0xF5) {
+                output[0] = '\0';
+                return false;
+            }
+            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

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

@@ -0,0 +1,91 @@
+#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;
+
+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;
+}
+[[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();
+}
+/** Parse dense ordinals, validate both copies of each identity, retain missing package names. */
+[[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) / 16) {
+        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 * 16;
+        std::size_t record{};
+        std::uint32_t identity{};
+        if (!read(bytes, entry, row.hash) || row.hash == 0 || !relative(bytes, entry + 8, record)
+            || bytes.size() - record < 0xE4 || !read(bytes, record, identity)
+            || identity != row.hash || !read(bytes, record + 0xDC, row.gameplaySettingsHash)
+            || !read(bytes, record + 0xDA, row.nativeType)
+            || !read(bytes, record + 0xE0, row.destination)) {
+            return false;
+        }
+        std::int64_t nameDelta{};
+        if (!read(bytes, record + 0x68, nameDelta)) {
+            return false;
+        }
+        if (nameDelta != 0) {
+            std::size_t name{};
+            if (!relative(bytes, record + 0x68, 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

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

@@ -5,49 +5,33 @@
 #include "../../../core/ui/modules/registry/ui_module_registry.h"
 #include "../../../core/ui/modules/ui_module_descriptor.h"
 #include "../activity_host/activity_host_panel.h"
-#include "../activity_override/activity_override_panel.h"
 
 namespace sunrise::server::ui::runtime {
 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. */
 constexpr std::string_view kHostStableId = "server.activity_host";
 /** Short menu label for the Activity Host page. */
 constexpr std::string_view kHostDisplayName = "Activity Host";
 
-core::ui::modules::registry::PageRegistration g_overridePage;
 core::ui::modules::registry::PageRegistration g_hostPage;
 
 } // namespace
 
 /** @return True when the Server module owns its Core UI registry slot. */
 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;
+    // Activity Launcher exposes manual overrides; keep the old panel unregistered.
+    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. */
 void shutdown() noexcept {
     g_hostPage.release();
-    g_overridePage.release();
 }
 
 } // 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