mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Merge exp/loot-blocker-followups: item source keys, quickbuild cost source, pet blockers, carver_only walls
This commit is contained in:
@@ -1537,7 +1537,10 @@ void Entity::PickupItem(const LWOOBJID& objectID) const {
|
||||
}
|
||||
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::POWERUP_PICKUPS, info.lot, GetPosition(), 1, this);
|
||||
} else {
|
||||
inv->AddItem(info.lot, info.count, eLootSourceType::PICKUP, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 1);
|
||||
// Live told the client which object the item dropped from (all 2,344 live pickups of a dropped item)
|
||||
LootMetrics metrics{};
|
||||
if (info.sourceLot != 0) metrics.sourceLot = info.sourceLot;
|
||||
inv->AddItem(info.lot, info.count, eLootSourceType::PICKUP, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 1, false, -1, metrics);
|
||||
}
|
||||
|
||||
droppedLoot.erase(objectID);
|
||||
@@ -2118,10 +2121,12 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfoEve
|
||||
bool Entity::MsgDropClientLoot(GameMessages::DropClientLootEvent& event) {
|
||||
const auto& dropLootMsg = event.msg;
|
||||
if (dropLootMsg.item != LOT_NULL && dropLootMsg.item != 0) {
|
||||
const auto* const source = Game::entityManager->GetEntity(dropLootMsg.sourceID);
|
||||
Loot::Info info{
|
||||
.id = dropLootMsg.lootID,
|
||||
.lot = dropLootMsg.item,
|
||||
.count = dropLootMsg.count,
|
||||
.sourceLot = source ? source->GetLOT() : 0,
|
||||
};
|
||||
AddLootItem(info);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,8 @@ void Trade::Complete() {
|
||||
recordCoins(m_CoinsA, characterA->GetID(), characterB->GetID());
|
||||
recordCoins(m_CoinsB, characterB->GetID(), characterA->GetID());
|
||||
|
||||
const auto giveItems = [zone](const std::vector<TradeItem>& items, InventoryComponent* from, MissionComponent* fromMissions,
|
||||
const auto tradeId = m_TradeId;
|
||||
const auto giveItems = [zone, tradeId](const std::vector<TradeItem>& items, InventoryComponent* from, MissionComponent* fromMissions,
|
||||
InventoryComponent* to, const LWOOBJID fromCharacter, const LWOOBJID toCharacter) {
|
||||
for (const auto& tradeItem : items) {
|
||||
auto* itemToRemove = from->FindItemById(tradeItem.itemId);
|
||||
@@ -204,9 +205,12 @@ void Trade::Complete() {
|
||||
const auto bound = itemToRemove->GetBound();
|
||||
itemToRemove->SetCount(itemToRemove->GetCount() - tradeItem.itemCount);
|
||||
fromMissions->Progress(eMissionTaskType::GATHER, tradeItem.itemLot, LWOOBJID_EMPTY, "", -static_cast<int32_t>(tradeItem.itemCount));
|
||||
// Live sent the trade's ID with the traded items
|
||||
ReceiveItemOptions options{};
|
||||
options.metrics.transactionId = tradeId;
|
||||
const auto received = whole
|
||||
? to->ReceiveItem(LWOOBJID_EMPTY, tradeItem.itemLot, tradeItem.itemCount, eLootSourceType::TRADE, config, subKey, bound)
|
||||
: to->ReceiveItem(LWOOBJID_EMPTY, tradeItem.itemLot, tradeItem.itemCount, eLootSourceType::TRADE);
|
||||
? to->ReceiveItem(LWOOBJID_EMPTY, tradeItem.itemLot, tradeItem.itemCount, eLootSourceType::TRADE, config, subKey, bound, options)
|
||||
: to->ReceiveItem(LWOOBJID_EMPTY, tradeItem.itemLot, tradeItem.itemCount, eLootSourceType::TRADE, {}, LWOOBJID_EMPTY, false, options);
|
||||
|
||||
EconomyLedger::RecordTransfer({ .method = IEconomyLedger::eTransferMethod::TRADE, .itemId = tradeItem.itemId, .newItemId = received.id,
|
||||
.lot = tradeItem.itemLot, .count = tradeItem.itemCount, .fromCharacter = fromCharacter, .toCharacter = toCharacter, .zone = zone,
|
||||
|
||||
@@ -228,7 +228,8 @@ void InventoryComponent::AddItem(
|
||||
const eInventoryType inventorySourceType,
|
||||
const int32_t sourceType,
|
||||
const bool bound,
|
||||
int32_t preferredSlot) {
|
||||
int32_t preferredSlot,
|
||||
const LootMetrics& metrics) {
|
||||
if (count == 0) {
|
||||
LOG("Attempted to add 0 of item (%i) to the inventory!", lot);
|
||||
|
||||
@@ -262,7 +263,7 @@ void InventoryComponent::AddItem(
|
||||
return;
|
||||
}
|
||||
|
||||
auto* item = new Item(lot, inventory, slot, count, config, parent, showFlyingLoot, isModMoveAndEquip, subKey, bound, lootSourceType);
|
||||
auto* item = new Item(lot, inventory, slot, count, config, parent, showFlyingLoot, isModMoveAndEquip, subKey, bound, lootSourceType, metrics);
|
||||
|
||||
if (missions != nullptr && !IsTransferInventory(inventoryType)) {
|
||||
missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count, IsTransferInventory(inventorySourceType));
|
||||
@@ -295,7 +296,7 @@ void InventoryComponent::AddItem(
|
||||
|
||||
left -= delta;
|
||||
|
||||
existing->SetCount(existing->GetCount() + delta, false, true, showFlyingLoot, lootSourceType);
|
||||
existing->SetCount(existing->GetCount() + delta, false, true, showFlyingLoot, lootSourceType, metrics);
|
||||
|
||||
if (isModMoveAndEquip) {
|
||||
existing->Equip();
|
||||
@@ -348,7 +349,7 @@ void InventoryComponent::AddItem(
|
||||
|
||||
continue;
|
||||
}
|
||||
auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType);
|
||||
auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType, metrics);
|
||||
|
||||
isModMoveAndEquip = false;
|
||||
}
|
||||
@@ -374,6 +375,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c
|
||||
GameMessages::AddItemToInventoryClientSync addItem;
|
||||
addItem.target = m_Parent->GetObjectID();
|
||||
addItem.SetItem(*item);
|
||||
addItem.AddMetrics(options.metrics);
|
||||
addItem.eLootTypeSource = lootSourceType;
|
||||
addItem.iSubkey = subKey;
|
||||
addItem.itemCount = static_cast<int>(count);
|
||||
@@ -398,7 +400,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c
|
||||
for (const auto& [itemId, item] : inventory->GetItems()) {
|
||||
if (item->GetLot() == lot) before[itemId] = item->GetCount();
|
||||
}
|
||||
AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot);
|
||||
AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot, options.metrics);
|
||||
|
||||
LWOOBJID receivedBy = LWOOBJID_EMPTY;
|
||||
int64_t largestGain = 0;
|
||||
@@ -420,7 +422,7 @@ bool InventoryComponent::IsUniqueLot(const LOT lot) {
|
||||
return !isBrick && info.stackSize <= 1;
|
||||
}
|
||||
|
||||
bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent) {
|
||||
bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent, const ItemRemovalSource& removalSource) {
|
||||
if (count == 0) {
|
||||
LOG("Attempted to remove 0 of item (%i) from the inventory!", lot);
|
||||
return false;
|
||||
@@ -437,7 +439,7 @@ bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInvent
|
||||
auto* item = FindItemByLot(lot, inventoryType, false, ignoreBound);
|
||||
if (!item) break;
|
||||
const auto delta = std::min<uint32_t>(left, item->GetCount());
|
||||
item->SetCount(item->GetCount() - delta, silent);
|
||||
item->SetCount(item->GetCount() - delta, silent, true, true, eLootSourceType::NONE, {}, removalSource);
|
||||
left -= delta;
|
||||
}
|
||||
return true;
|
||||
@@ -448,7 +450,7 @@ bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInvent
|
||||
auto* item = inventory->FindItemByLot(lot, false, ignoreBound);
|
||||
if (!item) break;
|
||||
const auto delta = std::min<uint32_t>(item->GetCount(), left);
|
||||
item->SetCount(item->GetCount() - delta, silent);
|
||||
item->SetCount(item->GetCount() - delta, silent, true, true, eLootSourceType::NONE, {}, removalSource);
|
||||
left -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "Loot.h"
|
||||
#include "LootMetrics.h"
|
||||
|
||||
class Entity;
|
||||
class ItemSet;
|
||||
@@ -57,6 +58,7 @@ struct ReceiveItemOptions {
|
||||
bool equip = false;
|
||||
int32_t preferredSlot = -1;
|
||||
eInventoryType sourceInventory = INVALID; // set when moving between the player's own inventories
|
||||
LootMetrics metrics{}; // where the items came from, sent with them (e.g. the mail or trade ID)
|
||||
};
|
||||
|
||||
// Which item or stack ReceiveItem put the items in
|
||||
@@ -153,6 +155,7 @@ public:
|
||||
* @param bound whether this item is bound
|
||||
* @param preferredSlot the preferred slot to store this item
|
||||
* @param lootSourceType The source of the loot. Defaults to none.
|
||||
* @param metrics where the items came from (source LOT, mission, activity...), sent with them and not kept
|
||||
*/
|
||||
void AddItem(
|
||||
LOT lot,
|
||||
@@ -167,7 +170,8 @@ public:
|
||||
eInventoryType inventorySourceType = INVALID,
|
||||
int32_t sourceType = 0,
|
||||
bool bound = false,
|
||||
int32_t preferredSlot = -1
|
||||
int32_t preferredSlot = -1,
|
||||
const LootMetrics& metrics = {}
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -190,8 +194,9 @@ public:
|
||||
* @param inventoryType optional inventory type to remove the item from
|
||||
* @param ignoreBound ignores bound items
|
||||
* @param silent silently remove the item
|
||||
* @param removalSource what took the items, sent with the removal (e.g. a quickbuild taking its item cost)
|
||||
*/
|
||||
bool RemoveItem(LOT lot, uint32_t count, eInventoryType inventoryType = INVALID, bool ignoreBound = false, bool silent = false);
|
||||
bool RemoveItem(LOT lot, uint32_t count, eInventoryType inventoryType = INVALID, bool ignoreBound = false, bool silent = false, const ItemRemovalSource& removalSource = {});
|
||||
|
||||
/**
|
||||
* Moves an existing item to an inventory of the entity
|
||||
|
||||
@@ -464,8 +464,11 @@ void MovementAIComponent::SetDestination(const NiPoint3 destination) {
|
||||
|
||||
// The path is walked without collision, so an enemy's stops at walls it can't cross (navmesh carvers, enemy only
|
||||
// blockers); with nothing left to walk it stays where it is. A patrol along its level path is left as designed.
|
||||
// Pets are walked here too, and their group (3) is the one the pet ranch's "PR - Pet Blocker" (group 18) touches:
|
||||
// the client's filter lets pets collide with it and players through.
|
||||
const bool patrolling = m_Path && m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle;
|
||||
if (m_BaseCombatAI && !patrolling) {
|
||||
const bool isPet = m_Parent->HasComponent(eReplicaComponentType::PET);
|
||||
if ((m_BaseCombatAI && !patrolling) || isPet) {
|
||||
computedPath = dpWorld::ClampPath(m_Parent->GetPosition(), std::move(computedPath), static_cast<uint32_t>(m_Parent->GetCollisionGroup()));
|
||||
if (computedPath.empty()) computedPath.push_back(m_Parent->GetPosition());
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "NiPoint3.h"
|
||||
@@ -21,6 +23,30 @@
|
||||
#include "Entity.h"
|
||||
#include "Logger.h"
|
||||
|
||||
namespace {
|
||||
struct KnownBox {
|
||||
NiPoint3 size; // width (x), height (y, up from the base) and depth (z)
|
||||
NiPoint3 offset; // where the box's base centre is, from the object's position in its own axes, before scaling
|
||||
};
|
||||
|
||||
/**
|
||||
* Assets whose collision the server stands in for with a box: the bounds of all the parts of the asset's collision
|
||||
* shape in the client's physics files (res/physics), by file name in lower case. They are the navmesh carvers of
|
||||
* Robot City (hedges, greebles, robot statues) and the Ninjago Monastery (benches).
|
||||
* The hedge corner is an L: its box also covers the inside of the corner.
|
||||
*/
|
||||
const std::map<std::string, KnownBox> KNOWN_BOXES = {
|
||||
{ "env_won_nim_bush_square-section.hkx", { { 4.3949f, 4.9645f, 9.0341f }, { -0.0343f, 0.0004f, -0.0527f } } },
|
||||
{ "env_won_nim_bush_square-corner.hkx", { { 9.1824f, 4.7916f, 9.7440f }, { -1.0403f, 0.2150f, 1.0503f } } },
|
||||
{ "env_nim_ag_bushinpot.hkx", { { 5.4603f, 5.7094f, 11.4660f }, { 0.1707f, -0.2426f, 0.5737f } } },
|
||||
{ "greeble_001w.hkx", { { 3.18f, 1.90f, 3.18f }, { 0.0f, 0.01f, 0.0f } } },
|
||||
{ "greeble_002w.hkx", { { 2.38f, 1.907f, 1.5802f }, { -0.4f, 0.01f, 0.0001f } } },
|
||||
{ "robot_statue_01.hkx", { { 1.8642f, 3.1844f, 1.67f }, { 0.4f, 0.01f, -0.275f } } },
|
||||
{ "robot_statue_02.hkx", { { 3.0994f, 4.1242f, 1.9228f }, { 0.0165f, 0.0117f, 0.0967f } } },
|
||||
{ "env_won_gnar_ninja-bench.hkx", { { 6.4f, 0.9796f, 1.6f }, { 0.0f, -0.0052f, 0.0005f } } },
|
||||
};
|
||||
}
|
||||
|
||||
PhysicsComponent::PhysicsComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
||||
m_Position = NiPoint3Constant::ZERO;
|
||||
m_Rotation = QuatUtils::IDENTITY;
|
||||
@@ -71,6 +97,56 @@ void PhysicsComponent::RegisterMovementBlocker(const eReplicaComponentType type,
|
||||
dpWorld::AddMovementBlocker(m_MovementBlocker, *filter);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool SettingIsTrue(const LwoNameValue& settings, const std::u16string& key) {
|
||||
const auto it = settings.find(key);
|
||||
return it != settings.end() && it->second && GeneralUtils::TryParse<int32_t>(it->second->GetValueAsString()).value_or(0) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool PhysicsComponent::IsCarverOnly(const LwoNameValue& settings) {
|
||||
return SettingIsTrue(settings, u"carver_only");
|
||||
}
|
||||
|
||||
bool PhysicsComponent::AddLevelMovementBlocker(const LWOOBJID id, const LOT lot, const LwoNameValue& settings, const NiPoint3& position, const NiQuaternion& rotation, const float scale) {
|
||||
auto* const registry = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
||||
auto* const physicsTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
||||
|
||||
// The physics component it would have: fixed simple physics is solid (no level object of this kind moves), a
|
||||
// phantom volume is not
|
||||
for (const auto type : { eReplicaComponentType::SIMPLE_PHYSICS, eReplicaComponentType::PHANTOM_PHYSICS }) {
|
||||
const auto componentID = registry->GetByIDAndType(lot, type, -1);
|
||||
if (componentID == -1) continue;
|
||||
|
||||
int32_t collisionGroup = 0;
|
||||
if (const auto* const info = physicsTable->GetByID(componentID)) collisionGroup = info->collisionGroup;
|
||||
if (const auto it = settings.find(u"CollisionGroupID"); it != settings.end() && it->second) {
|
||||
collisionGroup = GeneralUtils::TryParse<int32_t>(it->second->GetValueAsString()).value_or(collisionGroup);
|
||||
}
|
||||
|
||||
const bool solid = type == eReplicaComponentType::SIMPLE_PHYSICS;
|
||||
const auto filter = dpMovementBlockers::BlockingFilter(SettingIsTrue(settings, u"navmesh_carver"), solid, static_cast<uint32_t>(collisionGroup));
|
||||
if (!filter) return false;
|
||||
|
||||
auto shapePosition = position;
|
||||
bool isFallback = false;
|
||||
auto* const blocker = CreateAssetShape(id, lot, type, collisionGroup, scale, rotation, shapePosition, &isFallback);
|
||||
if (!blocker) return false;
|
||||
if (isFallback) {
|
||||
LOG_DEBUG("%llu (LOT %i) should block movement, but the server doesn't know its shape", id, lot);
|
||||
delete blocker;
|
||||
return false;
|
||||
}
|
||||
|
||||
blocker->SetScale(scale);
|
||||
blocker->SetRotation(rotation);
|
||||
blocker->SetPosition(shapePosition);
|
||||
dpWorld::AddOwnedMovementBlocker(blocker, *filter);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PhysicsComponent::OnGetPosition(GameMessages::GetPosition& msg) {
|
||||
msg.pos = GetPosition();
|
||||
return true;
|
||||
@@ -91,9 +167,13 @@ void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitia
|
||||
}
|
||||
|
||||
dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback) {
|
||||
return CreateAssetShape(m_Parent->GetObjectID(), m_Parent->GetLOT(), type, m_CollisionGroup, m_Parent->GetDefaultScale(), m_Rotation, m_Position, isFallback);
|
||||
}
|
||||
|
||||
dpEntity* PhysicsComponent::CreateAssetShape(const LWOOBJID id, const LOT lot, const eReplicaComponentType type, const int32_t collisionGroup, const float scale, const NiQuaternion& rotation, NiPoint3& position, bool* isFallback) {
|
||||
if (isFallback) *isFallback = false;
|
||||
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
||||
auto componentID = compRegistryTable->GetByIDAndType(m_Parent->GetLOT(), type);
|
||||
auto componentID = compRegistryTable->GetByIDAndType(lot, type);
|
||||
|
||||
CDPhysicsComponentTable* physComp = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
||||
|
||||
@@ -109,51 +189,55 @@ dpEntity* PhysicsComponent::CreatePhysicsEntity(eReplicaComponentType type, bool
|
||||
|
||||
dpEntity* toReturn;
|
||||
if (info->physicsAsset == "miscellaneous\\misc_phys_10x1x5.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 10.0f, 5.0f, 1.0f);
|
||||
toReturn = new dpEntity(id, 10.0f, 5.0f, 1.0f);
|
||||
} else if (info->physicsAsset == "miscellaneous\\misc_phys_640x640.hkx") {
|
||||
// TODO Fix physics simulation to do simulation at high velocities due to bullet through paper problem...
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 1638.4f, 13.521004f * 2.0f, 1638.4f);
|
||||
toReturn = new dpEntity(id, 1638.4f, 13.521004f * 2.0f, 1638.4f);
|
||||
|
||||
// Move this down by 13.521004 units so it is still effectively at the same height as before
|
||||
m_Position = m_Position - NiPoint3Constant::UNIT_Y * 13.521004f;
|
||||
position = position - NiPoint3Constant::UNIT_Y * 13.521004f;
|
||||
} else if (info->physicsAsset == "env\\trigger_wall_tall.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 10.0f, 25.0f, 1.0f);
|
||||
toReturn = new dpEntity(id, 10.0f, 25.0f, 1.0f);
|
||||
} else if (info->physicsAsset == "env\\env_gen_placeholderphysics.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 20.0f, 20.0f, 20.0f);
|
||||
toReturn = new dpEntity(id, 20.0f, 20.0f, 20.0f);
|
||||
} else if (assetFile == "poi_trigger_wall.hkx") {
|
||||
// test\POI_trigger_wall.hkx in the CDClient (e.g. "Clear threat list Trigger Wall"), env\ in the client's files:
|
||||
// a box 1 wide, 12.98 tall from its base and 20.45 deep
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 1.0f, 12.9755f, 20.45f);
|
||||
toReturn = new dpEntity(id, 1.0f, 12.9755f, 20.45f);
|
||||
} else if (assetFile == "trigger_rectangle_box.hkx") {
|
||||
// "Trigger Rectangle Box": a box 8 wide, 8 tall from its base and 4 deep
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 8.0f, 8.0f, 4.0f);
|
||||
toReturn = new dpEntity(id, 8.0f, 8.0f, 4.0f);
|
||||
} else if (info->physicsAsset == "env\\NG_NinjaGo\\env_ng_gen_gate_chamber_puzzle_ceiling_tile_falling_phantom.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 18.0f, 5.0f, 15.0f);
|
||||
m_Position += QuatUtils::Forward(m_Rotation) * 7.5f;
|
||||
toReturn = new dpEntity(id, 18.0f, 5.0f, 15.0f);
|
||||
position += QuatUtils::Forward(rotation) * 7.5f;
|
||||
} else if (info->physicsAsset == "env\\NG_NinjaGo\\ng_flamejet_brick_phantom.HKX") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 1.0f, 1.0f, 12.0f);
|
||||
m_Position += QuatUtils::Forward(m_Rotation) * 6.0f;
|
||||
toReturn = new dpEntity(id, 1.0f, 1.0f, 12.0f);
|
||||
position += QuatUtils::Forward(rotation) * 6.0f;
|
||||
} else if (info->physicsAsset == "env\\Ring_Trigger.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 6.0f, 6.0f, 6.0f);
|
||||
toReturn = new dpEntity(id, 6.0f, 6.0f, 6.0f);
|
||||
} else if (info->physicsAsset == "env\\vfx_propertyImaginationBall.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 4.5f);
|
||||
toReturn = new dpEntity(id, 4.5f);
|
||||
} else if (info->physicsAsset == "env\\env_won_fv_gas-blocking-volume.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 390.496826f, 111.467964f, 600.821534f, true);
|
||||
m_Position.y -= (111.467964f * m_Parent->GetDefaultScale()) / 2;
|
||||
toReturn = new dpEntity(id, 390.496826f, 111.467964f, 600.821534f, true);
|
||||
position.y -= (111.467964f * scale) / 2;
|
||||
// Leaving these out for now since they cause more issues than they solve in racing tracks without proper OBB checks.
|
||||
} /* else if (info->physicsAsset == "env\\GFTrack_DeathVolume1_CaveExit.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 112.416870f, 50.363434f, 87.679268f);
|
||||
toReturn = new dpEntity(id, 112.416870f, 50.363434f, 87.679268f);
|
||||
} else if (info->physicsAsset == "env\\GFTrack_DeathVolume2_RoadGaps.hkx") {
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 48.386536f, 50.363434f, 259.361755f);
|
||||
} */ else {
|
||||
toReturn = new dpEntity(id, 48.386536f, 50.363434f, 259.361755f);
|
||||
} */ else if (const auto known = KNOWN_BOXES.find(assetFile); known != KNOWN_BOXES.end()) {
|
||||
const auto& [size, offset] = known->second;
|
||||
toReturn = new dpEntity(id, size.x, size.y, size.z);
|
||||
position += (QuatUtils::Right(rotation) * offset.x + QuatUtils::Up(rotation) * offset.y + QuatUtils::Forward(rotation) * offset.z) * scale;
|
||||
} else {
|
||||
// LOG_DEBUG("This one is supposed to have %s", info->physicsAsset.c_str());
|
||||
|
||||
//add fallback cube:
|
||||
toReturn = new dpEntity(m_Parent->GetObjectID(), 2.0f, 2.0f, 2.0f);
|
||||
toReturn = new dpEntity(id, 2.0f, 2.0f, 2.0f);
|
||||
if (isFallback) *isFallback = true;
|
||||
}
|
||||
// Only touch what the client lets this group touch (e.g. POI walls ignore enemies, threat clearing walls ignore players)
|
||||
toReturn->SetCollisionGroup(static_cast<uint32_t>(m_CollisionGroup));
|
||||
toReturn->SetCollisionGroup(static_cast<uint32_t>(collisionGroup));
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "Component.h"
|
||||
#include "NiPoint3.h"
|
||||
#include "NiQuaternion.h"
|
||||
#include "dCommonVars.h"
|
||||
|
||||
struct LwoNameValue;
|
||||
|
||||
namespace GameMessages {
|
||||
struct GetObjectReportInfo;
|
||||
@@ -33,12 +36,33 @@ public:
|
||||
|
||||
int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; }
|
||||
void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; }
|
||||
|
||||
/**
|
||||
* Whether a level object is carver_only, which the client never loads (LWOResMgr2Interface::Run skips its load
|
||||
* when its config has carver_only set; LWOBasePhysComponent::LoadConfigData 0x00c495c9 reads it next to
|
||||
* navmesh_carver). Live never sent one.
|
||||
*/
|
||||
[[nodiscard]] static bool IsCarverOnly(const LwoNameValue& settings);
|
||||
|
||||
/**
|
||||
* For a level object the server doesn't spawn (carver_only): adds its shape to the world's movement blockers, the
|
||||
* way its physics component would have (RegisterMovementBlocker), owned by the world.
|
||||
* @return whether it blocks movement (its data makes it a wall and the server knows its shape)
|
||||
*/
|
||||
static bool AddLevelMovementBlocker(LWOOBJID id, LOT lot, const LwoNameValue& settings, const NiPoint3& position, const NiQuaternion& rotation, float scale);
|
||||
protected:
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& msg);
|
||||
|
||||
// isFallback, when given, says whether the asset had no known shape and got a stand in cube
|
||||
dpEntity* CreatePhysicsEntity(eReplicaComponentType type, bool* isFallback = nullptr);
|
||||
|
||||
/**
|
||||
* The shape of the physics asset of a LOT's physics component of the given type, for an object placed with this
|
||||
* scale and rotation, or nullptr when it has none. Some shapes move position (in and out) to line up with the
|
||||
* client's. isFallback, when given, says whether the asset had no known shape and got a stand in cube.
|
||||
*/
|
||||
static dpEntity* CreateAssetShape(LWOOBJID id, LOT lot, eReplicaComponentType type, int32_t collisionGroup, float scale, const NiQuaternion& rotation, NiPoint3& position, bool* isFallback = nullptr);
|
||||
|
||||
/**
|
||||
* Makes this object a wall the server's movers can't walk through when its data says it is one
|
||||
* (dpMovementBlockers::BlockingFilter: a navmesh carver, or a solid object only enemies collide with). Only
|
||||
|
||||
@@ -664,9 +664,11 @@ void QuickBuildComponent::TakeItemCosts(Entity& user) {
|
||||
if (!inventoryComponent) return;
|
||||
|
||||
// Live took the precondition items (for example the FV Stone Warrior pedestal's 5 Maelstrom Infected Bricks)
|
||||
// when the build started and gave them back when it was cancelled
|
||||
// when the build started and gave them back when it was cancelled. The removal names the quickbuild as its loot
|
||||
// source (Quickbuild, the quickbuild's object ID), as live's did.
|
||||
const ItemRemovalSource removalSource{ eLootSourceType::QUICKBUILD, m_Parent->GetObjectID() };
|
||||
for (const auto& cost : m_Precondition->GetItemCosts(&user)) {
|
||||
if (inventoryComponent->RemoveItem(cost.lot, cost.count, eInventoryType::ALL)) m_TakenItems.push_back(cost);
|
||||
if (inventoryComponent->RemoveItem(cost.lot, cost.count, eInventoryType::ALL, false, false, removalSource)) m_TakenItems.push_back(cost);
|
||||
}
|
||||
if (!m_TakenItems.empty()) m_TakenItemsFrom = user.GetObjectID();
|
||||
}
|
||||
|
||||
@@ -208,7 +208,11 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
|
||||
}
|
||||
|
||||
character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR, CoinSource::Object(*m_Parent));
|
||||
inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR);
|
||||
// Live sent the vendor's LOT, and the coins paid when there were any, with the bought items
|
||||
LootMetrics metrics{};
|
||||
metrics.sourceLot = m_Parent->GetLOT();
|
||||
if (coinCost > 0) metrics.currencyDelta = -static_cast<int32_t>(coinCost);
|
||||
inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, metrics);
|
||||
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Inventory.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Item.h"
|
||||
#include "LootMetrics.h"
|
||||
#include "BrickByBrick.h"
|
||||
|
||||
#include <ranges>
|
||||
@@ -53,6 +54,13 @@ namespace GameMessages {
|
||||
slotID = item.GetSlot();
|
||||
}
|
||||
|
||||
void AddItemToInventoryClientSync::AddMetrics(const LootMetrics& metrics) {
|
||||
const auto text = metrics.ToExtraInfo();
|
||||
if (text.empty()) return;
|
||||
if (!extraInfo.empty()) extraInfo += u',';
|
||||
extraInfo += text;
|
||||
}
|
||||
|
||||
void AddItemToInventoryClientSync::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(bBound);
|
||||
bitStream.Write(bIsBOE);
|
||||
|
||||
@@ -17,6 +17,8 @@ class Item;
|
||||
// and groups.
|
||||
// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order.
|
||||
// Received messages are handled by the target's InventoryComponent.
|
||||
struct LootMetrics;
|
||||
|
||||
namespace GameMessages {
|
||||
// Server -> client.
|
||||
struct AddItemToInventoryClientSync : public NetGameMsg {
|
||||
@@ -28,6 +30,9 @@ namespace GameMessages {
|
||||
// slot). The caller sets the rest.
|
||||
void SetItem(const Item& item);
|
||||
|
||||
// Adds where the items came from (LootMetrics) to the extra info, after the item's config
|
||||
void AddMetrics(const LootMetrics& metrics);
|
||||
|
||||
bool bBound{};
|
||||
bool bIsBOE{};
|
||||
bool bIsBOP{};
|
||||
|
||||
@@ -3,7 +3,8 @@ set(DGAME_DINVENTORY_SOURCES
|
||||
"Inventory.cpp"
|
||||
"Item.cpp"
|
||||
"ItemSet.cpp"
|
||||
"ItemSetPassiveAbility.cpp")
|
||||
"ItemSetPassiveAbility.cpp"
|
||||
"LootMetrics.cpp")
|
||||
|
||||
add_library(dInventory OBJECT ${DGAME_DINVENTORY_SOURCES})
|
||||
target_include_directories(dInventory PUBLIC "."
|
||||
|
||||
@@ -82,7 +82,8 @@ Item::Item(
|
||||
bool isModMoveAndEquip,
|
||||
LWOOBJID subKey,
|
||||
bool bound,
|
||||
eLootSourceType lootSourceType) {
|
||||
eLootSourceType lootSourceType,
|
||||
const LootMetrics& metrics) {
|
||||
if (!Inventory::IsValidItem(lot)) {
|
||||
return;
|
||||
}
|
||||
@@ -113,6 +114,7 @@ Item::Item(
|
||||
GameMessages::AddItemToInventoryClientSync addItem;
|
||||
addItem.target = entity->GetObjectID();
|
||||
addItem.SetItem(*this);
|
||||
addItem.AddMetrics(metrics);
|
||||
addItem.eLootTypeSource = lootSourceType;
|
||||
addItem.iSubkey = subKey;
|
||||
addItem.itemCount = static_cast<int>(this->count);
|
||||
@@ -178,7 +180,7 @@ PreconditionExpression* Item::GetPreconditionExpression() const {
|
||||
return preconditions;
|
||||
}
|
||||
|
||||
void Item::SetCount(const uint32_t value, const bool silent, const bool disassemble, const bool showFlyingLoot, eLootSourceType lootSourceType) {
|
||||
void Item::SetCount(const uint32_t value, const bool silent, const bool disassemble, const bool showFlyingLoot, eLootSourceType lootSourceType, const LootMetrics& metrics, const ItemRemovalSource& removalSource) {
|
||||
if (value == count) {
|
||||
return;
|
||||
}
|
||||
@@ -202,6 +204,7 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem
|
||||
GameMessages::AddItemToInventoryClientSync addItem;
|
||||
addItem.target = entity->GetObjectID();
|
||||
addItem.SetItem(*this);
|
||||
addItem.AddMetrics(metrics);
|
||||
addItem.eLootTypeSource = lootSourceType;
|
||||
addItem.itemCount = delta;
|
||||
addItem.newObjID = id;
|
||||
@@ -213,7 +216,8 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem
|
||||
removeItem.target = entity->GetObjectID();
|
||||
removeItem.bConfirmed = true;
|
||||
removeItem.eInvType = inventory->GetType();
|
||||
removeItem.eLootTypeSource = LOOTTYPE_NONE;
|
||||
removeItem.eLootTypeSource = static_cast<int32_t>(removalSource.type);
|
||||
removeItem.iLootTypeSource = removalSource.object;
|
||||
removeItem.iObjID = id;
|
||||
removeItem.iObjTemplate = lot;
|
||||
removeItem.iStackCount = delta;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Preconditions.h"
|
||||
#include "eInventoryType.h"
|
||||
#include "eLootSourceType.h"
|
||||
#include "LootMetrics.h"
|
||||
|
||||
namespace tinyxml2 {
|
||||
class XMLElement;
|
||||
@@ -58,6 +59,7 @@ public:
|
||||
* @param isModMoveAndEquip equips the item
|
||||
* @param subKey optional subkey for this item, e.g. for pets
|
||||
* @param bound if the item should be bound
|
||||
* @param metrics where the items came from, sent with the item (not kept)
|
||||
*/
|
||||
explicit Item(
|
||||
LOT lot,
|
||||
@@ -70,7 +72,8 @@ public:
|
||||
bool isModMoveAndEquip = false,
|
||||
LWOOBJID subKey = LWOOBJID_EMPTY,
|
||||
bool bound = false,
|
||||
eLootSourceType lootSourceType = eLootSourceType::NONE
|
||||
eLootSourceType lootSourceType = eLootSourceType::NONE,
|
||||
const LootMetrics& metrics = {}
|
||||
);
|
||||
|
||||
~Item();
|
||||
@@ -93,8 +96,10 @@ public:
|
||||
* @param silent if true, the client will not be notified of the change with GMs
|
||||
* @param disassemble if items were removed, this returns all the sub parts of the item individually if it had assembly part lots
|
||||
* @param showFlyingLoot shows flying loot to the client, if not silent
|
||||
* @param metrics where added items came from, sent with them (not kept)
|
||||
* @param removalSource what took removed items away, sent with the removal
|
||||
*/
|
||||
void SetCount(uint32_t value, bool silent = false, bool disassemble = true, bool showFlyingLoot = true, eLootSourceType lootSourceType = eLootSourceType::NONE);
|
||||
void SetCount(uint32_t value, bool silent = false, bool disassemble = true, bool showFlyingLoot = true, eLootSourceType lootSourceType = eLootSourceType::NONE, const LootMetrics& metrics = {}, const ItemRemovalSource& removalSource = {});
|
||||
|
||||
/**
|
||||
* Returns the number of items this item represents (e.g. for stacks)
|
||||
|
||||
27
dGame/dInventory/LootMetrics.cpp
Normal file
27
dGame/dInventory/LootMetrics.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "LootMetrics.h"
|
||||
|
||||
#include "LDFFormat.h"
|
||||
|
||||
namespace {
|
||||
template<typename T>
|
||||
void Append(std::u16string& out, const char16_t* key, const std::optional<T>& value) {
|
||||
if (!value) return;
|
||||
if (!out.empty()) out += u',';
|
||||
out += GeneralUtils::ASCIIToUTF16(LDFData<T>(key, *value).GetString());
|
||||
}
|
||||
}
|
||||
|
||||
bool LootMetrics::Empty() const {
|
||||
return !sourceLot && !missionId && !activityId && !currencyDelta && !mailId && !transactionId;
|
||||
}
|
||||
|
||||
std::u16string LootMetrics::ToExtraInfo() const {
|
||||
std::u16string out;
|
||||
Append<int32_t>(out, u"_Metric_Souce_LOT_Int", sourceLot);
|
||||
Append<int32_t>(out, u"_Metric_Mission_ID_Int", missionId);
|
||||
Append<int32_t>(out, u"_Metric_Activity_ID_Int", activityId);
|
||||
Append<int32_t>(out, u"_Metric_Currency_Delta_Int", currencyDelta);
|
||||
Append<uint64_t>(out, u"_Metric_Mail_ID_Int64", mailId);
|
||||
Append<LWOOBJID>(out, u"_Metric_Transaction_ID_Int64", transactionId);
|
||||
return out;
|
||||
}
|
||||
39
dGame/dInventory/LootMetrics.h
Normal file
39
dGame/dInventory/LootMetrics.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "eLootSourceType.h"
|
||||
|
||||
/**
|
||||
* Where added items came from, as live servers put it in the extra info of AddItemToInventoryClientSync next to the
|
||||
* item's own config. Only the message carries these keys; the item does not keep them.
|
||||
* Live: a picked up drop had _Metric_Souce_LOT_Int (the source object's LOT, 1 for a player sourced
|
||||
* drop) on all 2,344 pickups of a DropClientLoot; mission and achievement rewards the mission ID and source LOT 1;
|
||||
* activity rewards the activity ID and the activity object's LOT; vendor purchases the vendor's LOT and the coins paid
|
||||
* (negative); mail the mail ID; trades the trade ID. Package contents had none.
|
||||
*/
|
||||
struct LootMetrics {
|
||||
std::optional<LOT> sourceLot; // _Metric_Souce_LOT_Int (the key is misspelt in live's data)
|
||||
std::optional<int32_t> missionId; // _Metric_Mission_ID_Int
|
||||
std::optional<int32_t> activityId; // _Metric_Activity_ID_Int
|
||||
std::optional<int32_t> currencyDelta; // _Metric_Currency_Delta_Int
|
||||
std::optional<uint64_t> mailId; // _Metric_Mail_ID_Int64, name value type 8
|
||||
std::optional<LWOOBJID> transactionId; // _Metric_Transaction_ID_Int64, name value type 9
|
||||
|
||||
[[nodiscard]] bool Empty() const;
|
||||
|
||||
/**
|
||||
* The keys as name value text (key=type:value), comma separated like an item's config in the extra info.
|
||||
* Empty when there are none.
|
||||
*/
|
||||
[[nodiscard]] std::u16string ToExtraInfo() const;
|
||||
};
|
||||
|
||||
// What took items away, for RemoveItemFromInventory's loot source fields (e.g. a quickbuild taking its item cost)
|
||||
struct ItemRemovalSource {
|
||||
eLootSourceType type = eLootSourceType::NONE;
|
||||
LWOOBJID object = LWOOBJID_EMPTY;
|
||||
};
|
||||
@@ -510,6 +510,11 @@ void Mission::YieldRewards() {
|
||||
}
|
||||
}
|
||||
|
||||
// Live sent the mission's ID and the player's LOT with each reward item (225 of 225 live mission and achievement rewards)
|
||||
LootMetrics rewardMetrics{};
|
||||
rewardMetrics.sourceLot = entity->GetLOT();
|
||||
rewardMetrics.missionId = static_cast<int32_t>(info.id);
|
||||
|
||||
if (m_Completions > 0) {
|
||||
std::vector<std::pair<LOT, uint32_t>> items;
|
||||
|
||||
@@ -528,7 +533,7 @@ void Mission::YieldRewards() {
|
||||
// If a mission rewards zero of an item, make it reward 1.
|
||||
auto count = pair.second > 0 ? pair.second : 1;
|
||||
LOG("Player %llu is receiving %i of item %i from repeatable mission %i", entity->GetObjectID(), count, pair.first, info.id);
|
||||
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT);
|
||||
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, rewardMetrics);
|
||||
}
|
||||
|
||||
if (info.reward_currency_repeatable > 0 || coinsToSend > 0) {
|
||||
@@ -556,7 +561,7 @@ void Mission::YieldRewards() {
|
||||
// If a mission rewards zero of an item, make it reward 1.
|
||||
auto count = pair.second > 0 ? pair.second : 1;
|
||||
LOG("Player %llu is receiving %i of item %i from mission %i", entity->GetObjectID(), count, pair.first, info.id);
|
||||
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT);
|
||||
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, rewardMetrics);
|
||||
}
|
||||
|
||||
if (info.reward_currency > 0 || coinsToSend > 0) {
|
||||
|
||||
@@ -523,14 +523,22 @@ Loot::Return Loot::RollLootMatrix(Entity* player, uint32_t matrixIndex) {
|
||||
}
|
||||
|
||||
void Loot::GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType) {
|
||||
GiveLoot(player, matrixIndex, lootSourceType, LootMetrics{});
|
||||
}
|
||||
|
||||
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType) {
|
||||
GiveLoot(player, result, lootSourceType, LootMetrics{});
|
||||
}
|
||||
|
||||
void Loot::GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType, const LootMetrics& metrics) {
|
||||
player = player->GetOwner(); // If the owner is overwritten, we collect that here
|
||||
|
||||
const auto result = RollLootMatrix(player, matrixIndex);
|
||||
|
||||
GiveLoot(player, result, lootSourceType);
|
||||
GiveLoot(player, result, lootSourceType, metrics);
|
||||
}
|
||||
|
||||
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType) {
|
||||
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType, const LootMetrics& metrics) {
|
||||
player = player->GetOwner(); // if the owner is overwritten, we collect that here
|
||||
|
||||
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
@@ -539,7 +547,7 @@ void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType
|
||||
return;
|
||||
|
||||
for (const auto& pair : result) {
|
||||
inventoryComponent->AddItem(pair.first, pair.second, lootSourceType);
|
||||
inventoryComponent->AddItem(pair.first, pair.second, lootSourceType, eInventoryType::INVALID, {}, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, metrics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +585,11 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti
|
||||
|
||||
const auto [minCoins, maxCoins] = GetActivityCoinRange(*selectedReward);
|
||||
|
||||
GiveLoot(player, selectedReward->LootMatrixIndex, eLootSourceType::ACTIVITY);
|
||||
// Live sent the activity's ID and the activity object's LOT with each reward item
|
||||
LootMetrics metrics{};
|
||||
metrics.activityId = static_cast<int32_t>(activityID);
|
||||
if (const auto* const sourceEntity = Game::entityManager->GetEntity(source)) metrics.sourceLot = sourceEntity->GetLOT();
|
||||
GiveLoot(player, selectedReward->LootMatrixIndex, eLootSourceType::ACTIVITY, metrics);
|
||||
|
||||
uint32_t coins = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
class Entity;
|
||||
struct LootMetrics;
|
||||
struct CDActivityRewards;
|
||||
struct CDCurrencyTable;
|
||||
|
||||
@@ -20,6 +21,9 @@ namespace Loot {
|
||||
LWOOBJID id = 0;
|
||||
LOT lot = 0;
|
||||
int32_t count = 0;
|
||||
// The LOT of the object it dropped from (1 when a player was the source), 0 when that object was not found.
|
||||
// Sent back as the item's source when it is picked up (LootMetrics::sourceLot).
|
||||
LOT sourceLot = 0;
|
||||
};
|
||||
|
||||
using Return = std::map<LOT, int32_t>;
|
||||
@@ -28,6 +32,9 @@ namespace Loot {
|
||||
void CacheMatrix(const uint32_t matrixIndex);
|
||||
void GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType = eLootSourceType::NONE);
|
||||
void GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType = eLootSourceType::NONE);
|
||||
// As above, telling the client where the items came from (LootMetrics)
|
||||
void GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType, const LootMetrics& metrics);
|
||||
void GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType, const LootMetrics& metrics);
|
||||
void GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0);
|
||||
void DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins);
|
||||
void DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam = false, bool forceFfa = false);
|
||||
|
||||
@@ -239,7 +239,10 @@ namespace Mail {
|
||||
|
||||
LwoNameValue config;
|
||||
config.InsertLines(playerMail->itemConfig);
|
||||
const auto claimed = inv->ReceiveItem(playerMail->itemID, playerMail->itemLOT, playerMail->itemCount, eLootSourceType::MAIL, config, playerMail->itemSubkey);
|
||||
// Live sent the mail's ID with the attachment
|
||||
ReceiveItemOptions options{};
|
||||
options.metrics.mailId = static_cast<uint64_t>(mailID);
|
||||
const auto claimed = inv->ReceiveItem(playerMail->itemID, playerMail->itemLOT, playerMail->itemCount, eLootSourceType::MAIL, config, playerMail->itemSubkey, false, options);
|
||||
Database::Get()->ClaimMailItem(mailID);
|
||||
DashboardNotify::Changed("mail", playerMail->receiverId);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "dpGrid.h"
|
||||
#include "DetourCommon.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Game.h"
|
||||
@@ -24,6 +25,7 @@ namespace {
|
||||
bool phys_spatial_partitioning = true;
|
||||
|
||||
std::vector<dpMovementBlocker> m_MovementBlockers;
|
||||
std::vector<std::unique_ptr<dpEntity>> m_OwnedMovementBlockers;
|
||||
};
|
||||
|
||||
void dpWorld::Initialize(unsigned int zoneID, bool generateNewNavMesh) {
|
||||
@@ -88,6 +90,7 @@ void dpWorld::Shutdown() {
|
||||
}
|
||||
|
||||
m_MovementBlockers.clear();
|
||||
m_OwnedMovementBlockers.clear();
|
||||
}
|
||||
|
||||
bool dpWorld::IsLoaded() {
|
||||
@@ -197,6 +200,12 @@ void dpWorld::AddMovementBlocker(const dpEntity* entity, const uint32_t filter)
|
||||
m_MovementBlockers.push_back({ entity, filter });
|
||||
}
|
||||
|
||||
void dpWorld::AddOwnedMovementBlocker(dpEntity* entity, const uint32_t filter) {
|
||||
if (!entity) return;
|
||||
m_OwnedMovementBlockers.emplace_back(entity);
|
||||
AddMovementBlocker(entity, filter);
|
||||
}
|
||||
|
||||
void dpWorld::RemoveMovementBlocker(const dpEntity* entity) {
|
||||
std::erase_if(m_MovementBlockers, [entity](const dpMovementBlocker& blocker) { return blocker.entity == entity; });
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace dpWorld {
|
||||
* the caller, who removes it before deleting it; it is not stepped with the world.
|
||||
*/
|
||||
void AddMovementBlocker(const dpEntity* entity, uint32_t filter);
|
||||
/**
|
||||
* A movement blocker with no object of its own (a wall from the level files the server doesn't spawn), owned by the
|
||||
* world from now on and freed when it shuts down.
|
||||
*/
|
||||
void AddOwnedMovementBlocker(dpEntity* entity, uint32_t filter);
|
||||
void RemoveMovementBlocker(const dpEntity* entity);
|
||||
std::span<const dpMovementBlocker> GetMovementBlockers();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "LevelFile.h"
|
||||
#include "ZoneFileLog.h"
|
||||
#include "ClientVersion.h"
|
||||
#include "PhysicsComponent.h"
|
||||
#include "dConfig.h"
|
||||
#include <ranges>
|
||||
|
||||
@@ -166,6 +167,13 @@ void Level::LoadSceneObjects(const std::vector<SceneObject>& objects) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The client never loads a carver_only object (and live never sent one), but it still carves the AI navmesh:
|
||||
// keep it as a wall the server's movers can't walk through, without an object
|
||||
if (obj.lot != 176 && PhysicsComponent::IsCarverOnly(obj.settings)) {
|
||||
PhysicsComponent::AddLevelMovementBlocker(obj.id, obj.lot, obj.settings, obj.position, obj.rotation, obj.scale);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.lot == 176) { //Spawner
|
||||
MakeSpawner(obj, m_SceneID);
|
||||
} else { //Regular object
|
||||
|
||||
@@ -19,6 +19,7 @@ set(DCOMPONENTS_TESTS
|
||||
"ReplicaConstructionTests.cpp"
|
||||
"ReplicaDecoderTests.cpp"
|
||||
"ItemRemovalTests.cpp"
|
||||
"LootMetricsTests.cpp"
|
||||
)
|
||||
|
||||
# Get the folder name and prepend it to the files above
|
||||
|
||||
247
tests/dGameTests/dComponentsTests/LootMetricsTests.cpp
Normal file
247
tests/dGameTests/dComponentsTests/LootMetricsTests.cpp
Normal file
@@ -0,0 +1,247 @@
|
||||
#include "GameDependencies.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDComponentsRegistryTable.h"
|
||||
#include "CDItemComponentTable.h"
|
||||
#include "Character.h"
|
||||
#include "CharacterComponent.h"
|
||||
#include "Entity.h"
|
||||
#include "EntityManager.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "InventoryMessages.h"
|
||||
#include "Item.h"
|
||||
#include "LootMetrics.h"
|
||||
#include "QuickBuildComponent.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "DestroyableComponent.h"
|
||||
#include "LevelProgressionComponent.h"
|
||||
#include "eQuickBuildState.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
|
||||
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
||||
|
||||
using namespace GameMessageTestUtils;
|
||||
|
||||
// The _Metric_ keys live put in AddItemToInventoryClientSync's extra info saying where items came from
|
||||
// (LootMetrics), and the loot source of RemoveItemFromInventory.
|
||||
class LootMetricsTests : public GameDependenciesTest {
|
||||
protected:
|
||||
static constexpr LOT ITEM = 7000;
|
||||
static constexpr LOT SMASHABLE = 4712;
|
||||
static constexpr LWOOBJID PLAYER = 0x1000000000000001LL;
|
||||
|
||||
std::unique_ptr<Entity> entity;
|
||||
std::unique_ptr<Character> character;
|
||||
InventoryComponent* inventory{};
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE ItemSets (setID INTEGER, itemIDs TEXT);");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE Objects (id INTEGER, name TEXT, type TEXT);");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER);");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE SkillBehavior (skillID INTEGER, behaviorID INTEGER);");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);");
|
||||
RegisterLot(ITEM);
|
||||
|
||||
info.lot = 1; // a player
|
||||
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
||||
entity = std::make_unique<Entity>(PLAYER, info);
|
||||
inventory = entity->AddComponent<InventoryComponent>(-1);
|
||||
character = std::make_unique<Character>(1, nullptr);
|
||||
entity->SetCharacter(character.get());
|
||||
entity->AddComponent<CharacterComponent>(-1, character.get(), ClientAddress())->InitializeStatisticsFromString("");
|
||||
Game::entityManager->_addEntity(entity.get()); // drops from the player, and the quickbuild, look it up there
|
||||
ClearSent();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
Game::entityManager->_removeEntity(PLAYER);
|
||||
entity->SetCharacter(nullptr);
|
||||
entity.reset();
|
||||
character.reset();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
static void RegisterLot(const LOT lot) {
|
||||
const auto componentID = static_cast<uint32_t>(96000 + lot);
|
||||
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
|
||||
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
|
||||
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
|
||||
CDItemComponent component{};
|
||||
component.id = componentID;
|
||||
component.stackSize = 999;
|
||||
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
|
||||
}
|
||||
|
||||
static dServerMock& Server() { return *static_cast<dServerMock*>(Game::server); }
|
||||
|
||||
static void ClearSent() { Server().ClearSentPackets(); }
|
||||
|
||||
// The extra info of every AddItemToInventoryClientSync sent
|
||||
static std::vector<std::u16string> AddedExtraInfo() {
|
||||
std::vector<std::u16string> out;
|
||||
for (const auto& packet : Server().GetSentPackets()) {
|
||||
RakNet::BitStream bitStream(const_cast<uint8_t*>(packet.bytes.data()), packet.bytes.size(), false);
|
||||
LWOOBJID target{};
|
||||
MessageType::Game id{};
|
||||
if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, id)) continue;
|
||||
if (id != MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC) continue;
|
||||
GameMessages::AddItemToInventoryClientSync msg;
|
||||
EXPECT_TRUE(msg.Deserialize(bitStream));
|
||||
out.push_back(msg.extraInfo);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::vector<GameMessages::RemoveItemFromInventory> Removed() {
|
||||
std::vector<GameMessages::RemoveItemFromInventory> out;
|
||||
for (const auto& packet : Server().GetSentPackets()) {
|
||||
RakNet::BitStream bitStream(const_cast<uint8_t*>(packet.bytes.data()), packet.bytes.size(), false);
|
||||
LWOOBJID target{};
|
||||
MessageType::Game id{};
|
||||
if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, id)) continue;
|
||||
if (id != MessageType::Game::REMOVE_ITEM_FROM_INVENTORY) continue;
|
||||
auto& msg = out.emplace_back();
|
||||
EXPECT_TRUE(msg.Deserialize(bitStream));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Drops an item for the player from the given source object and picks it up
|
||||
void DropAndPickUp(const LWOOBJID source, const LWOOBJID lootId) {
|
||||
GameMessages::DropClientLoot drop;
|
||||
drop.target = PLAYER;
|
||||
drop.ownerID = PLAYER;
|
||||
drop.sourceID = source;
|
||||
drop.item = ITEM;
|
||||
drop.count = 1;
|
||||
drop.lootID = lootId;
|
||||
GameMessages::DropClientLootEvent event(drop);
|
||||
entity->MsgDropClientLoot(event);
|
||||
ClearSent();
|
||||
entity->PickupItem(lootId);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(LootMetricsTests, KeysAsLiveWroteThem) {
|
||||
EXPECT_TRUE(LootMetrics{}.Empty());
|
||||
EXPECT_EQ(LootMetrics{}.ToExtraInfo(), u"");
|
||||
|
||||
LootMetrics pickup{};
|
||||
pickup.sourceLot = 1;
|
||||
EXPECT_EQ(pickup.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:1");
|
||||
|
||||
LootMetrics mission{};
|
||||
mission.sourceLot = 1;
|
||||
mission.missionId = 889;
|
||||
EXPECT_EQ(mission.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:1,_Metric_Mission_ID_Int=1:889");
|
||||
|
||||
LootMetrics vendor{};
|
||||
vendor.sourceLot = 13380;
|
||||
vendor.currencyDelta = -20000;
|
||||
EXPECT_EQ(vendor.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:13380,_Metric_Currency_Delta_Int=1:-20000");
|
||||
|
||||
// Live: the mail ID as name value type 8, the trade ID as type 9
|
||||
LootMetrics mail{};
|
||||
mail.mailId = 1152921510716557582ULL;
|
||||
EXPECT_EQ(mail.ToExtraInfo(), u"_Metric_Mail_ID_Int64=8:1152921510716557582");
|
||||
LootMetrics trade{};
|
||||
trade.transactionId = 1152921510834827094LL;
|
||||
EXPECT_EQ(trade.ToExtraInfo(), u"_Metric_Transaction_ID_Int64=9:1152921510834827094");
|
||||
}
|
||||
|
||||
TEST_F(LootMetricsTests, PickupSaysWhichObjectItDroppedFrom) {
|
||||
info.lot = SMASHABLE;
|
||||
auto* const smashable = Game::entityManager->CreateEntity(info, nullptr, nullptr, false, 0x40040000000100LL);
|
||||
ASSERT_NE(smashable, nullptr);
|
||||
|
||||
DropAndPickUp(smashable->GetObjectID(), 0x5000);
|
||||
EXPECT_EQ(AddedExtraInfo(), std::vector<std::u16string>{ u"_Metric_Souce_LOT_Int=1:4712" });
|
||||
|
||||
// A second one joins the stack; the message still says where it came from
|
||||
DropAndPickUp(smashable->GetObjectID(), 0x5001);
|
||||
EXPECT_EQ(AddedExtraInfo(), std::vector<std::u16string>{ u"_Metric_Souce_LOT_Int=1:4712" });
|
||||
}
|
||||
|
||||
TEST_F(LootMetricsTests, PlayerSourcedPickupIsLotOne) {
|
||||
// Activity rewards and chests drop from the player (live: 1 on 741 pickups)
|
||||
DropAndPickUp(PLAYER, 0x5002);
|
||||
EXPECT_EQ(AddedExtraInfo(), std::vector<std::u16string>{ u"_Metric_Souce_LOT_Int=1:1" });
|
||||
}
|
||||
|
||||
TEST_F(LootMetricsTests, UnknownSourceSendsNoKey) {
|
||||
DropAndPickUp(0x40040000000999LL, 0x5003);
|
||||
EXPECT_EQ(AddedExtraInfo(), std::vector<std::u16string>{ u"" });
|
||||
}
|
||||
|
||||
TEST_F(LootMetricsTests, MetricsFollowTheItemConfig) {
|
||||
LwoNameValue config;
|
||||
config.Insert<int32_t>(u"a", 5);
|
||||
LootMetrics metrics{};
|
||||
metrics.sourceLot = 1;
|
||||
inventory->AddItem(ITEM, 1, eLootSourceType::PICKUP, eInventoryType::INVALID, config, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, metrics);
|
||||
EXPECT_EQ(AddedExtraInfo(), std::vector<std::u16string>{ u"a=1:5,_Metric_Souce_LOT_Int=1:1" });
|
||||
// Only the message carries them
|
||||
const auto* const item = inventory->FindItemByLot(ITEM);
|
||||
ASSERT_NE(item, nullptr);
|
||||
EXPECT_EQ(item->GetConfig().values.size(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(LootMetricsTests, RemovalSaysWhatTookTheItems) {
|
||||
inventory->AddItem(ITEM, 5, eLootSourceType::NONE);
|
||||
ClearSent();
|
||||
constexpr LWOOBJID QUICKBUILD = 0x40040000000200LL;
|
||||
ASSERT_TRUE(inventory->RemoveItem(ITEM, 5, eInventoryType::ALL, false, false, { eLootSourceType::QUICKBUILD, QUICKBUILD }));
|
||||
const auto removed = Removed();
|
||||
ASSERT_EQ(removed.size(), 1u);
|
||||
EXPECT_EQ(removed[0].eLootTypeSource, static_cast<int32_t>(eLootSourceType::QUICKBUILD));
|
||||
EXPECT_EQ(removed[0].iLootTypeSource, QUICKBUILD);
|
||||
EXPECT_EQ(removed[0].iStackCount, 5u);
|
||||
|
||||
// Without a source, as before
|
||||
inventory->AddItem(ITEM, 1, eLootSourceType::NONE);
|
||||
ClearSent();
|
||||
ASSERT_TRUE(inventory->RemoveItem(ITEM, 1));
|
||||
ASSERT_EQ(Removed().size(), 1u);
|
||||
EXPECT_EQ(Removed()[0].eLootTypeSource, LOOTTYPE_NONE);
|
||||
EXPECT_EQ(Removed()[0].iLootTypeSource, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
// Live: the FV Stone Warrior pedestal's 5 Maelstrom Infected Bricks were taken with the loot source Quickbuild and the
|
||||
// quickbuild as the source object
|
||||
TEST_F(LootMetricsTests, QuickbuildTakesItsCostAsTheSource) {
|
||||
constexpr LOT BRICK = 6194;
|
||||
constexpr int32_t PRECONDITION = 90099; // stands in for 99: have 5 of LOT 6194
|
||||
RegisterLot(BRICK);
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE IF NOT EXISTS Preconditions (id INTEGER, type INTEGER, targetLOT TEXT, targetCount INTEGER);");
|
||||
CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (" + std::to_string(PRECONDITION) + ", 2, '6194', 5);");
|
||||
inventory->AddItem(BRICK, 7, eLootSourceType::NONE);
|
||||
// What the precondition check looks at besides the inventory
|
||||
entity->AddComponent<MissionComponent>(-1);
|
||||
entity->AddComponent<DestroyableComponent>(-1);
|
||||
entity->AddComponent<LevelProgressionComponent>(-1);
|
||||
|
||||
auto pedestalInfo = info;
|
||||
pedestalInfo.lot = 8551;
|
||||
auto pedestal = std::make_unique<Entity>(0x0102030405060708LL, pedestalInfo);
|
||||
pedestal->SetVar<std::u16string>(u"CheckPrecondition", GeneralUtils::ASCIIToUTF16(std::to_string(PRECONDITION)));
|
||||
auto* const quickBuild = pedestal->AddComponent<QuickBuildComponent>(-1);
|
||||
quickBuild->SetResetTime(20.0f);
|
||||
quickBuild->SetCompleteTime(10.0f);
|
||||
ClearSent();
|
||||
|
||||
quickBuild->OnUse(entity.get());
|
||||
EXPECT_EQ(quickBuild->GetState(), eQuickBuildState::BUILDING);
|
||||
const auto removed = Removed();
|
||||
ASSERT_EQ(removed.size(), 1u);
|
||||
EXPECT_EQ(removed[0].iObjTemplate, BRICK);
|
||||
EXPECT_EQ(removed[0].iStackCount, 5u);
|
||||
EXPECT_EQ(removed[0].eLootTypeSource, static_cast<int32_t>(eLootSourceType::QUICKBUILD));
|
||||
EXPECT_EQ(removed[0].iLootTypeSource, pedestal->GetObjectID());
|
||||
EXPECT_EQ(inventory->GetLotCount(BRICK), 2u);
|
||||
|
||||
pedestal.reset();
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "Entity.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
#include "MovementAIComponent.h"
|
||||
#include "PetComponent.h"
|
||||
#include "PhantomPhysicsComponent.h"
|
||||
#include "SimplePhysicsComponent.h"
|
||||
|
||||
@@ -24,6 +25,11 @@ protected:
|
||||
static constexpr int32_t ENEMY_BLOCKER_PHYSICS = 4237;
|
||||
static constexpr int32_t CARVER_PHYSICS = 3901;
|
||||
static constexpr int32_t CLEAR_THREAT_PHYSICS = 6691;
|
||||
static constexpr LOT PET_BLOCKER_LOT = 3913; // PR - Pet Blocker: solid, group 18
|
||||
static constexpr int32_t PET_BLOCKER_PHYSICS = 1913;
|
||||
static constexpr LOT HEDGE_LOT = 3027; // AG - bush square section: solid, a navmesh carver in Robot City
|
||||
static constexpr int32_t HEDGE_PHYSICS = 90301;
|
||||
static constexpr int32_t PET_GROUP = 3;
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
@@ -42,10 +48,13 @@ protected:
|
||||
AddPhysics(ENEMY_BLOCKER_LOT, eReplicaComponentType::SIMPLE_PHYSICS, ENEMY_BLOCKER_PHYSICS, "miscellaneous\\misc_phys_10x1x5.hkx", 18);
|
||||
AddPhysics(CARVER_LOT, eReplicaComponentType::PHANTOM_PHYSICS, CARVER_PHYSICS, "miscellaneous\\misc_phys_10x1x5.hkx", 7);
|
||||
AddPhysics(CLEAR_THREAT_LOT, eReplicaComponentType::PHANTOM_PHYSICS, CLEAR_THREAT_PHYSICS, "test\\POI_trigger_wall.hkx", 18);
|
||||
AddPhysics(PET_BLOCKER_LOT, eReplicaComponentType::SIMPLE_PHYSICS, PET_BLOCKER_PHYSICS, "miscellaneous\\misc_phys_10x1x5.hkx", 18);
|
||||
AddPhysics(HEDGE_LOT, eReplicaComponentType::SIMPLE_PHYSICS, HEDGE_PHYSICS, "env\\env_won_nim_bush_square-section.hkx", 7);
|
||||
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
dpWorld::Shutdown(); // frees the blockers the world owns
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
@@ -136,3 +145,72 @@ TEST_F(MovementBlockerScenarioTest, NonEnemyMoverIsNotClamped) {
|
||||
movement->SetDestination({ 0.0f, 0.0f, 10.0f });
|
||||
EXPECT_NEAR(movement->GetDestination().z, 10.0f, 1e-3f);
|
||||
}
|
||||
|
||||
// Pets are walked by the server too; the pet ranch's pet blockers keep them in (group 18 touches pets, group 3)
|
||||
TEST_F(MovementBlockerScenarioTest, PetBlockerStopsAPet) {
|
||||
auto wall = Wall(PET_BLOCKER_LOT, 104);
|
||||
wall->AddComponent<SimplePhysicsComponent>(PET_BLOCKER_PHYSICS);
|
||||
ASSERT_EQ(dpWorld::GetMovementBlockers().size(), 1u);
|
||||
|
||||
EntityInfo petInfo = info;
|
||||
petInfo.pos = { 0.0f, 0.0f, -10.0f };
|
||||
Entity pet(204, petInfo);
|
||||
pet.AddComponent<SimplePhysicsComponent>(-1)->SetCollisionGroup(PET_GROUP);
|
||||
pet.AddComponent<PetComponent>(-1);
|
||||
auto* const movement = pet.AddComponent<MovementAIComponent>(-1, MovementAIInfo{});
|
||||
movement->SetDestination({ 0.0f, 0.0f, 10.0f });
|
||||
EXPECT_LT(movement->GetDestination().z, -0.5f);
|
||||
EXPECT_GT(movement->GetDestination().z, -3.0f);
|
||||
}
|
||||
|
||||
// A carver_only object is never spawned (the client never loads one), but it still stops the chase
|
||||
TEST_F(MovementBlockerScenarioTest, CarverOnlyWallBlocksWithoutAnObject) {
|
||||
LwoNameValue settings;
|
||||
settings.Insert<bool>(u"carver_only", true);
|
||||
settings.Insert<bool>(u"navmesh_carver", true);
|
||||
EXPECT_TRUE(PhysicsComponent::IsCarverOnly(settings));
|
||||
ASSERT_TRUE(PhysicsComponent::AddLevelMovementBlocker(105, CARVER_LOT, settings, NiPoint3Constant::ZERO, QuatUtils::IDENTITY, 1.0f));
|
||||
ASSERT_EQ(dpWorld::GetMovementBlockers().size(), 1u);
|
||||
|
||||
EntityInfo enemyInfo = info;
|
||||
enemyInfo.pos = { 0.0f, 0.0f, -10.0f };
|
||||
Entity enemy(205, enemyInfo);
|
||||
EXPECT_LT(ChaseAcross(enemy).z, -0.5f);
|
||||
|
||||
dpWorld::Shutdown();
|
||||
EXPECT_TRUE(dpWorld::GetMovementBlockers().empty());
|
||||
}
|
||||
|
||||
TEST_F(MovementBlockerScenarioTest, CarverOnlyNeedsItsFlagAndAWall) {
|
||||
LwoNameValue settings;
|
||||
EXPECT_FALSE(PhysicsComponent::IsCarverOnly(settings));
|
||||
settings.Insert<bool>(u"carver_only", false);
|
||||
EXPECT_FALSE(PhysicsComponent::IsCarverOnly(settings));
|
||||
// carver_only without navmesh_carver (most of them): nothing to block with
|
||||
settings.Insert<bool>(u"carver_only", true);
|
||||
EXPECT_TRUE(PhysicsComponent::IsCarverOnly(settings));
|
||||
EXPECT_FALSE(PhysicsComponent::AddLevelMovementBlocker(106, CARVER_LOT, settings, NiPoint3Constant::ZERO, QuatUtils::IDENTITY, 1.0f));
|
||||
EXPECT_TRUE(dpWorld::GetMovementBlockers().empty());
|
||||
}
|
||||
|
||||
// The Robot City hedges carve the navmesh; their size comes from the client's collision shape (4.39 x 4.96 x 9.03)
|
||||
TEST_F(MovementBlockerScenarioTest, HedgeHasItsRealSize) {
|
||||
auto hedge = Wall(HEDGE_LOT, 107);
|
||||
hedge->SetVar<bool>(u"navmesh_carver", true);
|
||||
hedge->AddComponent<SimplePhysicsComponent>(HEDGE_PHYSICS);
|
||||
const auto blockers = dpWorld::GetMovementBlockers();
|
||||
ASSERT_EQ(blockers.size(), 1u);
|
||||
const auto* const box = dynamic_cast<const dpShapeBox*>(blockers[0].entity->GetShape());
|
||||
ASSERT_NE(box, nullptr);
|
||||
EXPECT_NEAR(box->m_MaxX - box->m_MinX, 4.3949f, 1e-3f);
|
||||
EXPECT_NEAR(box->m_MaxY - box->m_MinY, 4.9645f, 1e-3f);
|
||||
EXPECT_NEAR(box->m_MaxZ - box->m_MinZ, 9.0341f, 1e-3f);
|
||||
// Centred where the shape is, a little off the object's position
|
||||
EXPECT_NEAR((box->m_MaxX + box->m_MinX) / 2.0f, -0.0343f, 1e-3f);
|
||||
EXPECT_NEAR((box->m_MaxZ + box->m_MinZ) / 2.0f, -0.0527f, 1e-3f);
|
||||
|
||||
EntityInfo enemyInfo = info;
|
||||
enemyInfo.pos = { 0.0f, 0.0f, -10.0f };
|
||||
Entity enemy(206, enemyInfo);
|
||||
EXPECT_LT(ChaseAcross(enemy).z, -4.5f);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user