diff --git a/dDatabase/CDClientDatabase/CDClientManager.cpp b/dDatabase/CDClientDatabase/CDClientManager.cpp index b5990a4f9..7d720b9ac 100644 --- a/dDatabase/CDClientDatabase/CDClientManager.cpp +++ b/dDatabase/CDClientDatabase/CDClientManager.cpp @@ -22,6 +22,7 @@ #include "CDObjectsTable.h" #include "CDPhysicsComponentTable.h" #include "CDRebuildComponentTable.h" +#include "CDCollectibleComponentTable.h" #include "CDScriptComponentTable.h" #include "CDSkillBehaviorTable.h" #include "CDZoneTableTable.h" @@ -100,6 +101,7 @@ DEFINE_TABLE_STORAGE(CDModularBuildComponentTable); DEFINE_TABLE_STORAGE(CDRailActivatorComponentTable); DEFINE_TABLE_STORAGE(CDRarityTableTable); DEFINE_TABLE_STORAGE(CDRebuildComponentTable); +DEFINE_TABLE_STORAGE(CDCollectibleComponentTable); DEFINE_TABLE_STORAGE(CDRewardCodesTable); DEFINE_TABLE_STORAGE(CDRewardsTable); DEFINE_TABLE_STORAGE(CDScriptComponentTable); @@ -118,6 +120,7 @@ void CDClientManager::LoadValuesFromDatabase() { CDBehaviorParameterTable::Instance().LoadValuesFromDatabase(); CDBehaviorTemplateTable::Instance().LoadValuesFromDatabase(); CDBrickIDTableTable::Instance().LoadValuesFromDatabase(); + CDCollectibleComponentTable::Instance().LoadValuesFromDatabase(); CDCLIENT_DONT_CACHE_TABLE(CDComponentsRegistryTable::Instance().LoadValuesFromDatabase()); CDCurrencyTableTable::Instance().LoadValuesFromDatabase(); CDDeletionRestrictionsTable::Instance().LoadValuesFromDatabase(); diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.cpp new file mode 100644 index 000000000..0317a765b --- /dev/null +++ b/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.cpp @@ -0,0 +1,20 @@ +#include "CDCollectibleComponentTable.h" + +void CDCollectibleComponentTable::LoadValuesFromDatabase() { + auto& entries = GetEntriesMutable(); + auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM CollectibleComponent"); + while (!tableData.eof()) { + CDCollectibleComponent entry; + entry.id = tableData.getIntField("id", -1); + entry.requirementMission = tableData.getIntField("requirement_mission", -1); + entries.insert_or_assign(entry.id, entry); + tableData.nextRow(); + } + tableData.finalize(); +} + +const CDCollectibleComponent* CDCollectibleComponentTable::GetByID(const int32_t id) const { + const auto& entries = GetEntries(); + const auto it = entries.find(id); + return it != entries.end() ? &it->second : nullptr; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.h new file mode 100644 index 000000000..c7387752e --- /dev/null +++ b/dDatabase/CDClientDatabase/CDClientTables/CDCollectibleComponentTable.h @@ -0,0 +1,25 @@ +#pragma once + +#include "CDTable.h" + +#include +#include + +/** + * CollectibleComponent: read only by the server (the 1.10.64 client has no string for the table). + * requirement_mission is the mission a collectible belongs to, usually the achievement its collection task is in; for + * some it is a mission that must be accepted first (e.g. 2040 for the Ninjago dragon relics, which the hidden + * achievements 2064-2067 collect). -1 when the row has none. + */ +struct CDCollectibleComponent { + int32_t id{}; + int32_t requirementMission{ -1 }; +}; + +class CDCollectibleComponentTable : public CDTable> { +public: + void LoadValuesFromDatabase(); + + // The row for a component ID, or nothing + [[nodiscard]] const CDCollectibleComponent* GetByID(int32_t id) const; +}; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CMakeLists.txt b/dDatabase/CDClientDatabase/CDClientTables/CMakeLists.txt index 81810c2a1..0767ab663 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CMakeLists.txt +++ b/dDatabase/CDClientDatabase/CDClientTables/CMakeLists.txt @@ -4,6 +4,7 @@ set(DDATABASE_CDCLIENTDATABASE_CDCLIENTTABLES_SOURCES "CDActivitiesTable.cpp" "CDBehaviorParameterTable.cpp" "CDBehaviorTemplateTable.cpp" "CDBrickIDTableTable.cpp" + "CDCollectibleComponentTable.cpp" "CDComponentsRegistryTable.cpp" "CDCurrencyTableTable.cpp" "CDDestructibleComponentTable.cpp" diff --git a/dGame/dComponents/CollectibleComponent.cpp b/dGame/dComponents/CollectibleComponent.cpp index 10b50c2d3..962fed6a4 100644 --- a/dGame/dComponents/CollectibleComponent.cpp +++ b/dGame/dComponents/CollectibleComponent.cpp @@ -3,10 +3,36 @@ #include "MissionComponent.h" #include "dServer.h" #include "Amf3.h" +#include "CDClientManager.h" +#include "CDCollectibleComponentTable.h" +#include "CDMissionsTable.h" +#include "eMissionState.h" CollectibleComponent::CollectibleComponent(Entity* parentEntity, const int32_t componentID, const int32_t collectibleId) : Component(parentEntity, componentID), m_CollectibleId(collectibleId) { RegisterMsg(&CollectibleComponent::MsgGetObjectReportInfo); + const auto* const row = CDClientManager::GetTable()->GetByID(componentID); + if (row) m_RequirementMission = row->requirementMission; +} + +bool CollectibleComponent::CountsFor(const Entity& player) const { + if (m_RequirementMission <= 0) return true; + bool found = false; + const auto& mission = CDClientManager::GetTable()->GetByMissionID(m_RequirementMission, found); + // Not a mission (rows with 66666666), or an achievement: nothing to accept first + if (!found || !mission.isMission) return true; + + const auto* const missionComponent = player.GetComponent(); + if (!missionComponent) return false; + switch (missionComponent->GetMissionState(m_RequirementMission)) { + case eMissionState::ACTIVE: + case eMissionState::READY_TO_COMPLETE: + case eMissionState::COMPLETE_ACTIVE: + case eMissionState::COMPLETE_READY_TO_COMPLETE: + return true; + default: + return false; + } } void CollectibleComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { @@ -20,6 +46,7 @@ bool CollectibleComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportI cmptType.PushDebug("Component ID") = GetComponentID(); cmptType.PushDebug("Collectible ID") = GetCollectibleId(); + cmptType.PushDebug("Requirement mission") = m_RequirementMission; cmptType.PushDebug("Mission Tracking ID (for save data)") = collectibleID; auto* localCharEntity = Game::entityManager->GetEntity(reportMsg.clientID); diff --git a/dGame/dComponents/CollectibleComponent.h b/dGame/dComponents/CollectibleComponent.h index 5c9b5ea05..09d08fe99 100644 --- a/dGame/dComponents/CollectibleComponent.h +++ b/dGame/dComponents/CollectibleComponent.h @@ -13,8 +13,20 @@ public: void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) override; bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + /** + * Whether a player's collecting it counts. The server-only CollectibleComponent.requirement_mission names the + * mission the collectible belongs to: when that is a mission to accept (Missions.isMission, not an achievement, + * which is always open), the collectible only counts while the player has it accepted and not handed in. + */ + bool CountsFor(const Entity& player) const; + + int32_t GetRequirementMission() const { return m_RequirementMission; } private: int16_t m_CollectibleId = 0; + + // CollectibleComponent.requirement_mission, -1 for none + int32_t m_RequirementMission = -1; }; #endif //!__COLLECTIBLECOMPONENT__H__ diff --git a/dGame/dGameMessages/MissionMessages.cpp b/dGame/dGameMessages/MissionMessages.cpp index c576f4cfc..9f9d71654 100644 --- a/dGame/dGameMessages/MissionMessages.cpp +++ b/dGame/dGameMessages/MissionMessages.cpp @@ -14,6 +14,7 @@ #include "Game.h" #include "Logger.h" #include "Mission.h" +#include "CollectibleComponent.h" #include "MissionComponent.h" #include "MissionOfferComponent.h" #include "EffectsMessages.h" @@ -242,6 +243,10 @@ namespace GameMessages { Entity* player = Game::entityManager->GetEntity(playerID); if (!player || entity.GetCollectibleID() == 0) return; + // Collectibles of a mission to accept only count while the player has it + const auto* const collectible = entity.GetComponent(); + if (collectible && !collectible->CountsFor(*player)) return; + MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); if (missionComponent) { missionComponent->Progress(eMissionTaskType::COLLECTION, entity.GetLOT(), entity.GetObjectID()); diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index e57c616df..5161ed7e7 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -11,6 +11,7 @@ set(DCOMPONENTS_TESTS "SimplePhysicsComponentTests.cpp" "SavingTests.cpp" "CharacterSaveFieldsTests.cpp" + "CollectibleComponentTests.cpp" "ReplicaComponentOrderTests.cpp" ) diff --git a/tests/dGameTests/dComponentsTests/CollectibleComponentTests.cpp b/tests/dGameTests/dComponentsTests/CollectibleComponentTests.cpp new file mode 100644 index 000000000..be93c527f --- /dev/null +++ b/tests/dGameTests/dComponentsTests/CollectibleComponentTests.cpp @@ -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 + +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(); + collectibles[78] = { 78, 2040 }; + collectibles[12] = { 12, 12 }; + collectibles[45] = { 45, 66666666 }; + collectibles[1] = { 1, -1 }; + auto& missions = CDClientManager::GetEntriesMutable(); + 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().clear(); + CDClientManager::GetEntriesMutable().clear(); + TearDownDependencies(); + } + + static void GiveMission(MissionComponent& missions, const char* state) { + tinyxml2::XMLDocument doc; + const std::string xml = std::string(R"()"; + 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(78, 5)->GetRequirementMission(), 2040); + Entity other(41, info); + EXPECT_EQ(other.AddComponent(999, 5)->GetRequirementMission(), -1); // no row +} + +TEST_F(CollectibleComponentTests, AchievementsAndMissingMissionsAlwaysCount) { + Entity player(42, info); + player.AddComponent(-1); + for (const int32_t component : { 12, 45, 1, 999 }) { + Entity object(43, info); + EXPECT_TRUE(object.AddComponent(component, 5)->CountsFor(player)) << component; + } +} + +TEST_F(CollectibleComponentTests, MissionCollectiblesCountOnlyWhileTheMissionIsAccepted) { + Entity relic(44, info); + const auto* const collectible = relic.AddComponent(78, 5); + + Entity player(45, info); + auto* const missions = player.AddComponent(-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)); +}