fix: a launch pad that does not use its table row is ruled by its level

LWORocketLaunchComponentCommon::LoadConfigData (0x00c55150) reads the
level's targetZone, targetScene, GMLevel, propertyDefaultTargetZone,
rocketLaunchPreCondition, rocketUseLaunchPrecondition and the alternative
landing keys, and the table row replaces them only while
rocketLaunchUseDBDefaults is true, its default (0x00c12ea0). The server
used the row for every pad.

31 pads in the 1.10.64 maps turn the key off. Their zones and scenes are
their rows'; what only the level has are the launch preconditions in use
on the Venture Explorer's four pads ("237;8;127|128;6|55;107;282") and
on Avant Gardens' pads to Nimbus Station, its two properties and the
Venture Explorer. Those are now checked before a launch, as the row's are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 15:55:07 -06:00
parent 02623be7fb
commit 9b386caf1e
3 changed files with 105 additions and 2 deletions

View File

@@ -48,6 +48,23 @@ void RocketLaunchpadControlComponent::LoadDataFromTemplate() {
result.finalize();
}
// The level's keys. LWORocketLaunchComponentCommon::LoadConfigData (0x00c55150) reads them into the fields the row
// would fill, and the row replaces them only while rocketLaunchUseDBDefaults is true, its default (0x00c12ea0). A pad
// that turns it off is ruled by its level: 31 pads in the 1.10.64 maps, whose zones and scenes are their rows' but
// whose launch preconditions only the level has (the Venture Explorer's and Avant Gardens' pads).
void RocketLaunchpadControlComponent::LoadConfigData() {
if (!m_Parent->HasVar(u"rocketLaunchUseDBDefaults") || m_Parent->GetVarAs<bool>(u"rocketLaunchUseDBDefaults")) return;
if (m_Parent->HasVar(u"targetZone")) m_TargetZone = m_Parent->GetVarAs<int32_t>(u"targetZone");
m_DefaultZone = m_Parent->GetVarAs<int32_t>(u"propertyDefaultTargetZone");
m_TargetScene = m_Parent->GetVarAsString(u"targetScene");
m_GMLevel = m_Parent->GetVarAs<int32_t>(u"GMLevel");
m_LaunchPrecondition = m_Parent->GetVarAs<bool>(u"rocketUseLaunchPrecondition") ? m_Parent->GetVarAsString(u"rocketLaunchPreCondition") : "";
m_AltLandingScene = m_Parent->GetVarAsString(u"rocketAltLandingSpawnName");
delete m_AltPrecondition;
m_AltPrecondition = new PreconditionExpression(m_Parent->GetVarAsString(u"rocketAltLandingPrecondition"));
}
RocketLaunchpadControlComponent::~RocketLaunchpadControlComponent() {
delete m_AltPrecondition;
}

View File

@@ -28,6 +28,9 @@ public:
~RocketLaunchpadControlComponent() override;
void LoadDataFromTemplate() override;
void LoadConfigData() override;
const std::string& GetTargetScene() const { return m_TargetScene; }
/**
* Whether the player may launch from this pad: LWORocketLaunchComponentCommon refuses a player under the pad's GM
@@ -146,12 +149,13 @@ private:
PreconditionExpression* m_AltPrecondition{};
/**
* The GM level a player needs to launch from this pad (the row's gmLevel)
* The GM level a player needs to launch from this pad (the row's gmLevel, or the level's GMLevel)
*/
int32_t m_GMLevel{};
/**
* The row's launchPrecondition, when its useLaunchPrecondition is set: what a player has to pass to launch
* The row's launchPrecondition, when its useLaunchPrecondition is set: what a player has to pass to launch. For a pad
* whose level turns rocketLaunchUseDBDefaults off, the level's rocketLaunchPreCondition and rocketUseLaunchPrecondition.
*/
std::string m_LaunchPrecondition;

View File

@@ -73,3 +73,85 @@ TEST_F(RocketLaunchTests, APadWithAGmLevelLaunchesOnlyThoseAtIt) {
player.SetGMLevel(eGameMasterLevel::OPERATOR);
EXPECT_TRUE(gmOnly->MayLaunch(player));
}
namespace {
void MakePadTables() {
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);");
// Row 3 is the Venture Explorer pad's (LOT 14214): no precondition in the table
// No precondition is known: one that is asked for fails for a player without components
CDClientDatabase::ExecuteDML("CREATE TABLE Preconditions (id INTEGER, type INTEGER, targetLOT TEXT, targetGroup TEXT, targetCount INTEGER, iconID INTEGER, localize INTEGER, validContexts INTEGER);");
CDClientDatabase::ExecuteDML("INSERT INTO RocketLaunchpadControlComponent VALUES (3, 1100, 0, 'AGLanding', 0, 0, NULL, 0, NULL, NULL);");
// Row 16: a Nimbus Station pad with the precondition in the table
CDClientDatabase::ExecuteDML("INSERT INTO RocketLaunchpadControlComponent VALUES (16, 1300, 0, 'GFLanding', 0, 1, '0', 0, '', '');");
}
}
// A pad whose level sets rocketLaunchUseDBDefaults to false is ruled by the level's keys, not its row:
// LWORocketLaunchComponentCommon::LoadConfigData (0x00c55150) reads them and the row is only loaded while the key is
// true (0x00c12ea0). The Venture Explorer's pads: the row has no precondition, the level has
// "237;8;127|128;6|55;107;282" with rocketUseLaunchPrecondition.
TEST_F(RocketLaunchTests, APadThatDoesNotUseTheTableIsRuledByItsLevel) {
MakePadTables();
Entity pad(PAD, info);
pad.SetVar<bool>(u"rocketLaunchUseDBDefaults", false);
pad.SetVar<int32_t>(u"targetZone", 1100);
pad.SetVar<std::u16string>(u"targetScene", u"AGLanding");
pad.SetVar<int32_t>(u"GMLevel", 0);
pad.SetVar<bool>(u"rocketUseLaunchPrecondition", true);
pad.SetVar<std::u16string>(u"rocketLaunchPreCondition", u"0"); // an empty precondition table: nobody passes "0"
// AddComponent loads the row, then the level's keys
auto* const component = pad.AddComponent<RocketLaunchpadControlComponent>(3);
Entity player(PLAYER, info);
// The row alone asks for nothing
Entity rowPad(PAD + 2, info);
EXPECT_TRUE(rowPad.AddComponent<RocketLaunchpadControlComponent>(3)->MayLaunch(player));
EXPECT_EQ(component->GetTargetZone(), 1100);
EXPECT_EQ(component->GetTargetScene(), "AGLanding");
EXPECT_FALSE(component->MayLaunch(player));
// The level's GM level counts as well (the test pads to the Gnarled Forest and Forbidden Valley properties: 9)
Entity gmPad(PAD + 1, info);
gmPad.SetVar<bool>(u"rocketLaunchUseDBDefaults", false);
gmPad.SetVar<int32_t>(u"targetZone", 1100);
gmPad.SetVar<int32_t>(u"GMLevel", 9);
auto* const gmOnly = gmPad.AddComponent<RocketLaunchpadControlComponent>(3);
EXPECT_FALSE(gmOnly->MayLaunch(player));
player.SetGMLevel(eGameMasterLevel::OPERATOR);
EXPECT_TRUE(gmOnly->MayLaunch(player));
}
// The level's precondition is only used with the level's rocketUseLaunchPrecondition (Nimbus Station's pad to
// Forbidden Valley's property has "115" without it)
TEST_F(RocketLaunchTests, ALevelPreconditionThatIsNotInUseIsNotChecked) {
MakePadTables();
Entity pad(PAD, info);
pad.SetVar<bool>(u"rocketLaunchUseDBDefaults", false);
pad.SetVar<int32_t>(u"targetZone", 1450);
pad.SetVar<bool>(u"rocketUseLaunchPrecondition", false);
pad.SetVar<std::u16string>(u"rocketLaunchPreCondition", u"0");
auto* const component = pad.AddComponent<RocketLaunchpadControlComponent>(3);
Entity player(PLAYER, info);
EXPECT_TRUE(component->MayLaunch(player));
EXPECT_EQ(component->GetTargetZone(), 1450);
}
// With rocketLaunchUseDBDefaults true (the client's default, and 12 pads in the maps say so) the row rules and the
// level's keys are not looked at
TEST_F(RocketLaunchTests, APadThatUsesTheTableIgnoresItsLevelKeys) {
MakePadTables();
for (const bool withKey : { true, false }) {
Entity pad(PAD, info);
if (withKey) pad.SetVar<bool>(u"rocketLaunchUseDBDefaults", true);
pad.SetVar<int32_t>(u"targetZone", 1400);
pad.SetVar<std::u16string>(u"targetScene", u"FVGate");
pad.SetVar<int32_t>(u"GMLevel", 9);
pad.SetVar<bool>(u"rocketUseLaunchPrecondition", false);
auto* const component = pad.AddComponent<RocketLaunchpadControlComponent>(16);
EXPECT_EQ(component->GetTargetZone(), 1300);
EXPECT_EQ(component->GetTargetScene(), "GFLanding");
Entity player(PLAYER, info);
EXPECT_FALSE(component->MayLaunch(player)); // the row's precondition, not the level's "none"
}
}