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

cleanup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt6a81fNQmbDNhbeAb7XuR
stan 22 часов назад
Родитель
Сommit
13de67ed68

+ 2 - 4
Sunrise/Sunrise.vcxproj

@@ -878,8 +878,6 @@
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_scriptable_world_links.cpp" />
     <ClCompile Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.cpp" />
-    <ClCompile Include="src\server\ui\activity_override\activity_override_panel.cpp" />
-    <ClCompile Include="src\server\ui\activity_override\activity_override_lists.cpp" />
     <ClCompile Include="src\server\transport\bap_listener.cpp" />
     <ClCompile Include="src\server\bap\bap_route.cpp" />
     <ClCompile Include="src\server\bap\bap_activity_link_view.cpp" />
@@ -1598,8 +1596,6 @@
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_behavior_view.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_scriptable_world_links.h" />
     <ClInclude Include="src\server\ui\activity_host\activity_host_sdk_squad_actions.h" />
-    <ClInclude Include="src\server\ui\activity_override\activity_override_panel.h" />
-    <ClInclude Include="src\server\ui\activity_override\activity_override_lists.h" />
     <ClInclude Include="src\server\bap\runtime.h" />
     <ClInclude Include="src\server\bap\activity_mission_seed_lease.h" />
     <ClInclude Include="src\server\bap\internal.h" />
@@ -2161,6 +2157,8 @@
     <ClInclude Include="src\client\ui\mission_launch\mission_launch_model.h" />
     <ClInclude Include="src\client\ui\mission_launch\mission_launch_cards.h" />
     <ClCompile Include="src\client\ui\mission_launch\mission_launch_art.cpp" />
+    <ClCompile Include="src\client\ui\mission_launch\mission_launch_lists.cpp" />
+    <ClInclude Include="src\client\ui\mission_launch\mission_launch_lists.h" />
     <ClInclude Include="src\client\ui\mission_launch\mission_launch_art.h" />
     <ClInclude Include="src\client\ui\mission_launch\mission_launch_manual.h" />
     <ClInclude Include="src\client\ui\mission_launch\mission_launch_detail_style.h" />

+ 377 - 159
Sunrise/src/client/activity/mission_launch.cpp

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

+ 10 - 4
Sunrise/src/client/activity/mission_launch.h

@@ -25,17 +25,23 @@ struct Snapshot {
     bool manual{};
     state::activity::forced::ForcedDestination destination{};
 };
+/** Finds the Director's selection entry points. @return True when every one was found. */
+[[nodiscard]] bool install() noexcept;
+/** Forgets the entry points, so a pending request fails instead of calling into a gone image. */
+void uninstall() noexcept;
 /** Render thread: enqueue one immutable public activity ordinal. No native calls here. */
 [[nodiscard]] bool request(std::uint16_t index) noexcept;
-/** Enqueues a copied draft. The game-frame owner validates and publishes it immediately before
- * launch. */
+/** Enqueues a copied draft; the game-frame owner validates and publishes it before launch. */
 [[nodiscard]] bool
 request_manual(std::uint16_t index,
                const state::activity::forced::ForcedDestination& destination) noexcept;
 /** @return A synchronized copy of the latest request and its native transition status. */
 [[nodiscard]] Snapshot snapshot() noexcept;
-/** Existing game-frame owner only. Orbit launch uses retail selection wrappers unchanged. */
-void poll() noexcept;
+/**
+ * Advances the pending request. Game thread only, once per frame.
+ * @param step The client's current boot-flow step.
+ */
+void poll(std::int32_t step) noexcept;
 /** @return A static UI message explaining the request result or next required action. */
 [[nodiscard]] const char* description(Status status) noexcept;
 } // namespace sunrise::client::activity::mission_launch

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

@@ -54,9 +54,10 @@ std::atomic_uint64_t g_publishedSliceSetTick{0};
 
 /** Publishes the client's own boot-flow step. */
 void poll_world_step() noexcept {
-    g_publishedStep.store(read_step(), std::memory_order_relaxed);
+    const std::int32_t step = read_step();
+    g_publishedStep.store(step, std::memory_order_relaxed);
     g_publishedTick.store(GetTickCount64(), std::memory_order_release);
-    client::activity::mission_launch::poll();
+    client::activity::mission_launch::poll(step);
 }
 
 /** Publishes the client's current local slice-set index. */

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

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

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

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

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

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

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

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

+ 10 - 30
Sunrise/src/client/ui/mission_launch/mission_launch_manual.h

@@ -2,16 +2,16 @@
 
 #include <imgui.h>
 
-#include "../../../server/ui/activity_override/activity_override_lists.h"
 #include "../../activity/mission_launch_options.h"
+#include "mission_launch_lists.h"
 #include "mission_launch_model.h"
 
 namespace sunrise::client::ui::mission_launch::manual {
 namespace options = client::activity::mission_launch;
-namespace picker = server::ui::activity_override;
+namespace picker = lists;
 namespace forced = state::activity::forced;
 
-// A local draft, never the standalone override panel's published state or picker globals.
+// A local draft, never the published override state.
 inline picker::Lists g_lists{};
 inline options::ManualScratch g_validation{};
 inline forced::ForcedDestination g_value{};
@@ -20,28 +20,7 @@ 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;
-}
-
+/** Restarts the draft on one destination and rebuilds its dependent rows. */
 inline void select_destination(std::string_view name) noexcept {
     g_value = {};
     if (name.size() > g_value.packageName.size()) {
@@ -62,6 +41,7 @@ inline void select_transport(std::uint16_t index) noexcept {
     g_value.activityIndex = g_value.hasActivityIndex ? index : 0;
 }
 
+/** Picks a bubble, drops the dependent slice and spawn picks, and defaults the slice. */
 inline void select_bubble(std::uint8_t bubble) noexcept {
     g_value.hasBubble = true;
     g_value.bubble = bubble;
@@ -109,6 +89,7 @@ inline void start_custom() noexcept {
     select_destination({});
 }
 
+/** Draws the destination picker of the custom launch page. */
 inline void custom_activity() noexcept {
     picker::refresh_activities(g_lists);
     if (g_revision != state::build_data::scenario_layout_count()) {
@@ -144,6 +125,7 @@ inline void custom_activity() noexcept {
     }
 }
 
+/** Draws the launch-route picker: the catalog rows whose package is the drafted destination. */
 inline void transport_picker(std::span<const Activity> rows,
                              std::span<const bool> available) noexcept {
     ImGui::TextUnformatted("Activity variant");
@@ -179,6 +161,7 @@ inline void transport_picker(std::span<const Activity> rows,
     }
 }
 
+/** Draws the bubble, slice and spawn pickers for the drafted destination. */
 inline void arrival_fields() noexcept {
     if (!g_enabled) {
         return;
@@ -249,11 +232,9 @@ inline void arrival_fields() noexcept {
     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();
+            spawnPreview = g_lists.spawns[i].data();
         }
     }
     if (ImGui::BeginCombo("##manual_spawn", spawnPreview)) {
@@ -276,8 +257,7 @@ inline void arrival_fields() noexcept {
                     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(),
+            if (ImGui::Selectable(g_lists.spawns[i].data(),
                                   g_value.hasSpawnSetHash
                                       && g_value.spawnSetHash == g_lists.spawnHashes[i])) {
                 g_value.hasSpawnSetHash = true;

+ 104 - 35
Sunrise/src/middleware/content/packages/tables/activity_presentation_table.h

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

+ 37 - 10
Sunrise/src/middleware/content/packages/tables/activity_table.h

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

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

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

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

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

+ 0 - 1
Sunrise/src/server/ui/runtime/server_ui_module_runtime.cpp

@@ -20,7 +20,6 @@ core::ui::modules::registry::PageRegistration g_hostPage;
 
 /** @return True when the Server module owns its Core UI registry slot. */
 bool initialize() noexcept {
-    // Activity Launcher exposes manual overrides; keep the old panel unregistered.
     return g_hostPage.acquire(core::ui::modules::Owner::server,
                               kHostStableId,
                               kHostDisplayName,