mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
feat(missions): keep the viewed flag of missions in progress, as the client
The client keeps a viewed flag on each mission in progress, sets it with SetMissionViewed (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0, only for missions in progress) and reads it as v on the mission's <cur> entry (LWOMissionComponent::LoadFromSaveData 0x00d171c0, a number or "true", false when missing). The server now handles SetMissionViewed, saves v="1" for a viewed mission in progress and loads it back; saves without v load as not viewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -618,6 +618,11 @@ void MissionComponent::SetMissionTypeState(const std::string& type, const std::s
|
||||
m_MissionTypeStates[type][subtype] = static_cast<eMissionLockState>(static_cast<int8_t>(state));
|
||||
}
|
||||
|
||||
void MissionComponent::SetMissionViewed(const uint32_t missionId, const bool viewed) {
|
||||
auto* const mission = GetMission(missionId);
|
||||
if (mission && !mission->IsComplete()) mission->SetViewed(viewed);
|
||||
}
|
||||
|
||||
void MissionComponent::AddCollectible(int32_t collectibleID) {
|
||||
// Check if this collectible is already in the list
|
||||
if (HasCollectible(collectibleID)) {
|
||||
|
||||
@@ -195,6 +195,10 @@ public:
|
||||
using MissionTypeStates = std::map<std::string, std::map<std::string, eMissionLockState>>;
|
||||
void SetMissionTypeState(const std::string& type, const std::string& subtype, eMissionLockState state);
|
||||
const MissionTypeStates& GetMissionTypeStates() const { return m_MissionTypeStates; }
|
||||
|
||||
// Marks a mission in progress as viewed or not (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0); missions not
|
||||
// in progress are left alone, as the client
|
||||
void SetMissionViewed(uint32_t missionId, bool viewed);
|
||||
private:
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
|
||||
bool OnGetMissionState(GameMessages::GetMissionState& getMissionState);
|
||||
|
||||
@@ -130,6 +130,7 @@ json ToJson(const GameMessages::SetFlag& m);
|
||||
json ToJson(const GameMessages::NotifyClientFlagChange& m);
|
||||
json ToJson(const GameMessages::HasBeenCollected& m);
|
||||
json ToJson(const GameMessages::SetMissionTypeState& m);
|
||||
json ToJson(const GameMessages::SetMissionViewed& m);
|
||||
json ToJson(const GameMessages::NotifyLevelRewards& m);
|
||||
json ToJson(const GameMessages::Teleport& m);
|
||||
json ToJson(const GameMessages::StartPathing& m);
|
||||
@@ -1319,6 +1320,12 @@ json ToJson(const GameMessages::SetMissionTypeState& m) {
|
||||
j["type"] = ToJson(m.type);
|
||||
return j;
|
||||
}
|
||||
json ToJson(const GameMessages::SetMissionViewed& m) {
|
||||
json j = json::object();
|
||||
j["missionID"] = ToJson(m.missionID);
|
||||
j["viewed"] = ToJson(m.viewed);
|
||||
return j;
|
||||
}
|
||||
json ToJson(const GameMessages::NotifyLevelRewards& m) {
|
||||
json j = json::object();
|
||||
j["level"] = ToJson(m.level);
|
||||
@@ -2643,6 +2650,7 @@ const std::vector<Entry>& Entries() {
|
||||
{ MessageType::Game::NOTIFY_CLIENT_FLAG_CHANGE, eDirection::TO_CLIENT, "NotifyClientFlagChange", &ReadWith<GameMessages::NotifyClientFlagChange> },
|
||||
{ MessageType::Game::HAS_BEEN_COLLECTED, eDirection::TO_SERVER, "HasBeenCollected", &ReadWith<GameMessages::HasBeenCollected> },
|
||||
{ MessageType::Game::SET_MISSION_TYPE_STATE, eDirection::TO_SERVER, "SetMissionTypeState", &ReadWith<GameMessages::SetMissionTypeState> },
|
||||
{ MessageType::Game::SET_MISSION_VIEWED, eDirection::TO_SERVER, "SetMissionViewed", &ReadWith<GameMessages::SetMissionViewed> },
|
||||
{ MessageType::Game::NOTIFY_LEVEL_REWARDS, eDirection::TO_CLIENT, "NotifyLevelRewards", &ReadWith<GameMessages::NotifyLevelRewards> },
|
||||
{ MessageType::Game::TELEPORT, eDirection::TO_CLIENT, "Teleport", &ReadWith<GameMessages::Teleport> },
|
||||
{ MessageType::Game::START_PATHING, eDirection::TO_CLIENT, "StartPathing", &ReadWith<GameMessages::StartPathing> },
|
||||
|
||||
@@ -82,6 +82,7 @@ namespace {
|
||||
{ SET_FLAG, []() { return std::make_unique<SetFlag>(); } },
|
||||
{ HAS_BEEN_COLLECTED, []() { return std::make_unique<HasBeenCollected>(); } },
|
||||
{ SET_MISSION_TYPE_STATE, []() { return std::make_unique<SetMissionTypeState>(); } },
|
||||
{ SET_MISSION_VIEWED, []() { return std::make_unique<SetMissionViewed>(); } },
|
||||
|
||||
// Effects, emotes, cinematics, UI
|
||||
{ PLAY_EMOTE, []() { return std::make_unique<PlayEmote>(); } },
|
||||
|
||||
@@ -273,6 +273,22 @@ namespace GameMessages {
|
||||
if (missionComponent) missionComponent->SetMissionTypeState(type, subtype, state);
|
||||
}
|
||||
|
||||
void SetMissionViewed::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(missionID);
|
||||
bitStream.Write(viewed);
|
||||
}
|
||||
|
||||
bool SetMissionViewed::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(missionID));
|
||||
VALIDATE_READ(bitStream.Read(viewed));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetMissionViewed::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
auto* const missionComponent = entity.GetComponent<MissionComponent>();
|
||||
if (missionComponent) missionComponent->SetMissionViewed(missionID, viewed);
|
||||
}
|
||||
|
||||
void NotifyLevelRewards::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(level);
|
||||
bitStream.Write(sendingRewards);
|
||||
|
||||
@@ -149,6 +149,18 @@ namespace GameMessages {
|
||||
std::string type{};
|
||||
};
|
||||
|
||||
// Client -> server. The player looked at a mission in progress (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0);
|
||||
// the server keeps it and saves it as v on the mission's <cur> entry, which the client reads on load.
|
||||
struct SetMissionViewed : public NetGameMsg {
|
||||
SetMissionViewed() : NetGameMsg(MessageType::Game::SET_MISSION_VIEWED) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
|
||||
|
||||
int32_t missionID{};
|
||||
bool viewed{};
|
||||
};
|
||||
|
||||
// Server -> client.
|
||||
struct NotifyLevelRewards : public NetGameMsg {
|
||||
NotifyLevelRewards() : NetGameMsg(MessageType::Game::NOTIFY_LEVEL_REWARDS) {}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "Mission.h"
|
||||
#include "ChatServerLink.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
|
||||
#include "CDClientManager.h"
|
||||
@@ -37,6 +40,17 @@
|
||||
|
||||
namespace {
|
||||
std::set<uint32_t> g_TestedMissions = { 773, 774, 775, 776, 777 }; // TODO Figure out why these missions are broken sometimes
|
||||
|
||||
// The client's save data bool: a non-zero number or "true" in any case
|
||||
bool ReadSaveBool(const tinyxml2::XMLElement& element, const char* const name) {
|
||||
const char* const value = element.Attribute(name);
|
||||
if (!value) return false;
|
||||
if (std::strtol(value, nullptr, 10) != 0) return true;
|
||||
|
||||
std::string text = value;
|
||||
std::ranges::transform(text, text.begin(), [](const unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return text == "true";
|
||||
}
|
||||
}
|
||||
|
||||
Mission::Mission(MissionComponent* missionComponent, const uint32_t missionId) {
|
||||
@@ -94,6 +108,9 @@ void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) {
|
||||
}
|
||||
// End custom XML
|
||||
|
||||
// LWOMissionComponent::LoadFromSaveData (0x00d171c0): v is the viewed flag, false when missing
|
||||
m_Viewed = ReadSaveBool(element, "v");
|
||||
|
||||
auto* task = element.FirstChildElement();
|
||||
|
||||
auto index = 0U;
|
||||
@@ -164,6 +181,8 @@ void Mission::UpdateXmlCur(tinyxml2::XMLElement& element) {
|
||||
|
||||
if (IsComplete()) return;
|
||||
|
||||
if (m_Viewed) element.SetAttribute("v", 1);
|
||||
|
||||
for (const auto* const task : m_Tasks) {
|
||||
auto* const child = element.InsertNewChildElement("sv");
|
||||
child->SetAttribute("v", static_cast<unsigned int>(task->GetProgress()));
|
||||
|
||||
@@ -246,6 +246,13 @@ public:
|
||||
|
||||
bool IsFailed() const;
|
||||
|
||||
/**
|
||||
* Whether the player has looked at this mission in progress (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0),
|
||||
* saved as v on the mission's <cur> entry
|
||||
*/
|
||||
bool GetViewed() const { return m_Viewed; }
|
||||
void SetViewed(const bool viewed) { m_Viewed = viewed; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Progresses all the newly accepted tasks for this mission after it has been accepted to reflect the state of the
|
||||
@@ -292,6 +299,11 @@ private:
|
||||
* The unique ID what order this mission was accepted in.
|
||||
*/
|
||||
uint32_t m_UniqueMissionID;
|
||||
|
||||
/**
|
||||
* Whether the player has looked at this mission in progress
|
||||
*/
|
||||
bool m_Viewed{ false };
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,6 +15,7 @@ set(DCOMPONENTS_TESTS
|
||||
"CharacterSaveFieldsTests.cpp"
|
||||
"CollectibleComponentTests.cpp"
|
||||
"MissionOfferTests.cpp"
|
||||
"MissionClientRulesTests.cpp"
|
||||
"ReplicaComponentOrderTests.cpp"
|
||||
"QuickBuildCompleteTests.cpp"
|
||||
"RocketLaunchTests.cpp"
|
||||
|
||||
112
tests/dGameTests/dComponentsTests/MissionClientRulesTests.cpp
Normal file
112
tests/dGameTests/dComponentsTests/MissionClientRulesTests.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// Mission rules the 1.10.64 client works by (LWOMissionComponent): the viewed flag, prerequisites, cooldowns and
|
||||
// turning a mission in.
|
||||
#include "GameDependencies.h"
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "CDMissionsTable.h"
|
||||
#include "Entity.h"
|
||||
#include "GameMessages.h"
|
||||
#include "Mission.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "MissionMessages.h"
|
||||
#include "eMissionState.h"
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
// Mission IDs only these tests use (the prerequisite cache is static)
|
||||
constexpr uint32_t VIEWED = 992001;
|
||||
constexpr uint32_t NOT_VIEWED = 992002;
|
||||
constexpr uint32_t FINISHED = 992003;
|
||||
|
||||
CDMissions Row(const uint32_t id, const bool repeatable = false, const std::string& prereq = "", const int64_t cooldown = -1) {
|
||||
CDMissions row{};
|
||||
row.id = id;
|
||||
row.isMission = true;
|
||||
row.repeatable = repeatable;
|
||||
row.prereqMissionID = prereq;
|
||||
row.cooldownTime = cooldown;
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
class MissionClientRulesTests : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);");
|
||||
auto& missions = CDClientManager::GetEntriesMutable<CDMissionsTable>();
|
||||
for (const auto id : { VIEWED, NOT_VIEWED, FINISHED }) missions.push_back(Row(id));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
CDClientManager::GetEntriesMutable<CDMissionsTable>().clear();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
static void Load(MissionComponent& missions, const std::string& done, const std::string& cur) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto xml = R"(<obj v="1"><mis><done>)" + done + "</done><cur>" + cur + "</cur></mis></obj>";
|
||||
ASSERT_EQ(doc.Parse(xml.c_str()), tinyxml2::XML_SUCCESS);
|
||||
missions.LoadFromSaveData(doc);
|
||||
}
|
||||
|
||||
static std::string Save(MissionComponent& missions) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
doc.Parse(R"(<obj v="1"/>)");
|
||||
missions.WriteSaveData(doc);
|
||||
tinyxml2::XMLPrinter printer(nullptr, true);
|
||||
doc.Print(&printer);
|
||||
return printer.CStr();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MissionClientRulesTests, ViewedFlagIsSavedAndLoaded) {
|
||||
Entity player(42, info);
|
||||
auto* const missions = player.AddComponent<MissionComponent>(-1);
|
||||
// Saves from before the flag have no v; the client reads a number or "true"
|
||||
Load(*missions, R"(<m id="992003" state="8" cct="1" cts="1000"/>)",
|
||||
R"(<m id="992001" state="2" v="true"/><m id="992002" state="2"/>)");
|
||||
EXPECT_TRUE(missions->GetMission(VIEWED)->GetViewed());
|
||||
EXPECT_FALSE(missions->GetMission(NOT_VIEWED)->GetViewed());
|
||||
|
||||
// SetMissionViewed (0x00bec7a0) only changes missions in progress
|
||||
GameMessages::SetMissionViewed message;
|
||||
message.missionID = NOT_VIEWED;
|
||||
message.viewed = true;
|
||||
message.Handle(player, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
missions->SetMissionViewed(FINISHED, true);
|
||||
EXPECT_TRUE(missions->GetMission(NOT_VIEWED)->GetViewed());
|
||||
EXPECT_FALSE(missions->GetMission(FINISHED)->GetViewed());
|
||||
|
||||
missions->SetMissionViewed(VIEWED, false);
|
||||
const auto saved = Save(*missions);
|
||||
EXPECT_NE(saved.find(R"(id="992002" v="1")"), std::string::npos) << saved;
|
||||
EXPECT_EQ(saved.find(R"(id="992001" v=)"), std::string::npos) << saved;
|
||||
const auto done = saved.substr(saved.find("<done>"), saved.find("<cur>") - saved.find("<done>"));
|
||||
EXPECT_EQ(done.find(" v="), std::string::npos) << saved;
|
||||
|
||||
// And back
|
||||
Entity reloaded(43, info);
|
||||
auto* const again = reloaded.AddComponent<MissionComponent>(-1);
|
||||
Load(*again, "", saved.substr(saved.find("<cur>") + 5, saved.find("</cur>") - saved.find("<cur>") - 5));
|
||||
EXPECT_TRUE(again->GetMission(NOT_VIEWED)->GetViewed());
|
||||
EXPECT_FALSE(again->GetMission(VIEWED)->GetViewed());
|
||||
}
|
||||
|
||||
TEST_F(MissionClientRulesTests, SetMissionViewedWire) {
|
||||
GameMessages::SetMissionViewed out;
|
||||
out.missionID = 1234;
|
||||
out.viewed = true;
|
||||
RakNet::BitStream stream;
|
||||
out.Serialize(stream);
|
||||
GameMessages::SetMissionViewed in;
|
||||
ASSERT_TRUE(in.Deserialize(stream));
|
||||
EXPECT_EQ(in.missionID, 1234);
|
||||
EXPECT_TRUE(in.viewed);
|
||||
EXPECT_EQ(stream.GetNumberOfBitsUsed(), 33);
|
||||
}
|
||||
Reference in New Issue
Block a user