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

Address emote-wheel PR review: scope the slot-14 fallback, fix boot ordering, harden the migration

- Gate the "no native equipment slot" fallback to the Emotes collection
  item's exact definition hash (3183180185) via one shared helper,
  state::account::inventory::resolve_native_equipment_slot(), used by all
  three call sites instead of three copies of the same loose check. Any
  other slotless character item is now rejected rather than aliasing the
  emote slot.
- Call ensure_character_emote_collection() from
  family4_snapshot_preparer.cpp, at the same boundary as
  ensure_profile_item_identities(), so the migration is guaranteed to run
  before the first (and every full-resync) Family-4 snapshot regardless of
  cache-hit vs. first-run extraction boot ordering.
- Fix ensure_character_emote_collection() returning true when the
  post-mutation account actually failed validation, and add a bounds
  check on the per-character inventory serial before incrementing it.
- Validate the collection item's own definition/detail before migrating
  (no native slot, exactly 4 ordinary socket lanes), validate every
  default plug hash is installed and allowed in its lane, and for an
  already-equipped item validate/repair its real socket contents instead
  of trusting a definition-hash match alone.
Millie 3 недель назад
Родитель
Сommit
3897d9cb9c

+ 5 - 9
Sunrise/src/middleware/datagen/family4/loadout/loadout_item_resolver.cpp

@@ -136,11 +136,14 @@ bool resolve_item(const authored_inventory::Item& authored,
     build_details::Definition itemDetail{};
     build_buckets::Descriptor bucket{};
     build_socket_lists::Definition socketList{};
+    std::uint8_t nativeEquipmentSlot = 0;
     if (!state::build_data::find_item_definition_hash(authored.definitionHash, itemDefinition)
         || !state::build_data::find_configured_item_detail(itemDefinition.definitionIndex,
                                                            itemDetail)
         || itemDefinition.bucketId != itemDetail.bucketId
-        || (itemDetail.equipmentSlot.has_value() && *itemDetail.equipmentSlot < 0)
+        || !authored_inventory::resolve_native_equipment_slot(
+            authored.definitionHash, itemDetail.equipmentSlot, nativeEquipmentSlot)
+        || static_cast<std::size_t>(nativeEquipmentSlot) >= build_details::kEquipmentSlotCount
         || !state::build_data::find_inventory_bucket_descriptor(itemDetail.bucketId, bucket)
         || bucket.arraySelector != build_buckets::ArraySelector::character
         || !state::build_data::find_socket_entry_list(itemDetail.socketEntryListIndex, socketList)
@@ -152,14 +155,7 @@ bool resolve_item(const authored_inventory::Item& authored,
 
     Candidate candidate{};
     candidate.bucket = bucket;
-    // The "Emotes" collection item's real content carries no native equipment-slot mapping at
-    // all, unlike every other character-scoped item. It is equipped in place of the individual
-    // emote (state::ensure_character_emote_collection), so it takes that slot's own native
-    // number here.
-    constexpr std::uint8_t kEmoteCollectionNativeSlot = 14;
-    candidate.item.equipmentSlot =
-        itemDetail.equipmentSlot.has_value() ? static_cast<std::uint8_t>(*itemDetail.equipmentSlot)
-                                             : kEmoteCollectionNativeSlot;
+    candidate.item.equipmentSlot = nativeEquipmentSlot;
     candidate.item.mutationSerial = authored.mutationSerial;
     candidate.item.flags = authored.flags;
     if (!resolve_quantity(authored, itemDetail, candidate.item.quantity)

+ 5 - 12
Sunrise/src/server/bap/encrypted/push/queuez/queuez_banner_push.cpp

@@ -343,25 +343,18 @@ bool append_socket_appearance_refresh_notification(
     if (target.instanceSoid != mutation.targetInstanceSoid) {
         return false;
     }
-    // The "Emotes" collection item's real content carries no native equipment-slot mapping at
-    // all, unlike every other character-scoped item. Kept self-consistent with the loadout
-    // resolver's own fallback: the emote slot's own native number, since that is where it is
-    // equipped.
-    constexpr std::uint8_t kEmoteCollectionNativeSlot = 14;
     state::build_data::items::details::Definition detail{};
+    std::uint8_t nativeEquipmentSlot = 0;
     if (!state::build_data::find_configured_item_detail(mutation.targetDefinitionIndex, detail)
         || detail.definitionIndex != mutation.targetDefinitionIndex
         || detail.definitionHash != mutation.targetDefinitionHash
         || detail.bucketId != mutation.targetBucketId
-        || (detail.equipmentSlot.has_value() && *detail.equipmentSlot < 0)
-        || (detail.equipmentSlot.has_value()
-            && static_cast<std::size_t>(*detail.equipmentSlot)
-                   >= state::build_data::items::details::kEquipmentSlotCount)) {
+        || !state::account::inventory::resolve_native_equipment_slot(
+            mutation.targetDefinitionHash, detail.equipmentSlot, nativeEquipmentSlot)
+        || static_cast<std::size_t>(nativeEquipmentSlot)
+               >= state::build_data::items::details::kEquipmentSlotCount) {
         return false;
     }
-    const std::uint8_t nativeEquipmentSlot = detail.equipmentSlot.has_value()
-                                                 ? static_cast<std::uint8_t>(*detail.equipmentSlot)
-                                                 : kEmoteCollectionNativeSlot;
     snapshot::Prepared prepared{};
     if (!snapshot::prepare_character_appearance_refresh(
             scratch,

+ 7 - 0
Sunrise/src/server/bap/encrypted/push/snapshot/family4_snapshot_preparer.cpp

@@ -68,6 +68,13 @@ bool prepare(Scratch& scratch,
     if (!state::ensure_profile_item_identities()) {
         return report_failure("profile_identities");
     }
+    // The investment-refresh migration runs on the content-extraction path, which is not
+    // guaranteed to finish before the first Family-4 subscription is served on a cache-hit boot.
+    // Repeating it here, idempotently, is the only boundary that is actually ordered ahead of
+    // every possible first image.
+    if (!state::ensure_character_emote_collection()) {
+        return report_failure("emote_collection");
+    }
     const state::AccountState account = state::account_snapshot();
     if (!state::account::valid(account)) {
         return report_failure("account_state");

+ 18 - 0
Sunrise/src/state/account/inventory/inventory_state.cpp

@@ -45,6 +45,24 @@ std::optional<EquipmentSlot> slot_from_name(std::string_view name) noexcept {
     return std::nullopt;
 }
 
+/** Resolves the native equipment slot a configured item detail occupies. */
+bool resolve_native_equipment_slot(std::uint32_t definitionHash,
+                                   const std::optional<std::int8_t>& detailEquipmentSlot,
+                                   std::uint8_t& nativeSlot) noexcept {
+    if (detailEquipmentSlot.has_value()) {
+        if (*detailEquipmentSlot < 0) {
+            return false;
+        }
+        nativeSlot = static_cast<std::uint8_t>(*detailEquipmentSlot);
+        return true;
+    }
+    if (definitionHash != kEmoteCollectionDefinitionHash) {
+        return false;
+    }
+    nativeSlot = kEmoteCollectionNativeEquipmentSlot;
+    return true;
+}
+
 /** Checks the canonical socket policy and every authored plug hash. */
 bool valid(const Sockets& sockets) noexcept {
     if (sockets.plugCount > sockets.plugs.size()) {

+ 31 - 0
Sunrise/src/state/account/inventory/inventory_state.h

@@ -67,6 +67,37 @@ inline constexpr std::uint32_t kLockedItemFlag = 0x1;
  */
 inline constexpr std::size_t kCharacterItemCapacity = 135;
 
+/**
+ * Definition hash of the real, non-equippable "Emotes" collection item. The Client opens its own
+ * wheel-configuration screen for this exact item once it is equipped with valid socket data.
+ */
+inline constexpr std::uint32_t kEmoteCollectionDefinitionHash = 3183180185U;
+/** Ordinary socket lane count the "Emotes" collection item's real content declares. */
+inline constexpr std::size_t kEmoteCollectionSocketLaneCount = 4;
+/**
+ * Native equipment slot the "Emotes" collection item is equipped under, in place of the individual
+ * emote it replaces. Its own real content carries no native equipment-slot mapping at all, unlike
+ * every other character-scoped item, so callers that need one for this item specifically fall back
+ * to this constant through resolve_native_equipment_slot() below.
+ */
+inline constexpr std::uint8_t kEmoteCollectionNativeEquipmentSlot =
+    static_cast<std::uint8_t>(EquipmentSlot::emote);
+
+/**
+ * Resolves the native equipment slot a configured item detail occupies.
+ * Every character-scoped item declares its own native slot except the "Emotes" collection item
+ * (kEmoteCollectionDefinitionHash), the one item whose real content has none. Any other item
+ * missing a native slot is rejected instead of silently aliasing this fallback.
+ * @param definitionHash Authored item definition hash being resolved.
+ * @param detailEquipmentSlot The installed item detail's own native slot, if it declares one.
+ * @param nativeSlot Receives the resolved native slot on success.
+ * @return True when the item declares its own non-negative slot, or is the Emotes collection item.
+ */
+[[nodiscard]] bool
+resolve_native_equipment_slot(std::uint32_t definitionHash,
+                              const std::optional<std::int8_t>& detailEquipmentSlot,
+                              std::uint8_t& nativeSlot) noexcept;
+
 /** One authored account-wide item, placed by the inventory bucket its definition names. */
 struct ProfileItem {
     /** Stable runtime identity required to materialize this row as an inventory action source. */

+ 7 - 16
Sunrise/src/state/equipment/light/resolution/configured_equipment_light_resolver.cpp

@@ -43,30 +43,21 @@ using NativeSlotMap = std::array<std::optional<std::size_t>, build_details::kEqu
  */
 [[nodiscard]] bool
 resolve_item(const authored::Item& item, std::size_t& nativeSlot, ItemScore& itemScore) noexcept {
-    // The "Emotes" collection item's real content carries no native equipment-slot mapping at
-    // all, unlike every other character-scoped item; it does not contribute to light either way.
-    // Kept self-consistent with the loadout resolver's own fallback for the same case (the emote
-    // slot's own native number, since that is where this item is equipped).
-    constexpr std::size_t kEmoteCollectionNativeSlot = 14;
     build_items::Definition definition{};
     build_details::Definition detail{};
+    std::uint8_t resolvedSlot = 0;
     if (!build_data::find_item_definition_hash(item.definitionHash, definition)
         || !build_data::find_configured_item_detail(definition.definitionIndex, detail)
         || detail.definitionIndex != definition.definitionIndex
-        || (detail.equipmentSlot.has_value() && *detail.equipmentSlot < 0)) {
+        || !authored::resolve_native_equipment_slot(
+            item.definitionHash, detail.equipmentSlot, resolvedSlot)
+        || static_cast<std::size_t>(resolvedSlot) >= build_details::kEquipmentSlotCount) {
         return false;
     }
-    if (!detail.equipmentSlot.has_value()) {
-        nativeSlot = kEmoteCollectionNativeSlot;
-        itemScore = ItemScore{definition.definitionIndex, 0};
-        return true;
-    }
-    if (static_cast<std::size_t>(*detail.equipmentSlot) >= build_details::kEquipmentSlotCount) {
-        return false;
-    }
-    nativeSlot = static_cast<std::size_t>(*detail.equipmentSlot);
+    nativeSlot = static_cast<std::size_t>(resolvedSlot);
+    // The "Emotes" collection item's real content contributes no light either way.
     std::int32_t power = 0;
-    if (!item_power(item.level, power)) {
+    if (detail.equipmentSlot.has_value() && !item_power(item.level, power)) {
         return false;
     }
     itemScore = ItemScore{definition.definitionIndex, power};

+ 115 - 16
Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

@@ -579,22 +579,103 @@ bool commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) no
     return ready;
 }
 
+namespace {
+
+/** Default plug hashes the wheel seeds when granting or repairing the collection item's sockets. */
+constexpr std::array<std::uint32_t, authored_inventory::kEmoteCollectionSocketLaneCount>
+    kEmoteCollectionDefaultPlugHashes{3134905452U, 4049365947U, 1046955906U, 181754010U};
+
+/**
+ * Resolves and cross-checks the "Emotes" collection item's own configured content.
+ * @param definition Receives the matching native item-definition row.
+ * @param detail Receives the matching configured item detail.
+ * @return True only when both rows agree with each other, carry no native equipment slot (the one
+ *         trait that singles this item out among every character-scoped item), and declare exactly
+ *         the expected 4 ordinary socket lanes.
+ */
+[[nodiscard]] bool resolve_emote_collection_definition(build_data::items::Definition& definition,
+                                                        item_details::Definition& detail) noexcept {
+    return build_data::find_item_definition_hash(authored_inventory::kEmoteCollectionDefinitionHash,
+                                                 definition)
+           && definition.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
+           && build_data::find_configured_item_detail(definition.definitionIndex, detail)
+           && detail.definitionIndex == definition.definitionIndex
+           && detail.definitionHash == authored_inventory::kEmoteCollectionDefinitionHash
+           && detail.bucketId == definition.bucketId && !detail.equipmentSlot.has_value()
+           && detail.ordinarySocketState == item_details::OrdinarySocketState::present
+           && detail.ordinarySocketCount == authored_inventory::kEmoteCollectionSocketLaneCount;
+}
+
+/**
+ * Checks that every one of the collection item's real plug pool candidates is actually installed
+ * and allowed in its intended lane, so a granted item can never carry a plug the client rejects.
+ */
+[[nodiscard]] bool default_plugs_valid(std::uint16_t collectionDefinitionIndex) noexcept {
+    for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
+        build_data::items::Definition plugDefinition{};
+        if (!build_data::find_item_definition_hash(kEmoteCollectionDefaultPlugHashes[lane],
+                                                    plugDefinition)
+            || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
+                                                   static_cast<std::uint8_t>(lane),
+                                                   plugDefinition.definitionIndex)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/**
+ * Checks an already-equipped collection item's own socket state, so a corrupted or stale set of
+ * plugs is repaired instead of trusted just because the definition hash already matches.
+ */
+[[nodiscard]] bool
+socket_state_sound(const authored_inventory::Item& item, std::uint16_t collectionDefinitionIndex) noexcept {
+    if (item.sockets.policy != authored_inventory::SocketPolicy::authored
+        || item.sockets.plugCount != authored_inventory::kEmoteCollectionSocketLaneCount) {
+        return false;
+    }
+    for (std::size_t lane = 0; lane < authored_inventory::kEmoteCollectionSocketLaneCount; ++lane) {
+        const std::optional<std::uint32_t>& plugHash = item.sockets.plugs[lane];
+        build_data::items::Definition plugDefinition{};
+        if (!plugHash.has_value()
+            || !build_data::find_item_definition_hash(*plugHash, plugDefinition)
+            || !build_data::is_socket_plug_allowed(collectionDefinitionIndex,
+                                                   static_cast<std::uint8_t>(lane),
+                                                   plugDefinition.definitionIndex)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+} // namespace
+
 /**
  * Equips each character with the "Emotes" collection item in the real emote slot, in place of an
  * individual emote. Unlike every other character-scoped item, its real content carries no native
  * equipment-slot mapping at all, so the resolvers this depends on (loadout resolution, light,
- * appearance refresh) fall back to that slot's own native number (14) for it specifically.
- * Its 4 ordinary sockets carry no native default plug, so 4 hashes from its real reusable plug
- * pool seed a default wheel; the client's own generic socket-plug request (opcode 1901) lets the
- * player reassign them afterward, the same mechanism it already uses for weapon mods and shaders.
+ * appearance refresh) fall back to authored_inventory::kEmoteCollectionNativeEquipmentSlot for it
+ * specifically, gated to its exact definition hash (state::account::inventory::
+ * resolve_native_equipment_slot). Its 4 ordinary sockets carry no native default plug, so 4 hashes
+ * from its real reusable plug pool seed a default wheel; the client's own generic socket-plug
+ * request (opcode 1901) lets the player reassign them afterward, the same mechanism it already uses
+ * for weapon mods and shaders.
  */
 bool ensure_character_emote_collection() noexcept {
-    constexpr std::uint32_t kEmoteCollectionHash = 3183180185U;
-    constexpr std::array<std::uint32_t, 4> kDefaultPlugHashes{
-        3134905452U, 4049365947U, 1046955906U, 181754010U};
     constexpr std::size_t kEmoteCollectionSlot =
         static_cast<std::size_t>(authored_inventory::EquipmentSlot::emote);
 
+    // The installed content must actually match what this migration assumes before anything is
+    // touched. A build whose "Emotes" collection item doesn't resolve this way yet -- rather than
+    // being wrong -- just isn't ready for the migration; skip this boot without failing the whole
+    // refresh, the same way the account-not-ready check below does.
+    build_data::items::Definition collectionDefinition{};
+    item_details::Definition collectionDetail{};
+    if (!resolve_emote_collection_definition(collectionDefinition, collectionDetail)
+        || !default_plugs_valid(collectionDefinition.definitionIndex)) {
+        return true;
+    }
+
     AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
     AccountState candidate = runtime::storage::g_state.account;
     if (!account::valid(candidate)) {
@@ -608,31 +689,49 @@ bool ensure_character_emote_collection() noexcept {
          ++characterIndex) {
         CharacterState& character = candidate.characters[characterIndex];
         auto& collectionSlot = character.equipment.slots[kEmoteCollectionSlot];
-        if (collectionSlot.has_value() && collectionSlot->definitionHash == kEmoteCollectionHash) {
+        const bool present = collectionSlot.has_value()
+                             && collectionSlot->definitionHash
+                                    == authored_inventory::kEmoteCollectionDefinitionHash;
+        if (present
+            && socket_state_sound(*collectionSlot, collectionDefinition.definitionIndex)) {
             continue;
         }
-        std::uint64_t instanceSoid = 0;
-        if (!next_item_instance_soid(candidate, instanceSoid)) {
+        if (character.nextInventorySerial
+            >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
+            failed = true;
+            break;
+        }
+        // A repair keeps the existing instance identity; only a fresh grant needs a new one.
+        std::uint64_t instanceSoid = present ? collectionSlot->instanceSoid : 0;
+        if (!present && !next_item_instance_soid(candidate, instanceSoid)) {
             failed = true;
             break;
         }
         authored_inventory::Item granted{};
         granted.instanceSoid = instanceSoid;
-        granted.definitionHash = kEmoteCollectionHash;
+        granted.definitionHash = authored_inventory::kEmoteCollectionDefinitionHash;
         granted.level = 0;
         granted.quantity = 1;
         granted.mutationSerial = static_cast<std::int32_t>(character.nextInventorySerial++);
         granted.sockets.policy = authored_inventory::SocketPolicy::authored;
-        granted.sockets.plugCount = kDefaultPlugHashes.size();
-        for (std::size_t lane = 0; lane < kDefaultPlugHashes.size(); ++lane) {
-            granted.sockets.plugs[lane] = kDefaultPlugHashes[lane];
+        granted.sockets.plugCount = kEmoteCollectionDefaultPlugHashes.size();
+        for (std::size_t lane = 0; lane < kEmoteCollectionDefaultPlugHashes.size(); ++lane) {
+            granted.sockets.plugs[lane] = kEmoteCollectionDefaultPlugHashes[lane];
         }
         collectionSlot = granted;
         changed = true;
     }
-    if (failed || !changed || !account::valid(candidate)) {
+    if (failed) {
         ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return !failed;
+        return false;
+    }
+    if (!changed) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return true;
+    }
+    if (!account::valid(candidate)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return false;
     }
     runtime::storage::g_state.account = candidate;
     ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);