From 7440e5744d98c6b0d4b934344854d87d482d401a Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Mon, 28 Sep 2026 23:34:42 -0500 Subject: [PATCH] fix(rails): rider flags default to the RailActivatorComponent row StartRailMovement's bDamageImmune, bNoAggro and bShowNameBillboard came only from the level keys rail_activator_damage_immune, rail_no_aggro and rail_show_name_billboard, which 23 of the 80 level rails do not have, so those rails sent false. The client reads the same flags from the RailActivatorComponent row (DamageImmune, NoAggro, ShowNameBillboard; all 11 rows are 1) in LWOPlayerForcedMovementComponent::LoadRailData (0x00c85dc0), and the message's values replace the row's unless bUseDB (msgStartRailMovement 0x00ccd9c0). The server now takes the row's value and lets a level key replace it when the key is there. Wire format is unchanged; only the flag values sent for the 23 rails change (to true, matching the 5 live samples). In game: ride the 23 rails without the keys, all in the Ninjago earth gauntlet (earthgauntlet levels: dart spinners, 8 spinners, millstones, boss) near enemies: you are not damaged or targeted while riding, and name billboards behave as on the other rails. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/RailActivatorComponent.cpp | 12 +++- dGame/dComponents/RailActivatorComponent.h | 4 ++ .../dComponentsTests/CMakeLists.txt | 1 + .../RailActivatorComponentTests.cpp | 59 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/dGameTests/dComponentsTests/RailActivatorComponentTests.cpp diff --git a/dGame/dComponents/RailActivatorComponent.cpp b/dGame/dComponents/RailActivatorComponent.cpp index 6108e7f1e..d3aef96d4 100644 --- a/dGame/dComponents/RailActivatorComponent.cpp +++ b/dGame/dComponents/RailActivatorComponent.cpp @@ -33,10 +33,16 @@ RailActivatorComponent::RailActivatorComponent(Entity* parent, const int32_t com m_LoopEffect = tableData.loopEffectID; m_StopEffect = tableData.stopEffectID; - m_DamageImmune = parent->GetVar(u"rail_activator_damage_immune"); - m_NoAggro = parent->GetVar(u"rail_no_aggro"); + // The RailActivatorComponent row gives these (the client reads DamageImmune, NoAggro and ShowNameBillboard in + // LWOPlayerForcedMovementComponent::LoadRailData 0x00c85dc0); a level key, when there, replaces the row's value + // as StartRailMovement's flags replace the row's in msgStartRailMovement (0x00ccd9c0) unless bUseDB is set. + const auto levelOr = [parent](const std::u16string& key, const bool tableValue) { + return parent->HasVar(key) ? parent->GetVar(key) : tableValue; + }; + m_DamageImmune = levelOr(u"rail_activator_damage_immune", tableData.damageImmune); + m_NoAggro = levelOr(u"rail_no_aggro", tableData.noAggro); m_NotifyArrived = parent->GetVar(u"rail_notify_activator_arrived"); - m_ShowNameBillboard = parent->GetVar(u"rail_show_name_billboard"); + m_ShowNameBillboard = levelOr(u"rail_show_name_billboard", tableData.showNameBillboard); m_UseDB = parent->GetVar(u"rail_use_db"); m_CameraLocked = tableData.cameraLocked; m_CollisionEnabled = tableData.playerCollision; diff --git a/dGame/dComponents/RailActivatorComponent.h b/dGame/dComponents/RailActivatorComponent.h index c3e5d3925..a94342766 100644 --- a/dGame/dComponents/RailActivatorComponent.h +++ b/dGame/dComponents/RailActivatorComponent.h @@ -36,6 +36,10 @@ public: * @param originator the entity that triggered the event */ void OnCancelRailMovement(Entity* originator); + + bool GetDamageImmune() const { return m_DamageImmune; } + bool GetNoAggro() const { return m_NoAggro; } + bool GetShowNameBillboard() const { return m_ShowNameBillboard; } private: /** * The entities that are currently traversing the rail diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 3dba2041a..2b96405e9 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -2,6 +2,7 @@ set(DCOMPONENTS_TESTS "DeletionRestrictionTests.cpp" "InventorySaveTests.cpp" "DestroyableLevelConfigTests.cpp" + "RailActivatorComponentTests.cpp" "VendorBuybackTests.cpp" "RacingWrongWayTests.cpp" "DestroyableComponentTests.cpp" diff --git a/tests/dGameTests/dComponentsTests/RailActivatorComponentTests.cpp b/tests/dGameTests/dComponentsTests/RailActivatorComponentTests.cpp new file mode 100644 index 000000000..00cdc93fc --- /dev/null +++ b/tests/dGameTests/dComponentsTests/RailActivatorComponentTests.cpp @@ -0,0 +1,59 @@ +#include "GameDependencies.h" + +#include "CDClientManager.h" +#include "CDComponentsRegistryTable.h" +#include "CDRailActivatorComponent.h" +#include "Entity.h" +#include "RailActivatorComponent.h" + +#include + +// The rider flags come from the RailActivatorComponent row; a level key, when there, replaces the row's value. +class RailActivatorComponentTests : public GameDependenciesTest { +protected: + static constexpr int32_t COMPONENT_ID = 9999; + std::unique_ptr entity; + + void SetUp() override { + SetUpDependencies(); + CDRailActivatorComponent row{}; + row.id = COMPONENT_ID; + row.damageImmune = true; + row.noAggro = true; + row.showNameBillboard = true; + CDClientManager::GetEntriesMutable().push_back(row); + + // The entity's own LOT has no components, so it doesn't look anything up in a database + CDClientManager::GetEntriesMutable().insert_or_assign(static_cast(info.lot), 0); + entity = std::make_unique(1, info); + } + + void TearDown() override { + entity.reset(); + CDClientManager::GetEntriesMutable().clear(); + TearDownDependencies(); + } +}; + +TEST_F(RailActivatorComponentTests, WithoutLevelKeysTheRowIsUsed) { + const auto* const rail = entity->AddComponent(COMPONENT_ID); + EXPECT_TRUE(rail->GetDamageImmune()); + EXPECT_TRUE(rail->GetNoAggro()); + EXPECT_TRUE(rail->GetShowNameBillboard()); +} + +TEST_F(RailActivatorComponentTests, LevelKeysReplaceTheRow) { + entity->SetVar(u"rail_activator_damage_immune", false); + entity->SetVar(u"rail_no_aggro", false); + entity->SetVar(u"rail_show_name_billboard", false); + const auto* const rail = entity->AddComponent(COMPONENT_ID); + EXPECT_FALSE(rail->GetDamageImmune()); + EXPECT_FALSE(rail->GetNoAggro()); + EXPECT_FALSE(rail->GetShowNameBillboard()); +} + +TEST_F(RailActivatorComponentTests, NoRowAndNoKeysIsNotImmune) { + const auto* const rail = entity->AddComponent(COMPONENT_ID + 1); + EXPECT_FALSE(rail->GetDamageImmune()); + EXPECT_FALSE(rail->GetNoAggro()); +}