Bläddra i källkod

Defer unlock-gated rewards, draw record rewards in full frames, and walk owed pass perks

Zach 4 veckor sedan
förälder
incheckning
0a014fc830

+ 4 - 4
Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp

@@ -69,6 +69,10 @@ selected_character(const state::AccountState& account) noexcept {
         report_reward_refusal("world_publish", request.itemDefinitionIndex, reason);
         return false;
     };
+    // A draw is not retained, so it waits for a buffer its whole frame is sure to fit.
+    if (response.size() < scratch.framed.size()) {
+        return false;
+    }
     std::uint64_t seed = 0;
     if (!middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)))) {
         return fail("random_source");
@@ -107,10 +111,6 @@ selected_character(const state::AccountState& account) noexcept {
                                                    scratch.framed,
                                                    framedSize)
         && framedSize != 0;
-    // Only a piggyback slot can be short; the grant waits for a full poll.
-    if (encoded && framedSize > response.size()) {
-        return fail("response_capacity");
-    }
     if (!state::commit_record_reward(*pending)) {
         return fail("inventory_commit");
     }

+ 26 - 11
Sunrise/src/state/rewards/reward_resolver.cpp

@@ -107,6 +107,7 @@ struct Resolver {
     std::array<const definitions::Entry*, kDrawCapacity> selectedEntries{};
     std::array<std::uint32_t, kDrawCapacity> selectedCategories{};
     std::size_t selectedCount{};
+    bool excludedByUnlocks{};
 
     double fraction() noexcept {
         // SplitMix64 makes a prepared seed replayable without shared random state.
@@ -137,6 +138,7 @@ struct Resolver {
             return false;
         }
         if (!enabled) {
+            excludedByUnlocks = true;
             return true;
         }
         double value = entry.weight;
@@ -163,6 +165,8 @@ struct Resolver {
             return refuse(context, "weight_shape");
         }
         if (value == 0) {
+            // A modifier can change this weight when the unlocks change.
+            excludedByUnlocks |= entry.modifiers.count != 0;
             return true;
         }
         if (entry.poolIndex != definitions::kAbsent) {
@@ -285,7 +289,8 @@ bool resolve_item(definitions::View data,
                   const Context& context,
                   std::uint16_t itemIndex,
                   std::uint32_t quantity,
-                  Result& result) noexcept {
+                  Result& result,
+                  bool& ineligible) noexcept {
     result = {};
     if (itemIndex >= data.items.size() || quantity == 0 || quantity > kMaximumQuantity) {
         return refuse(context, "reward_shape");
@@ -326,6 +331,7 @@ bool resolve_item(definitions::View data,
             }
         }
         if (staged.count == 0) {
+            ineligible = resolver.excludedByUnlocks;
             return refuse(context, "empty_reward");
         }
     }
@@ -344,25 +350,34 @@ bool eligible(std::span<const definitions::Instruction> instructions,
     return condition(instructions, context, result);
 }
 
-bool resolve(const Context& context,
-             std::uint16_t itemIndex,
-             std::uint32_t quantity,
-             Result& result) noexcept {
+Resolution resolve(const Context& context,
+                   std::uint16_t itemIndex,
+                   std::uint32_t quantity,
+                   Result& result) noexcept {
     struct Request {
         const Context& context;
         std::uint16_t item;
         std::uint32_t quantity;
         Result& result;
-    } request{context, itemIndex, quantity, result};
+        bool ineligible;
+    } request{context, itemIndex, quantity, result, false};
     result = {};
     if (context.refusal != nullptr) {
         *context.refusal = "reward_item";
     }
-    return build_data::read_reward_definitions(
-        &request, [](void* raw, definitions::View data) noexcept {
-            auto& value = *static_cast<Request*>(raw);
-            return resolve_item(data, value.context, value.item, value.quantity, value.result);
-        });
+    if (build_data::read_reward_definitions(&request,
+                                            [](void* raw, definitions::View data) noexcept {
+                                                auto& value = *static_cast<Request*>(raw);
+                                                return resolve_item(data,
+                                                                    value.context,
+                                                                    value.item,
+                                                                    value.quantity,
+                                                                    value.result,
+                                                                    value.ineligible);
+                                            })) {
+        return Resolution::resolved;
+    }
+    return request.ineligible ? Resolution::ineligible : Resolution::refused;
 }
 
 } // namespace sunrise::state::rewards

+ 7 - 4
Sunrise/src/state/rewards/reward_resolver.h

@@ -34,10 +34,13 @@ struct Context {
                             const Context& context,
                             bool& result) noexcept;
 
+/** How one resolution ended; ineligible means the saved unlocks left nothing to draw. */
+enum class Resolution : std::uint8_t { resolved, ineligible, refused };
+
 /** Plans an acquisition without changing State; retains wrappers opened separately. */
-[[nodiscard]] bool resolve(const Context& context,
-                           std::uint16_t itemIndex,
-                           std::uint32_t quantity,
-                           Result& result) noexcept;
+[[nodiscard]] Resolution resolve(const Context& context,
+                                 std::uint16_t itemIndex,
+                                 std::uint32_t quantity,
+                                 Result& result) noexcept;
 
 } // namespace sunrise::state::rewards

+ 2 - 6
Sunrise/src/state/runtime/runtime.h

@@ -780,12 +780,8 @@ inline constexpr std::uint16_t kArtifactUnlockProgressionIndex = 39;
 /** @return Seasonal XP published in the account progression bank. */
 [[nodiscard]] std::int32_t seasonal_experience() noexcept;
 
-/**
- * Publishes every seasonal value the seeded XP and artifact ownership imply.
- * @param accountObjectPublished Whether the caller's push carries the account object; earned-perk
- *        flags reach the client only there, so otherwise the perk walk waits for an XP grant.
- */
-[[nodiscard]] bool seed_seasonal_progression(bool accountObjectPublished) noexcept;
+/** Publishes every seasonal value the seeded XP and artifact ownership imply. */
+[[nodiscard]] bool seed_seasonal_progression() noexcept;
 
 /** @return One-based Season of Arrivals rank the published XP earns. */
 [[nodiscard]] std::uint16_t seasonal_rank() noexcept;

+ 15 - 8
Sunrise/src/state/runtime/state_account_reward_grant_runtime.cpp

@@ -174,10 +174,11 @@ enum class PassResolution { claim, replay };
         || !enabled) {
         return false;
     }
-    if (!rewards::resolve({flags, account.characters[character].characterClass, seed, reason},
-                          reward.itemIndex,
-                          reward.quantity,
-                          result)) {
+    if (rewards::resolve({flags, account.characters[character].characterClass, seed, reason},
+                         reward.itemIndex,
+                         reward.quantity,
+                         result)
+        != rewards::Resolution::resolved) {
         return false;
     }
     if (reward.socketCount != 0) {
@@ -331,10 +332,16 @@ RewardPreparation prepare_item_reward(std::uint16_t itemIndex,
         return RewardPreparation::deferred;
     }
     rewards::Result resolved{};
-    if (!rewards::resolve({flags, account.characters[character].characterClass, seed, &reason},
-                          itemIndex,
-                          quantity,
-                          resolved)) {
+    const auto resolution =
+        rewards::resolve({flags, account.characters[character].characterClass, seed, &reason},
+                         itemIndex,
+                         quantity,
+                         resolved);
+    // The saved unlocks can change, so an empty draw they caused is retried, not retired.
+    if (resolution == rewards::Resolution::ineligible) {
+        return RewardPreparation::deferred;
+    }
+    if (resolution != rewards::Resolution::resolved) {
         return catalog_refusal(reason);
     }
     reason = "reward_placement";

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

@@ -241,8 +241,7 @@ bool set_selected_character(std::uint64_t characterSoid) noexcept {
         investment::store::g_session.selected[index] = candidate.characters[index].selected;
     }
     investment::store::g_mutex.unlock();
-    // The selection patch carries no account object, so earned perks wait for the next XP grant.
-    (void)seed_seasonal_progression(false);
+    (void)seed_seasonal_progression();
     return true;
 }
 

+ 4 - 6
Sunrise/src/state/runtime/state_progression_runtime.cpp

@@ -263,7 +263,7 @@ void report_perk_refusal(std::size_t index, const char* reason) noexcept {
                       reason);
 }
 
-/** Set while a perk walk is owed to the next XP grant; only touched under g_mutex. */
+/** Set while a perk walk is owed; only touched under g_mutex. */
 bool g_perkWalkOwed = true;
 
 /** Earned perks set both flags in the transaction that publishes their qualifying rank. */
@@ -369,7 +369,7 @@ std::int32_t seasonal_experience() noexcept {
 }
 
 /** Publishes every seasonal value the seeded XP and artifact ownership imply. */
-bool seed_seasonal_progression(bool accountObjectPublished) noexcept {
+bool seed_seasonal_progression() noexcept {
     const std::int32_t experience = seasonal_experience();
     SaleRows rows{};
     std::size_t count = 0;
@@ -377,9 +377,6 @@ bool seed_seasonal_progression(bool accountObjectPublished) noexcept {
         return false;
     }
     investment::store::g_mutex.lock();
-    if (!accountObjectPublished) {
-        g_perkWalkOwed = true;
-    }
     investment::store::Transaction transaction;
     Family5State family;
     if (!transaction.ready() || !investment::store::read_family5(family)) {
@@ -410,10 +407,11 @@ bool seed_seasonal_progression(bool accountObjectPublished) noexcept {
     }
     const std::uint32_t mask = artifact_mask(rows, count);
     bool walked = false;
+    // A later character pick publishes only a selection patch, so only an owed walk runs here.
     const bool published =
         publish_experience_lanes(experience) && publish_artifact_locked(family, mask, experience)
         && investment::store::write_family5(family)
-        && (!accountObjectPublished || grant_progress_flags(walked)) && transaction.commit();
+        && (!g_perkWalkOwed || grant_progress_flags(walked)) && transaction.commit();
     if (published && walked) {
         g_perkWalkOwed = false;
     }

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

@@ -300,8 +300,7 @@ bool initialize(void* module,
     ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
     secure_reset(*initialized);
     // The seeded banks decide every derived bar, gate and seasonal counter, so both run last.
-    // The login snapshot carries the account object, so the seed walks earned perks now.
-    (void)seed_seasonal_progression(true);
+    (void)seed_seasonal_progression();
     unlocks::records::seed();
     return transaction.commit();
 }