mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
A trigger's SetPhysicsVolumeEffect is "type,amount,x,y,z,use distance,
min,max". In live phantom physics constructions:
- the direction has the amount as its length: "Push,50,0.91,-0.34,-0.22"
went out as 45.68, -17.07, -11.04. DLU multiplied the arguments as
they were, so a direction that isn't a unit vector pushed harder;
- a zero direction ("Repulse,150,0,0,0,true,25,100") is no direction;
- the distances go out when use distance is "true" ("true,150,175" as 1
and 175, "true,20,50" as 1 and 50) and not when it is "1"
("Push,40.0,0,1,0,1,20,50"). DLU never wrote them.
LWOPhantomPhysicsComponent::Deserialize (0x00c34db0) reads the two
distances as floats and keeps the least of the min and 1, and a max of at
least that.
Seven arguments no longer read an eighth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
340 lines
11 KiB
C++
340 lines
11 KiB
C++
/*
|
|
* Darkflame Universe
|
|
* Copyright 2018
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <iostream>
|
|
|
|
#include "PhantomPhysicsComponent.h"
|
|
#include "Game.h"
|
|
#include "LDFFormat.h"
|
|
#include "Logger.h"
|
|
#include "Entity.h"
|
|
#include "EntityManager.h"
|
|
#include "ControllablePhysicsComponent.h"
|
|
#include "GameMessages.h"
|
|
#include "MovementMessages.h"
|
|
#include "ePhysicsEffectType.h"
|
|
|
|
#include "CDClientManager.h"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDPhysicsComponentTable.h"
|
|
#include "dServer.h"
|
|
#include "EntityInfo.h"
|
|
#include "Amf3.h"
|
|
|
|
#include "dpWorld.h"
|
|
#include "dpEntity.h"
|
|
#include "dpShapeBox.h"
|
|
#include "dpShapeSphere.h"
|
|
#include "ObjectLoadState.h"
|
|
|
|
PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) {
|
|
RegisterMsg(&PhantomPhysicsComponent::OnGetObjectReportInfo);
|
|
|
|
m_Position = m_Parent->GetDefaultPosition();
|
|
m_Rotation = m_Parent->GetDefaultRotation();
|
|
m_Scale = m_Parent->GetDefaultScale();
|
|
m_dpEntity = nullptr;
|
|
|
|
m_EffectInfoDirty = false;
|
|
|
|
m_IsPhysicsEffectActive = false;
|
|
m_EffectType = ePhysicsEffectType::PUSH;
|
|
m_DirectionalMultiplier = 0.0f;
|
|
|
|
m_MinMax = false;
|
|
m_Min = 0;
|
|
m_Max = 1;
|
|
|
|
m_IsDirectional = false;
|
|
m_Direction = NiPoint3(); // * m_DirectionalMultiplier
|
|
}
|
|
|
|
// After the physics config: the volume's blocker, respawn settings and its physics
|
|
void PhantomPhysicsComponent::LoadConfigData() {
|
|
PhysicsComponent::LoadConfigData();
|
|
|
|
// A phantom volume enemies can't walk into when it carves the navmesh (e.g. the Sentinel camp walls)
|
|
RegisterMovementBlocker(ComponentType, false, m_Scale);
|
|
|
|
if (m_Parent->GetVar<bool>(u"create_physics")) {
|
|
m_dpEntity = CreatePhysicsLnv(m_Scale, ComponentType);
|
|
}
|
|
|
|
if (m_Parent->GetVar<bool>(u"respawnVol")) {
|
|
m_IsRespawnVolume = true;
|
|
}
|
|
|
|
if (m_IsRespawnVolume) {
|
|
const auto respawnPos = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspPos"), '\x1f');
|
|
m_RespawnPos = GeneralUtils::TryParse(respawnPos, NiPoint3Constant::ZERO);
|
|
|
|
const auto respawnRot = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"rspRot"), '\x1f');
|
|
m_RespawnRot = respawnRot.size() >= 4 ? NiQuaternion(
|
|
GeneralUtils::TryParse(respawnRot[0], 1.0f),
|
|
GeneralUtils::TryParse(respawnRot[1], 0.0f),
|
|
GeneralUtils::TryParse(respawnRot[2], 0.0f),
|
|
GeneralUtils::TryParse(respawnRot[3], 0.0f))
|
|
: QuatUtils::IDENTITY;
|
|
}
|
|
|
|
// HF - RespawnPoints. Legacy respawn entity.
|
|
if (m_Parent->GetLOT() == 4945) {
|
|
m_IsRespawnVolume = true;
|
|
m_RespawnPos = m_Position;
|
|
m_RespawnRot = m_Rotation;
|
|
}
|
|
|
|
if (!m_dpEntity) {
|
|
m_dpEntity = CreatePhysicsEntity(ComponentType);
|
|
if (!m_dpEntity) return;
|
|
m_dpEntity->SetScale(m_Scale);
|
|
m_dpEntity->SetRotation(m_Rotation);
|
|
m_dpEntity->SetPosition(m_Position);
|
|
dpWorld::AddEntity(m_dpEntity);
|
|
}
|
|
}
|
|
|
|
void ApplyCollisionEffect(const LWOOBJID& target, const ePhysicsEffectType effectType, const float effectScale);
|
|
|
|
PhantomPhysicsComponent::~PhantomPhysicsComponent() {
|
|
if (!m_dpEntity) return;
|
|
|
|
if (m_PhysicsActive) dpWorld::RemoveEntity(m_dpEntity);
|
|
else delete m_dpEntity;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetPhysicsActive(const bool active) {
|
|
if (!m_dpEntity || m_PhysicsActive == active) return;
|
|
m_PhysicsActive = active;
|
|
|
|
if (active) {
|
|
// Anything already inside enters on the next physics step
|
|
dpWorld::AddEntity(m_dpEntity);
|
|
return;
|
|
}
|
|
|
|
dpWorld::DetachEntity(m_dpEntity);
|
|
const std::vector<LWOOBJID> inside(m_dpEntity->GetCurrentlyCollidingObjects().begin(), m_dpEntity->GetCurrentlyCollidingObjects().end());
|
|
m_dpEntity->ClearCollisions();
|
|
for (const auto id : inside) {
|
|
ApplyCollisionEffect(id, m_EffectType, 1.0f);
|
|
m_Parent->OnCollisionLeavePhantom(id);
|
|
}
|
|
}
|
|
|
|
void PhantomPhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
PhysicsComponent::Serialize(outBitStream, bIsInitialUpdate);
|
|
|
|
outBitStream.Write(m_EffectInfoDirty || bIsInitialUpdate);
|
|
if (m_EffectInfoDirty || bIsInitialUpdate) {
|
|
outBitStream.Write(m_IsPhysicsEffectActive);
|
|
|
|
if (m_IsPhysicsEffectActive) {
|
|
outBitStream.Write(m_EffectType);
|
|
outBitStream.Write(m_DirectionalMultiplier);
|
|
|
|
// Live wrote the distances as the client keeps them: "true,150,175" went out as 1 and 175
|
|
outBitStream.Write(m_MinMax);
|
|
if (m_MinMax) {
|
|
const float min = std::min(m_Min, 1.0f);
|
|
outBitStream.Write(min);
|
|
outBitStream.Write(std::max(m_Max, min));
|
|
}
|
|
|
|
outBitStream.Write(m_IsDirectional);
|
|
if (m_IsDirectional) {
|
|
outBitStream.Write(m_Direction.x);
|
|
outBitStream.Write(m_Direction.y);
|
|
outBitStream.Write(m_Direction.z);
|
|
}
|
|
}
|
|
|
|
m_EffectInfoDirty = false;
|
|
}
|
|
}
|
|
|
|
// Even if we were to implement Friction server side,
|
|
// it also defaults to 1.0f in the last argument, so we dont need two functions to do the same thing.
|
|
void ApplyCollisionEffect(const LWOOBJID& target, const ePhysicsEffectType effectType, const float effectScale) {
|
|
switch (effectType) {
|
|
case ePhysicsEffectType::GRAVITY_SCALE: {
|
|
auto* targetEntity = Game::entityManager->GetEntity(target);
|
|
if (targetEntity) {
|
|
auto* controllablePhysicsComponent = targetEntity->GetComponent<ControllablePhysicsComponent>();
|
|
// dont want to apply an effect to nothing.
|
|
if (!controllablePhysicsComponent) return;
|
|
controllablePhysicsComponent->SetGravityScale(effectScale);
|
|
GameMessages::SetGravityScale gravityScale;
|
|
gravityScale.target = target;
|
|
gravityScale.scale = effectScale;
|
|
gravityScale.SendToClient(targetEntity->GetSystemAddress());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ePhysicsEffectType::ATTRACT:
|
|
case ePhysicsEffectType::FRICTION:
|
|
case ePhysicsEffectType::PUSH:
|
|
case ePhysicsEffectType::REPULSE:
|
|
default:
|
|
break;
|
|
}
|
|
// The other types are not handled by the server and are here to handle all cases of the enum.
|
|
}
|
|
|
|
void PhantomPhysicsComponent::Run(float deltaTime) {
|
|
if (!m_dpEntity || !m_PhysicsActive) return;
|
|
|
|
//Process enter events
|
|
for (const auto id : m_dpEntity->GetNewObjects()) {
|
|
ApplyCollisionEffect(id, m_EffectType, m_DirectionalMultiplier);
|
|
m_Parent->OnCollisionPhantom(id);
|
|
|
|
//If we are a respawn volume, inform the client:
|
|
if (m_IsRespawnVolume) {
|
|
auto* const entity = Game::entityManager->GetEntity(id);
|
|
|
|
if (entity) {
|
|
GameMessages::PlayerReachedRespawnCheckpoint respawnCheckpoint;
|
|
respawnCheckpoint.target = entity->GetObjectID();
|
|
respawnCheckpoint.pos = m_RespawnPos;
|
|
respawnCheckpoint.rot = m_RespawnRot;
|
|
respawnCheckpoint.SendToClient(entity->GetSystemAddress());
|
|
entity->SetRespawnPos(m_RespawnPos);
|
|
entity->SetRespawnRot(m_RespawnRot);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Process exit events
|
|
for (const auto id : m_dpEntity->GetRemovedObjects()) {
|
|
ApplyCollisionEffect(id, m_EffectType, 1.0f);
|
|
m_Parent->OnCollisionLeavePhantom(id);
|
|
}
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetDirection(const NiPoint3& pos) {
|
|
m_Direction = pos;
|
|
m_Direction.x *= m_DirectionalMultiplier;
|
|
m_Direction.y *= m_DirectionalMultiplier;
|
|
m_Direction.z *= m_DirectionalMultiplier;
|
|
|
|
m_EffectInfoDirty = true;
|
|
m_IsDirectional = true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SpawnVertices() const {
|
|
if (!m_dpEntity) {
|
|
LOG("No dpEntity to spawn vertices for %llu:%i", m_Parent->GetObjectID(), m_Parent->GetLOT());
|
|
return;
|
|
}
|
|
PhysicsComponent::SpawnVertices(m_dpEntity);
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetDirectionalMultiplier(float mul) {
|
|
m_DirectionalMultiplier = mul;
|
|
m_EffectInfoDirty = true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetEffectType(ePhysicsEffectType type) {
|
|
m_EffectType = type;
|
|
m_EffectInfoDirty = true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetMin(const float min) {
|
|
m_Min = min;
|
|
m_MinMax = true;
|
|
m_EffectInfoDirty = true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetMax(const float max) {
|
|
m_Max = max;
|
|
m_MinMax = true;
|
|
m_EffectInfoDirty = true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetVolumeEffect(std::vector<std::string> args) {
|
|
for (auto& arg : args) {
|
|
arg.erase(0, arg.find_first_not_of(' '));
|
|
arg.erase(arg.find_last_not_of(' ') + 1);
|
|
}
|
|
|
|
SetPhysicsEffectActive(true);
|
|
ePhysicsEffectType effectType = ePhysicsEffectType::PUSH;
|
|
if (!args.empty()) {
|
|
std::transform(args.at(0).begin(), args.at(0).end(), args.at(0).begin(), ::tolower);
|
|
if (args.at(0) == "push") effectType = ePhysicsEffectType::PUSH;
|
|
else if (args.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT;
|
|
else if (args.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE;
|
|
else if (args.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE;
|
|
else if (args.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION;
|
|
}
|
|
|
|
SetEffectType(effectType);
|
|
if (args.size() > 1) SetDirectionalMultiplier(GeneralUtils::TryParse(args.at(1), 0.0f));
|
|
if (args.size() > 4) {
|
|
// Live sent the direction with the amount as its length ("Push,50,0.91,-0.34,-0.22" as 45.68, -17.07, -11.04)
|
|
// and none when it was zero ("Repulse,150,0,0,0,true,25,100")
|
|
const NiPoint3 direction = GeneralUtils::TryParse(args.at(2), args.at(3), args.at(4), NiPoint3Constant::ZERO);
|
|
if (direction != NiPoint3Constant::ZERO) SetDirection(direction.Unitize());
|
|
}
|
|
// Live sent the distances for "true" only: volumes with "1,20,50" went out without them
|
|
if (args.size() > 7 && args.at(5) == "true") {
|
|
SetMin(GeneralUtils::TryParse(args.at(6), 0.0f));
|
|
SetMax(GeneralUtils::TryParse(args.at(7), 0.0f));
|
|
}
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetPosition(const NiPoint3& pos) {
|
|
PhysicsComponent::SetPosition(pos);
|
|
if (m_dpEntity) m_dpEntity->SetPosition(pos);
|
|
}
|
|
|
|
void PhantomPhysicsComponent::SetRotation(const NiQuaternion& rot) {
|
|
PhysicsComponent::SetRotation(rot);
|
|
if (m_dpEntity) m_dpEntity->SetRotation(rot);
|
|
}
|
|
|
|
bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
PhysicsComponent::OnGetObjectReportInfo(reportInfo);
|
|
if (!reportInfo.subCategory) {
|
|
return false;
|
|
}
|
|
auto& info = reportInfo.subCategory->PushDebug("Phantom Physics Info");
|
|
info.PushDebug<AMFDoubleValue>("Scale") = m_Scale;
|
|
info.PushDebug<AMFBoolValue>("Is Physics Effect Active") = m_IsPhysicsEffectActive;
|
|
info.PushDebug<AMFIntValue>("Effect Type") = static_cast<int>(m_EffectType);
|
|
info.PushDebug<AMFDoubleValue>("Directional Multiplier") = m_DirectionalMultiplier;
|
|
info.PushDebug<AMFBoolValue>("Is Directional") = m_IsDirectional;
|
|
auto& direction = info.PushDebug("Direction").PushDebug(m_Direction);
|
|
|
|
if (m_MinMax) {
|
|
auto& minMaxInfo = info.PushDebug("Min Max Info");
|
|
minMaxInfo.PushDebug<AMFDoubleValue>("Min") = m_Min;
|
|
minMaxInfo.PushDebug<AMFDoubleValue>("Max") = m_Max;
|
|
}
|
|
|
|
if (m_IsRespawnVolume) {
|
|
auto& respawnInfo = info.PushDebug("Respawn Info");
|
|
respawnInfo.PushDebug<AMFBoolValue>("Is Respawn Volume") = m_IsRespawnVolume;
|
|
respawnInfo.PushDebug("Respawn Position").PushDebug(m_RespawnPos);
|
|
respawnInfo.PushDebug("Respawn Rotation").PushDebug(m_RespawnRot);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PhantomPhysicsComponent::Allocator(Entity& entity, ObjectLoadState& state) {
|
|
const auto phantomPhysicsComponentID = state.GetComponentID(eReplicaComponentType::PHANTOM_PHYSICS);
|
|
if (state.hasModelBehaviors) {
|
|
// LWOPhantomPhysicsComponent::Allocator (0x010383b0): left to the model case
|
|
if (phantomPhysicsComponentID > 0) state.modelPhysicsComponentID = phantomPhysicsComponentID;
|
|
} else if (phantomPhysicsComponentID > 0 && !entity.HasComponent(eReplicaComponentType::PHANTOM_PHYSICS)) {
|
|
entity.AddComponent<PhantomPhysicsComponent>(phantomPhysicsComponentID)->SetPhysicsEffectActive(false);
|
|
}
|
|
}
|