Forráskód Böngészése

fix(equipment): keep displaced item in the clicked grid cell on equip swap

Equipping an unequipped weapon swapped rows correctly, but the displaced
item was stamped with a fresh greatest mutationSerial and the Client, which
orders a bucket's grid by serial rather than row, rebuilt it in the first
cell. Hand the displaced item the clicked item's prior serial after
finalize_equipment_transition runs and re-resolve the character to prove
the placement held. Matches live and Shadowkeep-era behavior.

Also document that mutationSerial doubles as the Client's grid ordering
token.

Based on upstream stanuwu/Sunrise PR #41 by joshrad-dev.
Thomas Shields 3 hete
szülő
commit
6164a3b3c2

+ 2 - 1
Sunrise/src/middleware/datagen/family4/inventory/layout.h

@@ -35,7 +35,8 @@ struct Entry {
     std::uint64_t instanceSoid{};
     /** Current stack quantity, set from the item definition for non-instanced rows. */
     std::int32_t quantity{};
-    /** Rising serial that keeps inventory mutation and eviction order. */
+    /** Rising serial that keeps inventory mutation and eviction order; the Client also orders a
+     *  bucket's grid cells by it. */
     std::int32_t mutationSerial{};
     /** Native item-state bits accumulated by inventory mutations. */
     std::uint32_t flags{};

+ 6 - 1
Sunrise/src/state/account/inventory/inventory_state.h

@@ -83,7 +83,12 @@ struct Item {
     std::uint32_t definitionHash{};
     std::int32_t level{};
     std::int32_t quantity{};
-    /** Rising per-character generation assigned whenever this item changes inventory rows. */
+    /**
+     * Rising per-character generation assigned whenever this item changes inventory rows. The
+     * Client also uses it as the stable ordering token inside a bucket's grid, so an item's cell
+     * follows its serial rather than its row index. Equip swaps therefore hand the displaced item
+     * the clicked item's prior serial so it takes the clicked cell.
+     */
     std::int32_t mutationSerial{};
     /** Native accumulated item-state bits such as the finisher favorite marker. */
     std::uint32_t flags{};

+ 2 - 1
Sunrise/src/state/runtime/state_account_equipment_runtime.cpp

@@ -161,7 +161,8 @@ namespace family4_loadout = middleware::datagen::family4::loadout;
  *
  * Every surviving instance must preserve its native bucket. A generation advances exactly when
  * its published native row or equipped marker changes, and a second resolution proves that the
- * stamped after-image retained the staged placement.
+ * stamped after-image retained the staged placement. Callers that need a moved item to keep an
+ * older grid cell (equip swaps) rewrite that item's serial afterwards and re-validate.
  */
 [[nodiscard]] bool
 finalize_equipment_transition(const AccountState& account,

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

@@ -256,6 +256,33 @@ bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
         return false;
     }
 
+    if (previousInstanceSoid != 0) {
+        // The serial on an unequipped row is also the Client's stable ordering token for that
+        // bucket.  Giving the displaced item a fresh, greatest serial makes the Client rebuild it
+        // in the first grid cell even though the character object places it in the selected row.
+        // Transfer the selected row's prior token along with the row instead: the newly equipped
+        // item still has a fresh generation, while the displaced item occupies the grid cell the
+        // player clicked.
+        authored_inventory::Item& displaced = after.inventory.values[inventoryIndex];
+        if (displaced.instanceSoid != previousInstanceSoid) {
+            return false;
+        }
+        displaced.mutationSerial = requestedPosition.mutationSerial;
+
+        AccountState checkedAccount = account;
+        checkedAccount.characters[characterIndex] = after;
+        family4_loadout::ResolvedLoadout checkedLoadout{};
+        ResolvedPosition displacedPosition{};
+        if (!account::valid(checkedAccount)
+            || !family4_loadout::resolve(checkedAccount, characterIndex, checkedLoadout)
+            || !find_resolved_position(checkedLoadout, previousInstanceSoid, displacedPosition)
+            || displacedPosition.equipped || displacedPosition.equipmentSlot != requestedNativeSlot
+            || displacedPosition.inventoryRow != requestedPosition.inventoryRow
+            || displacedPosition.mutationSerial != requestedPosition.mutationSerial) {
+            return false;
+        }
+    }
+
     mutation.beforeCharacter = before;
     mutation.afterCharacter = after;
     mutation.characterSoid = before.soid;