fix: a launch pad checks its GM level and launch precondition

LWORocketLaunchComponentCommon does not launch a player under the row's
gmLevel (msgFireEventClientSide 0x00cb78b0) or, with useLaunchPrecondition,
one that fails the row's launchPrecondition (msgCheckUseRequirements
0x00c7d0d0). The server launched on any request. Rows: the test launcher
asks for GM level 9, the pads to Gnarled Forest and Forbidden Valley for
mission 474, Starbase 3001's for mission 956.

The alternative landing precondition pointer also starts out null for a
pad without a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 15:14:44 -06:00
parent 38fe226e6f
commit 53968599d2
3 changed files with 52 additions and 2 deletions

View File

@@ -30,7 +30,7 @@ RocketLaunchpadControlComponent::RocketLaunchpadControlComponent(Entity* parent,
// The RocketLaunchpadControlComponent row
void RocketLaunchpadControlComponent::LoadDataFromTemplate() {
auto query = CDClientDatabase::CreatePreppedStmt(
"SELECT targetZone, defaultZoneID, targetScene, altLandingPrecondition, altLandingSpawnPointName FROM RocketLaunchpadControlComponent WHERE id = ?;");
"SELECT targetZone, defaultZoneID, targetScene, altLandingPrecondition, altLandingSpawnPointName, gmLevel, useLaunchPrecondition, launchPrecondition FROM RocketLaunchpadControlComponent WHERE id = ?;");
query.bind(1, m_ComponentID);
auto result = query.execQuery();
@@ -41,6 +41,8 @@ void RocketLaunchpadControlComponent::LoadDataFromTemplate() {
m_TargetScene = result.getStringField("targetScene");
m_AltPrecondition = new PreconditionExpression(result.getStringField("altLandingPrecondition"));
m_AltLandingScene = result.getStringField("altLandingSpawnPointName");
m_GMLevel = result.getIntField("gmLevel", 0);
if (result.getIntField("useLaunchPrecondition", 0) != 0) m_LaunchPrecondition = result.getStringField("launchPrecondition", "");
}
result.finalize();
@@ -57,6 +59,11 @@ GameMessages::FireEventClientSide RocketLaunchpadControlComponent::MakeRocketEqu
return rocketEquipped;
}
bool RocketLaunchpadControlComponent::MayLaunch(Entity& player) const {
if (static_cast<int32_t>(player.GetGMLevel()) < m_GMLevel) return false;
return m_LaunchPrecondition.empty() || PreconditionExpression(m_LaunchPrecondition).Check(&player);
}
void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId, LWOCLONEID cloneId, int32_t worldIndex) {
auto zone = mapId == LWOMAPID_INVALID ? m_TargetZone : mapId;
@@ -64,6 +71,8 @@ void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId,
return;
}
if (!MayLaunch(*originator)) return;
// This also gets triggered by a proximity monitor + item equip, I will set that up when havok is ready
auto* characterComponent = originator->GetComponent<CharacterComponent>();
auto* character = originator->GetCharacter();

View File

@@ -29,6 +29,13 @@ public:
void LoadDataFromTemplate() override;
/**
* Whether the player may launch from this pad: LWORocketLaunchComponentCommon refuses a player under the pad's GM
* level (msgFireEventClientSide 0x00cb78b0) or that fails its launch precondition (msgCheckUseRequirements
* 0x00c7d0d0), so a client does not ask
*/
bool MayLaunch(Entity& player) const;
/**
* Launches some entity to another world
* @param originator the entity to launch
@@ -136,7 +143,17 @@ private:
/**
* Some precondition that needs to be met to trigger the alternative landing scene
*/
PreconditionExpression* m_AltPrecondition;
PreconditionExpression* m_AltPrecondition{};
/**
* The GM level a player needs to launch from this pad (the row's gmLevel)
*/
int32_t m_GMLevel{};
/**
* The row's launchPrecondition, when its useLaunchPrecondition is set: what a player has to pass to launch
*/
std::string m_LaunchPrecondition;
/**
* Notifies the master server to prepare some world for a player to be able to travel to it

View File

@@ -1,6 +1,8 @@
#include "GameDependencies.h"
#include "dGameMessagesTests/GameMessageTestUtils.h"
#include "CDClientDatabase.h"
#include "Entity.h"
#include "ObjectMessages.h"
#include "RocketLaunchpadControlComponent.h"
@@ -49,3 +51,25 @@ TEST_F(RocketLaunchTests, RocketEquippedIsBroadcast) {
ASSERT_EQ(sent.size(), 1u);
EXPECT_TRUE(sent[0].broadcast);
}
// A pad's row can ask for a GM level (the AG test pad: 9); LWORocketLaunchComponentCommon::msgFireEventClientSide
// (0x00cb78b0) does not launch a player under it
TEST_F(RocketLaunchTests, APadWithAGmLevelLaunchesOnlyThoseAtIt) {
CDClientDatabase::Connect(":memory:");
CDClientDatabase::ExecuteDML("CREATE TABLE RocketLaunchpadControlComponent (id INTEGER, targetZone INTEGER, defaultZoneID INTEGER, targetScene TEXT, gmLevel INTEGER, useLaunchPrecondition INTEGER, launchPrecondition TEXT, useAltLandingPrecondition INTEGER, altLandingPrecondition TEXT, altLandingSpawnPointName TEXT);");
CDClientDatabase::ExecuteDML("INSERT INTO RocketLaunchpadControlComponent VALUES (1, 1100, 0, 'AGLaunch', 9, 0, '', 0, '', '');");
CDClientDatabase::ExecuteDML("INSERT INTO RocketLaunchpadControlComponent VALUES (2, 1200, 0, 'NS_NT', 0, 0, '47', 0, '', '');");
Entity gmPad(PAD, info);
auto* const gmOnly = gmPad.AddComponent<RocketLaunchpadControlComponent>(1);
gmOnly->LoadDataFromTemplate();
Entity openPad(PAD + 1, info);
auto* const open = openPad.AddComponent<RocketLaunchpadControlComponent>(2);
open->LoadDataFromTemplate();
Entity player(PLAYER, info);
EXPECT_FALSE(gmOnly->MayLaunch(player));
EXPECT_TRUE(open->MayLaunch(player)); // its precondition is not in use
player.SetGMLevel(eGameMasterLevel::OPERATOR);
EXPECT_TRUE(gmOnly->MayLaunch(player));
}