Browse Source

[-] feat(inventory): spend the stack an applied shader came from

Review: not a review point. Requested alongside the ownership fix,
because a stack that is never spent makes ownership meaningless.
Resolution: applying a shader takes one unit of it from the account.

Applying a shader took nothing from the account. The insertion cost some
plugs carry is a separate authored charge whose rows are not marked for
deletion, so it gates the action on a balance without spending anything,
and the plug's own stack was never touched. A stack could be applied any
number of times and its count never moved.

Takes one unit of the applied plug, for exactly the plugs the ownership
check covers. The authored-cost path cannot do this: it refuses any row
carrying an instance key, because it exists for the non-instanced
currency and material stacks, and an action source always carries one.
Spending one is therefore its own transition, keeping the row's identity
while units remain and releasing it with the row once the last one
goes.

Thomas Shields 3 weeks ago
parent
commit
eb388423b5
1 changed files with 46 additions and 0 deletions
  1. 46 0
      Sunrise/src/state/runtime/state_account_runtime.cpp

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

@@ -455,6 +455,40 @@ apply_collection_materials(const AccountState& before,
     return false;
 }
 
+/**
+ * Takes one unit of an owned socket action source, releasing the row when its last unit goes.
+ *
+ * An action source is an instanced profile row, so the authored-cost path cannot spend it. The
+ * row keeps its identity and position while units remain, because the Client addresses it by that
+ * identity. An emptied row is removed and the rows after it move up, which is the same shape the
+ * authored-cost path leaves behind when a stack empties.
+ *
+ * @param account Account whose profile stacks are spent in place.
+ * @param definitionHash Plug definition being applied.
+ * @return True when one unit was taken.
+ */
+[[nodiscard]] bool spend_plug_source(AccountState& account, std::uint32_t definitionHash) noexcept {
+    std::size_t row = account.profileItemCount;
+    for (std::size_t index = 0; index < account.profileItemCount; ++index) {
+        if (account.profileItems[index].definitionHash == definitionHash
+            && account.profileItems[index].quantity > 0) {
+            row = index;
+            break;
+        }
+    }
+    if (row >= account.profileItemCount) {
+        return false;
+    }
+    if (--account.profileItems[row].quantity > 0) {
+        return true;
+    }
+    for (std::size_t index = row; index + 1U < account.profileItemCount; ++index) {
+        account.profileItems[index] = account.profileItems[index + 1U];
+    }
+    account.profileItems[--account.profileItemCount] = {};
+    return true;
+}
+
 /** Applies one dense installed action-cost set resolved from the selected plug or action row. */
 [[nodiscard]] bool
 apply_action_materials(const AccountState& before,
@@ -1200,6 +1234,18 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
         return fail("materials");
     }
 
+    // Applying spends the stack the plug came from. The insertion cost above is a separate
+    // authored charge that leaves the plug itself untouched, so the unit is taken here.
+    //
+    // The authored-cost path cannot do this. It refuses any row carrying an instance key, because
+    // it exists for the non-instanced currency and material stacks, and an action source always
+    // carries one. Spending one is therefore its own transition: the row keeps its identity while
+    // any unit remains, and releases it with the row once the last unit goes.
+    if (consumesStack && !spend_plug_source(chargedAccount, plugDefinition.definitionHash)) {
+        return fail("plug_stack");
+    }
+    profileChanged = profileChanged || consumesStack;
+
     authored_inventory::Sockets authoredSockets{};
     if (target->sockets.policy == authored_inventory::SocketPolicy::nativeDefaults) {
         if (!materialize_native_sockets(detail, authoredSockets)) {