diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 74fd6033c..96e12c98a 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -38,6 +38,7 @@ #include "ModuleAssemblyComponent.h" #include "HavokVehiclePhysicsComponent.h" #include "CharacterComponent.h" +#include "ControllablePhysicsComponent.h" #include "dZoneManager.h" #include "PropertyManagementComponent.h" #include "DestroyableComponent.h" @@ -2407,8 +2408,17 @@ void InventoryComponent::OnClientItemConsumed(const GameMessages::ClientItemCons void InventoryComponent::OnUseNonEquipmentItem(const GameMessages::UseNonEquipmentItem& msg) { auto* item = FindItemById(msg.itemToUse); + if (!item) return; - if (item) item->UseNonEquip(item); + // As LWOInventoryComponent_Common::msgUseNonEquipmentItem (0x00cdeba0): nothing is used while GetStunned says + // canUseItem is false (LWOControllablePhysComponent::msgGetStunned 0x00be0df0: stunned from using items, or dead) + auto* const physics = m_Parent->GetComponent(); + GameMessages::IsDead isDead{}; + isDead.target = m_Parent->GetObjectID(); + const bool dead = m_Parent->HandleMsg(isDead) && isDead.bDead; + if (physics && (physics->GetStunnedFromUsingItems() || dead)) return; + + item->UseNonEquip(item); } void InventoryComponent::OnUpdateInventoryGroup(const GameMessages::UpdateInventoryGroup& msg) { diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp index cb87bbda6..c6c8545e4 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp @@ -370,6 +370,13 @@ void ControllablePhysicsComponent::DeactivateBubbleBuff() { Game::entityManager->SerializeEntity(m_Parent); }; +void ControllablePhysicsComponent::ApplyStun(const GameMessages::SetStunned& stun) { + // Only characters are stunned from using items + if (!stun.bCantUseItem || !m_Parent->IsPlayer()) return; + if (!stun.bIgnoreImmunity && m_ImmuneToStunUseItemCount > 0) return; + m_StunnedUseItemCount = std::max(0, m_StunnedUseItemCount + (stun.StateChangeType == eStateChangeType::PUSH ? 1 : -1)); +} + void ControllablePhysicsComponent::SetStunImmunity( const eStateChangeType state, const LWOOBJID originator, diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.h b/dGame/dComponents/Physics/ControllablePhysicsComponent.h index f2fd199f2..d82564ca8 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.h +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.h @@ -19,6 +19,9 @@ namespace tinyxml2 { class Entity; class dpEntity; enum class eStateChangeType : uint32_t; +namespace GameMessages { + struct SetStunned; +} /** * Handles the movement of controllable Entities, e.g. enemies and players @@ -289,6 +292,11 @@ public: const bool GetImmuneToStunTurn() { return m_ImmuneToStunTurnCount > 0;}; const bool GetImmuneToStunUseItem() { return m_ImmuneToStunUseItemCount > 0;}; + // A SetStunned sent for this entity, counted as LWOControllablePhysComponent::msgSetStunned (0x00bf1d10) counts it. + // Only the use-item stun is kept: the server checks it before using an item (msgGetStunned 0x00be0df0). + void ApplyStun(const GameMessages::SetStunned& stun); + bool GetStunnedFromUsingItems() const { return m_StunnedUseItemCount > 0; } + private: // The save's physics state (LoadFromSaveData), then where the character comes into the world void ReadSaveData(const tinyxml2::XMLDocument& doc); @@ -421,6 +429,9 @@ private: int32_t m_ImmuneToStunMoveCount; int32_t m_ImmuneToStunTurnCount; int32_t m_ImmuneToStunUseItemCount; + + // Stuns from using items (ApplyStun) + int32_t m_StunnedUseItemCount = 0; }; #endif // CONTROLLABLEPHYSICSCOMPONENT_H diff --git a/dGame/dGameMessages/CombatMessages.cpp b/dGame/dGameMessages/CombatMessages.cpp index 9242f6998..dba6dc2e4 100644 --- a/dGame/dGameMessages/CombatMessages.cpp +++ b/dGame/dGameMessages/CombatMessages.cpp @@ -208,6 +208,13 @@ namespace GameMessages { return true; } + void SetStunned::Send(const SystemAddress& sysAddr) const { + auto* const entity = Game::entityManager->GetEntity(target); + auto* const physics = entity ? entity->GetComponent() : nullptr; + if (physics) physics->ApplyStun(*this); + NetGameMsg::Send(sysAddr); + } + void SetStunned::Serialize(RakNet::BitStream& bitStream) const { BitStreamUtils::WriteOptional(bitStream, Originator, LWOOBJID_EMPTY); bitStream.Write(StateChangeType); diff --git a/dGame/dGameMessages/CombatMessages.h b/dGame/dGameMessages/CombatMessages.h index fc6778704..eccd28d2d 100644 --- a/dGame/dGameMessages/CombatMessages.h +++ b/dGame/dGameMessages/CombatMessages.h @@ -132,6 +132,8 @@ namespace GameMessages { // Server -> client. struct SetStunned : public NetGameMsg { SetStunned() : NetGameMsg(MessageType::Game::SET_STUNNED) {} + // Also counts the stun on the target, as the client does (ControllablePhysicsComponent::ApplyStun), then sends it + void Send(const SystemAddress& sysAddr) const; void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; diff --git a/tests/dGameTests/dComponentsTests/UseItemResultTests.cpp b/tests/dGameTests/dComponentsTests/UseItemResultTests.cpp index 8f3af50c3..d364220a4 100644 --- a/tests/dGameTests/dComponentsTests/UseItemResultTests.cpp +++ b/tests/dGameTests/dComponentsTests/UseItemResultTests.cpp @@ -5,11 +5,17 @@ #include "CDItemComponentTable.h" #include "CDLootMatrixTable.h" #include "CDPackageComponentTable.h" +#include "Character.h" +#include "CharacterComponent.h" +#include "CombatMessages.h" +#include "ControllablePhysicsComponent.h" +#include "DestroyableComponent.h" #include "Entity.h" #include "InventoryComponent.h" #include "InventoryMessages.h" #include "Item.h" #include "eReplicaComponentType.h" +#include "eStateChangeType.h" #include "dGameMessagesTests/GameMessageTestUtils.h" @@ -65,3 +71,90 @@ TEST_F(UseItemResultTests, AnOpenedPackageReportsSuccess) { EXPECT_TRUE(results[0].useItemResult); EXPECT_EQ(package->GetCount(), 1u); } + +// LWOInventoryComponent_Common::msgUseNonEquipmentItem (0x00cdeba0) uses nothing while GetStunned says the player can't +// use items (LWOControllablePhysComponent::msgGetStunned 0x00be0df0) +class UseItemStunTests : public UseItemResultTests { +protected: + std::unique_ptr character; + ControllablePhysicsComponent* physics{}; + Item* package{}; + + void SetUp() override { + UseItemResultTests::SetUp(); + // Again as a player + entity.reset(); + info.lot = 1; + CDClientManager::GetEntriesMutable().insert_or_assign(static_cast(info.lot), 0); + entity = std::make_unique(1, info); + inventoryComponent = entity->AddComponent(-1); + character = std::make_unique(1, nullptr); + entity->SetCharacter(character.get()); + entity->AddComponent(-1, character.get(), ClientAddress())->InitializeStatisticsFromString(""); + physics = entity->AddComponent(-1); + auto* const items = inventoryComponent->GetInventory(eInventoryType::ITEMS); + package = new Item(0x7000, PACKAGE, items, 0, 5, false, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE); + } + + void TearDown() override { + entity->SetCharacter(nullptr); + entity.reset(); + character.reset(); + UseItemResultTests::TearDown(); + } + + void Use() { + GameMessages::UseNonEquipmentItem msg; + msg.itemToUse = package->GetId(); + inventoryComponent->OnUseNonEquipmentItem(msg); + } + + void Stun(const eStateChangeType type, const bool cantUseItem = true) { + GameMessages::SetStunned stun; + stun.target = entity->GetObjectID(); + stun.StateChangeType = type; + stun.bCantUseItem = cantUseItem; + physics->ApplyStun(stun); + } +}; + +TEST_F(UseItemStunTests, NothingIsUsedWhileStunnedFromUsingItems) { + ASSERT_TRUE(entity->IsPlayer()); + Stun(eStateChangeType::PUSH); + Stun(eStateChangeType::PUSH); + Use(); + EXPECT_EQ(package->GetCount(), 5u); + + Stun(eStateChangeType::POP); + Use(); + EXPECT_EQ(package->GetCount(), 5u); + + Stun(eStateChangeType::POP); + Use(); + EXPECT_EQ(package->GetCount(), 4u); + + // Other stuns don't stop it, and a pop with nothing pushed stays at 0 + Stun(eStateChangeType::PUSH, false); + Stun(eStateChangeType::POP); + Use(); + EXPECT_EQ(package->GetCount(), 3u); +} + +TEST_F(UseItemStunTests, ImmunityKeepsTheStunOff) { + physics->SetStunImmunity(eStateChangeType::PUSH, LWOOBJID_EMPTY, false, false, false, false, false, false, true); + GameMessages::SetStunned stun; + stun.StateChangeType = eStateChangeType::PUSH; + stun.bCantUseItem = true; + stun.bIgnoreImmunity = false; + physics->ApplyStun(stun); + EXPECT_FALSE(physics->GetStunnedFromUsingItems()); + stun.bIgnoreImmunity = true; + physics->ApplyStun(stun); + EXPECT_TRUE(physics->GetStunnedFromUsingItems()); +} + +TEST_F(UseItemStunTests, NothingIsUsedWhileDead) { + entity->AddComponent(-1)->SetIsDead(true); + Use(); + EXPECT_EQ(package->GetCount(), 5u); +}