#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include InventoryService::InventoryService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) , m_dispatcher(aDispatcher) , m_transport(aTransport) { m_updateConnection = m_dispatcher.sink().connect<&InventoryService::OnUpdate>(this); m_inventoryConnection = m_dispatcher.sink().connect<&InventoryService::OnInventoryChangeEvent>(this); m_equipmentConnection = m_dispatcher.sink().connect<&InventoryService::OnEquipmentChangeEvent>(this); m_inventoryChangeConnection = m_dispatcher.sink().connect<&InventoryService::OnNotifyInventoryChanges>(this); m_equipmentChangeConnection = m_dispatcher.sink().connect<&InventoryService::OnNotifyEquipmentChanges>(this); } void InventoryService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { RunWeaponStateUpdates(); RunNakedNPCBugChecks(); } void InventoryService::OnInventoryChangeEvent(const InventoryChangeEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; auto view = m_world.view(); const auto iter = std::find_if(std::begin(view), std::end(view), [view, formId = acEvent.FormId](auto entity) { return view.get(entity).Id == formId; }); if (iter == std::end(view)) return; std::optional serverIdRes = Utils::GetServerId(*iter); if (!serverIdRes.has_value()) { spdlog::error(__FUNCTION__ ": failed to find server id, target form id: {:X}, item id: {:X}, count: {}", acEvent.FormId, acEvent.Item.BaseId.BaseId, acEvent.Item.Count); return; } RequestInventoryChanges request; request.ServerId = serverIdRes.value(); request.Item = acEvent.Item; request.Drop = acEvent.Drop; request.UpdateClients = acEvent.UpdateClients; m_transport.Send(request); spdlog::info("Sending item request, item: {:X}, count: {}, target object: {:X}", acEvent.Item.BaseId.BaseId, acEvent.Item.Count, acEvent.FormId); } void InventoryService::OnEquipmentChangeEvent(const EquipmentChangeEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; auto view = m_world.view(); const auto iter = std::find_if(std::begin(view), std::end(view), [view, formId = acEvent.ActorId](auto entity) { return view.get(entity).Id == formId; }); if (iter == std::end(view)) return; std::optional serverIdRes = Utils::GetServerId(*iter); if (!serverIdRes.has_value()) { spdlog::error(__FUNCTION__ ": failed to find server id, actor id: {:X}, item id: {:X}, isAmmo: {}, unequip: {}, slot: {:X}", acEvent.ActorId, acEvent.ItemId, acEvent.IsAmmo, acEvent.Unequip, acEvent.EquipSlotId); return; } Actor* pActor = Cast(TESForm::GetById(acEvent.ActorId)); if (!pActor) return; auto& modSystem = World::Get().GetModSystem(); RequestEquipmentChanges request; request.ServerId = serverIdRes.value(); if (!modSystem.GetServerModId(acEvent.EquipSlotId, request.EquipSlotId)) return; if (!modSystem.GetServerModId(acEvent.ItemId, request.ItemId)) return; request.Count = acEvent.Count; request.Unequip = acEvent.Unequip; request.IsSpell = acEvent.IsSpell; request.IsShout = acEvent.IsShout; request.IsAmmo = acEvent.IsAmmo; request.CurrentInventory = pActor->GetEquipment(); m_transport.Send(request); spdlog::info("Sending equipment request, item: {:X}, count: {}, target object: {:X}", acEvent.ItemId, acEvent.Count, acEvent.ActorId); } void InventoryService::OnNotifyInventoryChanges(const NotifyInventoryChanges& acMessage) noexcept { if (acMessage.Drop) { Actor* pActor = Utils::GetByServerId(acMessage.ServerId); if (!pActor) { spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); return; } ScopedInventoryOverride _; pActor->DropOrPickUpObject(acMessage.Item, nullptr, nullptr); } else { TESObjectREFR* pObject = Utils::GetByServerId(acMessage.ServerId); if (!pObject) return; ScopedInventoryOverride _; pObject->AddOrRemoveItem(acMessage.Item); } } void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& acMessage) noexcept { Actor* pActor = Utils::GetByServerId(acMessage.ServerId); if (!pActor) { spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); return; } auto& modSystem = World::Get().GetModSystem(); uint32_t itemId = modSystem.GetGameId(acMessage.ItemId); TESForm* pItem = TESForm::GetById(itemId); if (!pItem) { spdlog::error("Could not find inventory item {:X}:{:X}", acMessage.ItemId.ModId, acMessage.ItemId.BaseId); return; } uint32_t equipSlotId = modSystem.GetGameId(acMessage.EquipSlotId); TESForm* pEquipSlot = TESForm::GetById(equipSlotId); uint32_t slotId = 0; if (pEquipSlot == DefaultObjectManager::Get().rightEquipSlot) slotId = 1; auto* pEquipManager = EquipManager::Get(); if (acMessage.IsSpell) { if (acMessage.Unequip) pEquipManager->UnEquipSpell(pActor, pItem, slotId); else pEquipManager->EquipSpell(pActor, pItem, slotId); return; } else if (acMessage.IsShout) { if (acMessage.Unequip) pEquipManager->UnEquipShout(pActor, pItem); else pEquipManager->EquipShout(pActor, pItem); return; } auto* pObject = Cast(pItem); // TODO: ExtraData necessary? probably if (acMessage.Unequip) { pEquipManager->UnEquip(pActor, pItem, nullptr, acMessage.Count, pEquipSlot, false, true, false, false, nullptr); } else { // Unequip all armor first, since the game won't auto unequip armor Inventory wornArmor{}; if (pItem->formType == FormType::Armor) { wornArmor = pActor->GetWornArmor(); for (const auto& armor : wornArmor.Entries) { uint32_t armorId = modSystem.GetGameId(armor.BaseId); TESForm* pArmor = TESForm::GetById(armorId); if (pArmor) pEquipManager->UnEquip(pActor, pArmor, nullptr, 1, pEquipSlot, false, true, false, false, nullptr); } } pEquipManager->Equip(pActor, pItem, nullptr, acMessage.Count, pEquipSlot, false, true, false, false); for (const auto& armor : wornArmor.Entries) { uint32_t armorId = modSystem.GetGameId(armor.BaseId); TESForm* pArmor = TESForm::GetById(armorId); if (pArmor) pEquipManager->Equip(pActor, pArmor, nullptr, 1, pEquipSlot, false, true, false, false); } } } void InventoryService::RunWeaponStateUpdates() noexcept { if (!m_transport.IsConnected()) return; static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 500ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; auto view = m_world.view(); for (auto entity : view) { const auto& formIdComponent = view.get(entity); Actor* const pActor = Cast(TESForm::GetById(formIdComponent.Id)); auto& localComponent = view.get(entity); bool isWeaponDrawn = pActor->actorState.IsWeaponDrawn(); if (isWeaponDrawn != localComponent.IsWeaponDrawn) { localComponent.IsWeaponDrawn = isWeaponDrawn; DrawWeaponRequest request; request.Id = localComponent.Id; request.IsWeaponDrawn = isWeaponDrawn; m_transport.Send(request); } } } void InventoryService::RunNakedNPCBugChecks() noexcept { if (!m_transport.IsConnected()) return; static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 1000ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; auto view = m_world.view(); for (auto entity : view) { const auto& formIdComponent = view.get(entity); Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) continue; if (pActor->GetExtension()->IsPlayer()) continue; if (pActor->IsDead()) continue; if (pActor->IsWearingBodyPiece()) continue; if (!pActor->ShouldWearBodyPiece()) continue; // Don't broadcast changes, it'll just make things messier. // If all clients have this problem, they'll all fix it individually. ScopedEquipOverride seo; ScopedInventoryOverride sio; pActor->ResetInventory(false); } }