#include #include #include #include #include #include #include #include #include #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; /** Writes one exhaustive equipment-transaction checkpoint to the persistent diagnostic log. */ void report_equipment(std::string_view stage, std::string_view result, EquipmentMutationKind kind, std::uint64_t characterSoid, std::uint64_t previousSoid, std::uint64_t requestedSoid, std::size_t equipmentIndex, std::size_t inventoryIndex, std::uint8_t nativeSlot, std::size_t movedItemCount, std::uint32_t previousHash, std::uint32_t requestedHash) noexcept { const std::string_view operation = kind == EquipmentMutationKind::unequip ? "unequip" : "equip"; std::array line{}; const int count = std::snprintf( line.data(), line.size(), "ev=equip operation=%.*s stage=%.*s result=%.*s character=0x%llX previous=0x%llX " "requested=0x%llX equipment_index=%zu inventory_index=%zu native_slot=%u " "moved_items=%zu previous_hash=0x%08X requested_hash=0x%08X", static_cast(operation.size()), operation.data(), static_cast(stage.size()), stage.data(), static_cast(result.size()), result.data(), static_cast(characterSoid), static_cast(previousSoid), static_cast(requestedSoid), equipmentIndex, inventoryIndex, static_cast(nativeSlot), movedItemCount, previousHash, requestedHash); if (count > 0) { core::log::write(core::log::Channel::state, result == "ok" ? core::log::Level::debug : core::log::Level::warn, {line.data(), static_cast(count)}); } } /** Writes one exhaustive item-acquisition transaction checkpoint. */ void report_acquisition(std::string_view stage, std::string_view result, std::string_view reason, std::uint32_t definitionHash, std::uint64_t characterSoid, std::uint64_t instanceSoid, std::size_t inventoryIndex, std::uint16_t inventoryRow, std::uint8_t equipmentSlot, std::uint32_t nextInventorySerial) noexcept { std::array line{}; const int count = std::snprintf( line.data(), line.size(), "ev=acquire stage=%.*s result=%.*s reason=%.*s definition_hash=0x%08X character=0x%llX " "instance=0x%llX inventory_index=%zu inventory_row=%u equipment_slot=%u next_serial=%u", static_cast(stage.size()), stage.data(), static_cast(result.size()), result.data(), static_cast(reason.size()), reason.data(), definitionHash, static_cast(characterSoid), static_cast(instanceSoid), inventoryIndex, static_cast(inventoryRow), static_cast(equipmentSlot), nextInventorySerial); if (count > 0) { core::log::write(core::log::Channel::state, result == "ok" ? core::log::Level::debug : core::log::Level::warn, {line.data(), static_cast(count)}); } } } // namespace runtime::detail using namespace runtime::detail; /** Stores the active account key without publishing an incomplete account. */ bool set_primary_soid(std::uint64_t primarySoid) noexcept { if (primarySoid == 0) { return false; } AcquireSRWLockExclusive(&runtime::storage::g_stateLock); AccountState candidate = runtime::storage::g_state.account; candidate.primarySoid = primarySoid; // Characters belong to the account key the Client uses. The reference account and its // characters differ only in the low byte, so the authored rows are rebased onto that key. for (std::size_t index = 0; index < candidate.characterCount; ++index) { candidate.characters[index].soid = primarySoid + 1U + index; } if (!account::valid(candidate)) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } // Publish only after the settings and identity rules hold together. runtime::storage::g_state.account = candidate; ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return true; } /** Moves the selection to one authored character. */ bool set_selected_character(std::uint64_t characterSoid, bool& changed) noexcept { changed = false; if (characterSoid == 0) { return false; } AcquireSRWLockExclusive(&runtime::storage::g_stateLock); AccountState candidate = runtime::storage::g_state.account; std::size_t picked = candidate.characterCount; for (std::size_t index = 0; index < candidate.characterCount; ++index) { if (candidate.characters[index].soid == characterSoid) { picked = index; } } if (picked == candidate.characterCount) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } const bool alreadySelected = candidate.characters[picked].selected; for (CharacterState& character : candidate.characters) { character.selected = false; } candidate.characters[picked].selected = true; if (!account::valid(candidate)) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } // Publish only after the whole account still meets its identity rules. runtime::storage::g_state.account = candidate; ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); changed = !alreadySelected; return true; } /** Prepares one checked equip transition without changing account State. */ bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid, PendingEquipmentSwap& mutation) noexcept { mutation = {}; const AccountState account = account_snapshot(); if (requestedInstanceSoid == 0 || !account::valid(account)) { return false; } std::size_t characterIndex = account.characterCount; for (std::size_t index = 0; index < account.characterCount; ++index) { if (account.characters[index].selected) { characterIndex = index; break; } } if (characterIndex == account.characterCount) { return false; } const CharacterState& before = account.characters[characterIndex]; family4_loadout::ResolvedLoadout beforeLoadout{}; if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) { return false; } std::size_t inventoryIndex = before.inventory.count; for (std::size_t index = 0; index < before.inventory.count; ++index) { if (before.inventory.values[index].instanceSoid != requestedInstanceSoid) { continue; } if (inventoryIndex != before.inventory.count) { return false; } inventoryIndex = index; } if (inventoryIndex == before.inventory.count) { return false; } const authored_inventory::Item& requested = before.inventory.values[inventoryIndex]; std::uint8_t requestedNativeSlot = 0; std::size_t equipmentSlotIndex = authored_inventory::kEquipmentSlotCount; ResolvedPosition requestedPosition{}; if (!native_equipment_slot(requested, requestedNativeSlot) || !semantic_equipment_slot(requestedNativeSlot, equipmentSlotIndex) || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition) || requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) { return false; } CharacterState after = before; auto& equipped = after.equipment.slots[equipmentSlotIndex]; std::uint64_t previousInstanceSoid = 0; std::uint32_t previousDefinitionHash = 0; if (equipped.has_value()) { std::uint8_t previousNativeSlot = 0; ResolvedPosition previousPosition{}; if (!native_equipment_slot(*equipped, previousNativeSlot) || previousNativeSlot != requestedNativeSlot || !find_resolved_position(beforeLoadout, equipped->instanceSoid, previousPosition) || !previousPosition.equipped || previousPosition.equipmentSlot != requestedNativeSlot) { return false; } previousInstanceSoid = equipped->instanceSoid; previousDefinitionHash = equipped->definitionHash; std::swap(*equipped, after.inventory.values[inventoryIndex]); } else { equipped = after.inventory.values[inventoryIndex]; for (std::size_t index = inventoryIndex; index + 1U < after.inventory.count; ++index) { after.inventory.values[index] = after.inventory.values[index + 1U]; } --after.inventory.count; after.inventory.values[after.inventory.count] = {}; } std::size_t movedItemCount = 0; if (!finalize_equipment_transition(account, characterIndex, requestedInstanceSoid, EquipmentMutationKind::equip, requestedNativeSlot, beforeLoadout, after, movedItemCount)) { return false; } mutation.beforeCharacter = before; mutation.afterCharacter = after; mutation.characterSoid = before.soid; mutation.requestedInstanceSoid = requestedInstanceSoid; mutation.previousInstanceSoid = previousInstanceSoid; mutation.characterIndex = characterIndex; mutation.equipmentSlotIndex = equipmentSlotIndex; mutation.inventoryIndex = inventoryIndex; mutation.movedItemCount = movedItemCount; mutation.nativeEquipmentSlot = requestedNativeSlot; mutation.kind = EquipmentMutationKind::equip; mutation.prepared = true; report_equipment("prepare", "ok", mutation.kind, mutation.characterSoid, mutation.previousInstanceSoid, mutation.requestedInstanceSoid, mutation.equipmentSlotIndex, mutation.inventoryIndex, mutation.nativeEquipmentSlot, mutation.movedItemCount, previousDefinitionHash, requested.definitionHash); return true; } /** Prepares one checked equipped-to-inventory transition without changing account State. */ bool prepare_equipment_unequip(std::uint64_t requestedInstanceSoid, PendingEquipmentSwap& mutation) noexcept { mutation = {}; const AccountState account = account_snapshot(); if (requestedInstanceSoid == 0 || !account::valid(account)) { return false; } std::size_t characterIndex = account.characterCount; for (std::size_t index = 0; index < account.characterCount; ++index) { if (account.characters[index].selected) { characterIndex = index; break; } } if (characterIndex == account.characterCount) { return false; } const CharacterState& before = account.characters[characterIndex]; if (before.inventory.count >= before.inventory.values.size()) { return false; } family4_loadout::ResolvedLoadout beforeLoadout{}; if (!family4_loadout::resolve(account, characterIndex, beforeLoadout)) { return false; } std::size_t equipmentSlotIndex = before.equipment.slots.size(); for (std::size_t index = 0; index < before.equipment.slots.size(); ++index) { const auto& item = before.equipment.slots[index]; if (!item.has_value() || item->instanceSoid != requestedInstanceSoid) { continue; } if (equipmentSlotIndex != before.equipment.slots.size()) { return false; } equipmentSlotIndex = index; } if (equipmentSlotIndex == before.equipment.slots.size()) { return false; } const authored_inventory::Item& requested = *before.equipment.slots[equipmentSlotIndex]; std::uint8_t requestedNativeSlot = 0; std::uint8_t requestedBucketId = 0; std::size_t expectedSemanticIndex = authored_inventory::kEquipmentSlotCount; ResolvedPosition requestedPosition{}; if (!native_equipment_slot(requested, requestedNativeSlot) || !inventory_bucket_id(requested, requestedBucketId) || !semantic_equipment_slot(requestedNativeSlot, expectedSemanticIndex) || expectedSemanticIndex != equipmentSlotIndex || !find_resolved_position(beforeLoadout, requestedInstanceSoid, requestedPosition) || !requestedPosition.equipped || requestedPosition.equipmentSlot != requestedNativeSlot) { return false; } std::size_t inventoryIndex = before.inventory.count; for (std::size_t index = 0; index < before.inventory.count; ++index) { std::uint8_t inventoryBucketId = 0; if (!inventory_bucket_id(before.inventory.values[index], inventoryBucketId)) { return false; } if (inventoryBucketId == requestedBucketId) { inventoryIndex = index; break; } } CharacterState after = before; const authored_inventory::Item unequipped = *after.equipment.slots[equipmentSlotIndex]; for (std::size_t index = after.inventory.count; index > inventoryIndex; --index) { after.inventory.values[index] = after.inventory.values[index - 1U]; } after.inventory.values[inventoryIndex] = unequipped; ++after.inventory.count; after.equipment.slots[equipmentSlotIndex].reset(); std::size_t movedItemCount = 0; if (!finalize_equipment_transition(account, characterIndex, requestedInstanceSoid, EquipmentMutationKind::unequip, requestedNativeSlot, beforeLoadout, after, movedItemCount)) { return false; } mutation.beforeCharacter = before; mutation.afterCharacter = after; mutation.characterSoid = before.soid; mutation.requestedInstanceSoid = requestedInstanceSoid; mutation.characterIndex = characterIndex; mutation.equipmentSlotIndex = equipmentSlotIndex; mutation.inventoryIndex = inventoryIndex; mutation.movedItemCount = movedItemCount; mutation.nativeEquipmentSlot = requestedNativeSlot; mutation.kind = EquipmentMutationKind::unequip; mutation.prepared = true; report_equipment("prepare", "ok", mutation.kind, mutation.characterSoid, mutation.previousInstanceSoid, mutation.requestedInstanceSoid, mutation.equipmentSlotIndex, mutation.inventoryIndex, mutation.nativeEquipmentSlot, mutation.movedItemCount, 0, requested.definitionHash); return true; } /** Commits one prepared equipment after-image behind an exact character staleness guard. */ bool commit_equipment_swap(PendingEquipmentSwap& mutation) noexcept { const PendingEquipmentSwap prepared = mutation; mutation = {}; if (!prepared.prepared || prepared.characterSoid == 0 || prepared.requestedInstanceSoid == 0 || (prepared.kind != EquipmentMutationKind::equip && prepared.kind != EquipmentMutationKind::unequip) || prepared.characterIndex >= kCharacterCapacity || prepared.equipmentSlotIndex >= authored_inventory::kEquipmentSlotCount || prepared.inventoryIndex >= authored_inventory::kCharacterItemCapacity || prepared.nativeEquipmentSlot >= item_details::kEquipmentSlotCount || prepared.beforeCharacter.soid != prepared.characterSoid || prepared.afterCharacter.soid != prepared.characterSoid) { return false; } const std::uint32_t previousDefinitionHash = character_item_definition_hash(prepared.afterCharacter, prepared.previousInstanceSoid); const std::uint32_t requestedDefinitionHash = character_item_definition_hash(prepared.afterCharacter, prepared.requestedInstanceSoid); report_equipment("commit_begin", "ok", prepared.kind, prepared.characterSoid, prepared.previousInstanceSoid, prepared.requestedInstanceSoid, prepared.equipmentSlotIndex, prepared.inventoryIndex, prepared.nativeEquipmentSlot, prepared.movedItemCount, previousDefinitionHash, requestedDefinitionHash); AcquireSRWLockExclusive(&runtime::storage::g_stateLock); AccountState candidate = runtime::storage::g_state.account; if (prepared.characterIndex >= candidate.characterCount || !same_character(candidate.characters[prepared.characterIndex], prepared.beforeCharacter)) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } candidate.characters[prepared.characterIndex] = prepared.afterCharacter; family4_loadout::ResolvedLoadout checkedAfter{}; if (!account::valid(candidate) || !family4_loadout::resolve(candidate, prepared.characterIndex, checkedAfter)) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } ResolvedPosition requestedPosition{}; const bool expectedEquipped = prepared.kind == EquipmentMutationKind::equip; if (!find_resolved_position(checkedAfter, prepared.requestedInstanceSoid, requestedPosition) || requestedPosition.equipmentSlot != prepared.nativeEquipmentSlot || requestedPosition.equipped != expectedEquipped) { ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); return false; } runtime::storage::g_state.account = candidate; ReleaseSRWLockExclusive(&runtime::storage::g_stateLock); report_equipment("commit_end", "ok", prepared.kind, prepared.characterSoid, prepared.previousInstanceSoid, prepared.requestedInstanceSoid, prepared.equipmentSlotIndex, prepared.inventoryIndex, prepared.nativeEquipmentSlot, prepared.movedItemCount, previousDefinitionHash, requestedDefinitionHash); return true; } /** @return A copy of the active account state, read under the lock. */ AccountState account_snapshot() noexcept { AcquireSRWLockShared(&runtime::storage::g_stateLock); const AccountState snapshot = runtime::storage::g_state.account; ReleaseSRWLockShared(&runtime::storage::g_stateLock); return snapshot; } } // namespace sunrise::state