mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Every game message still written or read by hand is now a NetGameMsg with Serialize and Deserialize, in per-domain files: - MovementMessages: teleport, platforms (resync and its request), orient to angle, node rotation lock, gravity scale, jetpack mode, control scheme, respawn checkpoint, rails (set/start, ready, cancel, arrived), mount inventory ID, dismount complete, possession ack, ghost reference override and position, camera cycling (eCameraTargetCyclingMode moves here). - ZoneMessages: player loaded (the old PLAYER_LOADED case), ready for updates, player ready, restore to post load stats, server done loading, invalid zone transfer list, zone summary display/dismissed, level processing complete, object world state, and the localized announcement WorldMigration wrote by hand. - PlayerMessages: chat mode, GM level, LEGO score, currency, reputation, GM invis, pickup currency, zone and player statistics, chat commands, bug reports, verify ack. - ObjectMessages: fire event client/server side, notify client (zone) object, notify object, script network vars, failed preconditions, terminate interaction, set name, request use, request server object info. - QuickBuildMessages: notify state, enable, cancel. - ActivityMessages gains match response/update/request, leaderboard request and data, shooting gallery score/rotation/fire, activity state change. - MissionMessages gains MissionDialogueCancelled (a no-op, as before). The wire structs left in GameMessages.h move to their domains (tooltip and emote to Effects, loot and item use to Inventory, model build to Building, behavior sound to Property, skill sets to Skill) and gain the missing direction. The dismount logic moves to PossessorComponent::OnDismountComplete. Every inbound message is registered in the GameMessageHandler map; the switch is gone, and GameMessages.cpp only holds the GameMsg/NetGameMsg base code. Call sites build the structs (NotifyClientObject, TerminateInteraction, Teleport, PlatformResync, FireEventClientSide, NotifyObject and NotifyClientZoneObject get convenience constructors like PlayFXEffect). Dead senders are dropped: SendSetShootingGalleryParams (no callers, field order was a guess), SendTeamPickupItem (the struct already existed), SendRequestActivitySummaryLeaderboardData (the struct covers it). Verified with RemainingMessagesTests: every old Send* function is frozen verbatim in Legacy/RemainingMessagesLegacy.h and compared byte for byte (same bits, destination and broadcast flag) over grids of inputs; every old Handle* read sequence is frozen as a Read* oracle and compared with the struct's Deserialize; round trips, truncation and a golden packet. PlayerLoaded (0x00dc36f0), SetGMLevel (0x00dd6230), MissionDialogueCancelled (0x00d9cc10) and LocalizedAnnouncementServerToSingleClient (0x00f23c50) were checked against the client. Behaviour notes: an inbound message that fails to deserialize is dropped, so ParseChatMessage over MAX_MESSAGE_LENGTH is dropped instead of truncated, and PLAYER_LOADED / READY_FOR_UPDATES / MISSION_DIALOGUE_CANCELLED now read their (unused) client fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
671 lines
20 KiB
C++
671 lines
20 KiB
C++
#include "Mission.h"
|
|
#include "ChatServerLink.h"
|
|
|
|
#include <ctime>
|
|
|
|
#include "CDClientManager.h"
|
|
#include "Character.h"
|
|
#include "CharacterComponent.h"
|
|
#include "LevelProgressionComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "EntityManager.h"
|
|
#include "Game.h"
|
|
#include "GameMessages.h"
|
|
#include "MissionMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "Mail.h"
|
|
#include "MissionComponent.h"
|
|
#include "eRacingTaskParam.h"
|
|
#include "Logger.h"
|
|
#include "dServer.h"
|
|
#include "dZoneManager.h"
|
|
#include "InventoryComponent.h"
|
|
#include "User.h"
|
|
#include "Database.h"
|
|
#include "WorldConfig.h"
|
|
#include "eMissionState.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "eMissionLockState.h"
|
|
#include "eReplicaComponentType.h"
|
|
#include "Character.h"
|
|
|
|
#include "CDMissionEmailTable.h"
|
|
#include "ChatPackets.h"
|
|
#include "PlayerManager.h"
|
|
#include "StringifiedEnum.h"
|
|
#include "LiveEvents.h"
|
|
|
|
namespace {
|
|
std::set<uint32_t> g_TestedMissions = { 773, 774, 775, 776, 777 }; // TODO Figure out why these missions are broken sometimes
|
|
}
|
|
|
|
Mission::Mission(MissionComponent* missionComponent, const uint32_t missionId) {
|
|
m_MissionComponent = missionComponent;
|
|
|
|
m_Completions = 0;
|
|
|
|
m_Timestamp = 0;
|
|
|
|
m_UniqueMissionID = Game::zoneManager->GetUniqueMissionIdStartingValue();
|
|
|
|
m_Reward = 0;
|
|
|
|
m_State = eMissionState::UNKNOWN;
|
|
|
|
auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();
|
|
|
|
auto* mis = missionsTable->GetPtrByMissionID(missionId);
|
|
info = *mis;
|
|
|
|
if (mis == &CDMissionsTable::Default) {
|
|
LOG("Failed to find mission (%i)!", missionId);
|
|
|
|
return;
|
|
}
|
|
|
|
auto* tasksTable = CDClientManager::GetTable<CDMissionTasksTable>();
|
|
|
|
auto tasks = tasksTable->GetByMissionID(missionId);
|
|
|
|
for (auto i = 0U; i < tasks.size(); ++i) {
|
|
auto* info = tasks[i];
|
|
|
|
auto* task = new MissionTask(this, info, i);
|
|
|
|
m_Tasks.push_back(task);
|
|
}
|
|
}
|
|
|
|
void Mission::LoadFromXmlDone(const tinyxml2::XMLElement& element) {
|
|
// Start custom XML
|
|
m_State = static_cast<eMissionState>(element.UnsignedAttribute("state"));
|
|
// End custom XML
|
|
|
|
m_Completions = element.UnsignedAttribute("cct");
|
|
|
|
m_Timestamp = element.UnsignedAttribute("cts");
|
|
}
|
|
|
|
void Mission::LoadFromXmlCur(const tinyxml2::XMLElement& element) {
|
|
const auto* const character = GetCharacter();
|
|
// Start custom XML
|
|
if (element.Attribute("state") != nullptr) {
|
|
m_State = static_cast<eMissionState>(element.IntAttribute("state", -1));
|
|
}
|
|
// End custom XML
|
|
|
|
auto* task = element.FirstChildElement();
|
|
|
|
auto index = 0U;
|
|
|
|
while (task != nullptr) {
|
|
if (index >= m_Tasks.size()) {
|
|
break;
|
|
}
|
|
auto* const curTask = m_Tasks[index];
|
|
|
|
const auto type = curTask->GetType();
|
|
|
|
auto value = task->UnsignedAttribute("v");
|
|
curTask->SetProgress(value, false);
|
|
task = task->NextSiblingElement();
|
|
|
|
// Collection tasks and visit property tasks store each of the collected/visited targets after the progress value
|
|
if (type == eMissionTaskType::COLLECTION || type == eMissionTaskType::VISIT_PROPERTY) {
|
|
std::vector<uint32_t> uniques;
|
|
while (task != nullptr && value > 0) {
|
|
const auto unique = task->UnsignedAttribute("v");
|
|
|
|
uniques.push_back(unique);
|
|
|
|
if (m_MissionComponent != nullptr && type == eMissionTaskType::COLLECTION) {
|
|
m_MissionComponent->AddCollectible(unique);
|
|
}
|
|
|
|
task = task->NextSiblingElement();
|
|
value--;
|
|
}
|
|
|
|
curTask->SetUnique(uniques);
|
|
} else if (type == eMissionTaskType::PLAYER_FLAG) {
|
|
int32_t progress = 0; // Update the progress to not include session flags which are unset between logins
|
|
for (const auto flag : curTask->GetAllTargets()) {
|
|
if (character->GetPlayerFlag(flag)) progress++;
|
|
}
|
|
curTask->SetProgress(progress, false);
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
void Mission::UpdateXmlDone(tinyxml2::XMLElement& element) {
|
|
// Start custom XML
|
|
element.SetAttribute("state", static_cast<unsigned int>(m_State));
|
|
// End custom XML
|
|
|
|
element.DeleteChildren();
|
|
|
|
element.SetAttribute("id", static_cast<unsigned int>(info.id));
|
|
|
|
element.SetAttribute("cct", static_cast<unsigned int>(m_Completions));
|
|
|
|
element.SetAttribute("cts", static_cast<unsigned int>(m_Timestamp));
|
|
}
|
|
|
|
void Mission::UpdateXmlCur(tinyxml2::XMLElement& element) {
|
|
// Start custom XML
|
|
element.SetAttribute("state", static_cast<unsigned int>(m_State));
|
|
// End custom XML
|
|
|
|
element.DeleteChildren();
|
|
|
|
element.SetAttribute("id", static_cast<unsigned int>(info.id));
|
|
|
|
if (IsComplete()) return;
|
|
|
|
for (const auto* const task : m_Tasks) {
|
|
auto* const child = element.InsertNewChildElement("sv");
|
|
child->SetAttribute("v", static_cast<unsigned int>(task->GetProgress()));
|
|
|
|
// Collection and visit property tasks then need to store the collected/visited items after the progress
|
|
const auto taskType = task->GetType();
|
|
if (taskType == eMissionTaskType::COLLECTION || taskType == eMissionTaskType::VISIT_PROPERTY) {
|
|
for (const auto unique : task->GetUnique()) {
|
|
auto* uniqueElement = element.InsertNewChildElement("sv");
|
|
|
|
uniqueElement->SetAttribute("v", static_cast<unsigned int>(unique));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Mission::IsValidMission(const uint32_t missionId) {
|
|
auto* table = CDClientManager::GetTable<CDMissionsTable>();
|
|
|
|
const auto missions = table->Query([=](const CDMissions& entry) {
|
|
return entry.id == static_cast<int>(missionId);
|
|
});
|
|
|
|
return !missions.empty();
|
|
}
|
|
|
|
bool Mission::IsValidMission(const uint32_t missionId, CDMissions& info) {
|
|
auto* table = CDClientManager::GetTable<CDMissionsTable>();
|
|
|
|
const auto missions = table->Query([=](const CDMissions& entry) {
|
|
return entry.id == static_cast<int>(missionId);
|
|
});
|
|
|
|
if (missions.empty()) {
|
|
return false;
|
|
}
|
|
|
|
info = missions[0];
|
|
|
|
return true;
|
|
}
|
|
|
|
Entity* Mission::GetAssociate() const {
|
|
return m_MissionComponent->GetParent();
|
|
}
|
|
|
|
Character* Mission::GetCharacter() const {
|
|
return GetAssociate()->GetCharacter();
|
|
}
|
|
|
|
uint32_t Mission::GetMissionId() const {
|
|
return info.id;
|
|
}
|
|
|
|
const CDMissions& Mission::GetClientInfo() const {
|
|
return info;
|
|
}
|
|
|
|
uint32_t Mission::GetCompletions() const {
|
|
return m_Completions;
|
|
}
|
|
|
|
uint32_t Mission::GetTimestamp() const {
|
|
return m_Timestamp;
|
|
}
|
|
|
|
LOT Mission::GetReward() const {
|
|
return m_Reward;
|
|
}
|
|
|
|
std::vector<MissionTask*> Mission::GetTasks() const {
|
|
return m_Tasks;
|
|
}
|
|
|
|
eMissionState Mission::GetMissionState() const {
|
|
return m_State;
|
|
}
|
|
|
|
bool Mission::IsAchievement() const {
|
|
return !info.isMission;
|
|
}
|
|
|
|
bool Mission::IsMission() const {
|
|
return info.isMission;
|
|
}
|
|
|
|
bool Mission::IsRepeatable() const {
|
|
return info.repeatable;
|
|
}
|
|
|
|
bool Mission::IsComplete() const {
|
|
return m_State == eMissionState::COMPLETE;
|
|
}
|
|
|
|
bool Mission::IsActive() const {
|
|
return m_State == eMissionState::ACTIVE || m_State == eMissionState::COMPLETE_ACTIVE;
|
|
}
|
|
|
|
void Mission::MakeActive() {
|
|
SetMissionState(m_Completions == 0 ? eMissionState::ACTIVE : eMissionState::COMPLETE_ACTIVE);
|
|
}
|
|
|
|
bool Mission::IsReadyToComplete() const {
|
|
return m_State == eMissionState::READY_TO_COMPLETE || m_State == eMissionState::COMPLETE_READY_TO_COMPLETE;
|
|
}
|
|
|
|
bool Mission::IsFailed() const {
|
|
const auto underlying = GeneralUtils::ToUnderlying(m_State);
|
|
const auto target = GeneralUtils::ToUnderlying(eMissionState::FAILED);
|
|
return (underlying & target) != 0;
|
|
}
|
|
|
|
void Mission::MakeReadyToComplete() {
|
|
SetMissionState(m_Completions == 0 ? eMissionState::READY_TO_COMPLETE : eMissionState::COMPLETE_READY_TO_COMPLETE);
|
|
}
|
|
|
|
bool Mission::IsAvalible() const {
|
|
return m_State == eMissionState::AVAILABLE || m_State == eMissionState::COMPLETE_AVAILABLE;
|
|
}
|
|
|
|
bool Mission::IsFetchMission() const {
|
|
return m_Tasks.size() == 1 && m_Tasks[0]->GetType() == eMissionTaskType::TALK_TO_NPC;
|
|
}
|
|
|
|
void Mission::MakeAvalible() {
|
|
SetMissionState(m_Completions == 0 ? eMissionState::AVAILABLE : eMissionState::COMPLETE_AVAILABLE);
|
|
}
|
|
|
|
void Mission::Accept() {
|
|
SetMissionTypeState(eMissionLockState::NEW, info.defined_type, info.defined_subtype);
|
|
|
|
SetMissionState(m_Completions > 0 ? eMissionState::COMPLETE_ACTIVE : eMissionState::ACTIVE);
|
|
|
|
Catchup();
|
|
}
|
|
|
|
void Mission::Complete(const bool yieldRewards) {
|
|
if (m_State != eMissionState::ACTIVE && m_State != eMissionState::COMPLETE_ACTIVE) {
|
|
// If we are accepting a mission here there is no point to giving it a unique ID since we just complete it immediately.
|
|
Accept();
|
|
}
|
|
|
|
for (auto* task : m_Tasks) {
|
|
task->Complete();
|
|
}
|
|
|
|
SetMissionState(eMissionState::REWARDING, true);
|
|
|
|
if (yieldRewards) {
|
|
YieldRewards();
|
|
}
|
|
|
|
SetMissionState(eMissionState::COMPLETE);
|
|
|
|
m_Completions++;
|
|
|
|
m_Timestamp = std::time(nullptr);
|
|
|
|
auto* entity = GetAssociate();
|
|
|
|
if (entity == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->TrackMissionCompletion(!info.isMission);
|
|
}
|
|
|
|
auto* missionComponent = entity->GetComponent<MissionComponent>();
|
|
|
|
missionComponent->Progress(eMissionTaskType::META, info.id);
|
|
|
|
missionComponent->Progress(eMissionTaskType::RACING, info.id, static_cast<LWOOBJID>(eRacingTaskParam::COMPLETE_ANY_RACING_TASK));
|
|
|
|
missionComponent->Progress(eMissionTaskType::RACING, info.id, static_cast<LWOOBJID>(eRacingTaskParam::COMPLETE_TRACK_TASKS));
|
|
|
|
auto* missionEmailTable = CDClientManager::GetTable<CDMissionEmailTable>();
|
|
|
|
const auto missionId = GetMissionId();
|
|
|
|
const auto missionEmails = missionEmailTable->Query([missionId](const CDMissionEmail& entry) {
|
|
return entry.missionID == missionId;
|
|
});
|
|
|
|
for (const auto& email : missionEmails) {
|
|
const auto missionEmailBase = "MissionEmail_" + std::to_string(email.ID) + "_";
|
|
|
|
if (email.messageType == 1 /* Send an email to the player */) {
|
|
const auto subject = "%[" + missionEmailBase + "subjectText]";
|
|
const auto body = "%[" + missionEmailBase + "bodyText]";
|
|
const auto sender = "%[" + missionEmailBase + "senderName]";
|
|
|
|
Mail::SendMail(LWOOBJID_EMPTY, sender, GetAssociate(), subject, body, email.attachmentLOT, 1);
|
|
} else if (email.messageType == 2 /* Send an announcement in chat */) {
|
|
auto* character = entity->GetCharacter();
|
|
|
|
ChatPackets::AchievementNotify notify{};
|
|
notify.missionEmailID = email.ID;
|
|
notify.earningPlayerID = entity->GetObjectID();
|
|
notify.earnerName.string = character ? GeneralUtils::ASCIIToUTF16(character->GetName()) : u"";
|
|
|
|
// Sent to the chat server, not a game client
|
|
ChatServerLink::Send(notify, HIGH_PRIORITY);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mission::CheckCompletion() {
|
|
for (auto* task : m_Tasks) {
|
|
if (!task->IsComplete()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (IsAchievement()) {
|
|
Complete();
|
|
|
|
return;
|
|
}
|
|
|
|
MakeReadyToComplete();
|
|
}
|
|
|
|
void Mission::Catchup() {
|
|
auto* entity = GetAssociate();
|
|
|
|
auto* inventory = static_cast<InventoryComponent*>(entity->GetComponent(eReplicaComponentType::INVENTORY));
|
|
|
|
for (auto* task : m_Tasks) {
|
|
const auto type = task->GetType();
|
|
|
|
if (type == eMissionTaskType::GATHER) {
|
|
for (auto target : task->GetAllTargets()) {
|
|
const auto count = inventory->GetLotCountNonTransfer(target);
|
|
|
|
for (auto i = 0U; i < count; ++i) {
|
|
task->Progress(target);
|
|
}
|
|
}
|
|
} else if (type == eMissionTaskType::PLAYER_FLAG) {
|
|
for (int32_t target : task->GetAllTargets()) {
|
|
const auto flag = GetCharacter()->GetPlayerFlag(target);
|
|
|
|
if (!flag) {
|
|
continue;
|
|
}
|
|
|
|
task->Progress(target);
|
|
|
|
if (task->IsComplete()) {
|
|
break;
|
|
}
|
|
}
|
|
} else if (type == eMissionTaskType::RACING) {
|
|
// check if its a racing meta task ("4") and then set its progress to the current completed missions in all tasks
|
|
const auto& clientInfo = task->GetClientInfo();
|
|
if (clientInfo.taskParam1.starts_with("4")) {
|
|
// Each target is racing mission that needs to be completed.
|
|
// If its completed, progress the meta task by 1.
|
|
// at the end set the task progress to avoid sending excess msgs across the wire
|
|
uint32_t progress = 0;
|
|
for (const auto& target : task->GetAllTargets()) {
|
|
if (target == 0) continue;
|
|
|
|
auto* racingMission = m_MissionComponent->GetMission(target);
|
|
if (racingMission != nullptr && racingMission->IsComplete()) {
|
|
progress++;
|
|
}
|
|
}
|
|
task->SetProgress(progress);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mission::YieldRewards() {
|
|
auto* entity = GetAssociate();
|
|
|
|
if (entity == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto* character = GetCharacter();
|
|
|
|
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
|
auto* levelComponent = entity->GetComponent<LevelProgressionComponent>();
|
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
auto* missionComponent = entity->GetComponent<MissionComponent>();
|
|
|
|
// Remove mission items
|
|
for (auto* task : m_Tasks) {
|
|
if (task->GetType() != eMissionTaskType::GATHER) {
|
|
continue;
|
|
}
|
|
|
|
const auto& param = task->GetParameters();
|
|
|
|
if (param.empty() || (param[0] & 1) == 0) // Should items be removed?
|
|
{
|
|
for (const auto target : task->GetAllTargets()) {
|
|
// This is how live did it. ONLY remove item collection items from the items and hidden inventories and none of the others.
|
|
inventoryComponent->RemoveItem(target, task->GetClientInfo().targetValue, eInventoryType::ITEMS);
|
|
inventoryComponent->RemoveItem(target, task->GetClientInfo().targetValue, eInventoryType::QUEST);
|
|
|
|
missionComponent->Progress(eMissionTaskType::GATHER, target, LWOOBJID_EMPTY, "", -task->GetClientInfo().targetValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t coinsToSend = 0;
|
|
if (info.LegoScore > 0) {
|
|
eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT;
|
|
if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig().levelCap) {
|
|
// Since the character is at the level cap we reward them with coins instead of UScore.
|
|
coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig().levelCapCurrencyConversion;
|
|
} else {
|
|
const auto uscore = LiveEvents::ScaleUScore(info.LegoScore); // a live event's U-score bonus, if one is running here
|
|
characterComponent->SetUScore(characterComponent->GetUScore() + uscore, static_cast<uint32_t>(lootSource));
|
|
GameMessages::ModifyLEGOScore modifyScore;
|
|
modifyScore.target = entity->GetObjectID();
|
|
modifyScore.score = uscore;
|
|
modifyScore.sourceType = lootSource;
|
|
modifyScore.SendToClient(entity->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
// Even with no repeatable column, reputation is repeatable
|
|
if (info.reward_reputation > 0) {
|
|
missionComponent->Progress(eMissionTaskType::EARN_REPUTATION, 0, LWOOBJID_EMPTY, "", info.reward_reputation);
|
|
auto* const character = entity->GetComponent<CharacterComponent>();
|
|
if (character) {
|
|
character->SetReputation(character->GetReputation() + info.reward_reputation);
|
|
GameMessages::UpdateReputation updateReputation;
|
|
updateReputation.target = entity->GetObjectID();
|
|
updateReputation.reputation = character->GetReputation();
|
|
updateReputation.SendToClient(entity->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
if (m_Completions > 0) {
|
|
std::vector<std::pair<LOT, uint32_t>> items;
|
|
|
|
items.emplace_back(info.reward_item1_repeatable, info.reward_item1_repeat_count);
|
|
items.emplace_back(info.reward_item2_repeatable, info.reward_item2_repeat_count);
|
|
items.emplace_back(info.reward_item3_repeatable, info.reward_item3_repeat_count);
|
|
items.emplace_back(info.reward_item4_repeatable, info.reward_item4_repeat_count);
|
|
|
|
for (const auto& pair : items) {
|
|
// Some missions reward zero of an item and so they must be allowed through this clause,
|
|
// hence pair.second < 0 instead of pair.second <= 0.
|
|
if (pair.second < 0 || (m_Reward > 0 && pair.first != m_Reward)) {
|
|
continue;
|
|
}
|
|
|
|
// If a mission rewards zero of an item, make it reward 1.
|
|
auto count = pair.second > 0 ? pair.second : 1;
|
|
LOG("Player %llu is receiving %i of item %i from repeatable mission %i", entity->GetObjectID(), count, pair.first, info.id);
|
|
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT);
|
|
}
|
|
|
|
if (info.reward_currency_repeatable > 0 || coinsToSend > 0) {
|
|
eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT;
|
|
character->SetCoins(character->GetCoins() + info.reward_currency_repeatable + coinsToSend, lootSource);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<LOT, int32_t>> items;
|
|
|
|
items.emplace_back(info.reward_item1, info.reward_item1_count);
|
|
items.emplace_back(info.reward_item2, info.reward_item2_count);
|
|
items.emplace_back(info.reward_item3, info.reward_item3_count);
|
|
items.emplace_back(info.reward_item4, info.reward_item4_count);
|
|
|
|
for (const auto& pair : items) {
|
|
// Some missions reward zero of an item and so they must be allowed through this clause,
|
|
// hence pair.second < 0 instead of pair.second <= 0.
|
|
if (pair.second < 0 || (m_Reward > 0 && pair.first != m_Reward)) {
|
|
continue;
|
|
}
|
|
|
|
// If a mission rewards zero of an item, make it reward 1.
|
|
auto count = pair.second > 0 ? pair.second : 1;
|
|
LOG("Player %llu is receiving %i of item %i from mission %i", entity->GetObjectID(), count, pair.first, info.id);
|
|
inventoryComponent->AddItem(pair.first, count, IsMission() ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT);
|
|
}
|
|
|
|
if (info.reward_currency > 0 || coinsToSend > 0) {
|
|
eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT;
|
|
character->SetCoins(character->GetCoins() + info.reward_currency + coinsToSend, lootSource);
|
|
}
|
|
|
|
if (info.reward_maxinventory > 0) {
|
|
auto* inventory = inventoryComponent->GetInventory(ITEMS);
|
|
|
|
inventory->SetSize(inventory->GetSize() + info.reward_maxinventory);
|
|
}
|
|
|
|
if (info.reward_bankinventory > 0) {
|
|
auto* inventory = inventoryComponent->GetInventory(eInventoryType::VAULT_ITEMS);
|
|
auto modelInventory = inventoryComponent->GetInventory(eInventoryType::VAULT_MODELS);
|
|
|
|
inventory->SetSize(inventory->GetSize() + info.reward_bankinventory);
|
|
modelInventory->SetSize(modelInventory->GetSize() + info.reward_bankinventory);
|
|
}
|
|
|
|
if (info.reward_maxhealth > 0) {
|
|
destroyableComponent->SetMaxHealth(destroyableComponent->GetMaxHealth() + static_cast<float>(info.reward_maxhealth), true);
|
|
}
|
|
|
|
if (info.reward_maximagination > 0) {
|
|
destroyableComponent->SetMaxImagination(destroyableComponent->GetMaxImagination() + static_cast<float>(info.reward_maximagination), true);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(entity);
|
|
|
|
if (info.reward_emote > 0) {
|
|
character->UnlockEmote(info.reward_emote);
|
|
}
|
|
|
|
if (info.reward_emote2 > 0) {
|
|
character->UnlockEmote(info.reward_emote2);
|
|
}
|
|
|
|
if (info.reward_emote3 > 0) {
|
|
character->UnlockEmote(info.reward_emote3);
|
|
}
|
|
|
|
if (info.reward_emote4 > 0) {
|
|
character->UnlockEmote(info.reward_emote4);
|
|
}
|
|
}
|
|
|
|
void Mission::Progress(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count) {
|
|
const auto isRemoval = count < 0;
|
|
const bool testedMission = GetTestedMissions().contains(GetMissionId());
|
|
if (testedMission) LOG("%i Removal: %s complete: %s achievement: %s", GetMissionId(), isRemoval ? "true" : "false", IsComplete() ? "true" : "false", IsAchievement() ? "true" : "false");
|
|
if (isRemoval && (IsComplete() || IsAchievement())) {
|
|
return;
|
|
}
|
|
|
|
for (auto* task : m_Tasks) {
|
|
if (testedMission) LOG("Complete: %s Type: %s TaskType: %s", task->IsComplete() ? "true" : "false", StringifiedEnum::ToString(type).data(), StringifiedEnum::ToString(task->GetType()).data());
|
|
if (task->IsComplete() && !isRemoval) {
|
|
continue;
|
|
}
|
|
|
|
if (task->GetType() != type) {
|
|
continue;
|
|
}
|
|
|
|
if (isRemoval && !task->InAllTargets(value)) {
|
|
continue;
|
|
}
|
|
|
|
task->Progress(value, associate, targets, count);
|
|
}
|
|
}
|
|
|
|
void Mission::SetMissionState(const eMissionState state, const bool sendingRewards) {
|
|
this->m_State = state;
|
|
|
|
auto* entity = GetAssociate();
|
|
|
|
if (entity == nullptr) {
|
|
return;
|
|
}
|
|
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
|
if (!characterComponent) return;
|
|
|
|
GameMessages::NotifyMission notifyMission;
|
|
notifyMission.target = entity->GetObjectID();
|
|
notifyMission.missionID = info.id;
|
|
notifyMission.missionState = static_cast<int32_t>(state);
|
|
notifyMission.sendingRewards = sendingRewards;
|
|
notifyMission.SendToClient(characterComponent->GetSystemAddress());
|
|
}
|
|
|
|
void Mission::SetMissionTypeState(eMissionLockState state, const std::string& type, const std::string& subType) {
|
|
// TODO
|
|
}
|
|
|
|
void Mission::SetCompletions(const uint32_t value) {
|
|
m_Completions = value;
|
|
}
|
|
|
|
void Mission::SetReward(const LOT lot) {
|
|
m_Reward = lot;
|
|
}
|
|
|
|
Mission::~Mission() {
|
|
for (auto* task : m_Tasks) {
|
|
delete task;
|
|
}
|
|
|
|
m_Tasks.clear();
|
|
}
|
|
|
|
const std::set<uint32_t>& Mission::GetTestedMissions() const {
|
|
return g_TestedMissions;
|
|
}
|