| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- /**
- * Equipment placement helpers: native and semantic slots, resolved positions, and the
- * comparisons one equipment transition is checked against.
- */
- #include <Windows.h>
- #include <algorithm>
- #include <array>
- #include <cstddef>
- #include <cstdint>
- #include <cstdio>
- #include <limits>
- #include <string_view>
- #include <utility>
- #include "../../core/logging/log.h"
- #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
- #include "../build_data/runtime.h"
- #include "runtime.h"
- #include "state.h"
- #include "state_account_transaction_helpers.h"
- #include "storage/internal.h"
- namespace sunrise::state {
- namespace runtime::detail {
- namespace authored_inventory = account::inventory;
- namespace item_details = build_data::items::details;
- namespace inventory_buckets = build_data::inventory::buckets;
- namespace family4_loadout = middleware::datagen::family4::loadout;
- /** Resolves the installed native equipment slot for one configured authored item. */
- [[nodiscard]] bool native_equipment_slot(const authored_inventory::Item& item,
- std::uint8_t& slot) noexcept {
- build_data::items::Definition definition{};
- item_details::Definition detail{};
- if (!build_data::find_item_definition_hash(item.definitionHash, definition)
- || definition.definitionHash != item.definitionHash
- || !build_data::find_configured_item_detail(definition.definitionIndex, detail)
- || detail.definitionIndex != definition.definitionIndex
- || detail.definitionHash != definition.definitionHash
- || detail.bucketId != definition.bucketId || !detail.equipmentSlot.has_value()
- || *detail.equipmentSlot < 0
- || static_cast<std::size_t>(*detail.equipmentSlot) >= item_details::kEquipmentSlotCount) {
- return false;
- }
- slot = static_cast<std::uint8_t>(*detail.equipmentSlot);
- return true;
- }
- /** Resolves the installed physical inventory bucket for one configured authored item. */
- [[nodiscard]] bool inventory_bucket_id(const authored_inventory::Item& item,
- std::uint8_t& bucketId) noexcept {
- build_data::items::Definition definition{};
- item_details::Definition detail{};
- if (!build_data::find_item_definition_hash(item.definitionHash, definition)
- || definition.definitionHash != item.definitionHash
- || !build_data::find_configured_item_detail(definition.definitionIndex, detail)
- || detail.definitionIndex != definition.definitionIndex
- || detail.definitionHash != definition.definitionHash
- || detail.bucketId != definition.bucketId) {
- return false;
- }
- bucketId = detail.bucketId;
- return true;
- }
- /** Maps the 16 proven native equipment positions onto their stable authored State slots. */
- [[nodiscard]] bool semantic_equipment_slot(std::uint8_t nativeSlot,
- std::size_t& semanticIndex) noexcept {
- using EquipmentSlot = authored_inventory::EquipmentSlot;
- EquipmentSlot semanticSlot = EquipmentSlot::count;
- switch (nativeSlot) {
- case 0:
- semanticSlot = EquipmentSlot::subclass;
- break;
- case 1:
- semanticSlot = EquipmentSlot::helmet;
- break;
- case 2:
- semanticSlot = EquipmentSlot::gauntlets;
- break;
- case 4:
- semanticSlot = EquipmentSlot::chest;
- break;
- case 5:
- semanticSlot = EquipmentSlot::legs;
- break;
- case 6:
- semanticSlot = EquipmentSlot::classItem;
- break;
- case 7:
- semanticSlot = EquipmentSlot::kinetic;
- break;
- case 8:
- semanticSlot = EquipmentSlot::energy;
- break;
- case 9:
- semanticSlot = EquipmentSlot::heavy;
- break;
- case 10:
- semanticSlot = EquipmentSlot::ship;
- break;
- case 11:
- semanticSlot = EquipmentSlot::vehicle;
- break;
- case 12:
- semanticSlot = EquipmentSlot::ghost;
- break;
- case 13:
- semanticSlot = EquipmentSlot::emblem;
- break;
- case 14:
- semanticSlot = EquipmentSlot::emote;
- break;
- case 15:
- semanticSlot = EquipmentSlot::clanBanner;
- break;
- case 17:
- semanticSlot = EquipmentSlot::finisher;
- break;
- default:
- return false;
- }
- semanticIndex = static_cast<std::size_t>(semanticSlot);
- return semanticIndex < authored_inventory::kEquipmentSlotCount;
- }
- /** Finds one instance exactly once in a checked, row-sorted loadout. */
- [[nodiscard]] bool find_resolved_position(const family4_loadout::ResolvedLoadout& loadout,
- std::uint64_t instanceSoid,
- ResolvedPosition& position) noexcept {
- bool found = false;
- for (std::size_t index = 0; index < loadout.itemCount; ++index) {
- const family4_loadout::ResolvedItem& item = loadout.items[index];
- if (item.instance.instanceSoid != instanceSoid) {
- continue;
- }
- if (found) {
- return false;
- }
- found = true;
- position.inventoryRow = item.inventoryRow;
- position.equipmentSlot = item.equipmentSlot;
- position.equipped = item.equipped;
- position.mutationSerial = item.mutationSerial;
- }
- return found;
- }
- /** @return True when the native placement and equipped marker are unchanged. */
- [[nodiscard]] bool same_position(const ResolvedPosition& left,
- const ResolvedPosition& right) noexcept {
- return left.inventoryRow == right.inventoryRow && left.equipmentSlot == right.equipmentSlot
- && left.equipped == right.equipped;
- }
- /**
- * Applies canonical mutation generations after one shape-only equipment transition.
- *
- * 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. 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,
- std::size_t characterIndex,
- std::uint64_t requestedInstanceSoid,
- EquipmentMutationKind kind,
- std::uint8_t expectedNativeSlot,
- const family4_loadout::ResolvedLoadout& beforeLoadout,
- CharacterState& after,
- std::size_t& movedItemCount) noexcept {
- movedItemCount = 0;
- if (characterIndex >= account.characterCount || kind == EquipmentMutationKind::none) {
- return false;
- }
- AccountState candidate = account;
- candidate.characters[characterIndex] = after;
- family4_loadout::ResolvedLoadout placedAfter{};
- if (!account::valid(candidate)
- || !family4_loadout::resolve(candidate, characterIndex, placedAfter)
- || placedAfter.itemCount != beforeLoadout.itemCount) {
- return false;
- }
- ResolvedPosition beforeRequested{};
- ResolvedPosition afterRequested{};
- if (!find_resolved_position(beforeLoadout, requestedInstanceSoid, beforeRequested)
- || !find_resolved_position(placedAfter, requestedInstanceSoid, afterRequested)
- || beforeRequested.equipmentSlot != expectedNativeSlot
- || afterRequested.equipmentSlot != expectedNativeSlot
- || (kind == EquipmentMutationKind::equip
- && (beforeRequested.equipped || !afterRequested.equipped))
- || (kind == EquipmentMutationKind::unequip
- && (!beforeRequested.equipped || afterRequested.equipped))) {
- return false;
- }
- const auto count_move = [&](const authored_inventory::Item& item) noexcept {
- ResolvedPosition beforePosition{};
- ResolvedPosition afterPosition{};
- if (!find_resolved_position(beforeLoadout, item.instanceSoid, beforePosition)
- || !find_resolved_position(placedAfter, item.instanceSoid, afterPosition)
- || beforePosition.equipmentSlot != afterPosition.equipmentSlot) {
- return false;
- }
- movedItemCount += static_cast<std::size_t>(!same_position(beforePosition, afterPosition));
- return true;
- };
- for (const auto& item : after.equipment.slots) {
- if (item.has_value() && !count_move(*item)) {
- return false;
- }
- }
- for (std::size_t index = 0; index < after.inventory.count; ++index) {
- if (!count_move(after.inventory.values[index])) {
- return false;
- }
- }
- // The serial is signed on the wire, so it must stay inside the positive int32 range.
- constexpr std::uint32_t kMaximumInventorySerial =
- static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)());
- if (movedItemCount == 0 || after.nextInventorySerial > kMaximumInventorySerial
- || movedItemCount > kMaximumInventorySerial - after.nextInventorySerial) {
- return false;
- }
- const auto stamp_move = [&](authored_inventory::Item& item) noexcept {
- ResolvedPosition beforePosition{};
- ResolvedPosition afterPosition{};
- if (!find_resolved_position(beforeLoadout, item.instanceSoid, beforePosition)
- || !find_resolved_position(placedAfter, item.instanceSoid, afterPosition)
- || beforePosition.equipmentSlot != afterPosition.equipmentSlot) {
- return false;
- }
- if (!same_position(beforePosition, afterPosition)) {
- item.mutationSerial = static_cast<std::int32_t>(after.nextInventorySerial++);
- }
- return true;
- };
- for (auto& item : after.equipment.slots) {
- if (item.has_value() && !stamp_move(*item)) {
- return false;
- }
- }
- for (std::size_t index = 0; index < after.inventory.count; ++index) {
- if (!stamp_move(after.inventory.values[index])) {
- return false;
- }
- }
- candidate.characters[characterIndex] = after;
- family4_loadout::ResolvedLoadout checkedAfter{};
- if (!account::valid(candidate)
- || !family4_loadout::resolve(candidate, characterIndex, checkedAfter)
- || checkedAfter.itemCount != placedAfter.itemCount) {
- return false;
- }
- for (const auto& item : after.equipment.slots) {
- if (!item.has_value()) {
- continue;
- }
- ResolvedPosition placed{};
- ResolvedPosition checked{};
- if (!find_resolved_position(placedAfter, item->instanceSoid, placed)
- || !find_resolved_position(checkedAfter, item->instanceSoid, checked)
- || !same_position(placed, checked) || checked.mutationSerial != item->mutationSerial) {
- return false;
- }
- }
- for (std::size_t index = 0; index < after.inventory.count; ++index) {
- const authored_inventory::Item& item = after.inventory.values[index];
- ResolvedPosition placed{};
- ResolvedPosition checked{};
- if (!find_resolved_position(placedAfter, item.instanceSoid, placed)
- || !find_resolved_position(checkedAfter, item.instanceSoid, checked)
- || !same_position(placed, checked) || checked.mutationSerial != item.mutationSerial) {
- return false;
- }
- }
- return true;
- }
- /** @return True when two authored item values are identical, including socket policy and tail. */
- [[nodiscard]] bool same_item(const authored_inventory::Item& left,
- const authored_inventory::Item& right) noexcept {
- return left.instanceSoid == right.instanceSoid && left.definitionHash == right.definitionHash
- && left.level == right.level && left.quantity == right.quantity
- && left.flags == right.flags && left.sockets.policy == right.sockets.policy
- && left.sockets.plugCount == right.sockets.plugCount
- && left.sockets.plugs == right.sockets.plugs
- && left.movementAbilityEntry == right.movementAbilityEntry
- && left.grenadeAbilityEntry == right.grenadeAbilityEntry
- && left.superAbilityEntry == right.superAbilityEntry
- && left.meleeAbilityEntry == right.meleeAbilityEntry
- && left.classAbilityEntry == right.classAbilityEntry;
- }
- /** Records one checked native item-state transition. */
- void report_item_state(std::string_view stage,
- std::string_view result,
- std::string_view reason,
- std::uint64_t characterSoid,
- std::uint64_t instanceSoid,
- std::uint16_t definitionIndex,
- std::uint32_t beforeFlags,
- std::uint32_t afterFlags,
- bool equipped,
- std::size_t itemIndex) noexcept {
- std::array<char, core::log::kLineCapacity> line{};
- const int count = std::snprintf(
- line.data(),
- line.size(),
- "ev=item_state stage=%.*s result=%.*s reason=%.*s character=0x%llX instance=0x%llX "
- "definition=%u flags_before=0x%X flags_after=0x%X equipped=%u item_index=%zu",
- static_cast<int>(stage.size()),
- stage.data(),
- static_cast<int>(result.size()),
- result.data(),
- static_cast<int>(reason.size()),
- reason.data(),
- static_cast<unsigned long long>(characterSoid),
- static_cast<unsigned long long>(instanceSoid),
- static_cast<unsigned>(definitionIndex),
- beforeFlags,
- afterFlags,
- equipped ? 1U : 0U,
- itemIndex);
- if (count > 0) {
- core::log::write(core::log::Channel::state,
- result == "ok" ? core::log::Level::debug : core::log::Level::warn,
- {line.data(), static_cast<std::size_t>(count)});
- }
- }
- /** Exact stationary-item comparison, including its inventory mutation generation. */
- [[nodiscard]] bool same_stationary_item(const authored_inventory::Item& left,
- const authored_inventory::Item& right) noexcept {
- return same_item(left, right) && left.mutationSerial == right.mutationSerial;
- }
- /** @return True when every item-bearing field of two character views is identical. */
- [[nodiscard]] bool same_loadout(const CharacterState& left, const CharacterState& right) noexcept {
- if (left.soid != right.soid || left.selected != right.selected || left.race != right.race
- || left.gender != right.gender || left.characterClass != right.characterClass
- || left.level != right.level || left.accepted != right.accepted
- || left.previewAvailable != right.previewAvailable
- || left.appearanceValue != right.appearanceValue
- || left.lastOrbitedDestination != right.lastOrbitedDestination
- || left.contentBypass != right.contentBypass
- || left.nextInventorySerial != right.nextInventorySerial
- || left.inventory.count != right.inventory.count) {
- return false;
- }
- for (std::size_t index = 0; index < left.equipment.slots.size(); ++index) {
- const auto& leftItem = left.equipment.slots[index];
- const auto& rightItem = right.equipment.slots[index];
- if (leftItem.has_value() != rightItem.has_value()
- || (leftItem.has_value() && !same_stationary_item(*leftItem, *rightItem))) {
- return false;
- }
- }
- for (std::size_t index = 0; index < left.inventory.count; ++index) {
- if (!same_stationary_item(left.inventory.values[index], right.inventory.values[index])) {
- return false;
- }
- }
- return true;
- }
- /** Exact character comparison, including the canonical unused inventory tail. */
- [[nodiscard]] bool same_character(const CharacterState& left,
- const CharacterState& right) noexcept {
- if (!same_loadout(left, right)) {
- return false;
- }
- for (std::size_t index = left.inventory.count; index < left.inventory.values.size(); ++index) {
- if (!same_stationary_item(left.inventory.values[index], right.inventory.values[index])) {
- return false;
- }
- }
- return true;
- }
- /** Finds one item SOID exactly once in the character's equipment and dense inventory. */
- [[nodiscard]] bool find_character_item_location(const CharacterState& character,
- std::uint64_t instanceSoid,
- CharacterItemLocation& location) noexcept {
- location = {};
- bool found = false;
- for (std::size_t index = 0; index < character.equipment.slots.size(); ++index) {
- const auto& item = character.equipment.slots[index];
- if (!item.has_value() || item->instanceSoid != instanceSoid) {
- continue;
- }
- if (found) {
- return false;
- }
- found = true;
- location = {index, true};
- }
- for (std::size_t index = 0; index < character.inventory.count; ++index) {
- if (character.inventory.values[index].instanceSoid != instanceSoid) {
- continue;
- }
- if (found) {
- return false;
- }
- found = true;
- location = {index, false};
- }
- return found;
- }
- /** Borrows an item at a previously validated authored location. */
- [[nodiscard]] const authored_inventory::Item*
- character_item_at(const CharacterState& character, const CharacterItemLocation& location) noexcept {
- if (location.equipped) {
- if (location.index >= character.equipment.slots.size()
- || !character.equipment.slots[location.index].has_value()) {
- return nullptr;
- }
- return &*character.equipment.slots[location.index];
- }
- if (location.index >= character.inventory.count) {
- return nullptr;
- }
- return &character.inventory.values[location.index];
- }
- /** Borrows a mutable item at a previously validated authored location. */
- [[nodiscard]] authored_inventory::Item*
- character_item_at(CharacterState& character, const CharacterItemLocation& location) noexcept {
- if (location.equipped) {
- if (location.index >= character.equipment.slots.size()
- || !character.equipment.slots[location.index].has_value()) {
- return nullptr;
- }
- return &*character.equipment.slots[location.index];
- }
- if (location.index >= character.inventory.count) {
- return nullptr;
- }
- return &character.inventory.values[location.index];
- }
- } // namespace runtime::detail
- } // namespace sunrise::state
|