fix(shooting gallery): moving platform targets run on their mover, as live

sg_cannon.lua puts a moving platform target (the subs and the kraken) on its path with SetCurrentPath{ pathName, startPoint = 0 }; its SetPathingSpeed is read by nothing on a moving platform, so these run their paths at the waypoints' speeds (10, 15 and 35 in the zone), as the client's mover does. Live constructed them with the path and the mover waiting at waypoint 0 (9) and serialized the mover at each waypoint (9, then 2). They had a movement AI and a made up PlatformResync instead, and the kraken a speed chosen to look right, which goes back to the script's.

PlatformResync's constructor that overrode its arguments for a few LOTs (the FV tree platform 9483 among them) has no user left and is gone; a resync request is answered by movers only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 13:36:25 -06:00
parent 275243cf86
commit 11d7455cad
4 changed files with 40 additions and 57 deletions

View File

@@ -50,26 +50,6 @@ namespace GameMessages {
return true;
}
PlatformResync::PlatformResync(const Entity& entity, bool bStopAtDesiredWaypoint, int32_t iIndex, int32_t iDesiredWaypointIndex,
int32_t nextIndex, eMovementPlatformState movementState, bool special) : PlatformResync() {
const auto lot = entity.GetLOT();
if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308 || lot == 9483) {
iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0;
iIndex = lot == 9483 ? 1 : 0;
nextIndex = lot == 9483 && !special ? 1 : 0;
bStopAtDesiredWaypoint = true;
movementState = lot == 9483 && !special ? eMovementPlatformState::Stopped : eMovementPlatformState::Stationary;
}
target = entity.GetObjectID();
this->bStopAtDesiredWaypoint = bStopAtDesiredWaypoint;
this->iIndex = iIndex;
this->iDesiredWaypointIndex = iDesiredWaypointIndex;
this->iNextIndex = nextIndex;
this->eState = movementState;
}
void PlatformResync::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(bReverse);
bitStream.Write(bStopAtDesiredWaypoint);
@@ -121,21 +101,18 @@ namespace GameMessages {
}
void RequestPlatformResync::Handle(Entity& entity, const SystemAddress& sysAddr) {
// A mover answers with where the server has it (live: travelling 2 with its percent and position, or 25 at a
// waypoint with the time waited)
// A mover answers with where the server has it (live: travelling 2 with its percent and position, or waiting at
// a waypoint with the time waited)
auto* const platform = entity.GetComponent<MovingPlatformComponent>();
if (platform && platform->IsMover()) {
PlatformResync resync;
resync.target = entity.GetObjectID();
platform->FillResync(resync);
resync.Send(sysAddr);
if (!platform || !platform->IsMover()) {
// Nothing else has waypoints to resync to: the client's simple mover reads them from its own two point path
// (LWOPlatformSimpleMover::Resync 0x00bf9ec0, a crash without one) and would set off on a travelling state
return;
}
// A platform without a path has no waypoints to resync to; the client's simple mover reads them anyway and
// crashes (LWOPlatformSimpleMover::Resync 0x00bf9ec0 -> Path::GetWaypointAtIndex on a null path)
if (platform && !platform->HasPath()) return;
if (entity.GetLOT() == 6267 || entity.GetLOT() == 16141) return;
PlatformResync(entity).Send(sysAddr);
PlatformResync resync;
resync.target = entity.GetObjectID();
platform->FillResync(resync);
resync.Send(sysAddr);
}
void OrientToAngle::Serialize(RakNet::BitStream& bitStream) const {

View File

@@ -49,11 +49,6 @@ namespace GameMessages {
// Server -> client, to one client (UNASSIGNED broadcasts).
struct PlatformResync : public NetGameMsg {
PlatformResync() : NetGameMsg(MessageType::Game::PLATFORM_RESYNC) {}
// What DLU sends for a platform entity: the arguments, overridden for a few LOTs whose paths only have a
// start and an end (special is for the FV tree platform).
PlatformResync(const Entity& entity, bool bStopAtDesiredWaypoint = false,
int32_t iIndex = 0, int32_t iDesiredWaypointIndex = 1, int32_t nextIndex = 1,
eMovementPlatformState movementState = eMovementPlatformState::Moving, bool special = false);
void Serialize(RakNet::BitStream& bitStream) const override;
bool Deserialize(RakNet::BitStream& bitStream) override;

View File

@@ -11,6 +11,7 @@
#include "CharacterComponent.h"
#include "SimplePhysicsComponent.h"
#include "MovementAIComponent.h"
#include "MovingPlatformComponent.h"
#include "MissionComponent.h"
#include "Loot.h"
#include "InventoryComponent.h"
@@ -295,7 +296,17 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) {
auto* enemy = Game::entityManager->CreateEntity(info, nullptr, self);
auto* movementAI = enemy->AddComponent<MovementAIComponent>(-1, MovementAIInfo{});
// sg_cannon.lua (onChildLoaded): a moving platform target gets SetPathingSpeed, which nothing on a moving platform
// reads, and SetCurrentPath{ pathName, startPoint = 0 }: its mover runs the path at the waypoints' speeds, as the
// client's does. Live constructed these targets with the path and the mover waiting at waypoint 0 (9), and
// serialized the mover at each waypoint. The other targets follow their waypoints.
auto* const movingPlatform = toSpawn.isMovingPlatform ? enemy->GetComponent<MovingPlatformComponent>() : nullptr;
MovementAIComponent* movementAI = nullptr;
if (movingPlatform) {
movingPlatform->SetCurrentPath(path->pathName, 0);
} else {
movementAI = enemy->AddComponent<MovementAIComponent>(-1, MovementAIInfo{});
}
auto* simplePhysicsComponent = enemy->GetComponent<SimplePhysicsComponent>();
if (simplePhysicsComponent) {
simplePhysicsComponent->SetPhysicsMotionState(4);
@@ -303,20 +314,21 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) {
Game::entityManager->ConstructEntity(enemy);
movementAI->SetMaxSpeed(toSpawn.initialSpeed);
movementAI->SetCurrentSpeed(toSpawn.initialSpeed);
movementAI->SetHaltDistance(0.0f);
if (movementAI) {
movementAI->SetMaxSpeed(toSpawn.initialSpeed);
movementAI->SetCurrentSpeed(toSpawn.initialSpeed);
movementAI->SetHaltDistance(0.0f);
movementAI->SetPath(path->pathWaypoints);
movementAI->SetPath(path->pathWaypoints);
}
enemy->AddDieCallback([this, self, enemy, name]() {
RegisterHit(self, enemy, name);
});
// Save the enemy and tell it to start pathing
// Save the enemy
if (enemy != nullptr) {
const_cast<std::vector<LWOOBJID>&>(self->GetVar<std::vector<LWOOBJID>>(SpawnedObjects)).push_back(enemy->GetObjectID());
GameMessages::PlatformResync(*enemy).Send(UNASSIGNED_SYSTEM_ADDRESS);
}
}
}
@@ -1104,22 +1116,14 @@ std::vector<std::vector<SGEnemy>> SGCannon::GetWaves() {
1.0, false, true
},
// Ness temp fix for dlu where speeds are set to 7 to match a speed closer to live while we work on movingplatform components.
// Ness
{
std::vector<std::string> { "Wave_1_Ness_1", "Wave_1_Ness_2", "Wave_2_Ness_1" },
2565, 10.0, 15.0, true, 10.0, 15.0,
7.0, 10000, false, 0.0, 7.0,
7.0, true, true
2.0, 10000, false, 0.0, 1.0,
1.0, true, true
},
// // Ness
// {
// std::vector<std::string> { "Wave_1_Ness_1", "Wave_1_Ness_2", "Wave_2_Ness_1" },
// 2565, 10.0, 15.0, true, 10.0, 15.0,
// 2.0, 10000, false, 0.0, 1.0,
// 1.0, true, true
// },
// Friendly 1
{
std::vector<std::string> { "Wave_3_FShip_1", "Wave_3_FShip_2" },

View File

@@ -89,14 +89,21 @@ TEST_F(RemainingMessagesTests, TeleportMatchesLegacy) {
}
TEST_F(RemainingMessagesTests, StartPathingAndPlatformResyncMatchLegacy) {
for (const LOT lot : { 999, 12341, 9483, 11306, 6267 }) {
// The legacy sender overrode its arguments for a few LOTs; the message has no such cases, so only others compare
for (const LOT lot : { 999, 6267 }) {
info.lot = lot;
Entity entity(0x1000000000000001LL, info);
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendStartPathing(&entity); }, [&] { GameMessages::StartPathing msg; msg.target = entity.GetObjectID(); return msg; }());
for (uint32_t mask = 0; mask < 4; mask++) {
for (const auto state : { eMovementPlatformState::Moving, eMovementPlatformState::Stationary, eMovementPlatformState::Stopped }) {
for (const int32_t index : { 0, 1, 5 }) {
GameMessages::PlatformResync msg(entity, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1));
GameMessages::PlatformResync msg;
msg.target = entity.GetObjectID();
msg.bStopAtDesiredWaypoint = Bit(mask, 0);
msg.iIndex = index;
msg.iDesiredWaypointIndex = index + 1;
msg.iNextIndex = index + 2;
msg.eState = state;
ExpectSameAsLegacy([&](const SystemAddress& address) {
LegacyGameMessages::SendPlatformResync(&entity, address, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1));
}, msg);