mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Live sends used_mouse false, no caster latency, no clicked position and a zero originator rotation (present, all four components 0) in every echo: 78,841 of 78,841 in the live captures, player and server casts alike. DLU forwarded the casting client's values and wrote the caster's facing for server casts. The client copies the rotation into its local StartSkill (LWOSkillComponent::msgEchoStartSkill), which is why a real rotation made power-up pickups jitter; the per-call override that worked around that is no longer needed and is removed. Check: with two players, attack, use a thrown or ranged item and pick up power-ups; the other player sees the same animations and the caster no longer snaps or jitters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
568 lines
16 KiB
C++
568 lines
16 KiB
C++
/*
|
|
* Darkflame Universe
|
|
* Copyright 2018
|
|
*/
|
|
|
|
#include "SkillComponent.h"
|
|
#include "GeneralUtils.h"
|
|
#include "tinyxml2.h"
|
|
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#include "BehaviorContext.h"
|
|
#include "BehaviorBranchContext.h"
|
|
#include "Behavior.h"
|
|
#include "CDClientDatabase.h"
|
|
#include "dServer.h"
|
|
#include "EntityManager.h"
|
|
#include "Game.h"
|
|
#include "BitStreamUtils.h"
|
|
#include "BaseCombatAIComponent.h"
|
|
#include "ScriptComponent.h"
|
|
#include "BuffComponent.h"
|
|
#include "SkillMessages.h"
|
|
#include "CDClientManager.h"
|
|
#include "CDSkillBehaviorTable.h"
|
|
#include "ServiceType.h"
|
|
#include "MessageType/Client.h"
|
|
|
|
ProjectileSyncEntry::ProjectileSyncEntry() {
|
|
}
|
|
|
|
std::unordered_map<uint32_t, uint32_t> SkillComponent::m_skillBehaviorCache = {};
|
|
|
|
bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t skillUid, RakNet::BitStream& bitStream, const LWOOBJID target, uint32_t skillID) {
|
|
auto* context = new BehaviorContext(this->m_Parent->GetObjectID());
|
|
|
|
context->caster = m_Parent->GetObjectID();
|
|
|
|
context->skillID = skillID;
|
|
|
|
this->m_managedBehaviors.insert({ skillUid, context });
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
const auto branch = BehaviorBranchContext(target, 0);
|
|
|
|
behavior->Handle(context, bitStream, branch);
|
|
|
|
context->ExecuteUpdates();
|
|
|
|
if (!context->failed && m_Parent->IsPlayer()) StartCooldown(skillID);
|
|
|
|
return !context->failed;
|
|
}
|
|
|
|
void SkillComponent::StartCooldown(const uint32_t skillID) {
|
|
const auto& skill = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(skillID);
|
|
// The client keys cooldown groups 0 and up (a null group is -1); skills without a cooldown have none running
|
|
const auto group = static_cast<int32_t>(skill.cooldowngroup);
|
|
if (group < 0 || skill.cooldown <= 0.0f) return;
|
|
auto& left = m_CooldownGroups[group];
|
|
left = std::max(left, skill.cooldown);
|
|
}
|
|
|
|
void SkillComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
|
|
m_CooldownGroups.clear();
|
|
const auto* const obj = doc.FirstChildElement("obj");
|
|
const auto* const skil = obj ? obj->FirstChildElement("skil") : nullptr;
|
|
const char* const cooldowns = skil ? skil->Attribute("sc") : nullptr;
|
|
if (!cooldowns) return;
|
|
|
|
// Alternating group and seconds, split on ';' and ':' as the client does
|
|
const auto tokens = GeneralUtils::SplitString(std::string(cooldowns), ';');
|
|
for (const auto& entry : tokens) {
|
|
const auto pair = GeneralUtils::SplitString(entry, ':');
|
|
if (pair.size() != 2) continue;
|
|
const auto group = GeneralUtils::TryParse<int32_t>(pair[0]);
|
|
const auto left = GeneralUtils::TryParse<float>(pair[1]);
|
|
if (group && left && *left > 0.0f) m_CooldownGroups[*group] = *left;
|
|
}
|
|
}
|
|
|
|
void SkillComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
|
|
auto* const obj = doc.FirstChildElement("obj");
|
|
if (!obj) return;
|
|
auto* skil = obj->FirstChildElement("skil");
|
|
if (!skil) skil = obj->InsertNewChildElement("skil");
|
|
|
|
// Live: <skil/> with nothing running, else e.g. <skil sc="17:15.6958;"/>
|
|
std::ostringstream cooldowns;
|
|
for (const auto& [group, left] : m_CooldownGroups) cooldowns << group << ':' << left << ';';
|
|
if (cooldowns.view().empty()) skil->DeleteAttribute("sc");
|
|
else skil->SetAttribute("sc", cooldowns.str().c_str());
|
|
}
|
|
|
|
void SkillComponent::SyncPlayerSkill(const uint32_t skillUid, const uint32_t syncId, RakNet::BitStream& bitStream) {
|
|
const auto index = this->m_managedBehaviors.equal_range(skillUid);
|
|
|
|
if (index.first == this->m_managedBehaviors.end()) {
|
|
LOG("Failed to find skill with uid (%i)!", skillUid, syncId);
|
|
|
|
return;
|
|
}
|
|
|
|
bool foundSyncId = false;
|
|
for (auto it = index.first; it != index.second && !foundSyncId; ++it) {
|
|
const auto& context = it->second;
|
|
|
|
foundSyncId = context->SyncBehavior(syncId, bitStream);
|
|
}
|
|
|
|
if (!foundSyncId) {
|
|
LOG("Failed to find sync id (%i) for skill with uid (%i)!", syncId, skillUid);
|
|
}
|
|
}
|
|
|
|
|
|
void SkillComponent::SyncPlayerProjectile(const LWOOBJID projectileId, RakNet::BitStream& bitStream, const LWOOBJID target) {
|
|
auto index = -1;
|
|
|
|
for (auto i = 0u; i < this->m_managedProjectiles.size(); ++i) {
|
|
const auto& projectile = this->m_managedProjectiles.at(i);
|
|
|
|
if (projectile.id == projectileId) {
|
|
index = i;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index == -1) {
|
|
LOG("Failed to find projectile id (%llu)!", projectileId);
|
|
|
|
return;
|
|
}
|
|
|
|
const auto sync_entry = this->m_managedProjectiles.at(index);
|
|
|
|
auto query = CDClientDatabase::CreatePreppedStmt(
|
|
"SELECT behaviorID FROM SkillBehavior WHERE skillID = (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
|
|
query.bind(1, static_cast<int>(sync_entry.lot));
|
|
|
|
auto result = query.execQuery();
|
|
|
|
if (result.eof()) {
|
|
LOG("Failed to find skill id for (%i)!", sync_entry.lot);
|
|
|
|
return;
|
|
}
|
|
|
|
const auto behavior_id = static_cast<uint32_t>(result.getIntField("behaviorID"));
|
|
|
|
result.finalize();
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behavior_id);
|
|
|
|
auto branch = sync_entry.branchContext;
|
|
|
|
branch.isProjectile = true;
|
|
|
|
if (target != LWOOBJID_EMPTY) {
|
|
branch.target = target;
|
|
}
|
|
|
|
behavior->Handle(sync_entry.context, bitStream, branch);
|
|
|
|
this->m_managedProjectiles.erase(this->m_managedProjectiles.begin() + index);
|
|
|
|
GameMessages::ActivityNotify notify;
|
|
notify.notification.Insert<int32_t>(u"shot_done", sync_entry.skillId);
|
|
m_Parent->OnActivityNotify(notify);
|
|
}
|
|
|
|
void SkillComponent::RegisterPlayerProjectile(const LWOOBJID projectileId, BehaviorContext* context, const BehaviorBranchContext& branch, const LOT lot) {
|
|
ProjectileSyncEntry entry;
|
|
|
|
entry.context = context;
|
|
entry.branchContext = branch;
|
|
entry.lot = lot;
|
|
entry.id = projectileId;
|
|
entry.skillId = context->skillID;
|
|
|
|
this->m_managedProjectiles.push_back(entry);
|
|
}
|
|
|
|
void SkillComponent::Update(const float deltaTime) {
|
|
for (auto it = m_CooldownGroups.begin(); it != m_CooldownGroups.end();) {
|
|
it->second -= deltaTime;
|
|
it = it->second <= 0.0f ? m_CooldownGroups.erase(it) : std::next(it);
|
|
}
|
|
|
|
if (!m_Parent->HasComponent(eReplicaComponentType::BASE_COMBAT_AI) && m_Parent->GetLOT() != 1) {
|
|
CalculateUpdate(deltaTime);
|
|
}
|
|
|
|
if (m_Parent->IsPlayer()) {
|
|
for (const auto& pair : this->m_managedBehaviors) pair.second->UpdatePlayerSyncs(deltaTime);
|
|
}
|
|
|
|
std::multimap<uint32_t, BehaviorContext*> keep{};
|
|
|
|
for (const auto& pair : this->m_managedBehaviors) {
|
|
auto* context = pair.second;
|
|
|
|
if (context == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
if (context->clientInitalized) {
|
|
context->CalculateUpdate(deltaTime);
|
|
} else {
|
|
context->Update(deltaTime);
|
|
}
|
|
|
|
// Cleanup old behaviors
|
|
if (context->syncEntries.empty() && context->timerEntries.empty()) {
|
|
auto any = false;
|
|
|
|
for (const auto& projectile : this->m_managedProjectiles) {
|
|
if (projectile.context == context) {
|
|
any = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!any) {
|
|
context->Reset();
|
|
|
|
delete context;
|
|
|
|
context = nullptr;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
keep.insert({ pair.first, context });
|
|
}
|
|
|
|
this->m_managedBehaviors = keep;
|
|
}
|
|
|
|
void SkillComponent::Reset() {
|
|
for (const auto& behavior : this->m_managedBehaviors) {
|
|
delete behavior.second;
|
|
}
|
|
|
|
this->m_managedProjectiles.clear();
|
|
this->m_managedBehaviors.clear();
|
|
}
|
|
|
|
void SkillComponent::EndSkill(const uint32_t skillUid) {
|
|
const auto [begin, end] = m_managedBehaviors.equal_range(skillUid);
|
|
for (auto it = begin; it != end; ++it) {
|
|
auto* const context = it->second;
|
|
if (!context) continue;
|
|
std::erase_if(m_managedProjectiles, [context](const ProjectileSyncEntry& projectile) { return projectile.context == context; });
|
|
// End what the skill started, but drop its pending timers (Reset would run them now: a late hit)
|
|
for (const auto& entry : context->endEntries) entry.behavior->End(context, entry.branchContext, entry.second);
|
|
context->endEntries.clear();
|
|
context->timerEntries.clear();
|
|
context->syncEntries.clear();
|
|
context->scheduledUpdates.clear();
|
|
delete context;
|
|
}
|
|
m_managedBehaviors.erase(begin, end);
|
|
}
|
|
|
|
void SkillComponent::Interrupt() {
|
|
// TODO: need to check immunities on the destroyable component, but they aren't implemented
|
|
auto* combat = m_Parent->GetComponent<BaseCombatAIComponent>();
|
|
if (combat != nullptr && combat->GetStunImmune()) return;
|
|
|
|
for (const auto& behavior : this->m_managedBehaviors) {
|
|
for (const auto& behaviorEndEntry : behavior.second->endEntries) {
|
|
behaviorEndEntry.behavior->End(behavior.second, behaviorEndEntry.branchContext, behaviorEndEntry.second);
|
|
}
|
|
behavior.second->endEntries.clear();
|
|
if (m_Parent->IsPlayer()) continue;
|
|
behavior.second->Interrupt();
|
|
}
|
|
|
|
}
|
|
|
|
void SkillComponent::RegisterCalculatedProjectile(const LWOOBJID projectileId, BehaviorContext* context, const BehaviorBranchContext& branch, const LOT lot, const float maxTime,
|
|
const NiPoint3& startPosition, const NiPoint3& velocity, const bool trackTarget, const float trackRadius) {
|
|
ProjectileSyncEntry entry;
|
|
|
|
entry.context = context;
|
|
entry.branchContext = branch;
|
|
entry.lot = lot;
|
|
entry.calculation = true;
|
|
entry.time = 0;
|
|
entry.maxTime = maxTime;
|
|
entry.id = projectileId;
|
|
entry.startPosition = startPosition;
|
|
entry.lastPosition = startPosition;
|
|
entry.velocity = velocity;
|
|
entry.trackTarget = trackTarget;
|
|
entry.trackRadius = trackRadius;
|
|
|
|
this->m_managedProjectiles.push_back(entry);
|
|
}
|
|
|
|
bool SkillComponent::CastSkill(const uint32_t skillId, LWOOBJID target, const LWOOBJID optionalOriginatorID, const int32_t castType) {
|
|
uint32_t behaviorId = -1;
|
|
// try to find it via the cache
|
|
const auto& pair = m_skillBehaviorCache.find(skillId);
|
|
|
|
// if it's not in the cache look it up and cache it
|
|
if (pair == m_skillBehaviorCache.end()) {
|
|
auto skillTable = CDClientManager::GetTable<CDSkillBehaviorTable>();
|
|
behaviorId = skillTable->GetSkillByID(skillId).behaviorID;
|
|
m_skillBehaviorCache.insert_or_assign(skillId, behaviorId);
|
|
} else {
|
|
behaviorId = pair->second;
|
|
}
|
|
|
|
// check to see if we got back a valid behavior
|
|
if (behaviorId == -1) {
|
|
LOG_DEBUG("Tried to cast skill %i but found no behavior", skillId);
|
|
return false;
|
|
}
|
|
|
|
return CalculateBehavior(skillId, behaviorId, target, false, false, optionalOriginatorID, castType).success;
|
|
}
|
|
|
|
|
|
SkillExecutionResult SkillComponent::CalculateBehavior(
|
|
const uint32_t skillId,
|
|
const uint32_t behaviorId,
|
|
const LWOOBJID target,
|
|
const bool ignoreTarget,
|
|
const bool clientInitalized,
|
|
const LWOOBJID originatorOverride,
|
|
const int32_t castType) {
|
|
RakNet::BitStream bitStream{};
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
auto* context = new BehaviorContext(originatorOverride != LWOOBJID_EMPTY ? originatorOverride : this->m_Parent->GetObjectID(), true);
|
|
|
|
context->caster = m_Parent->GetObjectID();
|
|
|
|
context->skillID = skillId;
|
|
|
|
context->clientInitalized = clientInitalized;
|
|
|
|
context->foundTarget = target != LWOOBJID_EMPTY || ignoreTarget || clientInitalized;
|
|
|
|
behavior->Calculate(context, bitStream, { target, 0 });
|
|
|
|
m_Parent->GetScript()->OnSkillCast(m_Parent, skillId);
|
|
|
|
if (!context->foundTarget) {
|
|
delete context;
|
|
|
|
// Invalid attack
|
|
return { false, 0 };
|
|
}
|
|
|
|
this->m_managedBehaviors.insert({ context->skillUId, context });
|
|
|
|
if (!clientInitalized) {
|
|
// Echo start skill
|
|
GameMessages::EchoStartSkill start;
|
|
start.target = this->m_Parent->GetObjectID();
|
|
|
|
start.iCastType = castType;
|
|
start.skillID = skillId;
|
|
start.uiSkillHandle = context->skillUId;
|
|
start.optionalOriginatorID = context->originator;
|
|
start.optionalTargetID = target;
|
|
|
|
start.originatorRot = GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT;
|
|
//start.optionalTargetID = target;
|
|
|
|
start.sBitStream.assign(reinterpret_cast<char*>(bitStream.GetData()), bitStream.GetNumberOfBytesUsed());
|
|
|
|
start.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
context->ExecuteUpdates();
|
|
|
|
// Valid attack
|
|
return { true, context->skillTime };
|
|
}
|
|
|
|
void SkillComponent::CalculateUpdate(const float deltaTime) {
|
|
if (this->m_managedBehaviors.empty())
|
|
return;
|
|
|
|
for (const auto& managedBehavior : this->m_managedBehaviors) {
|
|
if (managedBehavior.second == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
managedBehavior.second->CalculateUpdate(deltaTime);
|
|
}
|
|
|
|
for (auto& managedProjectile : this->m_managedProjectiles) {
|
|
auto entry = managedProjectile;
|
|
|
|
if (!entry.calculation) continue;
|
|
|
|
entry.time += deltaTime;
|
|
|
|
auto* origin = Game::entityManager->GetEntity(entry.context->originator);
|
|
|
|
if (origin == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
const auto targets = origin->GetTargetsInPhantom();
|
|
|
|
const auto position = entry.startPosition + (entry.velocity * entry.time);
|
|
|
|
for (const auto& targetId : targets) {
|
|
auto* target = Game::entityManager->GetEntity(targetId);
|
|
|
|
const auto targetPosition = target->GetPosition();
|
|
|
|
const auto closestPoint = Vector3::ClosestPointOnLine(entry.lastPosition, position, targetPosition);
|
|
|
|
const auto distance = Vector3::DistanceSquared(targetPosition, closestPoint);
|
|
|
|
if (distance > 3 * 3) {
|
|
// TODO There is supposed to be an implementation for homing projectiles here
|
|
continue;
|
|
}
|
|
|
|
entry.branchContext.target = targetId;
|
|
|
|
SyncProjectileCalculation(entry);
|
|
|
|
entry.time = entry.maxTime;
|
|
|
|
break;
|
|
}
|
|
|
|
entry.lastPosition = position;
|
|
|
|
managedProjectile = entry;
|
|
}
|
|
|
|
std::vector<ProjectileSyncEntry> valid;
|
|
|
|
for (auto& entry : this->m_managedProjectiles) {
|
|
if (entry.calculation) {
|
|
if (entry.time >= entry.maxTime) {
|
|
entry.branchContext.target = LWOOBJID_EMPTY;
|
|
|
|
SyncProjectileCalculation(entry);
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
valid.push_back(entry);
|
|
}
|
|
|
|
this->m_managedProjectiles = valid;
|
|
}
|
|
|
|
|
|
void SkillComponent::SyncProjectileCalculation(const ProjectileSyncEntry& entry) const {
|
|
auto* other = Game::entityManager->GetEntity(entry.branchContext.target);
|
|
|
|
if (other == nullptr) {
|
|
if (entry.branchContext.target != LWOOBJID_EMPTY) {
|
|
LOG("Invalid projectile target (%llu)!", entry.branchContext.target);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
auto query = CDClientDatabase::CreatePreppedStmt(
|
|
"SELECT behaviorID FROM SkillBehavior WHERE skillID = (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
|
|
query.bind(1, static_cast<int>(entry.lot));
|
|
auto result = query.execQuery();
|
|
|
|
if (result.eof()) {
|
|
LOG("Failed to find skill id for (%i)!", entry.lot);
|
|
|
|
return;
|
|
}
|
|
|
|
const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID"));
|
|
|
|
result.finalize();
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
RakNet::BitStream bitStream{};
|
|
|
|
behavior->Calculate(entry.context, bitStream, entry.branchContext);
|
|
|
|
GameMessages::DoClientProjectileImpact projectileImpact;
|
|
projectileImpact.target = this->m_Parent->GetObjectID();
|
|
|
|
projectileImpact.sBitStream.assign(reinterpret_cast<char*>(bitStream.GetData()), bitStream.GetNumberOfBytesUsed());
|
|
projectileImpact.i64OwnerID = this->m_Parent->GetObjectID();
|
|
projectileImpact.i64OrgID = entry.id;
|
|
projectileImpact.i64TargetID = entry.branchContext.target;
|
|
|
|
projectileImpact.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
entry.context->ExecuteUpdates();
|
|
}
|
|
|
|
void SkillComponent::HandleUnmanaged(const uint32_t behaviorId, const LWOOBJID target, LWOOBJID source) {
|
|
BehaviorContext context{ source };
|
|
|
|
context.unmanaged = true;
|
|
context.caster = target;
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
RakNet::BitStream bitStream{};
|
|
|
|
behavior->Handle(&context, bitStream, { target });
|
|
}
|
|
|
|
void SkillComponent::CalculateUnmanaged(const uint32_t behaviorId, const LWOOBJID caster, const LWOOBJID target) {
|
|
BehaviorContext context{ caster, true };
|
|
|
|
context.unmanaged = true;
|
|
context.caster = caster;
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
RakNet::BitStream bitStream{};
|
|
|
|
behavior->Calculate(&context, bitStream, { target });
|
|
}
|
|
|
|
void SkillComponent::HandleUnCast(const uint32_t behaviorId, const LWOOBJID target) {
|
|
BehaviorContext context{ target };
|
|
|
|
context.caster = target;
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
behavior->UnCast(&context, { target });
|
|
}
|
|
|
|
SkillComponent::SkillComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
|
this->m_skillUid = 0;
|
|
}
|
|
|
|
SkillComponent::~SkillComponent() {
|
|
Reset();
|
|
}
|
|
|
|
void SkillComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
if (bIsInitialUpdate) outBitStream.Write0();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a unique skill ID for syncing behaviors to the client
|
|
/// </summary>
|
|
/// <returns>Unique skill ID</returns>
|
|
uint32_t SkillComponent::GetUniqueSkillId() {
|
|
return ++this->m_skillUid;
|
|
}
|