From 9b386caf1e53391fd68e666c5a085fd9e848385c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 15:55:07 -0600 Subject: [PATCH] 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 --- .../RocketLaunchpadControlComponent.cpp | 17 ++++ .../RocketLaunchpadControlComponent.h | 8 +- .../dComponentsTests/RocketLaunchTests.cpp | 82 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/RocketLaunchpadControlComponent.cpp b/dGame/dComponents/RocketLaunchpadControlComponent.cpp index 3b8a5024a..3ebc7a145 100644 --- a/dGame/dComponents/RocketLaunchpadControlComponent.cpp +++ b/dGame/dComponents/RocketLaunchpadControlComponent.cpp @@ -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(u"rocketLaunchUseDBDefaults")) return; + + if (m_Parent->HasVar(u"targetZone")) m_TargetZone = m_Parent->GetVarAs(u"targetZone"); + m_DefaultZone = m_Parent->GetVarAs(u"propertyDefaultTargetZone"); + m_TargetScene = m_Parent->GetVarAsString(u"targetScene"); + m_GMLevel = m_Parent->GetVarAs(u"GMLevel"); + m_LaunchPrecondition = m_Parent->GetVarAs(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; } diff --git a/dGame/dComponents/RocketLaunchpadControlComponent.h b/dGame/dComponents/RocketLaunchpadControlComponent.h index 13ddf856b..a4e72b81a 100644 --- a/dGame/dComponents/RocketLaunchpadControlComponent.h +++ b/dGame/dComponents/RocketLaunchpadControlComponent.h @@ -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; diff --git a/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp b/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp index b5f3a87c2..a0aa844d9 100644 --- a/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp +++ b/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp @@ -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(u"rocketLaunchUseDBDefaults", false); + pad.SetVar(u"targetZone", 1100); + pad.SetVar(u"targetScene", u"AGLanding"); + pad.SetVar(u"GMLevel", 0); + pad.SetVar(u"rocketUseLaunchPrecondition", true); + pad.SetVar(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(3); + + Entity player(PLAYER, info); + // The row alone asks for nothing + Entity rowPad(PAD + 2, info); + EXPECT_TRUE(rowPad.AddComponent(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(u"rocketLaunchUseDBDefaults", false); + gmPad.SetVar(u"targetZone", 1100); + gmPad.SetVar(u"GMLevel", 9); + auto* const gmOnly = gmPad.AddComponent(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(u"rocketLaunchUseDBDefaults", false); + pad.SetVar(u"targetZone", 1450); + pad.SetVar(u"rocketUseLaunchPrecondition", false); + pad.SetVar(u"rocketLaunchPreCondition", u"0"); + auto* const component = pad.AddComponent(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(u"rocketLaunchUseDBDefaults", true); + pad.SetVar(u"targetZone", 1400); + pad.SetVar(u"targetScene", u"FVGate"); + pad.SetVar(u"GMLevel", 9); + pad.SetVar(u"rocketUseLaunchPrecondition", false); + auto* const component = pad.AddComponent(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" + } +}