Compare commits

...

12 Commits

Author SHA1 Message Date
David Markowitz
fd547dfd10 fix: correct mission tracking
checked that live captures did not track achievements in this count
2025-11-15 13:17:07 -08:00
David Markowitz
ca60787055 fix: ffa -> shared loot for activities (#1925) 2025-10-26 01:01:21 -07:00
David Markowitz
396dcb0465 feat: add logger feature to log on function entry and exit (#1924)
* feat: add logger feature to log on function entry and exit

* i didnt save the file
2025-10-25 14:53:49 -05:00
David Markowitz
6e545eb1b9 Update Loot.cpp (#1923) 2025-10-24 21:53:00 -07:00
David Markowitz
46aac016fd fix: unintended stopping (#1922) 2025-10-23 23:41:16 -05:00
David Markowitz
83823fa64f fix: resurrect not available for non-gms (#1919) 2025-10-20 23:05:22 -07:00
David Markowitz
0dd504c803 feat: behavior states (#1918) 2025-10-20 01:16:36 -05:00
David Markowitz
a70c365c23 feat banana (#1917) 2025-10-19 14:00:14 -05:00
David Markowitz
281d9762ef fix: tac arc sorting and target acquisition (#1916) 2025-10-19 07:23:54 -05:00
David Markowitz
002aa896d8 feat: debug information (#1915) 2025-10-19 07:22:45 -05:00
David Markowitz
f3a5f60d81 feat: more destroyable debug info (#1912)
* feat: more destroyable info

* Change type and remove duplicate value
2025-10-16 14:15:02 -05:00
David Markowitz
4c9c773ec5 fix: powerup drops and hardcore loot drops (#1914)
tested the following are now functional
ag buff station
tiki torch
ve rocket part boxes
ns statue
property behavior
extra items from full inventory
hardcore drops (items and coins)
2025-10-16 14:13:38 -05:00
39 changed files with 602 additions and 138 deletions

View File

@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
}
}
newTeam->lootFlag = 1;
newTeam->lootFlag = 0;
TeamStatusUpdate(newTeam);

View File

@@ -374,6 +374,21 @@ public:
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
}
AMFArrayValue& PushDebug(const NiPoint3& point) {
PushDebug<AMFDoubleValue>("X") = point.x;
PushDebug<AMFDoubleValue>("Y") = point.y;
PushDebug<AMFDoubleValue>("Z") = point.z;
return *this;
}
AMFArrayValue& PushDebug(const NiQuaternion& rot) {
PushDebug<AMFDoubleValue>("W") = rot.w;
PushDebug<AMFDoubleValue>("X") = rot.x;
PushDebug<AMFDoubleValue>("Y") = rot.y;
PushDebug<AMFDoubleValue>("Z") = rot.z;
return *this;
}
private:
/**
* The associative portion. These values are key'd with strings to an AMFValue.

View File

@@ -96,3 +96,17 @@ bool Logger::GetLogToConsole() const {
}
return toReturn;
}
FuncEntry::FuncEntry(const char* funcName, const char* fileName, const uint32_t line) {
m_FuncName = funcName;
if (!m_FuncName) m_FuncName = "Unknown";
m_Line = line;
m_FileName = fileName;
LOG("--> %s::%s:%i", m_FileName, m_FuncName, m_Line);
}
FuncEntry::~FuncEntry() {
if (!m_FuncName || !m_FileName) return;
LOG("<-- %s::%s:%i", m_FileName, m_FuncName, m_Line);
}

View File

@@ -32,6 +32,19 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) {
#define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0)
#define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0)
// Place this right at the start of a function. Will log a message when called and then once you leave the function.
#define LOG_ENTRY auto str_ = GetFileNameFromAbsolutePath(__FILE__); FuncEntry funcEntry_(__FUNCTION__, str_, __LINE__)
class FuncEntry {
public:
FuncEntry(const char* funcName, const char* fileName, const uint32_t line);
~FuncEntry();
private:
const char* m_FuncName = nullptr;
const char* m_FileName = nullptr;
uint32_t m_Line = 0;
};
// Writer class for writing data to files.
class Writer {
public:

View File

@@ -424,6 +424,7 @@ void Entity::Initialize() {
comp->SetIsSmashable(destCompData[0].isSmashable);
comp->SetLootMatrixID(destCompData[0].LootMatrixIndex);
comp->SetCurrencyIndex(destCompData[0].CurrencyIndex);
Loot::CacheMatrix(destCompData[0].LootMatrixIndex);
// Now get currency information
@@ -2252,6 +2253,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
response.Insert("objectID", std::to_string(m_ObjectID));
response.Insert("serverInfo", true);
GameMessages::GetObjectReportInfo info{};
info.clientID = requestInfo.clientId;
info.bVerbose = requestInfo.bVerbose;
info.info = response.InsertArray("data");
auto& objectInfo = info.info->PushDebug("Object Details");

View File

@@ -10,7 +10,9 @@ void AndBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bitStream,
}
void AndBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitStream, const BehaviorBranchContext branch) {
LOG_ENTRY;
for (auto* behavior : this->m_behaviors) {
LOG("%i calculating %i", m_behaviorId, behavior->GetBehaviorID());
behavior->Calculate(context, bitStream, branch);
}
}

View File

@@ -95,4 +95,6 @@ public:
Behavior& operator=(const Behavior& other) = default;
Behavior& operator=(Behavior&& other) = default;
uint32_t GetBehaviorID() const { return m_behaviorId; }
};

View File

@@ -114,7 +114,6 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
for (auto validTarget : validTargets) {
if (targets.size() >= this->m_maxTargets) break;
if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) continue;
if (validTarget->GetIsDead()) continue;
@@ -147,13 +146,28 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bitS
}
}
std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) {
std::sort(targets.begin(), targets.end(), [this, reference, combatAi](Entity* a, Entity* b) {
const auto aDistance = Vector3::DistanceSquared(reference, a->GetPosition());
const auto bDistance = Vector3::DistanceSquared(reference, b->GetPosition());
return aDistance > bDistance;
return aDistance < bDistance;
});
if (m_useAttackPriority) {
// this should be using the attack priority column on the destroyable component
// We want targets with no threat level to remain the same order as above
// std::stable_sort(targets.begin(), targets.end(), [combatAi](Entity* a, Entity* b) {
// const auto aThreat = combatAi->GetThreat(a->GetObjectID());
// const auto bThreat = combatAi->GetThreat(b->GetObjectID());
// If enabled for this behavior, prioritize threat over distance
// return aThreat > bThreat;
// });
}
// After we've sorted and found our closest targets, size the vector down in case there are too many
if (m_maxTargets > 0 && targets.size() > m_maxTargets) targets.resize(m_maxTargets);
const auto hit = !targets.empty();
bitStream.Write(hit);

View File

@@ -27,8 +27,13 @@
#include "CDComponentsRegistryTable.h"
#include "CDPhysicsComponentTable.h"
#include "dNavMesh.h"
#include "Amf3.h"
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
{
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &BaseCombatAIComponent::MsgGetObjectReportInfo);
}
m_Target = LWOOBJID_EMPTY;
m_DirtyStateOrTarget = true;
m_State = AiState::spawn;
@@ -839,3 +844,73 @@ void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float valu
SetThreat(threat, 0.0f);
m_Target = LWOOBJID_EMPTY;
}
bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
using enum AiState;
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Base Combat AI");
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
auto& targetInfo = cmptType.PushDebug("Current Target Info");
targetInfo.PushDebug<AMFStringValue>("Current Target ID") = std::to_string(m_Target);
// if (m_Target != LWOOBJID_EMPTY) {
// LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget);
// SEND_GAMEOBJ_MSG(nameMsg);
// if (!nameMsg.msg.name.empty()) targetInfo.PushDebug("Name") = nameMsg.msg.name;
// }
auto& roundInfo = cmptType.PushDebug("Round Info");
// roundInfo.PushDebug<AMFDoubleValue>("Combat Round Time") = m_CombatRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Minimum Time") = m_MinRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Maximum Time") = m_MaxRoundLength;
// roundInfo.PushDebug<AMFDoubleValue>("Selected Time") = m_SelectedTime;
// roundInfo.PushDebug<AMFDoubleValue>("Combat Start Delay") = m_CombatStartDelay;
std::string curState;
switch (m_State) {
case idle: curState = "Idling"; break;
case aggro: curState = "Aggroed"; break;
case tether: curState = "Returning to Tether"; break;
case spawn: curState = "Spawn"; break;
case dead: curState = "Dead"; break;
default: curState = "Unknown or Undefined"; break;
}
cmptType.PushDebug<AMFStringValue>("Current Combat State") = curState;
//switch (m_CombatBehaviorType) {
// case 0: curState = "Passive"; break;
// case 1: curState = "Aggressive"; break;
// case 2: curState = "Passive (Turret)"; break;
// case 3: curState = "Aggressive (Turret)"; break;
// default: curState = "Unknown or Undefined"; break;
//}
//cmptType.PushDebug("Current Combat Behavior State") = curState;
//switch (m_CombatRole) {
// case 0: curState = "Melee"; break;
// case 1: curState = "Ranged"; break;
// case 2: curState = "Support"; break;
// default: curState = "Unknown or Undefined"; break;
//}
//cmptType.PushDebug("Current Combat Role") = curState;
auto& tetherPoint = cmptType.PushDebug("Tether Point");
tetherPoint.PushDebug<AMFDoubleValue>("X") = m_StartPosition.x;
tetherPoint.PushDebug<AMFDoubleValue>("Y") = m_StartPosition.y;
tetherPoint.PushDebug<AMFDoubleValue>("Z") = m_StartPosition.z;
cmptType.PushDebug<AMFDoubleValue>("Hard Tether Radius") = m_HardTetherRadius;
cmptType.PushDebug<AMFDoubleValue>("Soft Tether Radius") = m_SoftTetherRadius;
cmptType.PushDebug<AMFDoubleValue>("Aggro Radius") = m_AggroRadius;
cmptType.PushDebug<AMFDoubleValue>("Tether Speed") = m_TetherSpeed;
cmptType.PushDebug<AMFDoubleValue>("Aggro Speed") = m_TetherSpeed;
// cmptType.PushDebug<AMFDoubleValue>("Specified Min Range") = m_SpecificMinRange;
// cmptType.PushDebug<AMFDoubleValue>("Specified Max Range") = m_SpecificMaxRange;
auto& threats = cmptType.PushDebug("Target Threats");
for (const auto& [id, threat] : m_ThreatEntries) {
threats.PushDebug<AMFDoubleValue>(std::to_string(id)) = threat;
}
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
for (const auto& [id, threat] : m_ThreatEntries) {
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
}
return true;
}

View File

@@ -234,6 +234,8 @@ public:
// Ignore a threat for a certain amount of time
void IgnoreThreat(const LWOOBJID target, const float time);
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
private:
/**
* Returns the current target or the target that currently is the largest threat to this entity

View File

@@ -8,15 +8,33 @@
#include "GameMessages.h"
#include "BitStream.h"
#include "eTriggerEventType.h"
#include "Amf3.h"
BouncerComponent::BouncerComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
m_PetEnabled = false;
m_PetBouncerEnabled = false;
m_PetSwitchLoaded = false;
m_Destination = GeneralUtils::TryParse<NiPoint3>(
GeneralUtils::SplitString(m_Parent->GetVarAsString(u"bouncer_destination"), '\x1f'))
.value_or(NiPoint3Constant::ZERO);
m_Speed = GeneralUtils::TryParse<float>(m_Parent->GetVarAsString(u"bouncer_speed")).value_or(-1.0f);
m_UsesHighArc = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"bouncer_uses_high_arc")).value_or(false);
m_LockControls = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"lock_controls")).value_or(false);
m_IgnoreCollision = !GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"ignore_collision")).value_or(true);
m_StickLanding = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"stickLanding")).value_or(false);
m_UsesGroupName = GeneralUtils::TryParse<bool>(m_Parent->GetVarAsString(u"uses_group_name")).value_or(false);
m_GroupName = m_Parent->GetVarAsString(u"grp_name");
m_MinNumTargets = GeneralUtils::TryParse<int32_t>(m_Parent->GetVarAsString(u"num_targets_to_activate")).value_or(1);
m_CinematicPath = m_Parent->GetVarAsString(u"attached_cinematic_path");
if (parent->GetLOT() == 7625) {
LookupPetSwitch();
}
{
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &BouncerComponent::MsgGetObjectReportInfo);
}
}
BouncerComponent::~BouncerComponent() {
@@ -94,3 +112,54 @@ void BouncerComponent::LookupPetSwitch() {
});
}
}
bool BouncerComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Bouncer");
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
auto& destPos = cmptType.PushDebug("Destination Position");
if (m_Destination != NiPoint3Constant::ZERO) {
destPos.PushDebug(m_Destination);
} else {
destPos.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no target position, is likely missing config data");
}
if (m_Speed == -1.0f) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has no speed value, is likely missing config data");
} else {
cmptType.PushDebug<AMFDoubleValue>("Bounce Speed") = m_Speed;
}
cmptType.PushDebug<AMFStringValue>("Bounce trajectory arc") = m_UsesHighArc ? "High Arc" : "Low Arc";
cmptType.PushDebug<AMFBoolValue>("Collision Enabled") = m_IgnoreCollision;
cmptType.PushDebug<AMFBoolValue>("Stick Landing") = m_StickLanding;
cmptType.PushDebug<AMFBoolValue>("Locks character's controls") = m_LockControls;
if (!m_CinematicPath.empty()) cmptType.PushDebug<AMFStringValue>("Cinematic Camera Path (plays during bounce)") = m_CinematicPath;
auto* switchComponent = m_Parent->GetComponent<SwitchComponent>();
auto& respondsToFactions = cmptType.PushDebug("Responds to Factions");
if (!switchComponent || switchComponent->GetFactionsToRespondTo().empty()) respondsToFactions.PushDebug("Faction 1");
else {
for (const auto faction : switchComponent->GetFactionsToRespondTo()) {
respondsToFactions.PushDebug(("Faction " + std::to_string(faction)));
}
}
cmptType.PushDebug<AMFBoolValue>("Uses a group name for interactions") = m_UsesGroupName;
if (!m_UsesGroupName) {
if (m_MinNumTargets > 1) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Bouncer has a required number of objects to activate, but no group for interactions.");
}
if (!m_GroupName.empty()) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Has a group name for interactions , but is marked to not use that name.");
}
} else {
if (m_GroupName.empty()) {
cmptType.PushDebug("<font color=\'#FF0000\'>WARNING:</font> Set to use a group name for inter actions, but no group name is assigned");
}
cmptType.PushDebug<AMFIntValue>("Number of interactions to activate bouncer") = m_MinNumTargets;
}
return true;
}

View File

@@ -51,6 +51,8 @@ public:
*/
void LookupPetSwitch();
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
private:
/**
* Whether this bouncer needs to be activated by a pet
@@ -66,6 +68,36 @@ private:
* Whether the pet switch for this bouncer has been located
*/
bool m_PetSwitchLoaded;
// The bouncer destination
NiPoint3 m_Destination;
// The speed at which the player is bounced
float m_Speed{};
// Whether to use a high arc for the bounce trajectory
bool m_UsesHighArc{};
// Lock controls when bouncing
bool m_LockControls{};
// Ignore collision when bouncing
bool m_IgnoreCollision{};
// Stick the landing afterwards or let the player slide
bool m_StickLanding{};
// Whether or not there is a group name
bool m_UsesGroupName{};
// The group name for targets
std::string m_GroupName{};
// The number of targets to activate the bouncer
int32_t m_MinNumTargets{};
// The cinematic path to play during the bounce
std::string m_CinematicPath{};
};
#endif // BOUNCERCOMPONENT_H

View File

@@ -515,12 +515,12 @@ void CharacterComponent::RocketUnEquip(Entity* player) {
}
void CharacterComponent::TrackMissionCompletion(bool isAchievement) {
UpdatePlayerStatistic(MissionsCompleted);
// Achievements are tracked separately for the zone
if (isAchievement) {
const auto mapID = Game::zoneManager->GetZoneID().GetMapID();
GetZoneStatisticsForMap(mapID).m_AchievementsCollected++;
} else {
UpdatePlayerStatistic(MissionsCompleted);
}
}

View File

@@ -1,5 +1,39 @@
#include "CollectibleComponent.h"
#include "MissionComponent.h"
#include "dServer.h"
#include "Amf3.h"
CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) :
Component(parentEntity, componentID), m_CollectibleId(collectibleId) {
using namespace GameMessages;
RegisterMsg<GetObjectReportInfo>(this, &CollectibleComponent::MsgGetObjectReportInfo);
}
void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {
outBitStream.Write(GetCollectibleId());
}
bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Collectible");
auto collectibleID = static_cast<uint32_t>(m_CollectibleId) + static_cast<uint32_t>(Game::server->GetZoneID() << 8);
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
cmptType.PushDebug<AMFIntValue>("Collectible ID") = GetCollectibleId();
cmptType.PushDebug<AMFIntValue>("Mission Tracking ID (for save data)") = collectibleID;
auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID);
bool collected = false;
if (localCharEntity) {
auto* missionComponent = localCharEntity->GetComponent<MissionComponent>();
if (m_CollectibleId != 0) {
collected = missionComponent->HasCollectible(collectibleID);
}
}
cmptType.PushDebug<AMFBoolValue>("Has been collected") = collected;
return true;
}

View File

@@ -7,10 +7,12 @@
class CollectibleComponent final : public Component {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::COLLECTIBLE;
CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : Component(parentEntity, componentID), m_CollectibleId(collectibleId) {}
CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId);
int16_t GetCollectibleId() const { return m_CollectibleId; }
void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override;
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
private:
int16_t m_CollectibleId = 0;
};

View File

@@ -3,6 +3,9 @@
#include "Logger.h"
#include "Game.h"
#include "dConfig.h"
#include "CDLootMatrixTable.h"
#include "CDLootTableTable.h"
#include "CDRarityTableTable.h"
#include "Amf3.h"
#include "AmfSerialize.h"
@@ -982,7 +985,14 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
for (const auto item : itemMap | std::views::values) {
// Don't drop excluded items or null ones
if (!item || Game::entityManager->GetHardcoreExcludedItemDrops().contains(item->GetLot())) continue;
GameMessages::SendDropClientLoot(m_Parent, source, item->GetLot(), 0, m_Parent->GetPosition(), item->GetCount());
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = m_Parent->GetObjectID();
lootMsg.ownerID = m_Parent->GetObjectID();
lootMsg.sourceID = m_Parent->GetObjectID();
lootMsg.item = item->GetLot();
lootMsg.count = 1;
lootMsg.spawnPos = m_Parent->GetPosition();
for (int i = 0; i < item->GetCount(); i++) Loot::DropItem(*m_Parent, lootMsg);
item->SetCount(0, false, false);
}
Game::entityManager->SerializeEntity(m_Parent);
@@ -1005,12 +1015,24 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
//drop all coins:
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = m_Parent->GetObjectID();
lootMsg.ownerID = m_Parent->GetObjectID();
lootMsg.spawnPos = m_Parent->GetPosition();
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
lootMsg.Send(m_Parent->GetSystemAddress());
while (coinsToDrop > MAX_TO_DROP_PER_GM) {
LOG("Dropping 100,000, %llu left", coinsToDrop);
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, MAX_TO_DROP_PER_GM, m_Parent->GetPosition());
coinsToDrop -= MAX_TO_DROP_PER_GM;
lootMsg.currency = 100'000;
lootMsg.Send();
lootMsg.Send(m_Parent->GetSystemAddress());
coinsToDrop -= 100'000;
}
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coinsToDrop, m_Parent->GetPosition());
lootMsg.currency = coinsToDrop;
lootMsg.Send();
lootMsg.Send(m_Parent->GetSystemAddress());
}
return;
}
@@ -1041,38 +1063,89 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportInfo = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& destroyableInfo = reportInfo.info->PushDebug("Destroyable");
destroyableInfo.PushDebug<AMFIntValue>("Health") = m_iHealth;
destroyableInfo.PushDebug<AMFDoubleValue>("Max Health") = m_fMaxHealth;
destroyableInfo.PushDebug<AMFIntValue>("Armor") = m_iArmor;
destroyableInfo.PushDebug<AMFDoubleValue>("Max Armor") = m_fMaxArmor;
destroyableInfo.PushDebug<AMFIntValue>("Imagination") = m_iImagination;
destroyableInfo.PushDebug<AMFDoubleValue>("Max Imagination") = m_fMaxImagination;
destroyableInfo.PushDebug<AMFIntValue>("Damage To Absorb") = m_DamageToAbsorb;
destroyableInfo.PushDebug<AMFBoolValue>("Is GM Immune") = m_IsGMImmune;
destroyableInfo.PushDebug<AMFBoolValue>("Is Shielded") = m_IsShielded;
destroyableInfo.PushDebug<AMFIntValue>("DestructibleComponent DB Table Template ID") = m_ComponentID;
if (m_CurrencyIndex == -1) {
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Currency") = false;
} else {
destroyableInfo.PushDebug<AMFIntValue>("Loot Currency ID") = m_CurrencyIndex;
auto& detailedCoinInfo = destroyableInfo.PushDebug("Coin Info");
detailedCoinInfo.PushDebug<AMFIntValue>("Min Coins") = m_MinCoins;
detailedCoinInfo.PushDebug<AMFIntValue>("Max Coins") = m_MaxCoins;
}
if (m_LootMatrixID == -1 || m_LootMatrixID == 0) {
destroyableInfo.PushDebug<AMFBoolValue>("Has Loot Matrix") = false;
} else {
auto& lootInfo = destroyableInfo.PushDebug("Loot Info");
lootInfo.PushDebug<AMFIntValue>("Loot Matrix ID") = m_LootMatrixID;
auto* const componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
auto* const itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
auto* const lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
auto* const lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
auto* const rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
const auto& matrix = lootMatrixTable->GetMatrix(m_LootMatrixID);
for (const auto& entry : matrix) {
auto& thisEntry = lootInfo.PushDebug("Loot table Index - " + std::to_string(entry.LootTableIndex));
thisEntry.PushDebug<AMFDoubleValue>("Percent chance to drop") = entry.percent * 100.0f;
thisEntry.PushDebug<AMFDoubleValue>("Minimum amount to drop") = entry.minToDrop;
thisEntry.PushDebug<AMFDoubleValue>("Maximum amount to drop") = entry.maxToDrop;
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
auto& thisRarity = thisEntry.PushDebug("Rarity");
for (const auto& rarity : rarityTable) {
thisRarity.PushDebug<AMFDoubleValue>("Rarity " + std::to_string(rarity.rarity)) = rarity.randmax;
}
auto& thisItems = thisEntry.PushDebug("Drop(s) Info");
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
auto title = "%[Objects_" + std::to_string(loot.itemid) + "_name] " + std::to_string(loot.itemid);
if (loot.MissionDrop) title += " - Mission Drop";
thisItems.PushDebug(title);
}
}
}
auto* const entity = Game::entityManager->GetEntity(reportInfo.clientID);
destroyableInfo.PushDebug<AMFBoolValue>("Is on your team") = entity ? IsFriend(entity) : false;
auto& stats = destroyableInfo.PushDebug("Statistics");
stats.PushDebug<AMFIntValue>("Health") = m_iHealth;
stats.PushDebug<AMFDoubleValue>("Maximum Health") = m_fMaxHealth;
stats.PushDebug<AMFIntValue>("Armor") = m_iArmor;
stats.PushDebug<AMFDoubleValue>("Maximum Armor") = m_fMaxArmor;
stats.PushDebug<AMFIntValue>("Imagination") = m_iImagination;
stats.PushDebug<AMFDoubleValue>("Maximum Imagination") = m_fMaxImagination;
stats.PushDebug<AMFIntValue>("Damage Absorption Points") = m_DamageToAbsorb;
stats.PushDebug<AMFBoolValue>("Is GM Immune") = m_IsGMImmune;
stats.PushDebug<AMFBoolValue>("Is Shielded") = m_IsShielded;
destroyableInfo.PushDebug<AMFIntValue>("Attacks To Block") = m_AttacksToBlock;
destroyableInfo.PushDebug<AMFIntValue>("Damage Reduction") = m_DamageReduction;
auto& factions = destroyableInfo.PushDebug("Factions");
size_t i = 0;
std::stringstream factionsStream;
for (const auto factionID : m_FactionIDs) {
factions.PushDebug<AMFStringValue>(std::to_string(i++) + " " + std::to_string(factionID)) = "";
factionsStream << factionID << " ";
}
auto& enemyFactions = destroyableInfo.PushDebug("Enemy Factions");
i = 0;
destroyableInfo.PushDebug<AMFStringValue>("Factions") = factionsStream.str();
factionsStream.str("");
for (const auto enemyFactionID : m_EnemyFactionIDs) {
enemyFactions.PushDebug<AMFStringValue>(std::to_string(i++) + " " + std::to_string(enemyFactionID)) = "";
factionsStream << enemyFactionID << " ";
}
destroyableInfo.PushDebug<AMFBoolValue>("Is Smashable") = m_IsSmashable;
destroyableInfo.PushDebug<AMFBoolValue>("Is Dead") = m_IsDead;
destroyableInfo.PushDebug<AMFStringValue>("Enemy Factions") = factionsStream.str();
destroyableInfo.PushDebug<AMFBoolValue>("Is A Smashable") = m_IsSmashable;
destroyableInfo.PushDebug<AMFBoolValue>("Is Smashed") = m_IsSmashed;
destroyableInfo.PushDebug<AMFBoolValue>("Is Module Assembly") = m_IsModuleAssembly;
destroyableInfo.PushDebug<AMFDoubleValue>("Explode Factor") = m_ExplodeFactor;
destroyableInfo.PushDebug<AMFBoolValue>("Has Threats") = m_HasThreats;
destroyableInfo.PushDebug<AMFIntValue>("Loot Matrix ID") = m_LootMatrixID;
destroyableInfo.PushDebug<AMFIntValue>("Min Coins") = m_MinCoins;
destroyableInfo.PushDebug<AMFIntValue>("Max Coins") = m_MaxCoins;
destroyableInfo.PushDebug<AMFStringValue>("Killer ID") = std::to_string(m_KillerID);
// "Scripts"; idk what to do about scripts yet
@@ -1087,7 +1160,25 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
immuneCounts.PushDebug<AMFIntValue>("Quickbuild Interrupt") = m_ImmuneToQuickbuildInterruptCount;
immuneCounts.PushDebug<AMFIntValue>("Pull To Point") = m_ImmuneToPullToPointCount;
destroyableInfo.PushDebug<AMFIntValue>("Death Behavior") = m_DeathBehavior;
auto& deathInfo = destroyableInfo.PushDebug("Death Info");
deathInfo.PushDebug<AMFBoolValue>("Is Dead") = m_IsDead;
switch (m_DeathBehavior) {
case 0:
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Fade";
break;
case 1:
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Stay";
break;
case 2:
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Immediate";
break;
case -1:
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Invulnerable";
break;
default:
deathInfo.PushDebug<AMFStringValue>("Death Behavior") = "Other";
break;
}
destroyableInfo.PushDebug<AMFDoubleValue>("Damage Cooldown Timer") = m_DamageCooldownTimer;
return true;

View File

@@ -370,6 +370,8 @@ public:
*/
uint32_t GetLootMatrixID() const { return m_LootMatrixID; }
void SetCurrencyIndex(int32_t currencyIndex) { m_CurrencyIndex = currencyIndex; }
/**
* Returns the ID of the entity that killed this entity, if any
* @return the ID of the entity that killed this entity, if any
@@ -587,6 +589,9 @@ private:
*/
uint32_t m_LootMatrixID;
// The currency index to determine how much loot to drop
int32_t m_CurrencyIndex{ -1 };
/**
* The min amount of coins that will drop when this entity is smashed
*/

View File

@@ -1,9 +1,14 @@
#include "GhostComponent.h"
#include "Amf3.h"
#include "GameMessages.h"
GhostComponent::GhostComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
m_GhostReferencePoint = NiPoint3Constant::ZERO;
m_GhostOverridePoint = NiPoint3Constant::ZERO;
m_GhostOverride = false;
RegisterMsg<GameMessages::GetObjectReportInfo>(this, &GhostComponent::MsgGetObjectReportInfo);
}
GhostComponent::~GhostComponent() {
@@ -55,3 +60,12 @@ bool GhostComponent::IsObserved(LWOOBJID id) {
void GhostComponent::GhostEntity(LWOOBJID id) {
m_ObservedEntities.erase(id);
}
bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& cmptType = reportMsg.info->PushDebug("Ghost");
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
cmptType.PushDebug<AMFBoolValue>("Is GM Invis") = false;
return true;
}

View File

@@ -39,6 +39,8 @@ public:
void GhostEntity(const LWOOBJID id);
bool MsgGetObjectReportInfo(GameMessages::GameMsg& msg);
private:
NiPoint3 m_GhostReferencePoint;

View File

@@ -282,7 +282,14 @@ void InventoryComponent::AddItem(
case 1:
for (size_t i = 0; i < size; i++) {
GameMessages::SendDropClientLoot(this->m_Parent, this->m_Parent->GetObjectID(), lot, 0, this->m_Parent->GetPosition(), 1);
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = m_Parent->GetObjectID();
lootMsg.ownerID = m_Parent->GetObjectID();
lootMsg.sourceID = m_Parent->GetObjectID();
lootMsg.item = lot;
lootMsg.count = 1;
lootMsg.spawnPos = m_Parent->GetPosition();
Loot::DropItem(*m_Parent, lootMsg);
}
break;

View File

@@ -29,19 +29,22 @@ ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Comp
bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
auto& reset = static_cast<GameMessages::ResetModelToDefaults&>(msg);
for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset);
GameMessages::UnSmash unsmash;
unsmash.target = GetParent()->GetObjectID();
unsmash.duration = 0.0f;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
if (reset.bResetBehaviors) for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset);
m_Parent->SetPosition(m_OriginalPosition);
m_Parent->SetRotation(m_OriginalRotation);
if (reset.bUnSmash) {
GameMessages::UnSmash unsmash;
unsmash.target = GetParent()->GetObjectID();
unsmash.duration = 0.0f;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
m_NumActiveUnSmash = 0;
}
if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition);
if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation);
m_Parent->SetVelocity(NiPoint3Constant::ZERO);
m_Speed = 3.0f;
m_NumListeningInteract = 0;
m_NumActiveUnSmash = 0;
m_NumActiveAttack = 0;
GameMessages::SetFaction set{};

View File

@@ -67,6 +67,10 @@ public:
*/
static SwitchComponent* GetClosestSwitch(NiPoint3 position);
const std::vector<int32_t>& GetFactionsToRespondTo() const {
return m_FactionsToRespondTo;
}
private:
/**
* A list of all pet switches.

View File

@@ -1067,44 +1067,6 @@ void GameMessages::SendSetNetworkScriptVar(Entity* entity, const SystemAddress&
SEND_PACKET;
}
void GameMessages::SendDropClientLoot(Entity* entity, const LWOOBJID& sourceID, LOT item, int currency, NiPoint3 spawnPos, int count) {
if (Game::config->GetValue("disable_drops") == "1" || !entity) {
return;
}
bool bUsePosition = false;
NiPoint3 finalPosition;
LWOOBJID lootID = LWOOBJID_EMPTY;
LWOOBJID owner = entity->GetObjectID();
if (item != LOT_NULL && item != 0) {
lootID = ObjectIDManager::GenerateObjectID();
Loot::Info info;
info.id = lootID;
info.count = count;
info.lot = item;
entity->AddLootItem(info);
}
if (item == LOT_NULL && currency != 0) {
entity->RegisterCoinDrop(currency);
}
if (spawnPos != NiPoint3Constant::ZERO) {
bUsePosition = true;
//Calculate where the loot will go:
uint16_t degree = GeneralUtils::GenerateRandomNumber<uint16_t>(0, 360);
double rad = degree * 3.14 / 180;
double sin_v = sin(rad) * 4.2;
double cos_v = cos(rad) * 4.2;
finalPosition = NiPoint3(static_cast<float>(spawnPos.GetX() + sin_v), spawnPos.GetY(), static_cast<float>(spawnPos.GetZ() + cos_v));
}
}
void GameMessages::SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme) {
CBITSTREAM;
CMSGHEADER;

View File

@@ -153,7 +153,6 @@ namespace GameMessages {
void SendStop2DAmbientSound(Entity* entity, bool force, std::string audioGUID, bool result = false);
void SendPlay2DAmbientSound(Entity* entity, std::string audioGUID, bool result = false);
void SendSetNetworkScriptVar(Entity* entity, const SystemAddress& sysAddr, std::string data);
void SendDropClientLoot(Entity* entity, const LWOOBJID& sourceID, LOT item, int currency, NiPoint3 spawnPos = NiPoint3Constant::ZERO, int count = 1);
void SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme);
void SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPoint3& position, const NiQuaternion& rotation);
@@ -794,6 +793,7 @@ namespace GameMessages {
AMFArrayValue* info{};
AMFArrayValue* subCategory{};
bool bVerbose{};
LWOOBJID clientID{};
GetObjectReportInfo() : GameMsg(MessageType::Game::GET_OBJECT_REPORT_INFO, eGameMasterLevel::DEVELOPER) {}
};
@@ -848,6 +848,11 @@ namespace GameMessages {
struct ResetModelToDefaults : public GameMsg {
ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {}
bool bResetPos{ true };
bool bResetRot{ true };
bool bUnSmash{ true };
bool bResetBehaviors{ true };
};
struct EmotePlayed : public GameMsg {

View File

@@ -5,6 +5,7 @@
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "ModelComponent.h"
#include "StringifiedEnum.h"
#include <ranges>
@@ -178,13 +179,32 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) {
}
void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent);
auto& activeState = GetActiveState();
UpdateResult updateResult{};
activeState.Update(deltaTime, modelComponent, updateResult);
if (updateResult.newState.has_value() && updateResult.newState.value() != m_ActiveState) {
LOG("Behavior %llu is changing from state %s to %s", m_BehaviorId, StringifiedEnum::ToString(m_ActiveState).data(), StringifiedEnum::ToString(updateResult.newState.value()).data());
GameMessages::ResetModelToDefaults resetMsg{};
resetMsg.bResetPos = false;
resetMsg.bResetRot = false;
resetMsg.bUnSmash = false;
modelComponent.OnResetModelToDefaults(resetMsg);
HandleMsg(resetMsg);
m_ActiveState = updateResult.newState.value();
}
}
void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) {
for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(sMessage);
auto& activeState = GetActiveState();
activeState.OnChatMessageReceived(sMessage);
}
void PropertyBehavior::OnHit() {
for (auto& state : m_States | std::views::values) state.OnHit();
auto& activeState = GetActiveState();
activeState.OnHit();
}
State& PropertyBehavior::GetActiveState() {
DluAssert(m_States.contains(m_ActiveState));
return m_States[m_ActiveState];
}

View File

@@ -1,18 +1,23 @@
#ifndef __PROPERTYBEHAVIOR__H__
#define __PROPERTYBEHAVIOR__H__
#include "BehaviorStates.h"
#include "State.h"
#include <optional>
namespace tinyxml2 {
class XMLElement;
}
enum class BehaviorState : uint32_t;
class AMFArrayValue;
class BehaviorMessageBase;
class ModelComponent;
struct UpdateResult {
std::optional<BehaviorState> newState;
};
/**
* Represents the Entity of a Property Behavior and holds data associated with the behavior
*/
@@ -45,6 +50,7 @@ public:
void OnHit();
private:
State& GetActiveState();
// The current active behavior state. Behaviors can only be in ONE state at a time.
BehaviorState m_ActiveState;

View File

@@ -163,8 +163,8 @@ void State::Deserialize(const tinyxml2::XMLElement& state) {
}
}
void State::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent);
void State::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) {
for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent, updateResult);
}
void State::OnChatMessageReceived(const std::string& sMessage) {

View File

@@ -9,6 +9,7 @@ namespace tinyxml2 {
class AMFArrayValue;
class ModelComponent;
struct UpdateResult;
class State {
public:
@@ -21,7 +22,7 @@ public:
void Serialize(tinyxml2::XMLElement& state) const;
void Deserialize(const tinyxml2::XMLElement& state);
void Update(float deltaTime, ModelComponent& modelComponent);
void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
void OnChatMessageReceived(const std::string& sMessage);
void OnHit();

View File

@@ -13,6 +13,7 @@
#include "dChatFilter.h"
#include "DluAssert.h"
#include "Loot.h"
template <>
void Strip::HandleMsg(AddStripMessage& msg) {
@@ -148,17 +149,25 @@ void Strip::Spawn(LOT lot, Entity& entity) {
// Spawns a specific drop for all
void Strip::SpawnDrop(LOT dropLOT, Entity& entity) {
for (auto* const player : PlayerManager::GetAllPlayers()) {
GameMessages::SendDropClientLoot(player, entity.GetObjectID(), dropLOT, 0, entity.GetPosition());
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = player->GetObjectID();
lootMsg.ownerID = player->GetObjectID();
lootMsg.sourceID = entity.GetObjectID();
lootMsg.item = dropLOT;
lootMsg.count = 1;
lootMsg.spawnPos = entity.GetPosition();
Loot::DropItem(*player, lootMsg);
}
}
void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) {
auto& entity = *modelComponent.GetParent();
auto& nextAction = GetNextAction();
auto number = nextAction.GetValueParameterDouble();
auto valueStr = nextAction.GetValueParameterString();
auto numberAsInt = static_cast<int32_t>(number);
auto nextActionType = GetNextAction().GetType();
LOG_DEBUG("Processing Strip Action: %s with number %.2f and string %s", nextActionType.data(), number, valueStr.data());
// TODO replace with switch case and nextActionType with enum
/* BEGIN Move */
@@ -215,7 +224,8 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
modelComponent.AddUnSmash();
m_PausedTime = number;
// since it may take time for the message to relay to clients
m_PausedTime = number + 0.5f;
} else if (nextActionType == "Wait") {
m_PausedTime = number;
} else if (nextActionType == "Chat") {
@@ -250,6 +260,21 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup
}
/* END Gameplay */
/* BEGIN StateMachine */
else if (nextActionType == "ChangeStateHome") {
updateResult.newState = BehaviorState::HOME_STATE;
} else if (nextActionType == "ChangeStateCircle") {
updateResult.newState = BehaviorState::CIRCLE_STATE;
} else if (nextActionType == "ChangeStateSquare") {
updateResult.newState = BehaviorState::SQUARE_STATE;
} else if (nextActionType == "ChangeStateDiamond") {
updateResult.newState = BehaviorState::DIAMOND_STATE;
} else if (nextActionType == "ChangeStateTriangle") {
updateResult.newState = BehaviorState::TRIANGLE_STATE;
} else if (nextActionType == "ChangeStateStar") {
updateResult.newState = BehaviorState::STAR_STATE;
}
/* END StateMachine*/
else {
static std::set<std::string> g_WarnedActions;
if (!g_WarnedActions.contains(nextActionType.data())) {
@@ -322,7 +347,7 @@ bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) {
return moveFinished;
}
void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) {
// No point in running a strip with only one action.
// Strips are also designed to have 2 actions or more to run.
if (!HasMinimumActions()) return;
@@ -346,9 +371,9 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
// Check for trigger blocks and if not a trigger block proc this blocks action
if (m_NextActionIndex == 0) {
LOG("Behavior strip started %s", nextAction.GetType().data());
if (nextAction.GetType() == "OnInteract") {
modelComponent.AddInteract();
} else if (nextAction.GetType() == "OnChat") {
// logic here if needed
} else if (nextAction.GetType() == "OnAttack") {
@@ -357,7 +382,7 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
Game::entityManager->SerializeEntity(entity);
m_WaitingForAction = true;
} else { // should be a normal block
ProcNormalAction(deltaTime, modelComponent);
ProcNormalAction(deltaTime, modelComponent, updateResult);
}
}

View File

@@ -12,6 +12,7 @@ namespace tinyxml2 {
class AMFArrayValue;
class ModelComponent;
struct UpdateResult;
class Strip {
public:
@@ -33,9 +34,9 @@ public:
// Checks the movement logic for whether or not to proceed
// Returns true if the movement can continue, false if it needs to wait more.
bool CheckMovement(float deltaTime, ModelComponent& modelComponent);
void Update(float deltaTime, ModelComponent& modelComponent);
void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
void SpawnDrop(LOT dropLOT, Entity& entity);
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent);
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
void RemoveStates(ModelComponent& modelComponent) const;
// 2 actions are required for strips to work

View File

@@ -93,17 +93,19 @@ std::map<LOT, LootDropInfo> RollLootMatrix(uint32_t matrixIndex) {
// Generates a 'random' final position for the loot drop based on its input spawn position.
void CalcFinalDropPos(GameMessages::DropClientLoot& lootMsg) {
lootMsg.bUsePosition = true;
if (lootMsg.spawnPos != NiPoint3Constant::ZERO) {
lootMsg.bUsePosition = true;
//Calculate where the loot will go:
uint16_t degree = GeneralUtils::GenerateRandomNumber<uint16_t>(0, 360);
//Calculate where the loot will go:
uint16_t degree = GeneralUtils::GenerateRandomNumber<uint16_t>(0, 360);
double rad = degree * 3.14 / 180;
double sin_v = sin(rad) * 4.2;
double cos_v = cos(rad) * 4.2;
double rad = degree * 3.14 / 180;
double sin_v = sin(rad) * 4.2;
double cos_v = cos(rad) * 4.2;
const auto [x, y, z] = lootMsg.spawnPos;
lootMsg.finalPosition = NiPoint3(static_cast<float>(x + sin_v), y, static_cast<float>(z + cos_v));
const auto [x, y, z] = lootMsg.spawnPos;
lootMsg.finalPosition = NiPoint3(static_cast<float>(x + sin_v), y, static_cast<float>(z + cos_v));
}
}
// Visually drop the loot to all team members, though only the lootMsg.ownerID can pick it up
@@ -353,7 +355,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDro
// Coin roll is divided up between the members, rounded up, then dropped for each player
const uint32_t coinRoll = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
const auto droppedCoins = team ? std::ceil(coinRoll / team->members.size()) : coinRoll;
const auto droppedCoins = team ? std::ceil(static_cast<float>(coinRoll) / team->members.size()) : coinRoll;
if (team) {
for (auto member : team->members) {
GameMessages::DropClientLoot lootMsg{};
@@ -363,6 +365,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDro
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
CalcFinalDropPos(lootMsg);
lootMsg.Send();
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
@@ -375,6 +378,7 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDro
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
CalcFinalDropPos(lootMsg);
lootMsg.Send();
lootMsg.Send(player->GetSystemAddress());
}

View File

@@ -1052,7 +1052,7 @@ void SlashCommandHandler::Startup() {
.info = "Resurrects the player",
.aliases = { "resurrect" },
.handle = GMZeroCommands::Resurrect,
.requiredLevel = eGameMasterLevel::CIVILIAN
.requiredLevel = eGameMasterLevel::DEVELOPER
};
RegisterCommand(ResurrectCommand);

View File

@@ -7,6 +7,7 @@
#include "eReplicaComponentType.h"
#include "RenderComponent.h"
#include "eTerminateType.h"
#include "Loot.h"
void GfTikiTorch::OnStartup(Entity* self) {
LightTorch(self);
@@ -22,7 +23,14 @@ void GfTikiTorch::OnUse(Entity* self, Entity* killer) {
self->SetI64(u"userID", killer->GetObjectID());
for (int i = 0; i < m_numspawn; i++) {
GameMessages::SendDropClientLoot(killer, self->GetObjectID(), 935, 0, self->GetPosition());
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = killer->GetObjectID();
lootMsg.ownerID = killer->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.item = 935;
lootMsg.count = 1;
lootMsg.spawnPos = self->GetPosition();
Loot::DropItem(*killer, lootMsg);
}
self->AddTimer("InteractionCooldown", 4);

View File

@@ -4,6 +4,7 @@
#include "GameMessages.h"
#include "SkillComponent.h"
#include "TeamManager.h"
#include "Loot.h"
void AgSurvivalBuffStation::OnQuickBuildComplete(Entity* self, Entity* target) {
auto destroyableComponent = self->GetComponent<DestroyableComponent>();
@@ -55,7 +56,14 @@ void AgSurvivalBuffStation::OnTimerDone(Entity* self, std::string timerName) {
for (auto memberID : team) {
auto member = Game::entityManager->GetEntity(memberID);
if (member != nullptr && !member->GetIsDead()) {
GameMessages::SendDropClientLoot(member, self->GetObjectID(), powerupToDrop, 0, self->GetPosition());
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = member->GetObjectID();
lootMsg.ownerID = member->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.item = powerupToDrop;
lootMsg.count = 1;
lootMsg.spawnPos = self->GetPosition();
Loot::DropItem(*member, lootMsg, true, true);
}
}
}

View File

@@ -5,6 +5,7 @@
#include "EntityInfo.h"
#include "DestroyableComponent.h"
#include "eReplicaComponentType.h"
#include "Loot.h"
void AgImagSmashable::OnDie(Entity* self, Entity* killer) {
bool maxImagGreaterThanZero = false;
@@ -18,7 +19,14 @@ void AgImagSmashable::OnDie(Entity* self, Entity* killer) {
if (maxImagGreaterThanZero) {
int amount = GeneralUtils::GenerateRandomNumber<int>(0, 3);
for (int i = 0; i < amount; ++i) {
GameMessages::SendDropClientLoot(killer, self->GetObjectID(), 935, 0, self->GetPosition());
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = killer->GetObjectID();
lootMsg.ownerID = killer->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.item = 935;
lootMsg.count = 1;
lootMsg.spawnPos = self->GetPosition();
Loot::DropItem(*killer, lootMsg);
}
}
}

View File

@@ -55,36 +55,20 @@ void GfBanana::OnHit(Entity* self, Entity* attacker) {
return;
}
bananaEntity->Smash(LWOOBJID_EMPTY, eKillType::SILENT);
bananaEntity->SetPosition(bananaEntity->GetPosition() - NiPoint3Constant::UNIT_Y * 8);
auto* bananaDestroyable = bananaEntity->GetComponent<DestroyableComponent>();
bananaDestroyable->SetHealth(0);
bananaDestroyable->Smash(attacker->GetObjectID());
/*
auto position = self->GetPosition();
const auto rotation = self->GetRotation();
position.y += 12;
position.x -= rotation.GetRightVector().x * 5;
position.z -= rotation.GetRightVector().z * 5;
EntityInfo info {};
info.pos = position;
info.rot = rotation;
EntityInfo info{};
info.lot = 6718;
info.pos = self->GetPosition();
info.pos.y += 12;
info.pos.x -= QuatUtils::Right(rotation).x * 5;
info.pos.z -= QuatUtils::Right(rotation).z * 5;
info.rot = rotation;
info.spawnerID = self->GetObjectID();
auto* entity = Game::entityManager->CreateEntity(info);
Game::entityManager->ConstructEntity(entity, UNASSIGNED_SYSTEM_ADDRESS);
*/
Game::entityManager->SerializeEntity(self);
info.settings = { new LDFData<uint32_t>(u"motionType", 5) };
auto* const newEn = Game::entityManager->CreateEntity(info, nullptr, self);
Game::entityManager->ConstructEntity(newEn);
}
void GfBanana::OnTimerDone(Entity* self, std::string timerName) {

View File

@@ -1,5 +1,9 @@
#include "GfBananaCluster.h"
#include "Entity.h"
#include "dpWorld.h"
#include "dNavMesh.h"
#include "Loot.h"
#include "DestroyableComponent.h"
void GfBananaCluster::OnStartup(Entity* self) {
self->AddTimer("startup", 100);
@@ -10,3 +14,21 @@ void GfBananaCluster::OnTimerDone(Entity* self, std::string timerName) {
self->ScheduleKillAfterUpdate(nullptr);
}
}
// Hack in banana loot dropping from tree area since it seemed to do that in live for some reason
void GfBananaCluster::OnHit(Entity* self, Entity* attacker) {
auto* parentEntity = self->GetParentEntity();
GameMessages::GetPosition posMsg{};
if (parentEntity) {
posMsg.target = parentEntity->GetObjectID();
}
posMsg.Send();
const auto rotation = parentEntity ? parentEntity->GetRotation() : self->GetRotation();
if (dpWorld::GetNavMesh()) posMsg.pos.y = dpWorld::GetNavMesh()->GetHeightAtPoint(posMsg.pos) + 3.0f;
else posMsg.pos = posMsg.pos - (NiPoint3Constant::UNIT_Y * 8);
posMsg.pos.x -= QuatUtils::Right(rotation).x * 5;
posMsg.pos.z -= QuatUtils::Right(rotation).z * 5;
self->SetPosition(posMsg.pos);
}

View File

@@ -7,4 +7,5 @@ public:
void OnStartup(Entity* self) override;
void OnTimerDone(Entity* self, std::string timerName) override;
void OnHit(Entity* self, Entity* attacker) override;
};

View File

@@ -1,6 +1,7 @@
#include "NsQbImaginationStatue.h"
#include "EntityManager.h"
#include "GameMessages.h"
#include "Loot.h"
void NsQbImaginationStatue::OnStartup(Entity* self) {
@@ -35,6 +36,12 @@ void NsQbImaginationStatue::SpawnLoot(Entity* self) {
if (player == nullptr) return;
GameMessages::SendDropClientLoot(player, self->GetObjectID(), 935, 0);
GameMessages::SendDropClientLoot(player, self->GetObjectID(), 935, 0);
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = player->GetObjectID();
lootMsg.ownerID = player->GetObjectID();
lootMsg.sourceID = self->GetObjectID();
lootMsg.item = 935;
lootMsg.count = 1;
Loot::DropItem(*player, lootMsg);
Loot::DropItem(*player, lootMsg);
}