fix: trigger volume effects go out as live sent them

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>
This commit is contained in:
Aaron Kimbrell
2026-11-10 13:19:28 -06:00
parent a269ce0cc7
commit 5832567bc4
4 changed files with 159 additions and 45 deletions

View File

@@ -3,6 +3,7 @@
* Copyright 2018
*/
#include <algorithm>
#include <sstream>
#include <iostream>
@@ -136,13 +137,13 @@ void PhantomPhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bI
outBitStream.Write(m_EffectType);
outBitStream.Write(m_DirectionalMultiplier);
// forgive me father for i have sinned
outBitStream.Write0();
//outBitStream.Write(m_MinMax);
//if (m_MinMax) {
//outBitStream.Write(m_Min);
//outBitStream.Write(m_Max);
//}
// 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) {
@@ -244,18 +245,50 @@ void PhantomPhysicsComponent::SetEffectType(ePhysicsEffectType type) {
m_EffectInfoDirty = true;
}
void PhantomPhysicsComponent::SetMin(uint32_t min) {
void PhantomPhysicsComponent::SetMin(const float min) {
m_Min = min;
m_MinMax = true;
m_EffectInfoDirty = true;
}
void PhantomPhysicsComponent::SetMax(uint32_t max) {
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);
@@ -281,8 +314,8 @@ bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectRepor
if (m_MinMax) {
auto& minMaxInfo = info.PushDebug("Min Max Info");
minMaxInfo.PushDebug<AMFIntValue>("Min") = m_Min;
minMaxInfo.PushDebug<AMFIntValue>("Max") = m_Max;
minMaxInfo.PushDebug<AMFDoubleValue>("Min") = m_Min;
minMaxInfo.PushDebug<AMFDoubleValue>("Max") = m_Max;
}
if (m_IsRespawnVolume) {

View File

@@ -120,14 +120,16 @@ public:
void SpawnVertices() const;
/**
* Legacy stuff no clue what this does
* The distances the effect scales between; written only once one of them is set.
* LWOPhantomPhysicsComponent::Deserialize (0x00c34db0) keeps the least of the min and 1, and a max of at least that.
*/
void SetMin(uint32_t min);
void SetMin(float min);
void SetMax(float max);
/**
* Legacy stuff no clue what this does
* A trigger's SetPhysicsVolumeEffect: type, amount, direction x, y, z, use distance, min, max
*/
void SetMax(uint32_t max);
void SetVolumeEffect(std::vector<std::string> args);
private:
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
@@ -158,8 +160,8 @@ private:
float m_DirectionalMultiplier;
bool m_MinMax;
uint32_t m_Min;
uint32_t m_Max;
float m_Min;
float m_Max;
/**
* Whether or not this physics object is pointed in some direction

View File

@@ -407,34 +407,7 @@ void TriggerComponent::HandleSetPhysicsVolumeEffect(Entity* targetEntity, std::v
LOG_DEBUG("Phantom Physics component not found!");
return;
}
phantomPhysicsComponent->SetPhysicsEffectActive(true);
ePhysicsEffectType effectType = ePhysicsEffectType::PUSH;
if (!argArray.empty()) {
std::transform(argArray.at(0).begin(), argArray.at(0).end(), argArray.at(0).begin(), ::tolower); //Transform to lowercase
if (argArray.at(0) == "push") effectType = ePhysicsEffectType::PUSH;
else if (argArray.at(0) == "attract") effectType = ePhysicsEffectType::ATTRACT;
else if (argArray.at(0) == "repulse") effectType = ePhysicsEffectType::REPULSE;
else if (argArray.at(0) == "gravity") effectType = ePhysicsEffectType::GRAVITY_SCALE;
else if (argArray.at(0) == "friction") effectType = ePhysicsEffectType::FRICTION;
}
phantomPhysicsComponent->SetEffectType(effectType);
if (argArray.size() > 1) {
phantomPhysicsComponent->SetDirectionalMultiplier(GeneralUtils::TryParse(argArray.at(1), 0.0f));
}
if (argArray.size() > 4) {
const NiPoint3 direction =
GeneralUtils::TryParse(argArray.at(2), argArray.at(3), argArray.at(4), NiPoint3Constant::ZERO);
phantomPhysicsComponent->SetDirection(direction);
}
if (argArray.size() > 6) {
const uint32_t min = GeneralUtils::TryParse(argArray.at(6), 0);
phantomPhysicsComponent->SetMin(min);
const uint32_t max = GeneralUtils::TryParse(argArray.at(7), 0);
phantomPhysicsComponent->SetMax(max);
}
phantomPhysicsComponent->SetVolumeEffect(argArray);
Game::entityManager->SerializeEntity(targetEntity);
}

View File

@@ -21,6 +21,7 @@
#include "Item.h"
#include "ModelComponent.h"
#include "MovingPlatformComponent.h"
#include "PhantomPhysicsComponent.h"
#include "SimplePhysicsComponent.h"
#include "User.h"
#include "eGameActivity.h"
@@ -210,6 +211,111 @@ TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionCarriesJetPackMod
EXPECT_EQ(construct(), plain);
}
namespace {
struct VolumeEffect {
uint32_t type{};
float amount{};
bool hasDistance{};
float min{};
float max{};
bool isDirectional{};
NiPoint3 direction{};
};
// A phantom volume looks its LOT up in the registry when it is made
void ConnectRegistry() {
if (CDClientDatabase::isConnected) return;
CDClientDatabase::Connect(":memory:");
CDClientDatabase::ExecuteDML("CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);");
}
// The effect block of a phantom physics update made from a trigger's SetPhysicsVolumeEffect arguments
VolumeEffect ReadVolumeEffect(Entity& volume, const std::vector<std::string>& args) {
auto* const phantom = volume.AddComponent<PhantomPhysicsComponent>(-1);
phantom->SetVolumeEffect(args);
RakNet::BitStream stream;
phantom->Serialize(stream, false);
VolumeEffect effect;
bool flag{};
stream.Read(flag);
if (flag) stream.IgnoreBits(7 * 32); // position and rotation
stream.Read(flag);
EXPECT_TRUE(flag); // the effect block
stream.Read(flag);
EXPECT_TRUE(flag); // active
stream.Read(effect.type);
stream.Read(effect.amount);
stream.Read(effect.hasDistance);
if (effect.hasDistance) {
stream.Read(effect.min);
stream.Read(effect.max);
}
stream.Read(effect.isDirectional);
if (effect.isDirectional) {
stream.Read(effect.direction.x);
stream.Read(effect.direction.y);
stream.Read(effect.direction.z);
}
EXPECT_EQ(stream.GetNumberOfUnreadBits(), 0u);
return effect;
}
}
// Trigger volume effects as live sent them (phantom physics constructions in the live captures; the client reads
// them in LWOPhantomPhysicsComponent::Deserialize): the direction has the amount as its length, the distances go out
// for "true" only and as 1 and the max, and a zero direction is no direction.
TEST_F(ReplicaConstructionTest, PhantomPhysicsVolumeEffectsAsLive) {
ConnectRegistry();
Entity pushWithDistance(1001, info);
auto effect = ReadVolumeEffect(pushWithDistance, { "Push", "50", "0.91", "-0.34", "-0.22", "true", "20", "50" });
EXPECT_EQ(effect.type, 0u);
EXPECT_EQ(effect.amount, 50.0f);
EXPECT_TRUE(effect.hasDistance);
EXPECT_EQ(effect.min, 1.0f);
EXPECT_EQ(effect.max, 50.0f);
EXPECT_TRUE(effect.isDirectional);
EXPECT_NEAR(effect.direction.x, 45.68f, 0.01f);
EXPECT_NEAR(effect.direction.y, -17.07f, 0.01f);
EXPECT_NEAR(effect.direction.z, -11.04f, 0.01f);
Entity pushFar(1002, info);
effect = ReadVolumeEffect(pushFar, { "Push", "200", "1", "0", "0", "true", "150", "175" });
EXPECT_TRUE(effect.hasDistance);
EXPECT_EQ(effect.min, 1.0f);
EXPECT_EQ(effect.max, 175.0f);
EXPECT_EQ(effect.direction, NiPoint3(200.0f, 0.0f, 0.0f));
// "1" is not "true": no distances
Entity updraft(1003, info);
effect = ReadVolumeEffect(updraft, { "Push", "40.0", "0", "1", "0", "1", "20", "50" });
EXPECT_EQ(effect.amount, 40.0f);
EXPECT_FALSE(effect.hasDistance);
EXPECT_TRUE(effect.isDirectional);
EXPECT_EQ(effect.direction, NiPoint3(0.0f, 40.0f, 0.0f));
Entity repulse(1004, info);
effect = ReadVolumeEffect(repulse, { "Repulse", "150", "0", "0", "0", "true", "25", "100" });
EXPECT_EQ(effect.type, 2u);
EXPECT_EQ(effect.amount, 150.0f);
EXPECT_TRUE(effect.hasDistance);
EXPECT_EQ(effect.min, 1.0f);
EXPECT_EQ(effect.max, 100.0f);
EXPECT_FALSE(effect.isDirectional);
Entity slanted(1005, info);
effect = ReadVolumeEffect(slanted, { "Push", " 35", " .79", " 0", " .61" });
EXPECT_FALSE(effect.hasDistance);
EXPECT_NEAR(effect.direction.x, 27.70f, 0.01f);
EXPECT_NEAR(effect.direction.z, 21.39f, 0.01f);
Entity friction(1006, info);
effect = ReadVolumeEffect(friction, { "Friction", "0" });
EXPECT_EQ(effect.type, 4u);
EXPECT_FALSE(effect.hasDistance);
EXPECT_FALSE(effect.isDirectional);
}
// A guild change (docs/Guilds.md) is written once: the client reads guild id, a u8 length and 16-bit characters, and
// redraws the name billboard every time it reads a name.
TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) {