Procházet zdrojové kódy

fix(dismantle): pay materials for energy/heavy weapons and credit below currency caps

Dismantle payouts were gated on the native equipment slot with a bound
written for the semantic enum order (weapons first, then armor). Native
numbering places the three weapon slots after the armor slots, so energy
and heavy weapons paid nothing while kinetic and armor did. Route the gate
through the semantic slot map instead.

The shipped profile also seeded every payout currency at its native stack
cap, so even a paying dismantle credited zero. Lower the starting stacks,
add Glimmer to the payout, and bump the settings layout to 7 so existing
files take the new profile_items and dismantle_rewards.

Log a warn line whenever a payout row is dropped (capped stack, full
bucket, serial exhausted) so a zero payout is never silent again.

Reported by stanuwu.
Thomas Shields před 3 týdny
rodič
revize
6cc137ef95

+ 9 - 5
Sunrise/resources/default_settings.json

@@ -1,5 +1,5 @@
 {
-  "version": 6,
+  "version": 7,
   "core": {
     "logging": {
       "debugger_sink": true,
@@ -140,6 +140,10 @@
     "account": {
       "primary_soid": "0x9EAA300100100100",
       "dismantle_rewards": [
+        {
+          "definition_hash": "0xBC53E66E",
+          "quantity": 250
+        },
         {
           "definition_hash": "0x3CF2E8E2",
           "quantity": 4
@@ -152,15 +156,15 @@
       "profile_items": [
         {
           "definition_hash": "0xBC53E66E",
-          "quantity": 250000
+          "quantity": 100000
         },
         {
           "definition_hash": "0x3CF2E8E2",
-          "quantity": 999999
+          "quantity": 50000
         },
         {
           "definition_hash": "0xA7EE4765",
-          "quantity": 999999
+          "quantity": 50000
         },
         {
           "definition_hash": "0xB19439E5",
@@ -168,7 +172,7 @@
         },
         {
           "definition_hash": "0x28D6AC07",
-          "quantity": 9999
+          "quantity": 500
         },
         {
           "definition_hash": "0xE5B38AD2",

+ 1 - 1
Sunrise/src/core/settings/settings.h

@@ -18,7 +18,7 @@ namespace sunrise::core::settings {
  * Raise it when a key is renamed, removed, changes meaning, or must take a new default.
  * Adding a key needs no raise, because a missing key already takes its default.
  */
-inline constexpr std::uint32_t kSettingsVersion = 6;
+inline constexpr std::uint32_t kSettingsVersion = 7;
 
 /** Parsed read-only process settings. */
 struct Settings {

+ 3 - 2
Sunrise/src/core/settings/settings_upgrade.cpp

@@ -30,12 +30,13 @@ struct ReplacedMember {
  * Members replaced with the bundled default, each with the version that changed it.
  * A member is listed because its value form changed, or because its default changed.
  */
-constexpr std::array<ReplacedMember, 5> kReplacedMembers{{
+constexpr std::array<ReplacedMember, 6> kReplacedMembers{{
     {"\"key_bindings\"", 3},
     {"\"region_private\"", 5},
     {"\"topology\"", 5},
     {"\"characters\"", 5},
-    {"\"profile_items\"", 6},
+    {"\"profile_items\"", 7},
+    {"\"dismantle_rewards\"", 7},
 }};
 /** One splice per replaced member, plus the version member itself. */
 constexpr std::size_t kSpliceCapacity = kReplacedMembers.size() + 1;

+ 68 - 7
Sunrise/src/state/runtime/state_account_dismantle_staging.cpp

@@ -27,8 +27,31 @@ namespace item_details = build_data::items::details;
 namespace inventory_buckets = build_data::inventory::buckets;
 namespace family4_loadout = middleware::datagen::family4::loadout;
 
-/** Equipment slots 0-2 are weapons and 3-7 are class-specific armor. */
-constexpr std::uint8_t kGearEquipmentSlotCount = 8;
+/**
+ * @return True when a native equipment slot holds weapons or class-specific armor, the gear the
+ *         supported client pays materials for. Native slot numbers are not the semantic enum
+ *         order (kinetic is 7, energy 8, heavy 9), so the check goes through the semantic map.
+ */
+[[nodiscard]] bool gear_equipment_slot(std::uint8_t nativeSlot) noexcept {
+    using EquipmentSlot = authored_inventory::EquipmentSlot;
+    std::size_t semanticIndex = authored_inventory::kEquipmentSlotCount;
+    if (!semantic_equipment_slot(nativeSlot, semanticIndex)) {
+        return false;
+    }
+    switch (static_cast<EquipmentSlot>(semanticIndex)) {
+    case EquipmentSlot::kinetic:
+    case EquipmentSlot::energy:
+    case EquipmentSlot::heavy:
+    case EquipmentSlot::helmet:
+    case EquipmentSlot::gauntlets:
+    case EquipmentSlot::chest:
+    case EquipmentSlot::legs:
+    case EquipmentSlot::classItem:
+        return true;
+    default:
+        return false;
+    }
+}
 
 /** Writes one exhaustive item-dismantle transaction checkpoint. */
 void report_dismantle(std::string_view stage,
@@ -70,13 +93,37 @@ void report_dismantle(std::string_view stage,
     }
 }
 
+/** Records one payout row that could not be credited, so a silent zero payout is visible. */
+void report_dismantle_reward_dropped(std::string_view reason,
+                                     std::uint32_t definitionHash,
+                                     std::int32_t policyQuantity,
+                                     std::int32_t previousQuantity,
+                                     std::int32_t maxStackSize) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(line.data(),
+                                    line.size(),
+                                    "ev=dismantle stage=reward result=dropped reason=%.*s "
+                                    "definition_hash=0x%08X policy_quantity=%d held=%d "
+                                    "max_stack=%d",
+                                    static_cast<int>(reason.size()),
+                                    reason.data(),
+                                    definitionHash,
+                                    policyQuantity,
+                                    previousQuantity,
+                                    maxStackSize);
+    if (count > 0) {
+        core::log::write(core::log::Channel::state,
+                         core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
 /**
  * Credits the supported client's ordinary weapon/armor dismantle payout.
  *
- * Capped stacks
- * lose only the overflowing part, matching normal profile-inventory behavior.
- * Every credited row
- * receives a new mutation serial so the account observer can display it.
+ * Capped stacks lose only the overflowing part, matching normal profile-inventory behavior; a
+ * stack already at its native cap drops that row's payout and says so in the log. Every credited
+ * row receives a new mutation serial so the account observer can display it.
  */
 [[nodiscard]] bool
 apply_dismantle_rewards(const AccountState& before,
@@ -90,7 +137,7 @@ apply_dismantle_rewards(const AccountState& before,
     if (!valid_profile_inventory(before)) {
         return false;
     }
-    if (equipmentSlot >= kGearEquipmentSlotCount) {
+    if (!gear_equipment_slot(equipmentSlot)) {
         return true;
     }
 
@@ -139,6 +186,9 @@ apply_dismantle_rewards(const AccountState& before,
         const bool appended = profileIndex == after.profileItemCount;
         if ((appended && after.profileItemCount >= after.profileItems.size())
             || greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()) {
+            report_dismantle_reward_dropped(
+                appended ? "profile_full" : "serial_exhausted", policy.definitionHash,
+                policy.quantity, 0, detail.maxStackSize);
             continue;
         }
         const std::int32_t previousQuantity =
@@ -146,6 +196,12 @@ apply_dismantle_rewards(const AccountState& before,
         const std::int32_t available = detail.maxStackSize - previousQuantity;
         const std::int32_t credited = (std::min)(policy.quantity, available);
         if (credited <= 0) {
+            // Every stack of this currency is already at its native cap.
+            report_dismantle_reward_dropped("stack_capped",
+                                            policy.definitionHash,
+                                            policy.quantity,
+                                            previousQuantity,
+                                            detail.maxStackSize);
             continue;
         }
 
@@ -162,6 +218,11 @@ apply_dismantle_rewards(const AccountState& before,
         }
         // A full native bucket drops this reward, but never blocks deletion of the source item.
         if (!account::valid(candidate) || !valid_profile_inventory(candidate)) {
+            report_dismantle_reward_dropped("bucket_full",
+                                            policy.definitionHash,
+                                            policy.quantity,
+                                            previousQuantity,
+                                            detail.maxStackSize);
             continue;
         }
         if (rewardCount >= rewards.size()) {