fix: run speed modifiers stack as the client's do

LWOControllablePhysComponent::msgAddRunspeedModifier (0x00be0af0) and
msgRemoveRunSpeedModifier (0x00be0720): a modifier over 1000 or of exactly
500 counts for nothing, any slow (under 500) wins with the lowest one, and
otherwise the highest boost is the speed. Before, the last one applied won,
so a small boost cast after a big one, or a boost cast while slowed,
replaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 14:50:33 -06:00
parent 55a6d0babc
commit 9265ab0e10
3 changed files with 66 additions and 9 deletions

View File

@@ -333,8 +333,7 @@ void ControllablePhysicsComponent::RemovePickupRadiusScale(float value) {
void ControllablePhysicsComponent::AddSpeedboost(float value) {
m_ActiveSpeedBoosts.push_back(value);
m_SpeedBoost = value;
SetSpeedMultiplier(value / 500.0f); // 500 being the base speed
UpdateSpeedBoost();
}
void ControllablePhysicsComponent::RemoveSpeedboost(float value) {
@@ -346,16 +345,35 @@ void ControllablePhysicsComponent::RemoveSpeedboost(float value) {
return;
}
// Recalculate speedboost since we removed one
m_SpeedBoost = 500.0f; // Entities without level progression (enemies, pets) go back to the normal multiplier of 1
if (m_ActiveSpeedBoosts.empty()) { // no active speed boosts left, so return to base speed
UpdateSpeedBoost();
Game::entityManager->SerializeEntity(m_Parent);
}
void ControllablePhysicsComponent::UpdateSpeedBoost() {
// The client's rule (LWOControllablePhysComponent::msgAddRunspeedModifier 0x00be0af0, msgRemoveRunSpeedModifier
// 0x00be0720): a modifier over 1000 or of exactly the base 500 counts for nothing; with any slow (under 500) the
// lowest one is the speed, otherwise the highest boost is
constexpr float baseSpeed = 500.0f;
constexpr float maxModifier = 1000.0f;
float lowestSlow = baseSpeed;
float highestBoost = baseSpeed;
for (const auto boost : m_ActiveSpeedBoosts) {
if (boost > maxModifier) continue;
if (boost < lowestSlow) lowestSlow = boost;
if (boost > highestBoost) highestBoost = boost;
}
if (lowestSlow < baseSpeed) {
m_SpeedBoost = lowestSlow;
} else if (highestBoost > baseSpeed) {
m_SpeedBoost = highestBoost;
} else {
// No modifier counts: entities without level progression (enemies, pets) go back to the normal multiplier of 1
m_SpeedBoost = baseSpeed;
auto* levelProgressionComponent = m_Parent->GetComponent<LevelProgressionComponent>();
if (levelProgressionComponent) m_SpeedBoost = levelProgressionComponent->GetSpeedBase();
} else { // Used the last applied speedboost
m_SpeedBoost = m_ActiveSpeedBoosts.back();
}
SetSpeedMultiplier(m_SpeedBoost / 500.0f); // 500 being the base speed
Game::entityManager->SerializeEntity(m_Parent);
SetSpeedMultiplier(m_SpeedBoost / baseSpeed);
}
void ControllablePhysicsComponent::ActivateBubbleBuff(eBubbleType bubbleType, bool specialAnims) {

View File

@@ -395,6 +395,11 @@ private:
*/
bool m_IsTeleporting;
/**
* Works the speed out of the active speed boosts: the lowest slow if there is one, otherwise the highest boost
*/
void UpdateSpeedBoost();
/**
* The list of speed boosts for this entity
*/

View File

@@ -432,6 +432,40 @@ TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) {
player.SetCharacter(nullptr);
}
// Run speed modifiers stack as in LWOControllablePhysComponent::msgAddRunspeedModifier (0x00be0af0) and
// msgRemoveRunSpeedModifier (0x00be0720): the lowest slow wins, otherwise the highest boost; 500 and anything over 1000
// count for nothing
TEST_F(ReplicaConstructionTest, RunSpeedModifiersStackAsTheClientDoes) {
Entity entity(15, info);
auto* const physics = entity.AddComponent<ControllablePhysicsComponent>(-1);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.0f);
physics->AddSpeedboost(750.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f);
physics->AddSpeedboost(550.0f); // a smaller boost applied later does not replace the bigger one
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f);
physics->AddSpeedboost(250.0f); // any slow beats every boost
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.5f);
physics->AddSpeedboost(133.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f);
physics->AddSpeedboost(350.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f);
physics->AddSpeedboost(1500.0f);
physics->AddSpeedboost(500.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f);
physics->RemoveSpeedboost(133.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.5f);
physics->RemoveSpeedboost(250.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.7f);
physics->RemoveSpeedboost(350.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f);
physics->RemoveSpeedboost(750.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.1f);
physics->RemoveSpeedboost(550.0f);
EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.0f); // 1500 and 500 are left and count for nothing
}
// Live wrote the cheat block (gravity scale, speed multiplier) on construction only when one of them was changed: 216
// of 20,617 controllable-physics constructions had it (players at run speed 1.05, enemies with gravity 0), never 1/1.
TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionWritesCheatsOnlyWhenChanged) {