Parcourir la source

Merge pull request #96 from Confetti3/fix/weapon-power-stats

Fix missing weapon Power in character stats
stan il y a 4 jours
Parent
commit
a1d8f6f724

+ 6 - 2
Sunrise/src/client/content/items/packages/package_detail_build.cpp

@@ -76,6 +76,8 @@ namespace domain = state::build_data::items::details;
 constexpr std::size_t kConstantsPrefix = 8;
 /** Client offset of the stat row the banner's power number is searched by. */
 constexpr std::size_t kLightStatRowOffset = 592;
+/** Build-86657 sub_140553ED0 reads the weapon Power stat row at this client offset. */
+constexpr std::size_t kWeaponPowerStatRowOffset = 606;
 /**
  * Client offsets of the 6 character stat rows, in the two runs the blob stores them in.
  * The client reads these as 6 separate scalars, not as one array, so each is named here.
@@ -240,12 +242,14 @@ bool read_investment_constants(const reader::Source& source,
     }
     output.lightStatRow =
         std::to_integer<std::uint8_t>(blob[kConstantsPrefix + kLightStatRowOffset]);
+    output.weaponPowerStatRow =
+        std::to_integer<std::uint8_t>(blob[kConstantsPrefix + kWeaponPowerStatRowOffset]);
     for (std::size_t row = 0; row < std::size(kCharacterStatRowOffsets); ++row) {
         output.characterStatRows[row] =
             std::to_integer<std::uint8_t>(blob[kConstantsPrefix + kCharacterStatRowOffsets[row]]);
     }
-    output.extracted = true;
-    return true;
+    output.extracted = output.weaponPowerStatRow < state::build_data::constants::kStatRowCount;
+    return output.extracted;
 }
 
 } // namespace sunrise::client::content::items::packages

+ 48 - 25
Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_stats.cpp

@@ -3,6 +3,7 @@
 
 #include "../../../../core/logging/log.h"
 #include "../../../../state/build_data/runtime.h"
+#include "../../../../state/equipment/light/definition.h"
 #include "internal.h"
 
 namespace sunrise::middleware::datagen::character_record::appearance {
@@ -54,10 +55,11 @@ namespace constants = state::build_data::constants;
  * Collects every stat row one equipped item or its plugs declare.
  * @param equipped Effective plug lanes.
  * @param count Occupied entries, advanced per distinct row.
+ * @return False for invalid stat rows
+ * or insufficient storage.
  */
-void collect_rows(const Equipped& equipped,
-                  std::span<std::uint8_t> rows,
-                  std::size_t& count) noexcept {
+[[nodiscard]] bool
+collect_rows(const Equipped& equipped, std::span<std::uint8_t> rows, std::size_t& count) noexcept {
     const std::size_t lanes = equipped.laneCount + 1;
     for (std::size_t source = 0; source < lanes; ++source) {
         const std::uint16_t definitionIndex =
@@ -69,15 +71,24 @@ void collect_rows(const Equipped& equipped,
         }
         const std::size_t stats =
             detail.statCount < detail.stats.size() ? detail.statCount : detail.stats.size();
-        for (std::size_t entry = 0; entry < stats && count < rows.size(); ++entry) {
+        for (std::size_t entry = 0; entry < stats; ++entry) {
             const std::uint8_t row = detail.stats[entry].row;
-            if (row != details::kEmptyStatRow
-                && std::find(rows.begin(), rows.begin() + static_cast<std::ptrdiff_t>(count), row)
-                       == rows.begin() + static_cast<std::ptrdiff_t>(count)) {
+            if (row == details::kEmptyStatRow) {
+                continue;
+            }
+            if (row >= constants::kStatRowCount) {
+                return false;
+            }
+            if (std::find(rows.begin(), rows.begin() + static_cast<std::ptrdiff_t>(count), row)
+                == rows.begin() + static_cast<std::ptrdiff_t>(count)) {
+                if (count >= rows.size()) {
+                    return false;
+                }
                 rows[count++] = row;
             }
         }
     }
+    return true;
 }
 
 /**
@@ -100,34 +111,43 @@ void append(std::uint8_t row,
     ++count;
 }
 
-/**
- * Fills one per-weapon table with every row that weapon and its plugs declare, ascending.
- * @param equipped Effective plug lanes of one weapon.
- * @param table Per-weapon stat table.
- */
-void apply_weapon_table(const Equipped& equipped,
-                        std::array<layout::StatRow, layout::kStatRowCapacity>& table) noexcept {
-    std::array<std::uint8_t, layout::kStatRowCapacity> rows{};
-    std::size_t rowCount = 0;
-    collect_rows(equipped, rows, rowCount);
+/** Writes item Power and definition stats, rejecting invalid rows or insufficient space. */
+[[nodiscard]] bool
+apply_weapon_table(const Equipped& equipped,
+                   std::uint8_t powerRow,
+                   std::int32_t power,
+                   std::array<layout::StatRow, layout::kStatRowCapacity>& table) noexcept {
+    std::array<std::uint8_t, constants::kStatRowCount> rows{powerRow};
+    std::size_t rowCount = 1;
+    if (!collect_rows(equipped, rows, rowCount)) {
+        return false;
+    }
     std::sort(rows.begin(), rows.begin() + static_cast<std::ptrdiff_t>(rowCount));
+    std::array<layout::StatRow, layout::kStatRowCapacity> staged{};
     std::size_t written = 0;
     for (std::size_t entry = 0; entry < rowCount; ++entry) {
-        append(rows[entry], item_total(equipped, rows[entry]), table, written);
+        const std::uint8_t row = rows[entry];
+        const std::int32_t value = row == powerRow ? power : item_total(equipped, row);
+        if (value > 0 && written >= staged.size()) {
+            return false;
+        }
+        append(row, value, staged, written);
     }
+    table = staged;
+    return true;
 }
 
 /** Diagnostic latch: the constants are a boot-time domain, so one line settles their absence. */
 std::atomic<bool> g_reportedMissingConstants{};
 
-/** Reports once that the installed investment constants are not published. */
+/** Reports once that the installed investment constants are unavailable or invalid. */
 void report_missing_constants() noexcept {
     if (g_reportedMissingConstants.exchange(true, std::memory_order_relaxed)) {
         return;
     }
     core::log::write(core::log::Channel::server,
                      core::log::Level::warn,
-                     "ev=char_stats stage=constants result=absent");
+                     "ev=char_stats stage=constants result=unavailable_or_invalid");
 }
 
 } // namespace
@@ -137,9 +157,8 @@ bool apply_stats(const family4::loadout::ResolvedInstances& instances,
                  std::int32_t light,
                  layout::Appearance& appearance) noexcept {
     constants::InvestmentConstants named{};
-    if (!state::build_data::find_investment_constants(named)) {
-        // Without the named rows there is no light row either, so the record cannot be built at
-        // all. Report it once: the alternative is a silently empty stat table.
+    if (!state::build_data::find_investment_constants(named) || !constants::valid(named)) {
+        // Publishing an absent or unusable weapon Power row silently selects the damage floor.
         report_missing_constants();
         return false;
     }
@@ -168,8 +187,12 @@ bool apply_stats(const family4::loadout::ResolvedInstances& instances,
         }
         details::Definition detail{};
         Equipped equipped{};
-        if (resolve_equipped(instances.items[index], detail, equipped)) {
-            apply_weapon_table(equipped, appearance.weaponStats[weapon]);
+        std::int32_t power = 0;
+        if (!resolve_equipped(instances.items[index], detail, equipped)
+            || !state::equipment::light::item_power(instances.items[index].instance.level, power)
+            || !apply_weapon_table(
+                equipped, named.weaponPowerStatRow, power, appearance.weaponStats[weapon])) {
+            return false;
         }
     }
     return true;

+ 1 - 0
Sunrise/src/state/build_data/build_data_runtime.cpp

@@ -93,6 +93,7 @@ bool initialize(void* module, std::uint64_t configuredEquipmentHash) noexcept {
     const constants::InvestmentConstants cachedConstants{
         domains.constants.extracted != 0,
         domains.constants.lightStatRow,
+        domains.constants.weaponPowerStatRow,
         domains.constants.characterStatRows,
     };
     if (status != cache::LoadStatus::loaded || !constants::replace(cachedConstants)

+ 6 - 4
Sunrise/src/state/build_data/cache/records/cache_domain_validation.cpp

@@ -197,10 +197,12 @@ bool canonicalize(MutableDomains domains, const DomainCounts& counts) noexcept {
 
 /** Checks the structure rules, the sort order, and every cross-domain item reference. */
 bool valid_domains(Domains domains) noexcept {
-    if (domains.constants.extracted != 1U || domains.named.empty() || domains.items.empty()
-        || domains.collectibles.empty() || domains.materialRequirementSets.empty()
-        || domains.socketPlugRules.empty() || domains.socketPlugPools.empty()
-        || domains.inventoryBuckets.empty() || domains.socketEntryLists.empty()
+    if (domains.constants.extracted != 1U
+        || domains.constants.weaponPowerStatRow >= constants::kStatRowCount || domains.named.empty()
+        || domains.items.empty() || domains.collectibles.empty()
+        || domains.materialRequirementSets.empty() || domains.socketPlugRules.empty()
+        || domains.socketPlugPools.empty() || domains.inventoryBuckets.empty()
+        || domains.socketEntryLists.empty()
         || !std::all_of(domains.named.begin(), domains.named.end(), valid_name)
         || !strictly_ordered(domains.named, named_less) || !items::valid(domains.items)
         || !collectibles::valid(domains.collectibles)

+ 4 - 3
Sunrise/src/state/build_data/cache/records/format.h

@@ -29,7 +29,7 @@ inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', '
  * Bump it when a stored shape changes, and when the extraction filling it changes what it writes.
  * A cached row survives a code change, so a corrected walk keeps publishing the old rows.
  */
-inline constexpr std::uint32_t kCacheFormatVersion = 45;
+inline constexpr std::uint32_t kCacheFormatVersion = 46;
 /** Signed -1 on disk means there is no equipment slot. */
 inline constexpr std::int8_t kAbsentEquipmentSlot = -1;
 /** The standard 64-bit FNV-1a offset basis starts the payload checksum. */
@@ -47,12 +47,13 @@ struct Prefix {
 /**
  * Stat rows named by the installed investment constants blob.
  * The client searches the character's stat table by these rows, so they decide which rows the
- * generated table may carry. They are 7 bytes of scalars, so they ride in the header.
+ * generated tables may carry. They ride in the fixed cache header.
  */
 struct InvestmentConstants {
     /** Stat row the banner's power number is searched by. Row 0 is a real row, so there is no
      * unset value; an unextracted blob leaves `extracted` clear instead. */
     std::uint8_t lightStatRow{};
+    std::uint8_t weaponPowerStatRow{};
     std::array<std::uint8_t, constants::kCharacterStatRowCount> characterStatRows{};
     /** One when the constants blob was read, zero when the domain has never been extracted. */
     std::uint8_t extracted{};
@@ -420,7 +421,7 @@ struct RosterGroupRecord {
 
 static_assert(sizeof(Prefix) == kCacheMagic.size() + sizeof(std::uint32_t));
 static_assert(sizeof(InvestmentConstants)
-              == constants::kCharacterStatRowCount + 2 * sizeof(std::uint8_t));
+              == constants::kCharacterStatRowCount + 3 * sizeof(std::uint8_t));
 static_assert(sizeof(Header)
               == kCacheMagic.size() + 26 * sizeof(std::uint32_t) + 2 * sizeof(std::uint64_t)
                      + sizeof(InvestmentConstants));

+ 9 - 0
Sunrise/src/state/build_data/constants/definition.h

@@ -8,6 +8,8 @@ namespace sunrise::state::build_data::constants {
 
 /** Character stat rows the installed investment constants blob names. */
 inline constexpr std::size_t kCharacterStatRowCount = 6;
+/** Build-86657 native stat vectors contain 64 indexed values. */
+inline constexpr std::size_t kStatRowCount = 64;
 
 /**
  * Stat rows the client searches a character's stat table by.
@@ -17,8 +19,15 @@ struct InvestmentConstants {
     bool extracted{};
     /** Row the banner's power number is searched by. */
     std::uint8_t lightStatRow{};
+    /** Row converted from displayed weapon Power into native damage Power. */
+    std::uint8_t weaponPowerStatRow{};
     /** The 6 rows the character sheet shows, in the blob's own order. */
     std::array<std::uint8_t, kCharacterStatRowCount> characterStatRows{};
 };
 
+/** Requires the extracted weapon row to address the native stat vector. */
+[[nodiscard]] constexpr bool valid(const InvestmentConstants& value) noexcept {
+    return value.extracted && value.weaponPowerStatRow < kStatRowCount;
+}
+
 } // namespace sunrise::state::build_data::constants

+ 1 - 1
Sunrise/src/state/build_data/constants/investment_constant_catalog.cpp

@@ -19,7 +19,7 @@ void clear() noexcept {
 
 /** Publishes one extracted constants row. */
 bool replace(const InvestmentConstants& value) noexcept {
-    if (!value.extracted) {
+    if (!valid(value)) {
         return false;
     }
     const Lock::Exclusive guard(g_lock);

+ 1 - 0
Sunrise/src/state/build_data/runtime/persistence/build_data_persistence.cpp

@@ -48,6 +48,7 @@ template <typename Value, std::size_t Capacity>
 [[nodiscard]] cache::records::InvestmentConstants
 to_record(const constants::InvestmentConstants& value) noexcept {
     return {value.lightStatRow,
+            value.weaponPowerStatRow,
             value.characterStatRows,
             value.extracted ? std::uint8_t{1} : std::uint8_t{0}};
 }

+ 11 - 0
Sunrise/src/state/equipment/light/definition.h

@@ -3,6 +3,7 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
+#include <limits>
 #include <optional>
 
 #include "../../build_data/items/details/definition.h"
@@ -26,6 +27,16 @@ inline constexpr std::int32_t kPowerPerLevel = 10;
 /** A powered item never scores below this floor whatever its level. */
 inline constexpr std::int32_t kMinimumItemPower = 750;
 
+/** Converts an authored item level to displayed Power without overflowing the wire integer. */
+[[nodiscard]] constexpr bool item_power(std::int32_t level, std::int32_t& output) noexcept {
+    if (level < 0 || level > (std::numeric_limits<std::int32_t>::max)() / kPowerPerLevel) {
+        return false;
+    }
+    const std::int32_t power = kPowerPerLevel * level;
+    output = level == 0 ? 0 : (power < kMinimumItemPower ? kMinimumItemPower : power);
+    return true;
+}
+
 /** A slot at or below this score adds nothing and is not counted by the divisor. */
 inline constexpr std::int32_t kUnpoweredScore = 0;
 

+ 5 - 10
Sunrise/src/state/equipment/light/resolution/configured_equipment_light_resolver.cpp

@@ -34,15 +34,6 @@ using NativeSlotMap = std::array<std::optional<std::size_t>, build_details::kEqu
     return (std::min)(account.characterCount, account.characters.size());
 }
 
-/**
- * Converts one item level into its power score.
- * @param level Authored item level, where zero marks an unpowered item.
- * @return 10 power per level, raised to the floor, or zero for an unpowered item.
- */
-[[nodiscard]] constexpr std::int32_t item_power(std::int32_t level) noexcept {
-    return level > 0 ? (std::max)(kMinimumItemPower, kPowerPerLevel * level) : 0;
-}
-
 /**
  * Finds one authored item's native slot and computes its score.
  * @param item Already-checked authored equipment item.
@@ -62,7 +53,11 @@ resolve_item(const authored::Item& item, std::size_t& nativeSlot, ItemScore& ite
         return false;
     }
     nativeSlot = static_cast<std::size_t>(*detail.equipmentSlot);
-    itemScore = ItemScore{definition.definitionIndex, item_power(item.level)};
+    std::int32_t power = 0;
+    if (!item_power(item.level, power)) {
+        return false;
+    }
+    itemScore = ItemScore{definition.definitionIndex, power};
     return true;
 }