瀏覽代碼

fix: hold the character-select screen instead of falling through to orbit

Without this, the sign-in boot step's enter handler clears its own stay
flag on the first frame, so the client always falls straight through the
character-select screen into orbit -- with whatever character happens to
be selected -- immediately after create or delete rather than showing the
updated roster. This is the piece client-stability-fixes carries that
character-lifecycle actually depends on for its own responses (which
correctly say nothing needs to be waited on) to be honored: the client
has to still be on the character-select step for "stay here" to mean
anything.

Ported standalone rather than pulling in the rest of the client-
stability-fixes branch, which bundles five unrelated bootflow fixes this
feature doesn't need.
T4tsuk3i 1 月之前
父節點
當前提交
2a80800494

+ 1 - 0
Sunrise/Sunrise.vcxproj

@@ -322,6 +322,7 @@
     </ClCompile>
     </ClCompile>
     <ClCompile Include="src\client\hooks\bootflow\bootflow_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\bootflow_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\bootflow_texture_override.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\bootflow_texture_override.cpp" />
+    <ClCompile Include="src\client\hooks\bootflow\character_select_hold.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\lifetime_gate_probe.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\lifetime_gate_probe.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\world_step.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\world_step.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\spawn\slice_set_sample.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\spawn\slice_set_sample.cpp" />

+ 5 - 2
Sunrise/src/client/hooks/bootflow/bootflow_hook_lifecycle.cpp

@@ -20,12 +20,15 @@ bool install() noexcept {
     const bool worldStep = install_world_step();
     const bool worldStep = install_world_step();
     const bool sliceSet = spawn::install_targets();
     const bool sliceSet = spawn::install_targets();
     const bool probe = install_lifetime_gate_probe();
     const bool probe = install_lifetime_gate_probe();
-    g_installed.store(worldStep || sliceSet || probe, std::memory_order_release);
-    return worldStep && sliceSet && probe;
+    const bool characterSelectHold = install_character_select_hold();
+    g_installed.store(worldStep || sliceSet || probe || characterSelectHold,
+                      std::memory_order_release);
+    return worldStep && sliceSet && probe && characterSelectHold;
 }
 }
 
 
 /** Detaches the probe and clears both accessors, in the reverse order of install. */
 /** Detaches the probe and clears both accessors, in the reverse order of install. */
 void uninstall() noexcept {
 void uninstall() noexcept {
+    uninstall_character_select_hold();
     uninstall_lifetime_gate_probe();
     uninstall_lifetime_gate_probe();
     spawn::uninstall_targets();
     spawn::uninstall_targets();
     uninstall_world_step();
     uninstall_world_step();

+ 108 - 0
Sunrise/src/client/hooks/bootflow/character_select_hold.cpp

@@ -0,0 +1,108 @@
+#include <array>
+#include <atomic>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string_view>
+
+#include "../../../core/logging/log.h"
+#include "../../hooking/detour.h"
+#include "internal.h"
+
+namespace sunrise::client::hooks::bootflow {
+namespace {
+
+/**
+ * The character sign-in step's enter handler, anchored on its two setup stores.
+ * Those write the step's latch field and the paired setup words, so no sibling step matches.
+ */
+constexpr std::string_view kEnterSignatureText =
+    "40 53 48 83 EC ? 48 8B D9 C7 41 38 FF FF FF FF 66 C7 41 3C 00 00 33 D2";
+/** Compiled pattern bytes of the signature text above. */
+constexpr auto kEnterSignature =
+    signature<signature_length(kEnterSignatureText)>(kEnterSignatureText);
+
+/** Fields of the boot step this hook touches, as byte offsets from the step's own base. */
+struct StepLayout {
+    /**
+     * The stay flag. The exit gate needs it non-zero to stay on the select screen. The enter
+     * handler clears it, so the step would skip selection on its first frame.
+     */
+    static constexpr std::size_t stayFlag = 72;
+};
+
+/** Value the step's own listener writes to hold the screen. Matched here. */
+constexpr std::uint8_t kHold = 1;
+
+using EnterHandler = void(__fastcall*)(std::byte*);
+
+hooking::detour::Handle g_handle{};
+std::atomic<EnterHandler> g_original{nullptr};
+std::atomic_bool g_reported{false};
+
+/**
+ * Holds the character-select screen after the step sets itself up.
+ * Runs once per boot on the boot-step thread, after the original writes its defaults, so the
+ * original cannot overwrite this write.
+ * @param step Borrowed boot-step base pointer.
+ */
+__declspec(noinline) void __fastcall enter_handler(std::byte* step) noexcept {
+    const EnterHandler original = g_original.load(std::memory_order_acquire);
+    if (original != nullptr) {
+        original(step);
+    }
+    if (step == nullptr) {
+        return;
+    }
+    std::memcpy(step + StepLayout::stayFlag, &kHold, sizeof kHold);
+    if (!g_reported.exchange(true, std::memory_order_relaxed)) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::info,
+                         "ev=bootflow stage=character_select result=held");
+    }
+}
+
+} // namespace
+
+/**
+ * Attaches the character-select hold: without it, the sign-in boot step clears its own stay
+ * flag on entry and the screen falls straight through to orbit, whatever character was picked,
+ * created, or removed. The client's exit gate reads this flag every frame and only leaves the
+ * screen once something (the player picking a character) sets it again.
+ * @return True when the target is found and the detour attaches.
+ */
+bool install_character_select_hold() noexcept {
+    if (g_handle.attached) {
+        return true;
+    }
+    std::byte* const target = scan_main_image_unique(kEnterSignature, "character_signin_enter");
+    if (target == nullptr) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::warn,
+                         "ev=bootflow stage=character_select result=fail reason=target");
+        return false;
+    }
+    const hooking::detour::Spec spec{target, reinterpret_cast<void*>(&enter_handler)};
+    if (!hooking::detour::install(spec, g_handle)) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::warn,
+                         "ev=bootflow stage=character_select result=fail reason=attach");
+        return false;
+    }
+    g_original.store(reinterpret_cast<EnterHandler>(g_handle.original), std::memory_order_release);
+    core::log::write(core::log::Channel::client,
+                     core::log::Level::info,
+                     "ev=bootflow stage=character_select result=ok");
+    return true;
+}
+
+/** Detaches the character-select hold. */
+void uninstall_character_select_hold() noexcept {
+    if (g_handle.attached) {
+        (void)hooking::detour::uninstall(g_handle);
+    }
+    g_original.store(nullptr, std::memory_order_release);
+    g_reported.store(false, std::memory_order_release);
+}
+
+} // namespace sunrise::client::hooks::bootflow

+ 10 - 0
Sunrise/src/client/hooks/bootflow/internal.h

@@ -28,4 +28,14 @@ void uninstall_world_step() noexcept;
 /** Detaches the lifetime gate probe. */
 /** Detaches the lifetime gate probe. */
 void uninstall_lifetime_gate_probe() noexcept;
 void uninstall_lifetime_gate_probe() noexcept;
 
 
+/**
+ * Attaches the character-select hold, so the sign-in boot step stays on the character-select
+ * screen instead of falling through to orbit on its own.
+ * @return True when the target is found and the detour attaches.
+ */
+[[nodiscard]] bool install_character_select_hold() noexcept;
+
+/** Detaches the character-select hold. */
+void uninstall_character_select_hold() noexcept;
+
 } // namespace sunrise::client::hooks::bootflow
 } // namespace sunrise::client::hooks::bootflow