mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 21:01:29 +00:00
* Add faction logging category Probably should use this for more things * Add FactionAssociation struct This is simply just a struct that contains an array of faction ids and multiplier. This can hold a maximum of 10 entries (Seru hit is 8, so 2 extra) this can be raised if need be. * Add database changes and other data point changes This is all the database changes and loading changes Included is an optional SQL that will be used as a starting point, there is likely errors or typos, but we will fix those as they are discovered. * Add Client::RewardFaction function This just takes the faction ID and the magnitude of the primary faction hit and calculates the rest. The minimum change will be either 1 or -1. We stop processing after we see an ID of 0 and assume there will be no later entries. The primary faction ID will always receive a hit even if there is no faction association entries * Add users of RewardFaction to NPC death, tasks, and QuestRewards This will only use the new system if the magnitude is set, otherwise we will just use the old system still * Add quest system calls and lua QuestReward support * Add #factionassociation command This just calls RewardFaction, mostly useful for debugging
2479 lines
70 KiB
C++
2479 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"
|
|
|
|
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("[SendTaskHistory] 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("[EnableTask] New enabled task list");
|
|
for (int enabled_task : m_enabled_tasks) {
|
|
LogTasksDetail("[EnableTask] 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]);
|
|
}
|
|
|
|
std::string query = query_stream.str();
|
|
if (!tasks_enabled.empty()) {
|
|
database.QueryDatabase(query);
|
|
}
|
|
else {
|
|
LogTasks("[EnableTask] Called for character_id [{}] but, no tasks exist", character_id);
|
|
}
|
|
}
|
|
|
|
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("[DisableTask] New enabled task list ");
|
|
for (int enabled_task : m_enabled_tasks) {
|
|
LogTasks("[DisableTask] 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 << ")";
|
|
|
|
std::string query = queryStream.str();
|
|
|
|
if (tasks_disabled.size()) {
|
|
database.QueryDatabase(query);
|
|
}
|
|
else {
|
|
LogTasks(
|
|
"[DisableTask] DisableTask called for character_id [{}] ... but, no tasks exist",
|
|
character_id
|
|
);
|
|
}
|
|
}
|
|
|
|
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("[DeleteCompletedTasksFromDatabase] 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(
|
|
"[UnlockActivities] 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(
|
|
"[UnlockActivities] 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("[UnlockActivities] 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 = m_completed_tasks.size() - before;
|
|
|
|
LogTasksDetail("[RecordCompletedTask] 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("[RecordCompletedTask] [{}] 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;
|
|
}
|
|
|
|
// item is only checked for updates that provide an item (unlike npc which may be null for non-npcs)
|
|
if (activity.item_id != 0 && filter.item_id != 0 && activity.item_id != filter.item_id)
|
|
{
|
|
LogTasks("[CanUpdate] client [{}] task [{}]-[{}] failed item id filter", client->GetName(), task_id, client_activity.activity_id);
|
|
return false;
|
|
}
|
|
|
|
if (activity.npc_id != 0 && (!filter.npc || activity.npc_id != filter.npc->GetNPCTypeID()))
|
|
{
|
|
LogTasks("[CanUpdate] client [{}] task [{}]-[{}] failed npc id filter", client->GetName(), task_id, client_activity.activity_id);
|
|
return false;
|
|
}
|
|
|
|
if (!activity.CheckZone(zone->GetZoneID(), zone->GetInstanceVersion()))
|
|
{
|
|
LogTasks("[CanUpdate] 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("[CanUpdate] client [{}] task [{}]-[{}] failed area filter", client->GetName(), task_id, client_activity.activity_id);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (activity.item_goal_id != 0 && filter.item_id != 0 &&
|
|
!task_manager->m_goal_list_manager.IsInList(activity.item_goal_id, filter.item_id))
|
|
{
|
|
LogTasks("[CanUpdate] client [{}] task [{}]-[{}] failed item goallist filter", client->GetName(), task_id, client_activity.activity_id);
|
|
return false;
|
|
}
|
|
|
|
if (!activity.item_id_list.empty() && filter.item_id != 0 &&
|
|
!TaskGoalListManager::IsInMatchList(activity.item_id_list, std::to_string(filter.item_id)))
|
|
{
|
|
LogTasks("[CanUpdate] client [{}] task [{}]-[{}] failed item match filter", client->GetName(), task_id, client_activity.activity_id);
|
|
return false;
|
|
}
|
|
|
|
if (activity.npc_goal_id != 0 && (!filter.npc ||
|
|
!task_manager->m_goal_list_manager.IsInList(activity.npc_goal_id, filter.npc->GetNPCTypeID())))
|
|
{
|
|
LogTasks("[CanUpdate] client [{}] task [{}]-[{}] failed npc goallist 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.npc ||
|
|
(!TaskGoalListManager::IsInMatchListPartial(activity.npc_match_list, filter.npc->GetName()) &&
|
|
!TaskGoalListManager::IsInMatchListPartial(activity.npc_match_list, filter.npc->GetCleanName()) &&
|
|
!TaskGoalListManager::IsInMatchList(activity.npc_match_list, std::to_string(filter.npc->GetNPCTypeID())))))
|
|
{
|
|
LogTasks("[CanUpdate] 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))
|
|
{
|
|
LogTasks("[UpdateTasks] 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);
|
|
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("[HasExploreTask] 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.npc = 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.npc = 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.npc = 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, NPC* npc, 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 ActivityLoot, ActivityTradeSkill, ActivityFish or ActivityForage
|
|
|
|
LogTasks("[UpdateTasksForItem] activity_type [{}] item_id [{}]", static_cast<int>(type), item_id);
|
|
|
|
TaskUpdateFilter filter{};
|
|
filter.type = type;
|
|
filter.npc = npc; // looting may filter on npc id or name
|
|
filter.item_id = item_id;
|
|
|
|
UpdateTasks(client, filter, count);
|
|
}
|
|
|
|
void ClientTaskState::UpdateTasksOnExplore(Client* client, const glm::vec4& pos)
|
|
{
|
|
LogTasksDetail("[UpdateTasksOnExplore] 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("[UpdateTasksOnDeliver] npc [{}]", npc->GetName());
|
|
|
|
bool is_updated = false;
|
|
|
|
TaskUpdateFilter filter{};
|
|
filter.npc = 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)
|
|
{
|
|
// remove items used in updates
|
|
item->SetCharges(count - updated_count);
|
|
if (count == updated_count)
|
|
{
|
|
item = nullptr; // all items in trade slot consumed
|
|
}
|
|
is_updated = true;
|
|
}
|
|
}
|
|
|
|
return is_updated;
|
|
}
|
|
|
|
void ClientTaskState::UpdateTasksOnTouch(Client *client, int dz_switch_id)
|
|
{
|
|
LogTasks("[UpdateTasksOnTouch] 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.npc = 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(
|
|
"[IncrementDoneCount] 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(
|
|
"[IncrementDoneCount] 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) {
|
|
std::string 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 = activity_type == TaskActivityType::GiveCash ? 1 : count;
|
|
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("[IncrementDoneCount] 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("[IncrementDoneCount] 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) {
|
|
std::string export_string = fmt::format(
|
|
"{} {}",
|
|
info->task_id,
|
|
info->activity[activity_id].activity_id
|
|
);
|
|
parse->EventPlayer(EVENT_TASK_STAGE_COMPLETE, client, export_string, 0);
|
|
}
|
|
/* QS: PlayerLogTaskUpdates :: Update */
|
|
if (RuleB(QueryServ, PlayerLogTaskUpdates)) {
|
|
std::string event_desc = StringFormat(
|
|
"Task Stage Complete :: taskid:%i activityid:%i donecount:%i in zoneid:%i instid:%i",
|
|
info->task_id,
|
|
info->activity[activity_id].activity_id,
|
|
info->activity[activity_id].done_count,
|
|
client->GetZoneID(),
|
|
client->GetInstanceID());
|
|
QServ->PlayerLogEvent(Player_Log_Task_Updates, client->CharacterID(), event_desc);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
DispatchEventTaskComplete(client, *info, activity_id);
|
|
|
|
/* QS: PlayerLogTaskUpdates :: Complete */
|
|
if (RuleB(QueryServ, PlayerLogTaskUpdates)) {
|
|
std::string event_desc = StringFormat(
|
|
"Task Complete :: taskid:%i activityid:%i donecount:%i in zoneid:%i instid:%i",
|
|
info->task_id,
|
|
info->activity[activity_id].activity_id,
|
|
info->activity[activity_id].done_count,
|
|
client->GetZoneID(),
|
|
client->GetInstanceID());
|
|
QServ->PlayerLogEvent(Player_Log_Task_Updates, client->CharacterID(), event_desc);
|
|
}
|
|
|
|
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
|
|
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
|
|
);
|
|
}
|
|
|
|
task_manager->SaveClientState(client, this);
|
|
|
|
return count;
|
|
}
|
|
|
|
void ClientTaskState::DispatchEventTaskComplete(Client* client, ClientTaskInformation& info, int activity_id)
|
|
{
|
|
std::string export_string = fmt::format(
|
|
"{} {} {}",
|
|
info.activity[activity_id].done_count,
|
|
info.activity[activity_id].activity_id,
|
|
info.task_id
|
|
);
|
|
parse->EventPlayer(EVENT_TASK_COMPLETE, client, export_string, 0);
|
|
}
|
|
|
|
void ClientTaskState::RewardTask(Client *client, const TaskInformation *task_information, ClientTaskInformation& client_task)
|
|
{
|
|
|
|
if (!task_information || !client || client_task.was_rewarded) {
|
|
return;
|
|
}
|
|
|
|
client_task.was_rewarded = true;
|
|
client_task.updated = true;
|
|
|
|
if (!task_information->completion_emote.empty()) {
|
|
client->Message(Chat::Yellow, task_information->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
|
|
|
|
const EQ::ItemData *item_data;
|
|
std::vector<int> reward_list;
|
|
|
|
switch (task_information->reward_method) {
|
|
case METHODSINGLEID: {
|
|
if (task_information->reward_id) {
|
|
int16_t slot = client->GetInv().FindFreeSlot(true, true);
|
|
client->SummonItem(task_information->reward_id, -1, 0, 0, 0, 0, 0, 0, false, slot);
|
|
item_data = database.GetItem(task_information->reward_id);
|
|
if (item_data) {
|
|
client->MessageString(Chat::Yellow, YOU_HAVE_BEEN_GIVEN, item_data->Name);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case METHODLIST: {
|
|
reward_list = task_manager->m_goal_list_manager.GetListContents(task_information->reward_id);
|
|
for (int item_id : reward_list) {
|
|
int16_t slot = client->GetInv().FindFreeSlot(true, true);
|
|
client->SummonItem(item_id, -1, 0, 0, 0, 0, 0, 0, false, slot);
|
|
item_data = database.GetItem(item_id);
|
|
if (item_data) {
|
|
client->MessageString(Chat::Yellow, YOU_HAVE_BEEN_GIVEN, item_data->Name);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
// Nothing special done for METHODQUEST
|
|
break;
|
|
}
|
|
}
|
|
|
|
// just use normal NPC faction ID stuff
|
|
if (task_information->faction_reward && task_information->faction_amount == 0) {
|
|
client->SetFactionLevel(
|
|
client->CharacterID(),
|
|
task_information->faction_reward,
|
|
client->GetBaseClass(),
|
|
client->GetBaseRace(),
|
|
client->GetDeity()
|
|
);
|
|
} else if (task_information->faction_reward != 0 && task_information->faction_amount != 0) {
|
|
client->RewardFaction(
|
|
task_information->faction_reward,
|
|
task_information->faction_amount
|
|
);
|
|
}
|
|
|
|
if (task_information->cash_reward) {
|
|
int platinum, gold, silver, copper;
|
|
|
|
copper = task_information->cash_reward;
|
|
|
|
platinum = copper / 1000;
|
|
copper = copper - (platinum * 1000);
|
|
gold = copper / 100;
|
|
copper = copper - (gold * 100);
|
|
silver = copper / 10;
|
|
copper = copper - (silver * 10);
|
|
|
|
client->CashReward(copper, silver, gold, platinum);
|
|
}
|
|
int32 experience_reward = task_information->experience_reward;
|
|
if (experience_reward > 0) {
|
|
client->AddEXP(experience_reward);
|
|
}
|
|
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);
|
|
client->AddLevelBasedExp(exp_percent, max_level);
|
|
}
|
|
}
|
|
|
|
if (task_information->reward_points > 0)
|
|
{
|
|
if (task_information->reward_point_type == AltCurrencyType::RadiantCrystal)
|
|
{
|
|
client->AddCrystals(task_information->reward_points, 0);
|
|
}
|
|
else if (task_information->reward_point_type == AltCurrencyType::EbonCrystal)
|
|
{
|
|
client->AddCrystals(0, task_information->reward_points);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(
|
|
"[FailTask] 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
|
|
if (m_active_shared_task.task_id == task_id) {
|
|
client->SendTaskFailed(task_id, TASKSLOTSHAREDTASK, TaskType::Shared);
|
|
// Remove the task from the client
|
|
client->CancelTask(TASKSLOTSHAREDTASK, TaskType::Shared);
|
|
return;
|
|
}
|
|
|
|
// TODO: shared tasks
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Shared tasks
|
|
bool ClientTaskState::IsTaskActivityActive(int task_id, int activity_id)
|
|
{
|
|
LogTasks("[IsTaskActivityActive] 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(
|
|
"[IsTaskActivityActive] (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
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int ClientTaskState::IsTaskCompleted(int task_id)
|
|
{
|
|
|
|
// Returns: -1 if RecordCompletedTasks is not true
|
|
// +1 if the task has been completed
|
|
// 0 if the task has not been completed
|
|
|
|
if (!(RuleB(TaskSystem, RecordCompletedTasks))) {
|
|
return -1;
|
|
}
|
|
|
|
for (auto &completed_task : m_completed_tasks) {
|
|
LogTasks("[IsTaskCompleted] Comparing compelted task [{}] with [{}]", completed_task.task_id, task_id);
|
|
if (completed_task.task_id == task_id) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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)
|
|
{
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// type "shared"
|
|
if (m_active_shared_task.task_id != TASKSLOTEMPTY) {
|
|
if (TaskOutOfTime(TaskType::Shared, TASKSLOTSHAREDTASK)) {
|
|
// Send Red Task Failed Message
|
|
client->SendTaskFailed(m_active_shared_task.task_id, TASKSLOTSHAREDTASK, TaskType::Shared);
|
|
// Remove the task from the client
|
|
client->CancelTask(TASKSLOTSHAREDTASK, TaskType::Shared);
|
|
// 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(
|
|
"-- [GetTaskActivityState] 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, "[UPDATE] 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("[UPDATE] 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("[UPDATE] 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("[UPDATE] 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(
|
|
"[AcceptNewTask] 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(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,
|
|
"[UPDATE] 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);
|
|
std::string export_string = std::to_string(task_id);
|
|
|
|
NPC *npc = entity_list.GetID(npc_type_id)->CastToNPC();
|
|
if (npc) {
|
|
parse->EventNPC(EVENT_TASK_ACCEPTED, npc, client, export_string, 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(
|
|
"[SharedTaskIncrementDoneCount] 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.GetZone(dz_request.GetZoneID(), dz_request.GetZoneVersion());
|
|
dz_request.SetName(zone_info->long_name.empty() ? task->title : zone_info->long_name);
|
|
dz_request.SetMinPlayers(task->min_players);
|
|
dz_request.SetMaxPlayers(task->max_players);
|
|
|
|
// a task might create a dz from an objective so override dz duration to time remaining
|
|
// live probably creates the dz with the shared task and just adds members for objectives
|
|
std::chrono::seconds seconds(TaskTimeLeft(task_id));
|
|
if (task->duration == 0 || seconds.count() < 0)
|
|
{
|
|
// todo: maybe add a rule for duration
|
|
// 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::Task || task->type == TaskType::Quest)
|
|
{
|
|
if (task->type == TaskType::Task) {
|
|
dz_request.SetType(DynamicZoneType::Task);
|
|
} else {
|
|
dz_request.SetType(DynamicZoneType::Quest);
|
|
}
|
|
|
|
// todo: can enable non-shared task dz creation when dz ids are persisted for them (db also)
|
|
//DynamicZoneMember solo_member{ client->CharacterID(), client->GetCleanName() };
|
|
//DynamicZone::CreateNew(dz_request, { solo_member });
|
|
}
|
|
else if (task->type == TaskType::Shared)
|
|
{
|
|
dz_request.SetType(DynamicZoneType::Mission);
|
|
|
|
// shared task missions are created in world
|
|
EQ::Net::DynamicPacket dyn_pack = dz_request.GetSerializedDzPacket();
|
|
|
|
auto pack_size = sizeof(ServerSharedTaskCreateDynamicZone_Struct) + dyn_pack.Length();
|
|
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>(dyn_pack.Length());
|
|
memcpy(buf->cereal_data, dyn_pack.Data(), dyn_pack.Length());
|
|
|
|
worldserver.SendPacket(pack.get());
|
|
}
|
|
|
|
}
|
|
|
|
void ClientTaskState::ListTaskTimers(Client* client)
|
|
{
|
|
LogTasksDetail("[ListTaskTimers] 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(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(
|
|
"[IncrementDoneCount] 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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|