feat(collectibles): gate collectibles on CollectibleComponent.requirement_mission

CollectibleComponent (79 rows: id, requirement_mission) is only in the
CDClient for the server: the 1.10.64 client has no string for it. DLU
never read it.

What the column holds (1.10.64 CDClient, joined with ComponentsRegistry,
Missions and MissionTasks): for 63 rows it is the mission the collectible
belongs to. Mostly that is the achievement whose collection task
(taskType 3) targets the collectible's LOT (the flags, imagination bricks,
Johnny Thunder collectibles). For a few it is a mission to accept from an
NPC that does not collect the item itself: the four Ninjago dragon relics
(16483-16485) are collected by the hidden achievements 2064-2067 but
their requirement is 2040 (accepted from LOT 13789, "complete 2064-2067").
Other values: -1 and 66666666 (no such mission) on test rows.

So a collectible whose requirement_mission is a mission to accept
(Missions.isMission) now only counts (HasBeenCollected progresses
nothing) while the player has that mission accepted and not handed in
(ACTIVE, READY_TO_COMPLETE or their repeat states). Achievements, missing
missions and rows without one are unchanged. Without this, a player who
had not accepted 2040 could collect the relics early and have 2040
complete as soon as it was accepted. The gate itself is inferred from the
data: the captures show collections but not the live server's check.

The collectible's object report shows the requirement mission.

Check in game: in Ninjago Monastery, before accepting the dragon relic
mission (2040), touch a dragon relic: it does not count; accept 2040 and
collect them: each counts and 2040 completes after the fourth. Flags,
imagination bricks and Johnny Thunder collectibles still count as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:36:14 -05:00
parent 8a8fc263e1
commit 01cc349e76
9 changed files with 183 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ set(DCOMPONENTS_TESTS
"SimplePhysicsComponentTests.cpp"
"SavingTests.cpp"
"CharacterSaveFieldsTests.cpp"
"CollectibleComponentTests.cpp"
"ReplicaComponentOrderTests.cpp"
)

View File

@@ -0,0 +1,89 @@
// CollectibleComponent.requirement_mission (a server-only table): collectibles of a mission to accept only count while
// the player has that mission.
#include "GameDependencies.h"
#include "GameMessages.h"
#include "CDClientDatabase.h"
#include "CDClientManager.h"
#include "CDCollectibleComponentTable.h"
#include "CDMissionsTable.h"
#include "CollectibleComponent.h"
#include "Entity.h"
#include "MissionComponent.h"
#include "tinyxml2.h"
#include <gtest/gtest.h>
class CollectibleComponentTests : public GameDependenciesTest {
protected:
// Rows as in the 1.10.64 CDClient: 78 is the Ice Dragon Relic (requirement 2040, a mission to accept), 12 the Pirate
// Flag (requirement 12, an achievement), 45 a test item (66666666, no such mission), 1 none
void SetUp() override {
SetUpDependencies();
CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements
CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);");
auto& collectibles = CDClientManager::GetEntriesMutable<CDCollectibleComponentTable>();
collectibles[78] = { 78, 2040 };
collectibles[12] = { 12, 12 };
collectibles[45] = { 45, 66666666 };
collectibles[1] = { 1, -1 };
auto& missions = CDClientManager::GetEntriesMutable<CDMissionsTable>();
CDMissions relics{};
relics.id = 2040;
relics.isMission = true;
missions.push_back(relics);
CDMissions flags{};
flags.id = 12;
flags.isMission = false;
missions.push_back(flags);
}
void TearDown() override {
CDClientManager::GetEntriesMutable<CDCollectibleComponentTable>().clear();
CDClientManager::GetEntriesMutable<CDMissionsTable>().clear();
TearDownDependencies();
}
static void GiveMission(MissionComponent& missions, const char* state) {
tinyxml2::XMLDocument doc;
const std::string xml = std::string(R"(<obj v="1"><mis><done/><cur><m id="2040" state=")") + state + R"("/></cur></mis></obj>)";
ASSERT_EQ(doc.Parse(xml.c_str()), tinyxml2::XML_SUCCESS);
missions.LoadFromXml(doc);
}
};
TEST_F(CollectibleComponentTests, RequirementMissionIsRead) {
Entity object(40, info);
EXPECT_EQ(object.AddComponent<CollectibleComponent>(78, 5)->GetRequirementMission(), 2040);
Entity other(41, info);
EXPECT_EQ(other.AddComponent<CollectibleComponent>(999, 5)->GetRequirementMission(), -1); // no row
}
TEST_F(CollectibleComponentTests, AchievementsAndMissingMissionsAlwaysCount) {
Entity player(42, info);
player.AddComponent<MissionComponent>(-1);
for (const int32_t component : { 12, 45, 1, 999 }) {
Entity object(43, info);
EXPECT_TRUE(object.AddComponent<CollectibleComponent>(component, 5)->CountsFor(player)) << component;
}
}
TEST_F(CollectibleComponentTests, MissionCollectiblesCountOnlyWhileTheMissionIsAccepted) {
Entity relic(44, info);
const auto* const collectible = relic.AddComponent<CollectibleComponent>(78, 5);
Entity player(45, info);
auto* const missions = player.AddComponent<MissionComponent>(-1);
EXPECT_FALSE(collectible->CountsFor(player)); // never offered
GiveMission(*missions, "2"); // ACTIVE
EXPECT_TRUE(collectible->CountsFor(player));
GiveMission(*missions, "10"); // COMPLETE_ACTIVE (a repeat)
EXPECT_TRUE(collectible->CountsFor(player));
GiveMission(*missions, "1"); // AVAILABLE: offered, not accepted
EXPECT_FALSE(collectible->CountsFor(player));
Entity noMissions(46, info);
EXPECT_FALSE(collectible->CountsFor(noMissions));
}