From 8a238ab1d03ba9d19018110ee240c4d31788daf0 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 16:50:32 -0600 Subject: [PATCH] fix(shooting gallery): a target at the end of its path respawns and despawns, without a score SG_TARGET.lua, the targets' server script: at the last waypoint of its path (onArrived with isLastPoint, which the client's PushToLua 0x00b1a910 sets for a moving platform's Arrived as for a waypoint follower's) a target notifies the cannon, FinishedPath, and sg_cannon.lua spawns the next one as after a hit (checkSpawn). A target set to despawn there then dies silently after 1 s, a moving platform after 2 s. Here ships and subs sat at the end of their path until the game ended, and nothing came after them. The script cannot load (the targets have a script row of their own), so the cannon does it: - the respawn is split out of RegisterHit (CheckSpawn), and the die callback only registers a hit when the death is not the silent one; - a moving platform target tells through PlatformAtLastWaypoint, a waypoint follower through a new MovementAIComponent waypoint callback. Co-Authored-By: Claude Fable 5.2 --- dGame/dComponents/MovementAIComponent.cpp | 3 + dGame/dComponents/MovementAIComponent.h | 9 +++ .../ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp | 60 ++++++++++++++++--- dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h | 5 ++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 792139eec..44f904396 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -541,6 +541,9 @@ void MovementAIComponent::SetMaxSpeed(const float value) { void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // A callback may add another + const auto callbacks = m_WaypointReachedCallbacks; + for (const auto& callback : callbacks) callback(waypointNum); if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return; const auto& commands = m_Path->pathWaypoints[waypointNum].commands; diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 91fce3184..d4a508a70 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -16,6 +16,7 @@ #include "eReplicaComponentType.h" #include "Zone.h" #include "dpKnockback.h" +#include #include #include @@ -188,6 +189,12 @@ public: */ bool AtFinalWaypoint() const { return m_AtFinalWaypoint; } + /** + * Calls back at each waypoint of the path the entity follows, with the waypoint's number (what the Arrived game + * message tells an object's script) + */ + void AddWaypointReachedCallback(const std::function& callback) { m_WaypointReachedCallbacks.push_back(callback); } + /** * Renders the entity stationary */ @@ -380,6 +387,8 @@ private: // The number of waypoints that were on the path in the call to SetPath uint32_t m_CurrentPathWaypointCount{ 0 }; + std::vector> m_WaypointReachedCallbacks; + LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY }; /** diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index b7172380d..4b6262b08 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -287,7 +287,7 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { info.pos = path->pathWaypoints[0].position; info.settings.Insert(u"SpawnData", toSpawn); - info.settings.Insert(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded; + info.settings.Insert(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // never loaded (the targets have a script row): TargetFinishedPath does its part info.settings.Insert(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"); info.settings.Insert(u"attached_path", path->pathName); info.settings.Insert(u"attached_path_start", 0); @@ -322,10 +322,27 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { movementAI->SetPath(path->pathWaypoints); } - enemy->AddDieCallback([this, self, enemy, name]() { - RegisterHit(self, enemy, name); + // A hit target scores (live's cannon ball tells the cannon of its kills). One that got to the end of its path + // tells the cannon and dies silently a moment later (SG_TARGET.lua), which is no hit. + const auto silentDeath = std::make_shared(false); + enemy->AddDieCallback([this, self, enemy, name, silentDeath]() { + if (!*silentDeath) RegisterHit(self, enemy, name); }); + // SG_TARGET.lua (onArrived): the Arrived message says whether the waypoint is the path's last one, for a moving + // platform as for an object following waypoints (the script reads isLastPoint, which the client's PushToLua + // 0x00b1a910 sets from the path and the waypoint) + const auto lastWaypoint = static_cast(path->pathWaypoints.size() - 1); + if (movingPlatform) { + movingPlatform->AddPlatformAtLastWaypointCallback([this, self, enemy, name, silentDeath]() { + TargetFinishedPath(self, enemy, name, silentDeath); + }); + } else if (movementAI) { + movementAI->AddWaypointReachedCallback([this, self, enemy, name, silentDeath, lastWaypoint](const uint32_t waypoint) { + if (waypoint == lastWaypoint) TargetFinishedPath(self, enemy, name, silentDeath); + }); + } + // Save the enemy if (enemy != nullptr) { const_cast&>(self->GetVar>(SpawnedObjects)).push_back(enemy->GetObjectID()); @@ -684,6 +701,37 @@ void SGCannon::OnActivityNotify(Entity* self, GameMessages::ActivityNotify& noti self->SetVar>(u"CannonBallKills", {}); } +void SGCannon::CheckSpawn(Entity* self, Entity* target, const std::string& timerName) { + if (!self->GetVar(GameStartedVariable)) return; + const auto& spawnInfo = target->GetVar(u"SpawnData"); + + if (spawnInfo.respawns && target->GetVar(u"wave") == self->GetVar(ThisWaveVariable)) { + const auto respawnTime = GeneralUtils::GenerateRandomNumber(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime); + + ActivityTimerStart(self, timerName, respawnTime, respawnTime); + } +} + +// SG_TARGET.lua: at the last waypoint of its path a target notifies the cannon (FinishedPath: sg_cannon.lua spawns the +// next one as after a hit, without a score) and, when it is to despawn there, dies silently 1 s later, a moving +// platform after 2 s (so the clients' platforms get to the end of the path first). +void SGCannon::TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr& silentDeath) { + // Once: a platform sent on would tell again + if (target->GetVar(u"FinishedPath")) return; + target->SetVar(u"FinishedPath", true); + + CheckSpawn(self, target, timerName); + + const auto& spawnInfo = target->GetVar(u"SpawnData"); + if (!spawnInfo.despawnOnLastWaypoint) return; + target->AddCallbackTimer(spawnInfo.isMovingPlatform ? 2.0f : 1.0f, [target, silentDeath]() { + // Hit in the meantime + if (target->GetIsDead()) return; + *silentDeath = true; + target->Smash(target->GetObjectID(), eKillType::SILENT); + }); +} + void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& timerName) { if (!self->GetVar(GameStartedVariable)) return; @@ -692,11 +740,7 @@ void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& time self->SetVar>(u"CannonBallKills", cannonBallKills); const auto& spawnInfo = target->GetVar(u"SpawnData"); - if (spawnInfo.respawns && target->GetVar(u"wave") == self->GetVar(ThisWaveVariable)) { - const auto respawnTime = GeneralUtils::GenerateRandomNumber(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime); - - ActivityTimerStart(self, timerName, respawnTime, respawnTime); - } + CheckSpawn(self, target, timerName); int32_t score = spawnInfo.score; diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h index 3c9ce83bc..1d810a562 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "ActivityManager.h" @@ -89,6 +90,10 @@ private: void PauseChargeCannon(Entity* self); void StopGame(Entity* self, bool cancel = false); void RegisterHit(Entity* self, Entity* target, const std::string& timerName); + // sg_cannon.lua (checkSpawn): a target that respawns gets its next spawn, while its wave runs + void CheckSpawn(Entity* self, Entity* target, const std::string& timerName); + // SG_TARGET.lua (onArrived at the last waypoint): the cannon spawns the next one, and the target is gone a moment later + void TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr& silentDeath); void UpdateStreak(Entity* self); float_t GetCurrentBonus(Entity* self);