feat(rails): answer RequestRailActivatorState

The client's rail activator sends RequestRailActivatorState (1479, no
payload) when it is added to the world (LWORailActivatorComponent::
SendMessage 0x00c00da0) and live answered with
NotifyRailActivatorStateChange (1478, bActive) to that client; the client
stores rail_activator_active and updates the rail's pick type (usable or
not). Live: 152 requests, 413 answers, every one bActive = true. DLU
dropped the request and never answered.

RailActivatorComponent keeps the level key rail_activator_active
(default true; every rail in the live levels sets it to 1) and the
request is answered with it.

Check in game: in Ninjago Monastery and Nimbus Station, rails (spinjitzu
posts, the Nexus Tower rails) show as usable and work as before when
first loading the world and after zone changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:23:47 -05:00
parent e9ff5f402d
commit 3672b64dec
6 changed files with 68 additions and 0 deletions

View File

@@ -44,6 +44,7 @@ RailActivatorComponent::RailActivatorComponent(Entity* parent, const int32_t com
m_NotifyArrived = parent->GetVar<bool>(u"rail_notify_activator_arrived"); m_NotifyArrived = parent->GetVar<bool>(u"rail_notify_activator_arrived");
m_ShowNameBillboard = levelOr(u"rail_show_name_billboard", tableData.showNameBillboard); m_ShowNameBillboard = levelOr(u"rail_show_name_billboard", tableData.showNameBillboard);
m_UseDB = parent->GetVar<bool>(u"rail_use_db"); m_UseDB = parent->GetVar<bool>(u"rail_use_db");
m_Active = levelOr(u"rail_activator_active", true);
m_CameraLocked = tableData.cameraLocked; m_CameraLocked = tableData.cameraLocked;
m_CollisionEnabled = tableData.playerCollision; m_CollisionEnabled = tableData.playerCollision;
} }

View File

@@ -38,6 +38,11 @@ public:
void OnCancelRailMovement(Entity* originator); void OnCancelRailMovement(Entity* originator);
bool GetDamageImmune() const { return m_DamageImmune; } bool GetDamageImmune() const { return m_DamageImmune; }
/**
* Whether the rail can be used (level key rail_activator_active; every rail in the live levels has it set)
*/
bool GetActive() const { return m_Active; }
bool GetNoAggro() const { return m_NoAggro; } bool GetNoAggro() const { return m_NoAggro; }
bool GetShowNameBillboard() const { return m_ShowNameBillboard; } bool GetShowNameBillboard() const { return m_ShowNameBillboard; }
private: private:
@@ -111,6 +116,8 @@ private:
*/ */
bool m_DamageImmune; bool m_DamageImmune;
bool m_Active{ true };
/** /**
* Client flag * Client flag
*/ */

View File

@@ -215,6 +215,7 @@ namespace {
{ CLIENT_RAIL_MOVEMENT_READY, []() { return std::make_unique<ClientRailMovementReady>(); } }, { CLIENT_RAIL_MOVEMENT_READY, []() { return std::make_unique<ClientRailMovementReady>(); } },
{ CANCEL_RAIL_MOVEMENT, []() { return std::make_unique<CancelRailMovement>(); } }, { CANCEL_RAIL_MOVEMENT, []() { return std::make_unique<CancelRailMovement>(); } },
{ PLAYER_RAIL_ARRIVED_NOTIFICATION, []() { return std::make_unique<PlayerRailArrivedNotification>(); } }, { PLAYER_RAIL_ARRIVED_NOTIFICATION, []() { return std::make_unique<PlayerRailArrivedNotification>(); } },
{ REQUEST_RAIL_ACTIVATOR_STATE, []() { return std::make_unique<RequestRailActivatorState>(); } },
{ DISMOUNT_COMPLETE, []() { return std::make_unique<DismountComplete>(); } }, { DISMOUNT_COMPLETE, []() { return std::make_unique<DismountComplete>(); } },
{ ACKNOWLEDGE_POSSESSION, []() { return std::make_unique<AcknowledgePossession>(); } }, { ACKNOWLEDGE_POSSESSION, []() { return std::make_unique<AcknowledgePossession>(); } },
{ TOGGLE_GHOST_REFERENCE_OVERRIDE, []() { return std::make_unique<ToggleGhostReferenceOverride>(); } }, { TOGGLE_GHOST_REFERENCE_OVERRIDE, []() { return std::make_unique<ToggleGhostReferenceOverride>(); } },

View File

@@ -295,6 +295,24 @@ namespace GameMessages {
} }
} }
void RequestRailActivatorState::Handle(Entity& entity, const SystemAddress& sysAddr) {
const auto* const rail = entity.GetComponent<RailActivatorComponent>();
if (!rail) return;
NotifyRailActivatorStateChange notify;
notify.target = entity.GetObjectID();
notify.bActive = rail->GetActive();
notify.SendToClient(sysAddr);
}
void NotifyRailActivatorStateChange::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bActive);
}
bool NotifyRailActivatorStateChange::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(bActive));
return true;
}
void PlayerRailArrivedNotification::Serialize(RakNet::BitStream& bitStream) const { void PlayerRailArrivedNotification::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, pathName); BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, pathName);
bitStream.Write(waypointNumber); bitStream.Write(waypointNumber);

View File

@@ -197,6 +197,22 @@ namespace GameMessages {
bool bImmediate{}; bool bImmediate{};
}; };
// Client -> server, no payload. The client's rail activator asks for its state when it is added to the world
// (LWORailActivatorComponent::SendMessage 0x00c00da0); live answered NotifyRailActivatorStateChange to that client.
struct RequestRailActivatorState : public NetGameMsg {
RequestRailActivatorState() : NetGameMsg(MessageType::Game::REQUEST_RAIL_ACTIVATOR_STATE) {}
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
};
// Server -> client. The client sets rail_activator_active and updates the rail's pick type (usable or not).
struct NotifyRailActivatorStateChange : public NetGameMsg {
NotifyRailActivatorStateChange() : NetGameMsg(MessageType::Game::NOTIFY_RAIL_ACTOVATOR_STATE_CHANGE) {}
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;
bool bActive{ true };
};
// Client -> server. // Client -> server.
struct PlayerRailArrivedNotification : public NetGameMsg { struct PlayerRailArrivedNotification : public NetGameMsg {
PlayerRailArrivedNotification() : NetGameMsg(MessageType::Game::PLAYER_RAIL_ARRIVED_NOTIFICATION) {} PlayerRailArrivedNotification() : NetGameMsg(MessageType::Game::PLAYER_RAIL_ARRIVED_NOTIFICATION) {}

View File

@@ -4,6 +4,8 @@
#include "CDComponentsRegistryTable.h" #include "CDComponentsRegistryTable.h"
#include "CDRailActivatorComponent.h" #include "CDRailActivatorComponent.h"
#include "Entity.h" #include "Entity.h"
#include "MovementMessages.h"
#include "dGameMessagesTests/GameMessageTestUtils.h"
#include "RailActivatorComponent.h" #include "RailActivatorComponent.h"
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -57,3 +59,26 @@ TEST_F(RailActivatorComponentTests, NoRowAndNoKeysIsNotImmune) {
EXPECT_FALSE(rail->GetDamageImmune()); EXPECT_FALSE(rail->GetDamageImmune());
EXPECT_FALSE(rail->GetNoAggro()); EXPECT_FALSE(rail->GetNoAggro());
} }
// Live answered the client's RequestRailActivatorState (1479, no payload; 152 packets) with
// NotifyRailActivatorStateChange (1478) to that client: every one of the 413 live packets was bActive = true (80).
TEST_F(RailActivatorComponentTests, RequestRailActivatorStateIsAnswered) {
using namespace GameMessageTestUtils;
entity->AddComponent<RailActivatorComponent>(COMPONENT_ID);
GameMessages::RequestRailActivatorState request;
const auto packets = Capture([&] { request.Handle(*entity, ClientAddress()); });
ASSERT_EQ(packets.size(), 1u);
EXPECT_EQ(packets[0].sysAddr, ClientAddress());
EXPECT_FALSE(packets[0].broadcast);
EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 00 c6 05 80", 145), FromCapture(packets[0]));
}
TEST_F(RailActivatorComponentTests, InactiveRailIsReportedInactive) {
using namespace GameMessageTestUtils;
entity->SetVar<bool>(u"rail_activator_active", false);
const auto* const rail = entity->AddComponent<RailActivatorComponent>(COMPONENT_ID);
EXPECT_FALSE(rail->GetActive());
GameMessages::NotifyRailActivatorStateChange notify;
notify.bActive = false;
EXPECT_FALSE(RoundTrip(notify).bActive);
}