Browse Source

implements basic profile_setup web service

ehko 2 tuần trước cách đây
mục cha
commit
0bf0ccec41

+ 2 - 0
Sunrise/resources/default_settings.json

@@ -37,6 +37,7 @@
     },
     "suppress_peer_relay": true,
     "fade_release": true,
+    "skip_profile_setup": false,
     "region_private": false,
     "pin_replicated_record": true,
     "hold_spawn": true,
@@ -155,6 +156,7 @@
     },
     "account": {
       "primary_soid": "0x9EAA300100100100",
+      "profile_setup_completed": true,
       "dismantle_rewards": [
         { "definition_hash": "0xBC53E66E", "quantity": 25, "rarity": "common" },
         { "definition_hash": "0xBC53E66E", "quantity": 50, "rarity": "uncommon" },

+ 8 - 1
Sunrise/src/client/hooks/bootflow/profile_setup_skip.cpp

@@ -6,6 +6,7 @@
 #include <string_view>
 
 #include "../../../core/logging/log.h"
+#include "../../../core/settings/settings.h"
 #include "../../hooking/detour.h"
 #include "internal.h"
 
@@ -72,7 +73,13 @@ std::atomic_bool g_reported{false};
  */
 __declspec(noinline) char __fastcall update(std::byte* step) noexcept {
     const Update original = g_original.load(std::memory_order_acquire);
-    if (step != nullptr) {
+    if (!core::settings::get().client.skipProfileSetup) {
+        if (!g_reported.exchange(true, std::memory_order_relaxed)) {
+            core::log::write(core::log::Channel::client,
+                             core::log::Level::info,
+                             "ev=bootflow stage=profile_setup result=disabled");
+        }
+    } else if (step != nullptr) {
         std::uint32_t state = 0;
         std::memcpy(&state, step + StepLayout::state, sizeof state);
         if (is_waiting(state)) {

+ 6 - 0
Sunrise/src/core/settings/client/client_settings_parser.cpp

@@ -11,6 +11,7 @@ bool Parser::client_settings(client::Settings& output) noexcept {
     bool hasUserInterface = false;
     bool hasExternalServer = false;
     bool hasFadeRelease = false;
+    bool hasSkipProfileSetup = false;
     bool hasRegionPrivate = false;
     bool hasSkipOrbitCinematicWait = false;
     bool hasSuppressPeerRelay = false;
@@ -43,6 +44,11 @@ bool Parser::client_settings(client::Settings& output) noexcept {
                 return false;
             }
             hasFadeRelease = true;
+        } else if (key == "skip_profile_setup") {
+            if (hasSkipProfileSetup || !boolean(candidate.skipProfileSetup)) {
+                return false;
+            }
+            hasSkipProfileSetup = true;
         } else if (key == "region_private") {
             if (hasRegionPrivate || !boolean(candidate.regionPrivate)) {
                 return false;

+ 6 - 0
Sunrise/src/core/settings/client/definition.h

@@ -24,6 +24,12 @@ struct Settings {
      * and leaves the world black. On by default.
      */
     bool fadeRelease{true};
+    /**
+     * Skips the one-time profile setup bootflow screens.
+     * Off by default: the server-authored account completion byte should drive normal behavior.
+     * Keep this only as a fallback for incomplete profile-state implementations.
+     */
+    bool skipProfileSetup{false};
     /**
      * Reports a public region as private to the region transition.
      * On, a public region loads solo. Off, it waits for a public activity host, which is the

+ 6 - 0
Sunrise/src/core/settings/state_settings.cpp

@@ -261,6 +261,7 @@ bool Parser::account(state::AccountState& output) noexcept {
         return false;
     }
     bool hasPrimarySoid = false;
+    bool hasProfileSetupCompleted = false;
     bool hasSettings = false;
     bool hasDismantleRewards = false;
     if (consume('}')) {
@@ -276,6 +277,11 @@ bool Parser::account(state::AccountState& output) noexcept {
                 return false;
             }
             hasPrimarySoid = true;
+        } else if (key == "profile_setup_completed") {
+            if (hasProfileSetupCompleted || !boolean(output.profileSetupCompleted)) {
+                return false;
+            }
+            hasProfileSetupCompleted = true;
         } else if (key == "settings") {
             if (hasSettings || !account_settings(output.settings)) {
                 return false;

+ 1 - 0
Sunrise/src/middleware/datagen/family4/account/account_encoder.cpp

@@ -85,6 +85,7 @@ bool encode(const state::AccountState& state, std::span<std::byte> output) noexc
     layout::Object object{};
     object.accountSoid = state.primarySoid;
     object.selectedCharacterSoid = state::account::selected_character_soid(state);
+    object.profileSetupCompleted = state.profileSetupCompleted ? 1U : 0U;
     if (!roster::initialize(state, object.roster)
         || !preferences::encode(state.settings, object.preferences, object.bindings)) {
         return false;

+ 10 - 3
Sunrise/src/middleware/datagen/family4/account/layout.h

@@ -28,8 +28,10 @@ inline constexpr std::size_t kAccountHeaderTailSize = 128;
 inline constexpr std::size_t kRosterSelectionPaddingSize = 8;
 /** 24 reserved bytes separate selection from publicity deadlines. */
 inline constexpr std::size_t kSelectionPublicityPaddingSize = 24;
-/** 24 reserved bytes separate seen messages from account preferences. */
-inline constexpr std::size_t kSeenPreferencesPaddingSize = 24;
+/** 20 reserved bytes separate seen messages from the profile-setup completion byte. */
+inline constexpr std::size_t kSeenProfileSetupPaddingSize = 20;
+/** 3 reserved bytes align account preferences after the profile-setup completion byte. */
+inline constexpr std::size_t kProfileSetupPreferencesPaddingSize = 3;
 /** 607 reserved bytes separate preference and keybinding records. */
 inline constexpr std::size_t kPreferencesBindingsPaddingSize = 607;
 /** 456 reserved bytes follow the replicated keybinding record. */
@@ -68,6 +70,8 @@ inline constexpr std::size_t kSelectedCharacterSoidOffset = 1'832;
 inline constexpr std::size_t kPublicityExpiriesOffset = 1'864;
 /** The seen-message bit bank follows all fixed publicity deadlines. */
 inline constexpr std::size_t kSeenMessagesOffset = 2'888;
+/** One byte at native offset 0xB90 records whether the one-time profile setup is complete. */
+inline constexpr std::size_t kProfileSetupCompletedOffset = 2'960;
 /** The native preference record follows the seen-message padding. */
 inline constexpr std::size_t kPreferencesOffset = 2'964;
 /** The replicated keybinding record follows its fixed preference padding. */
@@ -131,7 +135,9 @@ struct Object {
     std::array<std::byte, kSelectionPublicityPaddingSize> selectionPublicityPadding{};
     std::array<std::uint64_t, kPublicityExpiryCapacity> publicityExpiries{};
     std::array<std::byte, kSeenMessageByteCount> seenMessages{};
-    std::array<std::byte, kSeenPreferencesPaddingSize> seenPreferencesPadding{};
+    std::array<std::byte, kSeenProfileSetupPaddingSize> seenProfileSetupPadding{};
+    std::uint8_t profileSetupCompleted{};
+    std::array<std::byte, kProfileSetupPreferencesPaddingSize> profileSetupPreferencesPadding{};
     preferences::Record preferences{};
     std::array<std::byte, kPreferencesBindingsPaddingSize> preferencesBindingsPadding{};
     preferences::BindingsRecord bindings{};
@@ -165,6 +171,7 @@ static_assert(offsetof(Object, roster) == kRosterOffset);
 static_assert(offsetof(Object, selectedCharacterSoid) == kSelectedCharacterSoidOffset);
 static_assert(offsetof(Object, publicityExpiries) == kPublicityExpiriesOffset);
 static_assert(offsetof(Object, seenMessages) == kSeenMessagesOffset);
+static_assert(offsetof(Object, profileSetupCompleted) == kProfileSetupCompletedOffset);
 static_assert(offsetof(Object, preferences) == kPreferencesOffset);
 static_assert(offsetof(Object, bindings) == kBindingsOffset);
 static_assert(offsetof(Object, profileItemCount) == kProfileItemCountOffset);

+ 136 - 1
Sunrise/src/server/web_service/web_service_runtime.cpp

@@ -147,6 +147,122 @@ bool encode_echo(const middleware::web_service::Message& message,
         message, ws::ResponseShape::generic, ws::StatusResponse{}, response, written);
 }
 
+/** Narrow semantic result from the prefix of reflected WS-701 schema 0x80807603. */
+struct ProfileSetupMarker {
+    bool present{};
+    bool completed{};
+};
+
+/** Reads the presence bit that precedes every optional WS-701 schema node. */
+[[nodiscard]] bool read_ws701_presence(middleware::encoding::bits::Reader& reader,
+                                       bool& present) noexcept {
+    std::uint64_t value = 0;
+    if (!reader.read(1, value)) {
+        return false;
+    }
+    present = value != 0;
+    return true;
+}
+
+/** Consumes one optional fixed-width field without retaining it. */
+[[nodiscard]] bool skip_ws701_optional(middleware::encoding::bits::Reader& reader,
+                                       std::size_t widthBits) noexcept {
+    bool present = false;
+    return read_ws701_presence(reader, present) && (!present || reader.skip(widthBits));
+}
+
+/**
+ * Reads only enough of WS-701 schema 0x80807603 to reach preference path 0.1.1.0.
+ *
+ * PR #71 maps that first preference scalar as the one-bit profile-setup marker. Everything after
+ * it belongs to the broader settings-write implementation and is deliberately left to that work.
+ * This function therefore validates the complete prefix, not the remainder of the request.
+ */
+[[nodiscard]] bool parse_profile_setup_marker(const middleware::web_service::Message& message,
+                                              ProfileSetupMarker& output) noexcept {
+    output = {};
+    if (message.opcode != messages::opcode701::kOpcode) {
+        return false;
+    }
+
+    middleware::encoding::bits::Reader reader(message.payload);
+    bool present = false;
+
+    // 0.0? client metadata.
+    if (!read_ws701_presence(reader, present)) {
+        return false;
+    }
+    if (present) {
+        // 0.0.0? [128] optional 64-bit publicity expiries.
+        bool publicityPresent = false;
+        if (!read_ws701_presence(reader, publicityPresent)) {
+            return false;
+        }
+        if (publicityPresent) {
+            for (std::size_t index = 0; index < 128; ++index) {
+                if (!skip_ws701_optional(reader, 64)) {
+                    return false;
+                }
+            }
+        }
+
+        // 0.0.1? [13] required 32-bit seen-message values.
+        bool seenMessagesPresent = false;
+        if (!read_ws701_presence(reader, seenMessagesPresent)
+            || (seenMessagesPresent && !reader.skip(13U * 32U))) {
+            return false;
+        }
+    }
+
+    // 0.1? account data.
+    bool accountPresent = false;
+    if (!read_ws701_presence(reader, accountPresent)) {
+        return false;
+    }
+    if (!accountPresent) {
+        return true;
+    }
+
+    // 0.1.0? [2] optional calibration vectors, each containing two required real32 values.
+    bool calibrationPresent = false;
+    if (!read_ws701_presence(reader, calibrationPresent)) {
+        return false;
+    }
+    if (calibrationPresent) {
+        for (std::size_t index = 0; index < 2; ++index) {
+            bool vectorPresent = false;
+            if (!read_ws701_presence(reader, vectorPresent)
+                || (vectorPresent && !reader.skip(2U * 32U))) {
+                return false;
+            }
+        }
+    }
+
+    // 0.1.1? preference record.
+    bool preferencesPresent = false;
+    if (!read_ws701_presence(reader, preferencesPresent)) {
+        return false;
+    }
+    if (!preferencesPresent) {
+        return true;
+    }
+
+    // 0.1.1.0? one-bit profile-setup marker.
+    if (!read_ws701_presence(reader, output.present)) {
+        return false;
+    }
+    if (!output.present) {
+        return true;
+    }
+
+    std::uint64_t completed = 0;
+    if (!reader.read(1, completed)) {
+        return false;
+    }
+    output.completed = completed != 0;
+    return true;
+}
+
 /**
  * Issues the family-5 server clock the Client extrapolates its family-5 time from.
  * The wire field counts whole seconds. A repeated value reads as no change and stalls the
@@ -296,6 +412,7 @@ bool consume(std::span<const std::byte> request,
     // a valid no-op heartbeat, so that one success is tracked separately from mutation presence.
     bool dispatched = true;
     bool acceptedWithoutMutation = false;
+    bool profileSetupRefused = false;
     if (message.opcode == messages::opcode504::kOpcode) {
         select_character(message, outcome);
     } else if (message.opcode == messages::opcode402::kOpcode) {
@@ -315,6 +432,24 @@ bool consume(std::span<const std::byte> request,
     } else if (message.opcode == messages::opcode701::kOpcode) {
         const state::SettingsUpdateDisposition disposition = mutate_settings(message, outcome);
         acceptedWithoutMutation = disposition == state::SettingsUpdateDisposition::acceptedNoChange;
+        // The completion marker is applied here. The shared status path below reports the result.
+        ProfileSetupMarker marker{};
+        const bool parsed = parse_profile_setup_marker(message, marker);
+        if (!parsed) {
+            // Preserve Sunrise's existing WS-701 success behavior outside this narrow feature.
+            // PR #71 owns complete settings-write validation and can later subsume this prefix.
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             "ev=ws701 stage=profile_setup result=ignored reason=prefix_parse");
+        } else if (marker.present && marker.completed) {
+            if (!state::complete_profile_setup()) {
+                profileSetupRefused = true;
+            } else {
+                core::log::write(core::log::Channel::server,
+                                 core::log::Level::info,
+                                 "ev=ws701 stage=profile_setup result=complete marker=1");
+            }
+        }
     } else if (message.opcode == messages::opcode1820::kOpcode) {
         acquire_item(message, outcome);
     } else {
@@ -329,7 +464,7 @@ bool consume(std::span<const std::byte> request,
         // Nothing is published from here. A staged mutation re-encodes this with its own revision.
         status.value = middleware::web_service::kNoFamily4Publication;
     }
-    if (dispatched && !prepared && !acceptedWithoutMutation) {
+    if ((dispatched && !prepared && !acceptedWithoutMutation) || profileSetupRefused) {
         status.code = middleware::web_service::kRefusedStatusCode;
     }
     if (!middleware::web_service::encode_response(message, shape, status, response, written)) {

+ 2 - 0
Sunrise/src/state/account/account_state.h

@@ -181,6 +181,8 @@ struct AccountState {
     std::size_t profileItemCount{};
     std::array<CharacterState, kCharacterCapacity> characters{};
     std::size_t characterCount{};
+    /** True after this account has completed the client's one-time profile setup flow. */
+    bool profileSetupCompleted{};
     account::settings::AccountSettings settings;
 };
 

+ 8 - 0
Sunrise/src/state/runtime/runtime.h

@@ -345,6 +345,14 @@ void publish_sign_in_time(std::uint64_t seconds) noexcept;
  */
 [[nodiscard]] bool set_primary_soid(std::uint64_t primarySoid) noexcept;
 
+/**
+ * Permanently closes the process-local one-time profile-setup gate for the active account.
+ *
+ * The transition is monotonic: repeated profile-setting writes after completion are harmless.
+ * @return False only when no complete active account can be updated.
+ */
+[[nodiscard]] bool complete_profile_setup() noexcept;
+
 /**
  * Moves the selection to one authored character.
  * The Client names its pick only in the select-character request, so this is where a player's

+ 21 - 0
Sunrise/src/state/runtime/state_account_runtime.cpp

@@ -266,6 +266,27 @@ bool set_primary_soid(std::uint64_t primarySoid) noexcept {
     return true;
 }
 
+/** Closes the account's one-time profile-setup gate. */
+bool complete_profile_setup() noexcept {
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState& accountState = runtime::storage::g_state.account;
+    if (accountState.primarySoid == 0 || !account::valid(accountState)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return false;
+    }
+
+    const bool changed = !accountState.profileSetupCompleted;
+    accountState.profileSetupCompleted = true;
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+
+    if (changed) {
+        core::log::write(core::log::Channel::state,
+                         core::log::Level::info,
+                         "ev=profile_setup stage=complete result=ok");
+    }
+    return true;
+}
+
 /** Moves the selection to one authored character. */
 bool set_selected_character(std::uint64_t characterSoid, bool& changed) noexcept {
     changed = false;