eqemu-server/zone/task_client_state.cpp
hg 55155ff800
[Expeditions] Move expedition code into DynamicZone (#4672)
This removes the separate Expedition class and moves lockout code and
/dz command handlers into DynamicZone classes. It also refactors some
code to reduce bloat and some database usage.

This completes the effort of moving everything to DynamicZone that
started when implementing shared tasks. It also makes sense to do this
since expeditions are just dynamic zones internally despite dzs being
used for other types. Expedition specific things are just handled with
dz type checks.

Functionally nothing should change. This is mainly internal refactoring
and moving code around along with some bug fixes and reduced database
usage.

Main changes:

 - The `expeditions` database table has been removed

 - Expeditions no longer use a separate id, the expedition id is just the dz id

 - Expedition lock state and replay timer option were moved to the
   `dynamic_zones` table

 - Expeditions no longer have a separate cache from dynamic zones

 - Expedition creation no longer has every zone query the database to cache it

 - Expedition internal lockouts are now stored on DynamicZone

 - The `expedition_lockouts` table has been renamed to `dynamic_zone_lockouts`

 - Fixed a small bug with the UpdateLockoutDuration api where the
   internal lockout would get the time added twice in memory in the
   initiating zone (this api is likely rarely used)

 - Fixed an issue where use of the group/raid DoesAnyMemberHaveExpeditionLockout
   api would query once for every out of zone character.

   - This api now checks all members in the current zone first and only
     performs a single bulk query for out of zone members if that check
     is exhausted

 - Deprecated the max_check_count param of DoesAnyMemberHaveExpeditionLockout,
   the quest api still exists to avoid api break but a passed arg has no effect
2025-02-15 18:40:35 -06:00

2528 lines
70 KiB
C++

#include "../common/global_define.h"
#include "../common/misc_functions.h"
#include "../common/repositories/character_activities_repository.h"
#include "../common/repositories/character_task_timers_repository.h"
#include "../common/repositories/character_tasks_repository.h"
#include "../common/repositories/completed_tasks_repository.h"
#include "../common/rulesys.h"
#include "client.h"
#include "queryserv.h"
#include "quest_parser_collection.h"
#include "task_client_state.h"
#include "zonedb.h"
#include "../common/shared_tasks.h"
#include "worldserver.h"
#include "dynamic_zone.h"
#include "string_ids.h"
#include "../common/events/player_event_logs.h"
#define EBON_CRYSTAL 40902
#define RADIANT_CRYSTAL 40903
extern WorldServer worldserver;
extern QueryServ *QServ;
ClientTaskState::ClientTaskState()
{
m_active_task_count = 0;
m_last_completed_task_loaded = 0;
for (int i = 0; i < MAXACTIVEQUESTS; i++) {
m_active_quests[i].slot = i;
m_active_quests[i].task_id = TASKSLOTEMPTY;
}
m_active_task = {};
m_active_task.slot = 0;
m_active_task.task_id = TASKSLOTEMPTY;
m_active_shared_task = {};
m_active_shared_task.slot = 0;
m_active_shared_task.task_id = TASKSLOTEMPTY;
}
ClientTaskState::~ClientTaskState()
{
}
void ClientTaskState::SendTaskHistory(Client *client, int task_index)
{
LogTasks("Task history requested for completed task index [{}]", task_index);
// We only sent the most recent 50 completed tasks, so we need to offset the Index the client sent to us.
int adjusted_task_index = task_index;
if (m_completed_tasks.size() > 50) {
adjusted_task_index += (m_completed_tasks.size() - 50);
}
if ((adjusted_task_index < 0) || (adjusted_task_index >= (int) m_completed_tasks.size())) {
return;
}
int task_id = m_completed_tasks[adjusted_task_index].task_id;
if (task_id < 0) {
return;
}
const auto task_data = task_manager->GetTaskData(task_id);
if (!task_data) {
return;
}
TaskHistoryReplyHeader_Struct *task_history_reply;
TaskHistoryReplyData1_Struct *task_history_reply_data_1;
TaskHistoryReplyData2_Struct *task_history_reply_data_2;
char *reply;
int completed_activity_count = 0;
int packet_length = sizeof(TaskHistoryReplyHeader_Struct);
for (int i = 0; i < task_data->activity_count; i++) {
if (m_completed_tasks[adjusted_task_index].activity_done[i]) {
completed_activity_count++;
packet_length = packet_length + sizeof(TaskHistoryReplyData1_Struct) +
task_data->activity_information[i].target_name.size() + 1 +
task_data->activity_information[i].item_list.size() + 1 +
sizeof(TaskHistoryReplyData2_Struct) +
task_data->activity_information[i].description_override.size() + 1;
}
}
auto outapp = new EQApplicationPacket(OP_TaskHistoryReply, packet_length);
task_history_reply = (TaskHistoryReplyHeader_Struct *) outapp->pBuffer;
// We use the TaskIndex the client sent in the request
task_history_reply->TaskID = task_index;
task_history_reply->ActivityCount = completed_activity_count;
reply = (char *) task_history_reply + sizeof(TaskHistoryReplyHeader_Struct);
for (int i = 0; i < task_data->activity_count; i++) {
if (m_completed_tasks[adjusted_task_index].activity_done[i]) {
task_history_reply_data_1 = (TaskHistoryReplyData1_Struct *) reply;
task_history_reply_data_1->ActivityType = static_cast<uint32_t>(task_data->activity_information[i].activity_type);
reply = (char *) task_history_reply_data_1 + sizeof(TaskHistoryReplyData1_Struct);
VARSTRUCT_ENCODE_STRING(reply, task_data->activity_information[i].target_name.c_str());
VARSTRUCT_ENCODE_STRING(reply, task_data->activity_information[i].item_list.c_str());
task_history_reply_data_2 = (TaskHistoryReplyData2_Struct *) reply;
task_history_reply_data_2->GoalCount = task_data->activity_information[i].goal_count;
task_history_reply_data_2->unknown04 = 0xffffffff;
task_history_reply_data_2->unknown08 = 0xffffffff;
task_history_reply_data_2->ZoneID = task_data->activity_information[i].zone_ids.empty() ? 0
: task_data->activity_information[i].zone_ids.front();
task_history_reply_data_2->unknown16 = 0x00000000;
reply = (char *) task_history_reply_data_2 + sizeof(TaskHistoryReplyData2_Struct);
VARSTRUCT_ENCODE_STRING(reply, task_data->activity_information[i].description_override.c_str());
}
}
client->QueuePacket(outapp);
safe_delete(outapp);
}
void ClientTaskState::EnableTask(int character_id, int task_count, int *task_list)
{
// Check if the Task is already enabled for this client
std::vector<int> tasks_enabled;
for (int i = 0; i < task_count; i++) {
auto iterator = m_enabled_tasks.begin();
bool add_task = true;
while (iterator != m_enabled_tasks.end()) {
// If this task is already enabled, stop looking
if ((*iterator) == task_list[i]) {
add_task = false;
break;
}
// Our list of enabled tasks is sorted, so we can quit if we find a taskid higher than
// the one we are looking for.
if ((*iterator) > task_list[i]) {
break;
}
++iterator;
}
if (add_task) {
m_enabled_tasks.insert(iterator, task_list[i]);
// Make a note of the task we enabled, for later SQL generation
tasks_enabled.push_back(task_list[i]);
}
}
LogTasksDetail("New enabled task list");
for (int enabled_task : m_enabled_tasks) {
LogTasksDetail("enabled [{}] character_id [{}]", enabled_task, character_id);
}
if (tasks_enabled.empty()) {
return;
}
std::stringstream query_stream;
query_stream << "REPLACE INTO character_enabledtasks (charid, taskid) VALUES ";
for (unsigned int i = 0; i < tasks_enabled.size(); i++) {
query_stream << (i ? ", " : "") << StringFormat("(%i, %i)", character_id, tasks_enabled[i]);
}
database.QueryDatabase(query_stream.str());
}
void ClientTaskState::DisableTask(int character_id, int task_count, int *task_list)
{
// Check if the Task is enabled for this client
std::vector<int> tasks_disabled;
for (int task_id = 0; task_id < task_count; task_id++) {
auto iterator = m_enabled_tasks.begin();
bool removeTask = false;
while (iterator != m_enabled_tasks.end()) {
if ((*iterator) == task_list[task_id]) {
removeTask = true;
break;
}
if ((*iterator) > task_list[task_id]) {
break;
}
++iterator;
}
if (removeTask) {
m_enabled_tasks.erase(iterator);
tasks_disabled.push_back(task_list[task_id]);
}
}
LogTasks("New enabled task list ");
for (int enabled_task : m_enabled_tasks) {
LogTasks("enabled_tasks [{}]", enabled_task);
}
if (tasks_disabled.empty()) {
return;
}
std::stringstream queryStream;
queryStream << StringFormat("DELETE FROM character_enabledtasks WHERE charid = %i AND (", character_id);
for (unsigned int i = 0; i < tasks_disabled.size(); i++)
queryStream
<< (i ? StringFormat("taskid = %i ", tasks_disabled[i]) : StringFormat(
"OR taskid = %i ",
tasks_disabled[i]
));
queryStream << ")";
database.QueryDatabase(queryStream.str());
}
bool ClientTaskState::IsTaskEnabled(int task_id)
{
std::vector<int>::iterator iterator;
iterator = m_enabled_tasks.begin();
while (iterator != m_enabled_tasks.end()) {
if ((*iterator) == task_id) { return true; }
if ((*iterator) > task_id) { break; }
++iterator;
}
return false;
}
int ClientTaskState::EnabledTaskCount(int task_set_id)
{
// Return the number of tasks in TaskSet that this character is enabled for.
unsigned int enabled_task_index = 0;
unsigned int task_set_index = 0;
int enabled_task_count = 0;
if ((task_set_id <= 0) || (task_set_id >= MAXTASKSETS)) { return -1; }
while ((enabled_task_index < m_enabled_tasks.size()) &&
(task_set_index < task_manager->m_task_sets[task_set_id].size())) {
if (m_enabled_tasks[enabled_task_index] == task_manager->m_task_sets[task_set_id][task_set_index]) {
enabled_task_count++;
enabled_task_index++;
task_set_index++;
continue;
}
if (m_enabled_tasks[enabled_task_index] < task_manager->m_task_sets[task_set_id][task_set_index]) {
enabled_task_index++;
}
else {
task_set_index++;
}
}
return enabled_task_count;
}
int ClientTaskState::ActiveTasksInSet(int task_set_id)
{
if ((task_set_id <= 0) || (task_set_id >= MAXTASKSETS)) {
return -1;
}
int active_task_in_set_count = 0;
for (int task_id : task_manager->m_task_sets[task_set_id]) {
if (IsTaskActive(task_id)) {
active_task_in_set_count++;
}
}
return active_task_in_set_count;
}
int ClientTaskState::CompletedTasksInSet(int task_set_id)
{
if ((task_set_id <= 0) || (task_set_id >= MAXTASKSETS)) {
return -1;
}
int completed_tasks_count = 0;
for (int i : task_manager->m_task_sets[task_set_id]) {
if (IsTaskCompleted(i)) {
completed_tasks_count++;
}
}
return completed_tasks_count;
}
bool ClientTaskState::HasSlotForTask(const TaskInformation* task)
{
if (task == nullptr) {
return false;
}
switch (task->type) {
case TaskType::Task:
return m_active_task.task_id == TASKSLOTEMPTY;
case TaskType::Shared:
return m_active_shared_task.task_id == TASKSLOTEMPTY;
case TaskType::Quest:
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id == TASKSLOTEMPTY) {
return true;
}
}
case TaskType::E:
return false; // removed on live
}
return false;
}
int ClientTaskState::GetActiveTaskID(int index)
{
// Return the task_id from the client's specified Active Task slot.
if ((index < 0) || (index >= MAXACTIVEQUESTS)) {
return 0;
}
return m_active_quests[index].task_id;
}
static void DeleteCompletedTaskFromDatabase(int character_id, int task_id)
{
LogTasks("character_id [{}], task_id [{}]", character_id, task_id);
CompletedTasksRepository::DeleteWhere(
database,
fmt::format("charid = {} and taskid = {}", character_id, task_id)
);
}
bool ClientTaskState::UnlockActivities(Client* client, ClientTaskInformation& task_info)
{
LogTasksDetail(
"Fetching task info for character_id [{}] task [{}] slot [{}] accepted_time [{}] updated [{}]",
client->CharacterID(),
task_info.task_id,
task_info.slot,
task_info.accepted_time,
task_info.updated
);
const auto task = task_manager->GetTaskData(task_info.task_id);
if (!task)
{
return true;
}
for (int i = 0; i < task->activity_count; ++i)
{
if (task_info.activity[i].activity_id >= 0) {
LogTasksDetail(
"character_id [{}] task [{}] activity_id [{}] done_count [{}] activity_state [{}] updated [{}]",
client->CharacterID(),
task_info.task_id,
task_info.activity[i].activity_id,
task_info.activity[i].done_count,
task_info.activity[i].activity_state,
task_info.activity[i].updated
);
}
}
auto res = Tasks::GetActiveElements(task->activity_information, task_info.activity, task->activity_count);
for (int activity_id : res.active)
{
ClientActivityInformation& client_activity = task_info.activity[activity_id];
if (client_activity.activity_state == ActivityHidden)
{
LogTasksDetail("task [{}] activity [{}] (ActivityActive)", task_info.task_id, activity_id);
client_activity.activity_state = ActivityActive;
client_activity.updated = true;
}
}
if (res.is_task_complete && RuleB(TaskSystem, RecordCompletedTasks))
{
RecordCompletedTask(client->CharacterID(), *task, task_info);
}
// check if client has an explore task in current zone to enable task explore processing
m_has_explore_task = HasExploreTask(client);
return res.is_task_complete;
}
void ClientTaskState::RecordCompletedTask(uint32_t character_id, const TaskInformation& task, const ClientTaskInformation& client_task)
{
// If we are only keeping one completed record per task, and the player has done
// the same task again, erase the previous completed entry for this task.
if (RuleB(TasksSystem, KeepOneRecordPerCompletedTask))
{
size_t before = m_completed_tasks.size();
m_completed_tasks.erase(std::remove_if(m_completed_tasks.begin(), m_completed_tasks.end(),
[&](const CompletedTaskInformation& completed) { return completed.task_id == client_task.task_id; }
), m_completed_tasks.end());
size_t erased = before - m_completed_tasks.size();
LogTasksDetail("KeepOneRecord erased [{}] elements", erased);
if (erased > 0)
{
m_last_completed_task_loaded -= erased;
DeleteCompletedTaskFromDatabase(character_id, client_task.task_id);
}
}
if (task.type != TaskType::Shared)
{
CompletedTaskInformation completed{};
completed.task_id = client_task.task_id;
completed.completed_time = std::time(nullptr);
for (int i = 0; i < task.activity_count; ++i)
{
completed.activity_done[i] = (client_task.activity[i].activity_state == ActivityCompleted);
}
LogTasksDetail("[{}] for character [{}]", client_task.task_id, character_id);
m_completed_tasks.push_back(completed);
}
}
const TaskInformation* ClientTaskState::GetTaskData(const ClientTaskInformation& client_task) const
{
if (client_task.task_id == TASKSLOTEMPTY)
{
return nullptr;
}
return task_manager->GetTaskData(client_task.task_id);
}
bool ClientTaskState::CanUpdate(Client* client, const TaskUpdateFilter& filter, int task_id,
const ActivityInformation& activity, const ClientActivityInformation& client_activity) const
{
if (activity.goal_method == METHODQUEST && activity.goal_method != filter.method)
{
return false;
}
// todo: some tasks do allow hidden/unlocked elements to silently update
if (client_activity.activity_state != ActivityActive)
{
return false;
}
if (activity.activity_type != filter.type)
{
return false;
}
if (activity.dz_switch_id != 0 && activity.dz_switch_id != filter.dz_switch_id)
{
return false;
}
if (!activity.CheckZone(zone->GetZoneID(), zone->GetInstanceVersion()))
{
LogTasks("client [{}] task [{}]-[{}] failed zone filter", client->GetName(), task_id, client_activity.activity_id);
return false;
}
if (activity.has_area && !filter.ignore_area && RuleB(TaskSystem, EnableTaskProximity))
{
const glm::vec4& pos = filter.use_pos ? filter.pos : client->GetPosition();
if (pos.x < activity.min_x || pos.x > activity.max_x ||
pos.y < activity.min_y || pos.y > activity.max_y ||
pos.z < activity.min_z || pos.z > activity.max_z)
{
LogTasksDetail("client [{}] task [{}]-[{}] failed area filter", client->GetName(), task_id, client_activity.activity_id);
return false;
}
}
// item is only checked for updates that provide an item to check (unlike npc which may be null for non-npcs)
if (!activity.item_id_list.empty() && filter.item_id != 0 &&
!Tasks::IsInMatchList(activity.item_id_list, std::to_string(filter.item_id)))
{
LogTasks("client [{}] task [{}]-[{}] failed item match filter", client->GetName(), task_id, client_activity.activity_id);
return false;
}
// npc filter supports both npc names and ids in match lists
if (!activity.npc_match_list.empty() && (!filter.mob ||
(!Tasks::IsInMatchListPartial(activity.npc_match_list, filter.mob->GetName()) &&
!Tasks::IsInMatchListPartial(activity.npc_match_list, filter.mob->GetCleanName()) &&
!Tasks::IsInMatchList(activity.npc_match_list, std::to_string(filter.mob->GetNPCTypeID())))))
{
LogTasks("client [{}] task [{}]-[{}] failed npc match filter", client->GetName(), task_id, client_activity.activity_id);
return false;
}
return true;
}
int ClientTaskState::UpdateTasks(Client* client, const TaskUpdateFilter& filter, int count)
{
if (!task_manager)
{
return 0;
}
int max_updated = 0;
for (const auto& client_task : m_active_tasks)
{
const auto task = GetTaskData(client_task);
if (!task)
{
continue;
}
// legacy eqemu task update logic loops through group on kill of npc to update a single task
// shared tasks only require one client to receive an update to propagate
if (filter.type == TaskActivityType::Kill && task->type == TaskType::Shared && client != filter.exp_client)
{
continue;
}
for (const ClientActivityInformation& client_activity : client_task.activity)
{
const ActivityInformation& activity = task->activity_information[client_activity.activity_id];
if (CanUpdate(client, filter, client_task.task_id, activity, client_activity))
{
if (parse->PlayerHasQuestSub(EVENT_TASK_BEFORE_UPDATE)) {
const auto& export_string = fmt::format(
"{} {} {}",
count,
client_activity.activity_id,
client_task.task_id
);
if (parse->EventPlayer(EVENT_TASK_BEFORE_UPDATE, client, export_string, 0) != 0) {
LogTasks(
"client [{}] task [{}]-[{}] update prevented by quest",
client->GetName(),
client_task.task_id,
client_activity.activity_id
);
continue;
}
}
LogTasks(
"client [{}] task [{}] activity [{}] increment [{}]",
client->GetName(),
client_task.task_id,
client_activity.activity_id,
count
);
int updated = IncrementDoneCount(client, task, client_task.slot, client_activity.activity_id, count);
max_updated = std::max(max_updated, updated);
if (RuleB(TaskSystem, UpdateOneElementPerTask))
{
break; // only one element updated per task, move to next task
}
}
}
}
return max_updated;
}
std::pair<int, int> ClientTaskState::FindTask(Client* client, const TaskUpdateFilter& filter) const
{
if (!task_manager)
{
return std::make_pair(0, 0);
}
for (const auto& client_task : m_active_tasks)
{
const auto task = GetTaskData(client_task);
if (!task || (filter.task_id != 0 && client_task.task_id != filter.task_id))
{
continue;
}
for (const ClientActivityInformation& client_activity : client_task.activity)
{
const ActivityInformation& activity = task->activity_information[client_activity.activity_id];
if (CanUpdate(client, filter, client_task.task_id, activity, client_activity))
{
return std::make_pair(client_task.task_id, client_activity.activity_id);
}
}
}
return std::make_pair(0, 0);
}
bool ClientTaskState::HasExploreTask(Client* client) const
{
TaskUpdateFilter filter{};
filter.type = TaskActivityType::Explore;
filter.ignore_area = true; // we don't care if client is currently in the explore area
auto result = FindTask(client, filter);
bool has_explore = result.first != 0;
LogTasksDetail("client [{}] has explore task in current zone [{}]", client->GetName(), has_explore);
return has_explore;
}
bool ClientTaskState::UpdateTasksOnSpeakWith(Client* client, NPC* npc)
{
return UpdateTasksByNPC(client, TaskActivityType::SpeakWith, npc);
}
bool ClientTaskState::UpdateTasksByNPC(Client* client, TaskActivityType type, NPC* npc)
{
TaskUpdateFilter filter{};
filter.type = type;
filter.mob = npc;
return UpdateTasks(client, filter) > 0;
}
int ClientTaskState::ActiveSpeakTask(Client* client, NPC* npc)
{
// This method is to be used from Perl quests only and returns the task_id of the first
// active task found which has an active SpeakWith activity_information for this NPC.
TaskUpdateFilter filter{};
filter.type = TaskActivityType::SpeakWith;
filter.mob = npc;
filter.method = METHODQUEST;
auto result = FindTask(client, filter);
return result.first; // task id
}
int ClientTaskState::ActiveSpeakActivity(Client* client, NPC* npc, int task_id)
{
// This method is to be used from Perl quests only and returns the activity_id of the first
// active activity_information found in the specified task which is to SpeakWith this NPC.
if (task_id <= 0)
{
return -1;
}
TaskUpdateFilter filter{};
filter.type = TaskActivityType::SpeakWith;
filter.mob = npc;
filter.method = METHODQUEST;
filter.task_id = task_id;
auto result = FindTask(client, filter);
return result.first != 0 ? result.second : -1; // activity id
}
void ClientTaskState::UpdateTasksForItem(Client* client, TaskActivityType type, int item_id, int count)
{
// This method updates the client's task activities of the specified type which relate
// to the specified item.
//
// Type should be one of ActivityTradeSkill, ActivityFish or ActivityForage
LogTasks("activity_type [{}] item_id [{}] count [{}]", static_cast<int>(type), item_id, count);
TaskUpdateFilter filter{};
filter.type = type;
filter.item_id = item_id;
UpdateTasks(client, filter, count);
}
void ClientTaskState::UpdateTasksOnLoot(Client* client, Corpse* corpse, int item_id, int count)
{
LogTasks("corpse [{}] item_id [{}] count [{}]", corpse->GetName(), item_id, count);
TaskUpdateFilter filter{};
filter.type = TaskActivityType::Loot;
filter.mob = corpse;
filter.item_id = item_id;
UpdateTasks(client, filter, count);
}
void ClientTaskState::UpdateTasksOnExplore(Client* client, const glm::vec4& pos)
{
LogTasksDetail("client [{}]", client->GetName());
TaskUpdateFilter filter{};
filter.type = TaskActivityType::Explore;
filter.pos = pos;
filter.use_pos = true;
UpdateTasks(client, filter);
}
bool ClientTaskState::UpdateTasksOnDeliver(Client* client, std::vector<EQ::ItemInstance*>& items, Trade& trade, NPC* npc)
{
LogTasks("npc [{}]", npc->GetName());
bool is_updated = false;
TaskUpdateFilter filter{};
filter.mob = npc;
int cash = trade.cp + (trade.sp * 10) + (trade.gp * 100) + (trade.pp * 1000);
if (cash != 0)
{
filter.type = TaskActivityType::GiveCash;
int updated_count = UpdateTasks(client, filter, cash);
if (updated_count > 0)
{
// todo: remove used coin and use Deliver with explicit coin fields instead of custom type
is_updated = true;
}
}
filter.type = TaskActivityType::Deliver;
for (EQ::ItemInstance*& item : items)
{
// items may have gaps for unused trade slots
if (!item)
{
continue;
}
filter.item_id = item->GetID();
int count = item->IsStackable() ? item->GetCharges() : 1;
int updated_count = UpdateTasks(client, filter, count);
if (updated_count > 0)
{
item->SetTaskDeliveredCount(updated_count);
is_updated = true;
}
}
return is_updated;
}
void ClientTaskState::UpdateTasksOnTouch(Client *client, int dz_switch_id)
{
LogTasks("dz switch [{}] ", dz_switch_id);
TaskUpdateFilter filter{};
filter.type = TaskActivityType::Touch;
filter.dz_switch_id = dz_switch_id;
UpdateTasks(client, filter);
}
void ClientTaskState::UpdateTasksOnKill(Client* client, Client* exp_client, NPC* npc)
{
TaskUpdateFilter filter{};
filter.type = TaskActivityType::Kill;
filter.mob = npc;
filter.pos = npc->GetPosition(); // or should areas be filtered by client position?
filter.use_pos = true;
filter.exp_client = exp_client;
UpdateTasks(client, filter);
}
int ClientTaskState::IncrementDoneCount(
Client *client,
const TaskInformation* task_data,
int task_index,
int activity_id,
int count,
bool ignore_quest_update
)
{
auto info = GetClientTaskInfo(task_data->type, task_index);
if (info == nullptr) {
return 0;
}
LogTasks(
"client [{}] task_id [{}] activity_id [{}] count [{}]",
client->GetCleanName(),
info->task_id,
activity_id,
count
);
int remaining = task_data->activity_information[activity_id].goal_count - info->activity[activity_id].done_count;
count = std::min(count, remaining);
// shared task shim
// intercept and pass to world first before processing normally
if (!client->m_shared_task_update && task_data->type == TaskType::Shared) {
// struct
auto pack = new ServerPacket(ServerOP_SharedTaskUpdate, sizeof(ServerSharedTaskActivityUpdate_Struct));
auto *r = (ServerSharedTaskActivityUpdate_Struct *) pack->pBuffer;
// fill
r->source_character_id = client->CharacterID();
r->task_id = info->task_id;
r->activity_id = activity_id;
r->done_count = info->activity[activity_id].done_count + count;
r->ignore_quest_update = ignore_quest_update;
LogTasksDetail(
"shared_task sending client [{}] task_id [{}] activity_id [{}] count [{}] ignore_quest_update [{}]",
r->source_character_id,
r->task_id,
r->activity_id,
r->done_count,
(ignore_quest_update ? "true" : "false")
);
SyncSharedTaskZoneClientDoneCountState(
client,
task_data->type,
task_index,
activity_id,
r->done_count
);
// send
worldserver.SendPacket(pack);
safe_delete(pack);
return count;
}
info->activity[activity_id].done_count += count;
if (!ignore_quest_update) {
if (parse->PlayerHasQuestSub(EVENT_TASK_UPDATE)) {
const auto& export_string = fmt::format(
"{} {} {}",
info->activity[activity_id].done_count,
info->activity[activity_id].activity_id,
info->task_id
);
parse->EventPlayer(EVENT_TASK_UPDATE, client, export_string, 0);
}
}
if (task_data->type != TaskType::Shared) {
// live messages for each increment of non-shared tasks
auto activity_type = task_data->activity_information[activity_id].activity_type;
int msg_count = 1;
if (activity_type != TaskActivityType::GiveCash) {
msg_count = std::min(count, RuleI(TaskSystem, MaxUpdateMessages));
}
for (int i = 0; i < msg_count; ++i) {
client->MessageString(Chat::DefaultText, TASK_UPDATED, task_data->title.c_str());
}
}
info->activity[activity_id].updated = true;
// Have we reached the goal count for this activity_information ?
if (info->activity[activity_id].done_count >= task_data->activity_information[activity_id].goal_count) {
LogTasks("done_count [{}] goal_count [{}] activity_id [{}]",
info->activity[activity_id].done_count,
task_data->activity_information[activity_id].goal_count,
activity_id
);
// Flag the activity_information as complete
info->activity[activity_id].activity_state = ActivityCompleted;
// Unlock subsequent activities for this task
bool task_complete = UnlockActivities(client, *info);
LogTasks("task_complete is [{}]", task_complete);
// shared tasks only send update messages on activity completion
if (task_data->type == TaskType::Shared) {
client->MessageString(Chat::DefaultText, TASK_UPDATED, task_data->title.c_str());
}
// and by the 'Task Stage Completed' message
client->SendTaskActivityComplete(info->task_id, activity_id, task_index, task_data->type);
// Send the updated task/activity_information list to the client
task_manager->SendSingleActiveTaskToClient(client, *info, task_complete, false);
if (!ignore_quest_update) {
if (parse->PlayerHasQuestSub(EVENT_TASK_STAGE_COMPLETE)) {
const auto& export_string = fmt::format(
"{} {}",
info->task_id,
info->activity[activity_id].activity_id
);
parse->EventPlayer(EVENT_TASK_STAGE_COMPLETE, client, export_string, 0);
}
}
// If this task is now complete, the Completed tasks will have been
// updated in UnlockActivities. Send the completed task list to the
// client. This is the same sequence the packets are sent on live.
if (task_complete) {
// world adds timers for shared tasks
if (task_data->type != TaskType::Shared) {
AddReplayTimer(client, *info, *task_data);
}
int event_res = DispatchEventTaskComplete(client, *info, activity_id);
if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_COMPLETE)) {
auto e = PlayerEvent::TaskCompleteEvent{
.task_id = static_cast<uint32>(info->task_id),
.task_name = task_manager->GetTaskName(static_cast<uint32>(info->task_id)),
.activity_id = static_cast<uint32>(info->activity[activity_id].activity_id),
.done_count = static_cast<uint32>(info->activity[activity_id].done_count)
};
RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_COMPLETE, e);
}
client->SendTaskActivityComplete(info->task_id, 0, task_index, task_data->type, 0);
// If Experience and/or cash rewards are set, reward them from the task even if reward_method is METHODQUEST
// do not reward client if EVENT_TASK_COMPLETE returns non-zero
if (event_res == 0)
{
RewardTask(client, task_data, *info);
}
//RemoveTask(c, TaskIndex);
// shared tasks linger at the completion step and do not get removed from the task window unlike quests/task
if (task_data->type != TaskType::Shared) {
task_manager->SendCompletedTasksToClient(client, this);
client->CancelTask(task_index, task_data->type);
}
}
}
else {
// Send an updated packet for this single activity_information
task_manager->SendTaskActivityLong(
client,
info->task_id,
activity_id,
task_index
);
if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_UPDATE)) {
auto e = PlayerEvent::TaskUpdateEvent{
.task_id = static_cast<uint32>(info->task_id),
.task_name = task_manager->GetTaskName(static_cast<uint32>(info->task_id)),
.activity_id = static_cast<uint32>(info->activity[activity_id].activity_id),
.done_count = static_cast<uint32>(info->activity[activity_id].done_count)
};
RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_UPDATE, e);
}
}
task_manager->SaveClientState(client, this);
return count;
}
int ClientTaskState::DispatchEventTaskComplete(Client* client, ClientTaskInformation& info, int activity_id)
{
if (parse->PlayerHasQuestSub(EVENT_TASK_COMPLETE)) {
const auto& export_string = fmt::format(
"{} {} {}",
info.activity[activity_id].done_count,
info.activity[activity_id].activity_id,
info.task_id
);
return parse->EventPlayer(EVENT_TASK_COMPLETE, client, export_string, 0);
}
return 0;
}
void ClientTaskState::RewardTask(Client *c, const TaskInformation *ti, ClientTaskInformation& client_task)
{
if (!ti || !c || client_task.was_rewarded) {
return;
}
client_task.was_rewarded = true;
client_task.updated = true;
if (!ti->completion_emote.empty()) {
c->Message(Chat::Yellow, ti->completion_emote.c_str());
}
// TODO: this function should sometimes use QuestReward_Struct and CashReward_Struct
// assumption is they use QuestReward_Struct when there is more than 1 thing getting rewarded
if (ti->reward_method != METHODQUEST) {
for (const auto &i: Strings::Split(ti->reward_id_list, "|")) {
// handle charges
int16 charges = -1;
uint32 item_id = Strings::IsNumber(i) ? Strings::ToInt(i) : 0;
if (Strings::Contains(i, ",")) {
auto s = Strings::Split(i, ",");
if (!s.empty() && s.size() == 2) {
item_id = Strings::IsNumber(s[0]) ? Strings::ToInt(s[0]) : 0;
charges = Strings::IsNumber(s[1]) ? Strings::ToInt(s[1]) : 0;
}
}
if (item_id > 0) {
std::unique_ptr<EQ::ItemInstance> inst(database.CreateItem(item_id, charges));
if (inst && inst->GetItem()) {
c->CheckItemDiscoverability(item_id);
bool stacked = c->TryStacking(inst.get());
if (!stacked) {
int16_t slot = c->GetInv().FindFreeSlot(inst->IsClassBag(), true, inst->GetItem()->Size);
c->SummonItem(item_id, charges, 0, 0, 0, 0, 0, 0, false, slot);
}
c->MessageString(Chat::Yellow, YOU_HAVE_BEEN_GIVEN, inst->GetItem()->Name);
}
}
}
}
// just use normal NPC faction ID stuff
if (ti->faction_reward && ti->faction_amount == 0) {
zone->LoadNPCFaction(ti->faction_reward);
c->SetFactionLevel(
c->CharacterID(),
ti->faction_reward,
c->GetBaseClass(),
c->GetBaseRace(),
c->GetDeity()
);
} else if (ti->faction_reward != 0 && ti->faction_amount != 0) {
// faction_reward is a faction ID
zone->LoadFactionAssociation(ti->faction_reward);
c->RewardFaction(
ti->faction_reward,
ti->faction_amount
);
}
if (ti->cash_reward) {
int platinum, gold, silver, copper;
copper = ti->cash_reward;
platinum = copper / 1000;
copper = copper - (platinum * 1000);
gold = copper / 100;
copper = copper - (gold * 100);
silver = copper / 10;
copper = copper - (silver * 10);
c->CashReward(copper, silver, gold, platinum);
}
auto experience_reward = ti->experience_reward;
if (experience_reward > 0) {
c->AddEXP(ExpSource::Task, experience_reward);
} else if (experience_reward < 0) {
uint32 pos_reward = experience_reward * -1;
// Minimal Level Based Exp reward Setting is 101 (1% exp at level 1)
if (pos_reward > 100 && pos_reward < 25700) {
uint8 max_level = pos_reward / 100;
uint8 exp_percent = pos_reward - (max_level * 100);
c->AddLevelBasedExp(ExpSource::Task, exp_percent, max_level, RuleB(TaskSystem, ExpRewardsIgnoreLevelBasedEXPMods));
}
}
if (ti->reward_points > 0) {
if (ti->reward_point_type == static_cast<int32_t>(zone->GetCurrencyID(RADIANT_CRYSTAL))) {
c->AddRadiantCrystals(ti->reward_points);
} else if (ti->reward_point_type == static_cast<int32_t>(zone->GetCurrencyID(EBON_CRYSTAL))) {
c->AddEbonCrystals(ti->reward_points);
} else {
for (const auto& ac : zone->AlternateCurrencies) {
if (ti->reward_point_type == ac.id) {
const EQ::ItemData *item = database.GetItem(ac.item_id);
if (item) {
c->AddAlternateCurrencyValue(ti->reward_point_type, ti->reward_points);
c->Message(Chat::Yellow, fmt::format("You have received ({}) {}!", ti->reward_points, item->Name).c_str());
}
}
}
}
}
}
bool ClientTaskState::IsTaskActive(int task_id)
{
if (m_active_task.task_id == task_id) {
return true;
}
if (m_active_shared_task.task_id == task_id) {
return true;
}
if (m_active_task_count == 0 || task_id == 0) {
return false;
}
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id == task_id) {
return true;
}
}
return false;
}
void ClientTaskState::FailTask(Client *client, int task_id)
{
LogTasks(
"Failing task for character [{}] task_id [{}] task_count [{}]",
client->GetCleanName(),
task_id,
m_active_task_count
);
// type: Task
if (m_active_task.task_id == task_id) {
client->SendTaskFailed(task_id, TASKSLOTTASK, TaskType::Task);
// Remove the task from the client
client->CancelTask(TASKSLOTTASK, TaskType::Task);
return;
}
// type: Shared Task (failed via world for all members)
if (m_active_shared_task.task_id == task_id) {
task_manager->EndSharedTask(*client, task_id, true);
return;
}
if (m_active_task_count == 0) {
return;
}
for (int i = 0; i < MAXACTIVEQUESTS; i++) {
if (m_active_quests[i].task_id == task_id) {
client->SendTaskFailed(m_active_quests[i].task_id, i, TaskType::Quest);
// Remove the task from the client
client->CancelTask(i, TaskType::Quest);
return;
}
}
}
bool ClientTaskState::IsTaskActivityActive(int task_id, int activity_id)
{
LogTasks("task_id [{}] activity_id [{}]", task_id, activity_id);
// Quick sanity check
if (activity_id < 0) {
return false;
}
if (m_active_task_count == 0 && m_active_task.task_id == TASKSLOTEMPTY &&
m_active_shared_task.task_id == TASKSLOTEMPTY) {
return false;
}
int active_task_index = -1;
auto task_type = TaskType::Task;
if (m_active_task.task_id == task_id) {
active_task_index = TASKSLOTTASK;
}
if (m_active_shared_task.task_id == task_id) {
task_type = TaskType::Shared;
active_task_index = TASKSLOTSHAREDTASK;
}
if (active_task_index == -1) {
for (int i = 0; i < MAXACTIVEQUESTS; i++) {
if (m_active_quests[i].task_id == task_id) {
active_task_index = i;
task_type = TaskType::Quest;
break;
}
}
}
// The client does not have this task
if (active_task_index == -1) {
return false;
}
auto info = GetClientTaskInfo(task_type, active_task_index);
if (info == nullptr) {
return false;
}
const auto task_data = task_manager->GetTaskData(info->task_id);
if (!task_data) {
return false;
}
// The activity_id is out of range
if (activity_id >= task_data->activity_count) {
return false;
}
LogTasks(
"(Update) task_id [{}] activity_id [{}] activity_state [{}]",
task_id,
activity_id,
info->activity[activity_id].activity_state
);
return (info->activity[activity_id].activity_state == ActivityActive);
}
void ClientTaskState::UpdateTaskActivity(
Client *client,
int task_id,
int activity_id,
int count,
bool ignore_quest_update /*= false*/)
{
LogTasks(
"[UpdateTaskActivity] Increment done count (pre) on UpdateTaskActivity task_id [{}] activity_id [{}] count [{}]",
task_id,
activity_id,
count
);
// Quick sanity check
if (activity_id < 0 || (m_active_task_count == 0 && m_active_task.task_id == TASKSLOTEMPTY &&
m_active_shared_task.task_id == TASKSLOTEMPTY)) {
return;
}
int active_task_index = -1;
auto type = TaskType::Task;
if (m_active_task.task_id == task_id) {
active_task_index = TASKSLOTTASK;
}
if (m_active_shared_task.task_id == task_id) {
type = TaskType::Shared;
active_task_index = TASKSLOTSHAREDTASK;
}
if (active_task_index == -1) {
for (int i = 0; i < MAXACTIVEQUESTS; i++) {
if (m_active_quests[i].task_id == task_id) {
active_task_index = i;
type = TaskType::Quest;
break;
}
}
}
// The client does not have this task
if (active_task_index == -1) {
return;
}
auto info = GetClientTaskInfo(type, active_task_index);
if (info == nullptr) {
return;
}
const auto task_data = task_manager->GetTaskData(info->task_id);
if (!task_data) {
return;
}
// The activity_id is out of range
if (activity_id >= task_data->activity_count) {
return;
}
// The activity_information is not currently active
if (info->activity[activity_id].activity_state == ActivityHidden) {
return;
}
LogTasks(
"[UpdateTaskActivity] Increment done count (done) on UpdateTaskActivity task_id [{}] activity_id [{}] count [{}]",
task_id,
activity_id,
count
);
IncrementDoneCount(client, task_data, active_task_index, activity_id, count, ignore_quest_update);
}
void ClientTaskState::ResetTaskActivity(Client *client, int task_id, int activity_id)
{
LogTasks(
"[ResetTaskActivity] (pre) client [{}] task_id [{}] activity_id [{}]",
client->GetCleanName(),
task_id,
activity_id
);
// Quick sanity check
if (activity_id < 0 || (m_active_task_count == 0 && m_active_task.task_id == TASKSLOTEMPTY &&
m_active_shared_task.task_id == TASKSLOTEMPTY)) {
return;
}
int active_task_index = -1;
auto type = TaskType::Task;
if (m_active_task.task_id == task_id) {
active_task_index = TASKSLOTTASK;
}
if (m_active_shared_task.task_id == task_id) {
type = TaskType::Shared;
active_task_index = TASKSLOTSHAREDTASK;
}
if (active_task_index == -1) {
for (int i = 0; i < MAXACTIVEQUESTS; i++) {
if (m_active_quests[i].task_id == task_id) {
active_task_index = i;
type = TaskType::Quest;
break;
}
}
}
// The client does not have this task
if (active_task_index == -1) {
return;
}
auto info = GetClientTaskInfo(type, active_task_index);
if (info == nullptr) {
return;
}
const auto task_data = task_manager->GetTaskData(info->task_id);
if (!task_data) {
return;
}
// The activity_id is out of range
if (activity_id >= task_data->activity_count) {
return;
}
// The activity_information is not currently active
if (info->activity[activity_id].activity_state == ActivityHidden) {
return;
}
LogTasks(
"[ResetTaskActivity] (IncrementDoneCount) client [{}] task_id [{}] activity_id [{}]",
client->GetCleanName(),
task_id,
activity_id
);
IncrementDoneCount(
client,
task_data,
active_task_index,
activity_id,
(info->activity[activity_id].done_count * -1),
false
);
}
bool ClientTaskState::CompleteTask(Client *c, uint32 task_id)
{
const auto task_data = task_manager->GetTaskData(task_id);
if (!task_data) {
return false;
}
for (
int activity_id = 0;
activity_id < task_manager->GetActivityCount(task_id);
activity_id++
) {
c->UpdateTaskActivity(
task_id,
activity_id,
task_data->activity_information[activity_id].goal_count
);
}
return true;
}
void ClientTaskState::ShowClientTaskInfoMessage(ClientTaskInformation *task, Client *c)
{
const auto task_data = task_manager->GetTaskData(task->task_id);
c->Message(Chat::White, "------------------------------------------------");
c->Message(
Chat::White,
fmt::format(
"Task {} | Title: {} ID: {} Type: {}",
task->slot,
task_data->title,
task->task_id,
Tasks::GetTaskTypeDescription(task_data->type)
).c_str()
);
c->Message(Chat::White, "------------------------------------------------");
c->Message(
Chat::White,
fmt::format(
"Description | {}",
task_data->description
).c_str()
);
for (int activity_id = 0; activity_id < task_manager->GetActivityCount(task->task_id); activity_id++) {
std::vector<std::string> update_increments = { "1", "5", "10", "20", "50" };
std::vector<std::string> update_saylinks;
for (auto &increment: update_increments) {
auto task_update_saylink = Saylink::Silent(
fmt::format(
"#task update {} {} {}",
task->task_id,
task->activity[activity_id].activity_id,
increment
),
increment
);
update_saylinks.push_back(task_update_saylink);
}
c->Message(
Chat::White,
fmt::format(
"Activity {} | Count: {} State: {} ({}) [{}]",
task->activity[activity_id].activity_id,
task->activity[activity_id].done_count,
task->activity[activity_id].activity_state,
Tasks::GetActivityStateDescription(task->activity[activity_id].activity_state),
Strings::Implode(" | ", update_saylinks)
).c_str()
);
}
}
void ClientTaskState::ShowClientTasks(Client* who, Client *to)
{
to->SendChatLineBreak();
to->Message(
Chat::White,
fmt::format(
"Task Information for {}",
to->GetTargetDescription(who, TargetDescriptionType::UCSelf)
).c_str()
);
if (m_active_task.task_id != TASKSLOTEMPTY) {
ShowClientTaskInfoMessage(&m_active_task, to);
}
if (m_active_shared_task.task_id != TASKSLOTEMPTY) {
ShowClientTaskInfoMessage(&m_active_shared_task, to);
}
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id == TASKSLOTEMPTY) {
continue;
}
ShowClientTaskInfoMessage(&active_quest, to);
}
to->SendChatLineBreak();
}
// TODO: Shared Task
int ClientTaskState::TaskTimeLeft(int task_id)
{
// type "task"
if (m_active_task.task_id == task_id) {
int time_now = time(nullptr);
const auto task_data = task_manager->GetTaskData(task_id);
if (!task_data) {
return -1;
}
if (!task_data->duration) {
return -1;
}
int time_left = (m_active_task.accepted_time + task_data->duration - time_now);
return (time_left > 0 ? time_left : 0);
}
// type "shared task"
if (m_active_shared_task.task_id == task_id) {
int time_now = time(nullptr);
const auto task_data = task_manager->GetTaskData(task_id);
if (!task_data) {
return -1;
}
if (!task_data->duration) {
return -1;
}
int time_left = (m_active_shared_task.accepted_time + task_data->duration - time_now);
return (time_left > 0 ? time_left : 0);
}
if (m_active_task_count == 0) {
return -1;
}
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id != task_id) {
continue;
}
int time_now = time(nullptr);
const auto task_data = task_manager->GetTaskData(active_quest.task_id);
if (!task_data) {
return -1;
}
if (!task_data->duration) {
return -1;
}
int time_left = (active_quest.accepted_time + task_data->duration - time_now);
// If Timeleft is negative, return 0, else return the number of seconds left
return (time_left > 0 ? time_left : 0);
}
return -1;
}
bool ClientTaskState::IsTaskCompleted(int task_id)
{
if (!RuleB(TaskSystem, RecordCompletedTasks)) {
return false;
}
for (const auto& e : m_completed_tasks) {
LogTasks("Comparing completed task [{}] with [{}]", e.task_id, task_id);
if (e.task_id == task_id) {
return true;
}
}
return false;
}
bool ClientTaskState::AreTasksCompleted(const std::vector<int>& task_ids)
{
if (!RuleB(TaskSystem, RecordCompletedTasks)) {
return false;
}
for (const auto& e : task_ids) {
if (!IsTaskCompleted(e)) {
return false;
}
}
return true;
}
bool ClientTaskState::TaskOutOfTime(TaskType task_type, int index)
{
// Returns true if the Task in the specified slot has a time limit that has been exceeded.
auto info = GetClientTaskInfo(task_type, index);
if (info == nullptr) {
return false;
}
// make sure the task_id is at least maybe in our array
if (info->task_id <= 0) {
return false;
}
int time_now = time(nullptr);
const auto task_data = task_manager->GetTaskData(info->task_id);
if (!task_data) {
return false;
}
return (task_data->duration && (info->accepted_time + task_data->duration <= time_now));
}
void ClientTaskState::TaskPeriodicChecks(Client *client)
{
// shared task expiration is handled by world
// type "task"
if (m_active_task.task_id != TASKSLOTEMPTY) {
if (TaskOutOfTime(TaskType::Task, TASKSLOTTASK)) {
// Send Red Task Failed Message
client->SendTaskFailed(m_active_task.task_id, TASKSLOTTASK, TaskType::Task);
// Remove the task from the client
client->CancelTask(TASKSLOTTASK, TaskType::Task);
// It is a conscious decision to only fail one task per call to this method,
// otherwise the player will not see all the failed messages where multiple
// tasks fail at the same time.
return;
}
}
if (m_active_task_count == 0) {
return;
}
// Check for tasks that have failed because they have not been completed in the specified time
//
for (int task_index = 0; task_index < MAXACTIVEQUESTS; task_index++) {
if (m_active_quests[task_index].task_id == TASKSLOTEMPTY) {
continue;
}
if (TaskOutOfTime(TaskType::Quest, task_index)) {
// Send Red Task Failed Message
client->SendTaskFailed(m_active_quests[task_index].task_id, task_index, TaskType::Quest);
// Remove the task from the client
client->CancelTask(task_index, TaskType::Quest);
// It is a conscious decision to only fail one task per call to this method,
// otherwise the player will not see all the failed messages where multiple
// tasks fail at the same time.
break;
}
}
}
bool ClientTaskState::IsTaskActivityCompleted(TaskType task_type, int index, int activity_id)
{
switch (task_type) {
case TaskType::Task:
if (index != TASKSLOTTASK) {
return false;
}
return m_active_task.activity[activity_id].activity_state == ActivityCompleted;
case TaskType::Shared:
if (index != TASKSLOTSHAREDTASK) {
return false;
}
return m_active_shared_task.activity[activity_id].activity_state == ActivityCompleted;
case TaskType::Quest:
if (index < MAXACTIVEQUESTS) {
return m_active_quests[index].activity[activity_id].activity_state == ActivityCompleted;
}
default:
return false;
}
}
// should we be defaulting to hidden?
ActivityState ClientTaskState::GetTaskActivityState(TaskType task_type, int index, int activity_id)
{
ActivityState return_state = ActivityHidden;
switch (task_type) {
case TaskType::Task:
if (index != TASKSLOTTASK) {
return_state = ActivityHidden;
break;
}
return_state = m_active_task.activity[activity_id].activity_state;
break;
case TaskType::Shared:
if (index != TASKSLOTSHAREDTASK) {
return_state = ActivityHidden;
break;
}
return_state = m_active_shared_task.activity[activity_id].activity_state;
break;
case TaskType::Quest:
if (index < MAXACTIVEQUESTS) {
return_state = m_active_quests[index].activity[activity_id].activity_state;
break;
}
break;
default:
return_state = ActivityHidden;
}
LogTasksDetail(
"-- task_type [{}] ({}) index [{}] activity_id [{}] activity_state [{}] ({})",
Tasks::GetTaskTypeIdentifier(task_type),
Tasks::GetTaskTypeDescription(task_type),
index,
activity_id,
Tasks::GetActivityStateIdentifier(return_state),
Tasks::GetActivityStateDescription(return_state)
);
return return_state;
}
int ClientTaskState::GetTaskActivityDoneCount(TaskType task_type, int index, int activity_id)
{
switch (task_type) {
case TaskType::Task:
if (index != TASKSLOTTASK) {
return 0;
}
return m_active_task.activity[activity_id].done_count;
case TaskType::Shared:
if (index != TASKSLOTSHAREDTASK) {
return 0;
}
return m_active_shared_task.activity[activity_id].done_count;
case TaskType::Quest:
if (index < MAXACTIVEQUESTS) {
return m_active_quests[index].activity[activity_id].done_count;
}
default:
return 0;
}
}
int ClientTaskState::GetTaskActivityDoneCountFromTaskID(int task_id, int activity_id)
{
// type "task"
if (m_active_task.task_id == task_id) {
return m_active_task.activity[activity_id].done_count;
}
// type "shared"
if (m_active_shared_task.task_id == task_id) {
return m_active_shared_task.activity[activity_id].done_count;
}
int active_task_index = -1;
for (int task_index = 0; task_index < MAXACTIVEQUESTS; task_index++) {
if (m_active_quests[task_index].task_id == task_id) {
active_task_index = task_index;
break;
}
}
if (active_task_index == -1) {
return 0;
}
if (m_active_quests[active_task_index].activity[activity_id].done_count) {
return m_active_quests[active_task_index].activity[activity_id].done_count;
}
else {
return 0;
}
}
int ClientTaskState::GetTaskStartTime(TaskType task_type, int index)
{
switch (task_type) {
case TaskType::Task:
return m_active_task.accepted_time;
case TaskType::Quest:
return m_active_quests[index].accepted_time;
case TaskType::Shared:
return m_active_shared_task.accepted_time;
default:
return -1;
}
}
void ClientTaskState::CancelAllTasks(Client *client)
{
// This method exists solely to be called during #task reloadall
// It removes tasks from the in-game client state ready for them to be
// resent to the client, in case an updated task fails to load
// task
// these cancels lock up the client for some reason
CancelTask(client, TASKSLOTTASK, TaskType::Task, false);
m_active_task.task_id = TASKSLOTEMPTY;
// shared task
client->m_requested_shared_task_removal = true;
CancelTask(client, TASKSLOTSHAREDTASK, TaskType::Shared, false);
client->m_requested_shared_task_removal = false;
m_active_shared_task.task_id = TASKSLOTEMPTY;
// "quests"
for (int task_index = 0; task_index < MAXACTIVEQUESTS; task_index++)
if (m_active_quests[task_index].task_id != TASKSLOTEMPTY) {
CancelTask(client, task_index, TaskType::Quest, false);
m_active_quests[task_index].task_id = TASKSLOTEMPTY;
}
}
void ClientTaskState::CancelTask(Client *c, int sequence_number, TaskType task_type, bool remove_from_db)
{
LogTasks("CancelTask");
// shared task middleware
// intercept and pass to world first before processing normally
if (!c->m_requested_shared_task_removal && task_type == TaskType::Shared && m_active_shared_task.task_id != 0) {
// struct
auto pack = new ServerPacket(ServerOP_SharedTaskQuit, sizeof(ServerSharedTaskQuit_Struct));
auto *r = (ServerSharedTaskQuit_Struct *) pack->pBuffer;
// fill
r->requested_character_id = c->CharacterID();
r->task_id = m_active_shared_task.task_id;
r->remove_from_db = remove_from_db;
// send
worldserver.SendPacket(pack);
safe_delete(pack);
return;
}
// packet
auto outapp = new EQApplicationPacket(OP_CancelTask, sizeof(CancelTask_Struct));
// fill
auto *cts = (CancelTask_Struct *) outapp->pBuffer;
cts->SequenceNumber = sequence_number;
cts->type = static_cast<uint32>(task_type);
// send
c->QueuePacket(outapp);
safe_delete(outapp);
// persistence
if (remove_from_db) {
RemoveTask(c, sequence_number, task_type);
m_has_explore_task = HasExploreTask(c);
}
}
void ClientTaskState::KickPlayersSharedTask(Client* client)
{
uint32_t pack_size = sizeof(ServerSharedTaskKickPlayers_Struct);
auto pack = std::make_unique<ServerPacket>(ServerOP_SharedTaskKickPlayers, pack_size);
auto buf = reinterpret_cast<ServerSharedTaskKickPlayers_Struct*>(pack->pBuffer);
buf->source_character_id = client->CharacterID();
buf->task_id = m_active_shared_task.task_id;
worldserver.SendPacket(pack.get());
}
void ClientTaskState::RemoveTask(Client *client, int sequence_number, TaskType task_type)
{
int character_id = client->CharacterID();
Log(Logs::General, Logs::Tasks, "ClientTaskState Cancel Task %i ", sequence_number);
int task_id = -1;
switch (task_type) {
case TaskType::Task:
if (sequence_number == TASKSLOTTASK) {
task_id = m_active_task.task_id;
}
break;
case TaskType::Quest:
if (sequence_number < MAXACTIVEQUESTS) {
task_id = m_active_quests[sequence_number].task_id;
}
break;
case TaskType::Shared:
if (sequence_number == TASKSLOTSHAREDTASK) {
task_id = m_active_shared_task.task_id;
}
break;
default:
break;
}
CharacterActivitiesRepository::DeleteWhere(
database,
fmt::format("charid = {} AND taskid = {}", character_id, task_id)
);
CharacterTasksRepository::DeleteWhere(
database,
fmt::format("charid = {} AND taskid = {} AND type = {}", character_id, task_id, static_cast<int>(task_type))
);
switch (task_type) {
case TaskType::Task:
m_active_task.task_id = TASKSLOTEMPTY;
break;
case TaskType::Shared:
m_active_shared_task.task_id = TASKSLOTEMPTY;
break;
case TaskType::Quest:
m_active_quests[sequence_number].task_id = TASKSLOTEMPTY;
m_active_task_count--;
break;
default:
break;
}
}
void ClientTaskState::RemoveTaskByTaskID(Client *client, uint32 task_id)
{
switch (task_manager->GetTaskType(task_id)) {
case TaskType::Task: {
if (m_active_task.task_id == task_id) {
LogTasks("RemoveTaskByTaskID found Task [{}]", task_id);
CancelTask(client, TASKSLOTTASK, TaskType::Task, true);
}
break;
}
case TaskType::Shared: {
if (m_active_shared_task.task_id == task_id) {
LogTasks("RemoveTaskByTaskID found Shared Task [{}]", task_id);
CancelTask(client, TASKSLOTSHAREDTASK, TaskType::Shared, true);
}
break;
}
case TaskType::Quest: {
for (int active_quest = 0; active_quest < MAXACTIVEQUESTS; active_quest++) {
if (m_active_quests[active_quest].task_id == task_id) {
LogTasks("RemoveTaskByTaskID found Quest [{}] at index [{}]", task_id, active_quest);
CancelTask(client, active_quest, TaskType::Quest, true);
}
}
}
default: {
break;
}
}
}
void ClientTaskState::AcceptNewTask(
Client *client,
int task_id,
int npc_type_id,
time_t accept_time,
bool enforce_level_requirement
)
{
if (!task_manager || task_id < 0) {
client->Message(Chat::Red, "Task system not functioning, or task_id %i out of range.", task_id);
return;
}
const auto task = task_manager->GetTaskData(task_id);
if (!task) {
client->Message(Chat::Red, "Invalid task_id %i", task_id);
return;
}
// shared task
// intercept and pass to world first before processing normally
if (!client->m_requesting_shared_task && task->type == TaskType::Shared) {
LogTasksDetail(
"Initiating shared_task request | task_id [{}] character_id [{}] name [{}]",
task_id,
client->CharacterID(),
client->GetCleanName()
);
// struct
auto pack = new ServerPacket(ServerOP_SharedTaskRequest, sizeof(ServerSharedTaskRequest_Struct));
auto *r = (ServerSharedTaskRequest_Struct *) pack->pBuffer;
// fill
r->requested_character_id = client->CharacterID();
r->requested_task_id = task_id;
r->requested_npc_type_id = npc_type_id;
// send
worldserver.SendPacket(pack);
safe_delete(pack);
return;
}
bool max_tasks = false;
switch (task->type) {
case TaskType::Task:
if (m_active_task.task_id != TASKSLOTEMPTY) {
max_tasks = true;
}
break;
case TaskType::Shared: // TODO: shared tasks
if (m_active_shared_task.task_id != TASKSLOTEMPTY) {
max_tasks = true;
}
break;
case TaskType::Quest:
if (m_active_task_count == MAXACTIVEQUESTS) {
max_tasks = true;
}
break;
default:
break;
}
if (max_tasks) {
client->MessageString(Chat::Yellow, MAX_ACTIVE_TASKS, ".", ".", client->GetName());
return;
}
// only Quests can have more than one, so don't need to check others
if (task->type == TaskType::Quest) {
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id == task_id) {
// live doesn't have an eqstr for it but this seems to be the string used for this scenario
client->Message(Chat::Yellow, "You are already working on a task for this person, you must finish it before asking for another.");
return;
}
}
}
if (enforce_level_requirement && !task_manager->ValidateLevel(task_id, client->GetLevel())) {
client->MessageString(Chat::Yellow, TASK_NOT_RIGHT_LEVEL);
return;
}
if (!task_manager->IsTaskRepeatable(task_id) && IsTaskCompleted(task_id)) {
return;
}
// solo task timer lockout validation
if (task->type != TaskType::Shared)
{
auto task_timers = CharacterTaskTimersRepository::GetWhere(database, fmt::format(
SQL(
character_id = {}
AND (task_id = {}
OR (timer_group > 0 AND timer_type = {} AND timer_group = {})
OR (timer_group > 0 AND timer_type = {} AND timer_group = {}))
AND expire_time > NOW() ORDER BY timer_type ASC LIMIT 1
),
client->CharacterID(),
task_id,
static_cast<int>(TaskTimerType::Replay),
task->replay_timer_group,
static_cast<int>(TaskTimerType::Request),
task->request_timer_group
));
if (!task_timers.empty())
{
auto timer_type = static_cast<TaskTimerType>(task_timers.front().timer_type);
auto seconds = task_timers.front().expire_time - std::time(nullptr);
auto days = fmt::format_int(seconds / 86400).str();
auto hours = fmt::format_int((seconds / 3600) % 24).str();
auto mins = fmt::format_int((seconds / 60) % 60).str();
// these solo task messages are in SharedTaskMessage for convenience
if (timer_type == TaskTimerType::Replay)
{
client->MessageString(Chat::Red, TaskStr::ASSIGN_REPLAY_TIMER, days.c_str(), hours.c_str(), mins.c_str());
}
else if (timer_type == TaskTimerType::Request)
{
auto eqstr = TaskStr::Get(TaskStr::ASSIGN_REQUEST_TIMER);
client->Message(Chat::Red, fmt::format(fmt::runtime(eqstr), days, hours, mins).c_str());
}
return;
}
}
// We do it this way, because when the Client cancels a task, it retains the sequence number of the remaining
// tasks in it's window, until something causes the TaskDescription packets to be sent again. We could just
// resend all the active task data to the client when it cancels a task, but that could be construed as a
// waste of bandwidth.
//
ClientTaskInformation *active_slot = nullptr;
switch (task->type) {
case TaskType::Task:
active_slot = &m_active_task;
break;
case TaskType::Shared:
active_slot = &m_active_shared_task;
break;
case TaskType::Quest:
for (int task_index = 0; task_index < MAXACTIVEQUESTS; task_index++) {
Log(Logs::General, Logs::Tasks,
"ClientTaskState Looking for free slot in slot %i, found task_id of %i", task_index,
m_active_quests[task_index].task_id);
if (m_active_quests[task_index].task_id == 0) {
active_slot = &m_active_quests[task_index];
break;
}
}
break;
default:
break;
}
// This shouldn't happen unless there is a bug in the handling of ActiveTaskCount somewhere
if (active_slot == nullptr) {
client->MessageString(Chat::Yellow, MAX_ACTIVE_TASKS, ".", ".", client->GetName());
return;
}
active_slot->task_id = task_id;
active_slot->accepted_time = static_cast<int>(accept_time);
active_slot->updated = true;
active_slot->was_rewarded = false;
for (int activity_id = 0; activity_id < task->activity_count; activity_id++) {
active_slot->activity[activity_id].activity_id = activity_id;
active_slot->activity[activity_id].done_count = 0;
active_slot->activity[activity_id].activity_state = ActivityHidden;
active_slot->activity[activity_id].updated = true;
}
UnlockActivities(client, *active_slot);
if (task->type == TaskType::Quest) {
m_active_task_count++;
}
// add request timer (shared task timers are added to members by world)
if (task->request_timer_seconds > 0)
{
auto expire_time = active_slot->accepted_time + task->request_timer_seconds;
auto seconds = expire_time - std::time(nullptr);
if (seconds > 0) // not already expired
{
if (task->type != TaskType::Shared)
{
auto timer = CharacterTaskTimersRepository::NewEntity();
timer.character_id = client->CharacterID();
timer.task_id = task_id;
timer.timer_type = static_cast<int>(TaskTimerType::Request);
timer.timer_group = task->request_timer_group;
timer.expire_time = expire_time;
CharacterTaskTimersRepository::InsertOne(database, timer);
}
client->Message(Chat::Yellow, fmt::format(
TaskStr::Get(TaskStr::RECEIVED_REQUEST_TIMER),
task->title,
fmt::format_int(seconds / 86400).c_str(), // days
fmt::format_int((seconds / 3600) % 24).c_str(), // hours
fmt::format_int((seconds / 60) % 60).c_str() // minutes
).c_str());
}
}
task_manager->SendSingleActiveTaskToClient(client, *active_slot, false, true);
client->StartTaskRequestCooldownTimer();
client->MessageString(Chat::DefaultText, YOU_ASSIGNED_TASK, task->title.c_str());
task_manager->SaveClientState(client, this);
NPC *npc = entity_list.GetID(npc_type_id)->CastToNPC();
if (npc) {
if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_ACCEPT)) {
auto e = PlayerEvent::TaskAcceptEvent{
.npc_id = static_cast<uint32>(npc_type_id),
.npc_name = npc->GetCleanName(),
.task_id = static_cast<uint32>(task_id),
.task_name = task_manager->GetTaskName(static_cast<uint32>(task_id)),
};
RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_ACCEPT, e);
}
if (parse->HasQuestSub(npc->GetNPCTypeID(), EVENT_TASK_ACCEPTED)) {
parse->EventNPC(EVENT_TASK_ACCEPTED, npc, client, std::to_string(task_id), 0);
}
} else {
if (player_event_logs.IsEventEnabled(PlayerEvent::TASK_ACCEPT)) {
auto e = PlayerEvent::TaskAcceptEvent{
.npc_id = 0,
.npc_name = "No NPC",
.task_id = static_cast<uint32>(task_id),
.task_name = task_manager->GetTaskName(static_cast<uint32>(task_id)),
};
RecordPlayerEventLogWithClient(client, PlayerEvent::TASK_ACCEPT, e);
}
}
if (parse->PlayerHasQuestSub(EVENT_TASK_ACCEPTED)) {
parse->EventPlayer(EVENT_TASK_ACCEPTED, client, std::to_string(task_id), 0);
}
}
void ClientTaskState::ProcessTaskProximities(Client *client, float x, float y, float z)
{
if (!m_has_explore_task) {
return;
}
float last_x = client->ProximityX();
float last_y = client->ProximityY();
float last_z = client->ProximityZ();
if ((last_x == x) && (last_y == y) && (last_z == z)) {
return;
}
UpdateTasksOnExplore(client, glm::vec4(x, y, z, 0.0f));
}
void ClientTaskState::SharedTaskIncrementDoneCount(
Client *client,
int task_id,
int activity_id,
int done_count,
bool ignore_quest_update
)
{
const auto t = task_manager->GetTaskData(task_id);
auto info = GetClientTaskInfo(t->type, TASKSLOTSHAREDTASK);
if (info == nullptr) {
return;
}
// absolute value update
info->activity[activity_id].done_count = done_count;
LogTasksDetail(
"Setting task_id [{}] to absolute done_count value of [{}] via increment [{}]",
task_id,
info->activity[activity_id].done_count,
done_count
);
IncrementDoneCount(
client,
t,
TASKSLOTSHAREDTASK,
activity_id,
0, // no op
ignore_quest_update
);
}
const ClientTaskInformation &ClientTaskState::GetActiveSharedTask() const
{
return m_active_shared_task;
}
bool ClientTaskState::HasActiveSharedTask()
{
return GetActiveSharedTask().task_id != 0;
}
void ClientTaskState::CreateTaskDynamicZone(Client* client, int task_id, DynamicZone& dz_request)
{
const auto task = task_manager->GetTaskData(task_id);
if (!task)
{
return;
}
// dz should be named the version-based zone name (used in choose zone window and dz window on live)
auto zone_info = zone_store.GetZoneWithFallback(dz_request.GetZoneID(), dz_request.GetZoneVersion());
dz_request.SetName(zone_info && !zone_info->long_name.empty() ? zone_info->long_name : task->title);
dz_request.SetMinPlayers(task->min_players);
dz_request.SetMaxPlayers(task->max_players);
// a task might create a dz from an element so override dz duration to time remaining
std::chrono::seconds seconds(TaskTimeLeft(task_id));
if (task->duration == 0 || seconds.count() < 0)
{
// cap unlimited duration tasks so instance isn't held indefinitely
// expected behavior is to re-acquire any unlimited tasks that have an expired dz
seconds = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::hours(24));
}
dz_request.SetDuration(static_cast<uint32_t>(seconds.count()));
if (task->type == TaskType::Shared)
{
dz_request.SetType(DynamicZoneType::Mission);
// shared task missions are created in world
std::ostringstream ss = dz_request.GetSerialized();
std::string_view sv = ss.view();
auto pack_size = sizeof(ServerSharedTaskCreateDynamicZone_Struct) + sv.size();
auto pack = std::make_unique<ServerPacket>(ServerOP_SharedTaskCreateDynamicZone, static_cast<uint32_t>(pack_size));
auto buf = reinterpret_cast<ServerSharedTaskCreateDynamicZone_Struct*>(pack->pBuffer);
buf->source_character_id = client->CharacterID();
buf->task_id = task_id;
buf->cereal_size = static_cast<uint32_t>(sv.size());
memcpy(buf->cereal_data, sv.data(), sv.size());
worldserver.SendPacket(pack.get());
}
}
void ClientTaskState::ListTaskTimers(Client* client)
{
LogTasksDetail("Client [{}]", client->GetCleanName());
// this isn't live-like but we need to throttle query (alternative is caching timers)
if (!client->m_list_task_timers_rate_limit.Check()) {
client->Message(Chat::White, "Sending messages too fast");
return;
}
auto character_task_timers = CharacterTaskTimersRepository::GetWhere(
database, fmt::format(
"character_id = {} AND expire_time > NOW()",
client->CharacterID()
)
);
for (const auto& task_timer : character_task_timers)
{
const auto task = task_manager->GetTaskData(task_timer.task_id);
if (task)
{
auto timer_type = static_cast<TaskTimerType>(task_timer.timer_type);
auto seconds = task_timer.expire_time - std::time(nullptr);
auto days = fmt::format_int(seconds / 86400).str();
auto hours = fmt::format_int((seconds / 3600) % 24).str();
auto mins = fmt::format_int((seconds / 60) % 60).str();
if (timer_type == TaskTimerType::Replay)
{
client->MessageString(Chat::Yellow, TaskStr::REPLAY_TIMER_REMAINING,
task->title.c_str(), days.c_str(), hours.c_str(), mins.c_str());
}
else
{
auto eqstr = TaskStr::Get(TaskStr::REQUEST_TIMER_REMAINING);
client->Message(Chat::Yellow, fmt::format(fmt::runtime(eqstr), task->title, days, hours, mins).c_str());
}
}
}
if (character_task_timers.empty()) {
client->MessageString(Chat::Yellow, TaskStr::NO_REPLAY_TIMERS);
}
}
void ClientTaskState::AddReplayTimer(Client* client, ClientTaskInformation& client_task, const TaskInformation& task)
{
if (task.replay_timer_seconds > 0)
{
// solo task replay timers are based on completion time
auto expire_time = std::time(nullptr) + task.replay_timer_seconds;
auto timer = CharacterTaskTimersRepository::NewEntity();
timer.character_id = client->CharacterID();
timer.task_id = client_task.task_id;
timer.timer_type = static_cast<int>(TaskTimerType::Replay);
timer.timer_group = task.replay_timer_group;
timer.expire_time = expire_time;
// replace any existing replay timer
CharacterTaskTimersRepository::DeleteWhere(database, fmt::format(
"(task_id = {} OR (timer_group > 0 AND timer_group = {})) AND timer_type = {} AND character_id = {}",
client_task.task_id,
task.replay_timer_group,
static_cast<int>(TaskTimerType::Replay),
client->CharacterID()));
CharacterTaskTimersRepository::InsertOne(database, timer);
client->Message(Chat::Yellow, fmt::format(
TaskStr::Get(TaskStr::RECEIVED_REPLAY_TIMER),
task.title,
fmt::format_int(task.replay_timer_seconds / 86400).c_str(), // days
fmt::format_int((task.replay_timer_seconds / 3600) % 24).c_str(), // hours
fmt::format_int((task.replay_timer_seconds / 60) % 60).c_str() // minutes
).c_str());
}
}
// Sync zone client donecount state
//
// World is always authoritative, but we still need to keep zone state in sync with reality and in this case we need
// to update zone since world hasn't had a chance to let clients at the zone level know it's ok to progress to the next
// donecount
//
// If we send updates too fast and world has not had a chance to confirm and then inform clients to set their
// absolute state we will miss and discard updates because each update sets the same donecount
//
// Example: say we have a 100 kill task and we kill 10 mobs in an AOE, world gets 10 updates at once but they are
// all from the same donecount so world only confirms 1 was actually killed and the task updates only 1 which is not
// intended behavior.
//
// In this case we need to ensure that clients at the zone level increment internally even if they aren't
// necessarily confirmed by world yet because any of them could inform world of the next donecount so we need to sync
// zone-level before sending updates to world
void ClientTaskState::SyncSharedTaskZoneClientDoneCountState(
Client *p_client,
TaskType type,
int task_index,
int activity_id,
uint32 done_count
)
{
for (auto &e : entity_list.GetClientList()) {
auto c = e.second;
if (c->GetSharedTaskId() == p_client->GetSharedTaskId()) {
auto t = c->GetTaskState()->GetClientTaskInfo(type, task_index);
if (t == nullptr) {
continue;
}
LogTasksDetail(
"Setting internally client [{}] to donecount [{}]",
c->GetCleanName(),
done_count
);
t->activity[activity_id].done_count = (int) done_count;
}
}
}
bool ClientTaskState::HasActiveTasks()
{
if (!task_manager) {
return false;
}
if (m_active_task.task_id != TASKSLOTEMPTY) {
return true;
}
if (m_active_shared_task.task_id != TASKSLOTEMPTY) {
return true;
}
bool has_active_quest = false;
for (auto &active_quest : m_active_quests) {
if (active_quest.task_id == TASKSLOTEMPTY) {
continue;
}
return true;
}
return false;
}
void ClientTaskState::LockSharedTask(Client* client, bool lock)
{
if (m_active_shared_task.task_id != TASKSLOTEMPTY)
{
ServerPacket pack(ServerOP_SharedTaskLock, sizeof(ServerSharedTaskLock_Struct));
auto buf = reinterpret_cast<ServerSharedTaskLock_Struct*>(pack.pBuffer);
buf->source_character_id = client->CharacterID();
buf->task_id = m_active_shared_task.task_id;
buf->lock = lock;
worldserver.SendPacket(&pack);
}
}
void ClientTaskState::EndSharedTask(Client* client, bool send_fail)
{
if (task_manager && m_active_shared_task.task_id != TASKSLOTEMPTY)
{
task_manager->EndSharedTask(*client, m_active_shared_task.task_id, send_fail);
}
}
bool ClientTaskState::CanAcceptNewTask(Client* client, int task_id, int npc_entity_id) const
{
auto it = std::find_if(m_last_offers.begin(), m_last_offers.end(),
[&](const TaskOffer& offer) { return offer.task_id == task_id; });
if (it == m_last_offers.end())
{
LogTasks("Client [{}] accepted unoffered task [{}]", client->GetName(), task_id);
return false;
}
if (npc_entity_id != it->npc_entity_id)
{
LogTasks("Client [{}] accepted task [{}] with wrong npc entity id [{}] vs offered [{}]",
client->GetName(), task_id, npc_entity_id, it->npc_entity_id);
return false;
}
NPC* npc = entity_list.GetID(it->npc_entity_id)->CastToNPC();
if (!npc) // client window disappears in this case
{
LogTasks("Client [{}] accepted task [{}] from missing npc", client->GetName(), task_id);
return false;
}
auto dist = npc->CalculateDistance(client->GetX(), client->GetY(), client->GetZ());
if (dist > MAX_TASK_SELECT_DISTANCE)
{
LogTasks("Client [{}] accepted task [{}] from npc [{}] out of range [{}]",
client->GetName(), task_id, npc->GetCleanName(), dist);
return false;
}
return true;
}