#include "../common/global_define.h" #include "../common/misc_functions.h" #include "../common/repositories/character_activities_repository.h" #include "../common/repositories/character_tasks_repository.h" #include "../common/repositories/completed_tasks_repository.h" #include "../common/repositories/task_activities_repository.h" #include "../common/repositories/tasks_repository.h" #include "../common/repositories/tasksets_repository.h" #include "client.h" #include "string_ids.h" #include "task_manager.h" TaskManager::TaskManager() { for (auto &task : p_task_data) { task = nullptr; } } TaskManager::~TaskManager() { for (auto &task : p_task_data) { if (task != nullptr) { safe_delete(task); } } } bool TaskManager::LoadTaskSets() { // Clear all task sets in memory. Done so we can reload them on the fly if required by just calling // this method again. for (auto &task_set : task_sets) { task_set.clear(); } auto rows = TasksetsRepository::GetWhere( content_db, fmt::format( "`id` > 0 AND `id` < {} AND `taskid` >= 0 AND `taskid` < {} ORDER BY `id`, `taskid` ASC", MAXTASKSETS, MAXTASKS ) ); for (auto &task_set: rows) { task_sets[task_set.id].push_back(task_set.taskid); LogTasksDetail("[LoadTaskSets] Adding task_id [{}] to task_set [{}]", task_set.taskid, task_set.id); } return true; } void TaskManager::ReloadGoalLists() { if (!goal_list_manager.LoadLists()) { Log(Logs::Detail, Logs::Tasks, "TaskManager::LoadTasks LoadLists failed"); } } bool TaskManager::LoadTasks(int single_task) { std::string task_query_filter = fmt::format("id = {}", single_task); std::string query; if (single_task == 0) { if (!goal_list_manager.LoadLists()) { LogTasks("[TaskManager::LoadTasks] LoadLists failed"); } if (!LoadTaskSets()) { LogTasks("[TaskManager::LoadTasks] LoadTaskSets failed"); } task_query_filter = fmt::format("id < {}", MAXTASKS); } // load task level data auto repo_tasks = TasksRepository::GetWhere(content_db, task_query_filter); for (auto &task: repo_tasks) { int task_id = task.id; if ((task_id <= 0) || (task_id >= MAXTASKS)) { // This shouldn't happen, as the SELECT is bounded by MAXTASKS LogError("[TASKS]Task ID [{}] out of range while loading tasks from database", task_id); continue; } // load task data p_task_data[task_id] = new TaskInformation; p_task_data[task_id]->type = static_cast(task.type); p_task_data[task_id]->duration = task.duration; p_task_data[task_id]->duration_code = static_cast(task.duration_code); p_task_data[task_id]->title = task.title; p_task_data[task_id]->description = task.description; p_task_data[task_id]->reward = task.reward; p_task_data[task_id]->reward_id = task.rewardid; p_task_data[task_id]->cash_reward = task.cashreward; p_task_data[task_id]->experience_reward = task.xpreward; p_task_data[task_id]->reward_method = (TaskMethodType) task.rewardmethod; p_task_data[task_id]->faction_reward = task.faction_reward; p_task_data[task_id]->min_level = task.minlevel; p_task_data[task_id]->max_level = task.maxlevel; p_task_data[task_id]->repeatable = task.repeatable; p_task_data[task_id]->completion_emote = task.completion_emote; p_task_data[task_id]->activity_count = 0; p_task_data[task_id]->sequence_mode = ActivitiesSequential; p_task_data[task_id]->last_step = 0; LogTasksDetail( "[LoadTasks] (Task) task_id [{}] type [{}] duration [{}] duration_code [{}] title [{}] description [{}] " " reward [{}] rewardid [{}] cashreward [{}] xpreward [{}] rewardmethod [{}] faction_reward [{}] minlevel [{}] " " maxlevel [{}] repeatable [{}] completion_emote [{}] ", task.id, task.type, task.duration, task.duration_code, task.title, task.description, task.reward, task.rewardid, task.cashreward, task.xpreward, task.rewardmethod, task.faction_reward, task.minlevel, task.maxlevel, task.repeatable, task.completion_emote ); } LogTasks("Loaded [{}] Tasks", repo_tasks.size()); std::string activities_query_filter = fmt::format( "taskid = {} and activityid < {} ORDER BY taskid, activityid ASC", single_task, MAXACTIVITIESPERTASK ); // if loading only a single task if (single_task == 0) { activities_query_filter = fmt::format( "taskid < {} and activityid < {} ORDER BY taskid, activityid ASC", MAXTASKS, MAXACTIVITIESPERTASK ); } // load activity data auto task_activities = TaskActivitiesRepository::GetWhere(content_db, activities_query_filter); for (auto &task_activity: task_activities) { int task_id = task_activity.taskid; int step = task_activity.step; int activity_id = task_activity.activityid; // create pointer to activity data since declarations get unruly long int activity_index = p_task_data[task_id]->activity_count; ActivityInformation *p_activity_data = &p_task_data[task_id]->activity_information[activity_index]; if ((task_id <= 0) || (task_id >= MAXTASKS) || (activity_id < 0) || (activity_id >= MAXACTIVITIESPERTASK)) { // This shouldn't happen, as the SELECT is bounded by MAXTASKS LogTasks( "Error: Task or activity_information ID ([{}], [{}]) out of range while loading activities from database", task_id, activity_id ); continue; } if (p_task_data[task_id] == nullptr) { LogTasks( "Error: activity_information for non-existent task ([{}], [{}]) while loading activities from database", task_id, activity_id ); continue; } p_task_data[task_id]->activity_information[p_task_data[task_id]->activity_count].step_number = step; if (step != 0) { p_task_data[task_id]->sequence_mode = ActivitiesStepped; } if (step > p_task_data[task_id]->last_step) { p_task_data[task_id]->last_step = step; } // Task Activities MUST be numbered sequentially from 0. If not, log an error // and set the task to nullptr. Subsequent activities for this task will raise // ERR_NOTASK errors. // Change to (activityID != (Tasks[taskID]->activity_count + 1)) to index from 1 if (activity_id != p_task_data[task_id]->activity_count) { LogError( "[TASKS]Activities for Task [{}] are not sequential starting at 0. Not loading task", task_id, activity_id ); p_task_data[task_id] = nullptr; continue; } // set activity data p_activity_data->activity_type = task_activity.activitytype; p_activity_data->target_name = task_activity.target_name; p_activity_data->item_list = task_activity.item_list; p_activity_data->skill_list = task_activity.skill_list; p_activity_data->skill_id = std::stoi(task_activity.skill_list); // for older clients p_activity_data->spell_list = task_activity.spell_list; p_activity_data->spell_id = std::stoi(task_activity.spell_list); // for older clients p_activity_data->description_override = task_activity.description_override; p_activity_data->goal_id = task_activity.goalid; p_activity_data->goal_method = (TaskMethodType) task_activity.goalmethod; p_activity_data->goal_count = task_activity.goalcount; p_activity_data->deliver_to_npc = task_activity.delivertonpc; // zones p_activity_data->zones = task_activity.zones; auto zones = SplitString( task_activity.zones, ';' ); for (auto &&e : zones) { p_activity_data->zone_ids.push_back(std::stoi(e)); } p_activity_data->optional = task_activity.optional; LogTasksDetail( "[LoadTasks] (Activity) task_id [{}] activity_id [{}] slot [{}] activity_type [{}] goal_id [{}] goal_method [{}] goal_count [{}] zones [{}]" " target_name [{}] item_list [{}] skill_list [{}] spell_list [{}] description_override [{}]", task_id, activity_id, p_task_data[task_id]->activity_count, p_activity_data->activity_type, p_activity_data->goal_id, p_activity_data->goal_method, p_activity_data->goal_count, p_activity_data->zones.c_str(), p_activity_data->target_name.c_str(), p_activity_data->item_list.c_str(), p_activity_data->skill_list.c_str(), p_activity_data->spell_list.c_str(), p_activity_data->description_override.c_str() ); p_task_data[task_id]->activity_count++; } LogTasks("Loaded [{}] Task Activities", task_activities.size()); return true; } bool TaskManager::SaveClientState(Client *client, ClientTaskState *client_task_state) { // I am saving the slot in the ActiveTasks table, because unless a Task is cancelled/completed, the client // doesn't seem to like tasks moving slots between zoning and you can end up with 'bogus' activities if the task // previously in that slot had more activities than the one now occupying it. Hopefully retaining the slot // number for the duration of a session will overcome this. if (!client || !client_task_state) { return false; } const char *ERR_MYSQLERROR = "[TASKS]Error in TaskManager::SaveClientState %s"; int character_id = client->CharacterID(); LogTasks("[SaveClientState] character_id [{}]", character_id); if (client_task_state->active_task_count > 0 || client_task_state->active_task.task_id != TASKSLOTEMPTY) { // TODO: tasks for (auto &active_task : client_task_state->active_tasks) { int task_id = active_task.task_id; if (task_id == TASKSLOTEMPTY) { continue; } int slot = active_task.slot; if (active_task.updated) { LogTasks( "[SaveClientState] character_id [{}] updating task_index [{}] task_id [{}]", character_id, slot, task_id ); std::string query = StringFormat( "REPLACE INTO character_tasks (charid, taskid, slot, type, acceptedtime) " "VALUES (%i, %i, %i, %i, %i)", character_id, task_id, slot, static_cast(p_task_data[task_id]->type), active_task.accepted_time ); auto results = database.QueryDatabase(query); if (!results.Success()) { LogError(ERR_MYSQLERROR, results.ErrorMessage().c_str()); } else { active_task.updated = false; } } std::string query = "REPLACE INTO character_activities (charid, taskid, activityid, donecount, completed) " "VALUES "; int updated_activity_count = 0; for (int activity_index = 0; activity_index < p_task_data[task_id]->activity_count; ++activity_index) { if (!active_task.activity[activity_index].updated) { continue; } LogTasks( "[SaveClientState] Updating activity character_id [{}] updating task_index [{}] task_id [{}] activity_index [{}]", character_id, slot, task_id, activity_index ); if (updated_activity_count == 0) { query += StringFormat( "(%i, %i, %i, %i, %i)", character_id, task_id, activity_index, active_task.activity[activity_index].done_count, active_task.activity[activity_index].activity_state == ActivityCompleted ); } else { query += StringFormat( ", (%i, %i, %i, %i, %i)", character_id, task_id, activity_index, active_task.activity[activity_index].done_count, active_task.activity[activity_index].activity_state == ActivityCompleted ); } updated_activity_count++; } if (updated_activity_count == 0) { continue; } auto results = database.QueryDatabase(query); if (!results.Success()) { LogError(ERR_MYSQLERROR, results.ErrorMessage().c_str()); continue; } active_task.updated = false; for (int activity_index = 0; activity_index < p_task_data[task_id]->activity_count; ++activity_index) active_task.activity[activity_index].updated = false; } } if (!RuleB(TaskSystem, RecordCompletedTasks) || (client_task_state->completed_tasks.size() <= (unsigned int) client_task_state->last_completed_task_loaded)) { client_task_state->last_completed_task_loaded = client_task_state->completed_tasks.size(); return true; } const char *completed_task_query = "REPLACE INTO completed_tasks (charid, completedtime, taskid, activityid) " "VALUES (%i, %i, %i, %i)"; for (unsigned int task_index = client_task_state->last_completed_task_loaded; task_index < client_task_state->completed_tasks.size(); task_index++) { int task_id = client_task_state->completed_tasks[task_index].task_id; if ((task_id <= 0) || (task_id >= MAXTASKS) || (p_task_data[task_id] == nullptr)) { continue; } // First we save a record with an activity_id of -1. // This indicates this task was completed at the given time. We infer that all // none optional activities were completed. // std::string query = StringFormat( completed_task_query, character_id, client_task_state->completed_tasks[task_index].completed_time, task_id, -1 ); auto results = database.QueryDatabase(query); if (!results.Success()) { LogError(ERR_MYSQLERROR, results.ErrorMessage().c_str()); continue; } // If the Rule to record non-optional task completion is not enabled, don't save it if (!RuleB(TaskSystem, RecordCompletedOptionalActivities)) { continue; } // Insert one record for each completed optional task. for (int activity_id = 0; activity_id < p_task_data[task_id]->activity_count; activity_id++) { if (!p_task_data[task_id]->activity_information[activity_id].optional || !client_task_state->completed_tasks[task_index].activity_done[activity_id]) { continue; } query = StringFormat( completed_task_query, character_id, client_task_state->completed_tasks[task_index].completed_time, task_id, activity_id ); results = database.QueryDatabase(query); if (!results.Success()) { LogError(ERR_MYSQLERROR, results.ErrorMessage().c_str()); } } } client_task_state->last_completed_task_loaded = client_task_state->completed_tasks.size(); return true; } int TaskManager::FirstTaskInSet(int task_set) { if ((task_set <= 0) || (task_set >= MAXTASKSETS)) { return 0; } if (task_sets[task_set].empty()) { return 0; } auto iterator = task_sets[task_set].begin(); while (iterator != task_sets[task_set].end()) { if ((*iterator) > 0) { return (*iterator); } ++iterator; } return 0; } int TaskManager::LastTaskInSet(int task_set) { if ((task_set <= 0) || (task_set >= MAXTASKSETS)) { return 0; } if (task_sets[task_set].empty()) { return 0; } return task_sets[task_set][task_sets[task_set].size() - 1]; } int TaskManager::NextTaskInSet(int task_set, int task_id) { if ((task_set <= 0) || (task_set >= MAXTASKSETS)) { return 0; } if (task_sets[task_set].empty()) { return 0; } for (int i : task_sets[task_set]) { if (i > task_id) { return i; } } return 0; } bool TaskManager::ValidateLevel(int task_id, int player_level) { if (p_task_data[task_id] == nullptr) { return false; } if (p_task_data[task_id]->min_level && (player_level < p_task_data[task_id]->min_level)) { return false; } if (p_task_data[task_id]->max_level && (player_level > p_task_data[task_id]->max_level)) { return false; } return true; } std::string TaskManager::GetTaskName(uint32 task_id) { if (task_id > 0 && task_id < MAXTASKS) { if (p_task_data[task_id] != nullptr) { return p_task_data[task_id]->title; } } return std::string(); } TaskType TaskManager::GetTaskType(uint32 task_id) { if (task_id > 0 && task_id < MAXTASKS) { if (p_task_data[task_id] != nullptr) { return p_task_data[task_id]->type; } } return TaskType::Task; } void TaskManager::TaskSetSelector(Client *client, ClientTaskState *client_task_state, Mob *mob, int task_set_id) { int task_list[MAXCHOOSERENTRIES]; int task_list_index = 0; int player_level = client->GetLevel(); LogTasks( "TaskSetSelector called for task_set_id [{}] EnableTaskSize is [{}]", task_set_id, client_task_state->enabled_tasks.size() ); if (task_set_id <= 0 || task_set_id >= MAXTASKSETS) { return; } if (task_sets[task_set_id].empty()) { // I think this is suppose to be yellow mob->SayString( client, Chat::Yellow, MAX_ACTIVE_TASKS, client->GetName() ); return; } bool all_enabled = false; // A task_id of 0 in a TaskSet indicates that all Tasks in the set are enabled for all players. if (task_sets[task_set_id][0] == 0) { LogTasks("[TaskSetSelector] TaskSets[{}][0] == 0. All Tasks in Set enabled.", task_set_id); all_enabled = true; } auto iterator = task_sets[task_set_id].begin(); if (all_enabled) { ++iterator; } // skip first when all enabled since it's useless data while (iterator != task_sets[task_set_id].end() && task_list_index < MAXCHOOSERENTRIES) { auto task = *iterator; // verify level, we're not currently on it, repeatable status, if it's a (shared) task // we aren't currently on another, and if it's enabled if not all_enabled if ((all_enabled || client_task_state->IsTaskEnabled(task)) && ValidateLevel(task, player_level) && !client_task_state->IsTaskActive(task) && client_task_state->HasSlotForTask(p_task_data[task]) && // this slot checking is a bit silly, but we allow mixing of task types ... (IsTaskRepeatable(task) || !client_task_state->IsTaskCompleted(task))) { task_list[task_list_index++] = task; } ++iterator; } if (task_list_index > 0) { SendTaskSelector(client, mob, task_list_index, task_list); } else { // TODO: check color, I think this might be only for (Shared) Tasks, w/e -- think should be yellow mob->SayString( client, Chat::Yellow, MAX_ACTIVE_TASKS, client->GetName() ); } } // unlike the non-Quest version of this function, it does not check enabled, that is assumed the responsibility of the quest to handle // we do however still want it to check the other stuff like level, active, room, etc void TaskManager::TaskQuestSetSelector( Client *client, ClientTaskState *client_task_state, Mob *mob, int count, int *tasks ) { int task_list[MAXCHOOSERENTRIES]; int task_list_index = 0; int player_level = client->GetLevel(); LogTasks("[UPDATE] TaskQuestSetSelector called for array size [{}]", count); if (count <= 0) { return; } for (int i = 0; i < count; ++i) { auto task = tasks[i]; // verify level, we're not currently on it, repeatable status, if it's a (shared) task // we aren't currently on another, and if it's enabled if not all_enabled if (ValidateLevel(task, player_level) && !client_task_state->IsTaskActive(task) && client_task_state->HasSlotForTask(p_task_data[task]) && // this slot checking is a bit silly, but we allow mixing of task types ... (IsTaskRepeatable(task) || !client_task_state->IsTaskCompleted(task))) { task_list[task_list_index++] = task; } } if (task_list_index > 0) { SendTaskSelector(client, mob, task_list_index, task_list); } else { // TODO: check color, I think this might be only for (Shared) Tasks, w/e -- think should be yellow mob->SayString( client, Chat::Yellow, MAX_ACTIVE_TASKS, client->GetName() ); } } // sends task selector to client void TaskManager::SendTaskSelector(Client *client, Mob *mob, int task_count, int *task_list) { if (client->ClientVersion() >= EQ::versions::ClientVersion::RoF) { SendTaskSelectorNew(client, mob, task_count, task_list); return; } // Titanium OpCode: 0x5e7c LogTasks("TaskSelector for [{}] Tasks", task_count); int player_level = client->GetLevel(); // Check if any of the tasks exist for (int i = 0; i < task_count; i++) { if (p_task_data[task_list[i]] != nullptr) { break; } } int valid_task_count = 0; for (int i = 0; i < task_count; i++) { if (!ValidateLevel(task_list[i], player_level)) { continue; } if (client->IsTaskActive(task_list[i])) { continue; } if (!IsTaskRepeatable(task_list[i]) && client->IsTaskCompleted(task_list[i])) { continue; } valid_task_count++; } if (valid_task_count == 0) { return; } SerializeBuffer buf(50 * valid_task_count); buf.WriteUInt32(valid_task_count); buf.WriteUInt32(2); // task type, live doesn't let you send more than one type, but we do? buf.WriteUInt32(mob->GetID()); for (int task_index = 0; task_index < task_count; task_index++) { if (!ValidateLevel(task_list[task_index], player_level)) { continue; } if (client->IsTaskActive(task_list[task_index])) { continue; } if (!IsTaskRepeatable(task_list[task_index]) && client->IsTaskCompleted(task_list[task_index])) { continue; } buf.WriteUInt32(task_list[task_index]); // task_id // affects color, difficulty? if (client->ClientVersion() != EQ::versions::ClientVersion::Titanium) { buf.WriteFloat(1.0f); } buf.WriteUInt32(p_task_data[task_list[task_index]]->duration); buf.WriteUInt32(static_cast(p_task_data[task_list[task_index]]->duration_code)); buf.WriteString(p_task_data[task_list[task_index]]->title); // max 64 with null buf.WriteString(p_task_data[task_list[task_index]]->description); // max 4000 with null // Has reward set flag if (client->ClientVersion() != EQ::versions::ClientVersion::Titanium) { buf.WriteUInt8(0); } buf.WriteUInt32(p_task_data[task_list[task_index]]->activity_count); for (int activity_index = 0; activity_index < p_task_data[task_list[task_index]]->activity_count; ++activity_index) { buf.WriteUInt32(activity_index); // ActivityNumber auto &activity = p_task_data[task_list[task_index]]->activity_information[activity_index]; buf.WriteUInt32(activity.activity_type); buf.WriteUInt32(0); // solo, group, raid? buf.WriteString(activity.target_name); // max length 64, "target name" so like loot x foo from bar (this is bar) buf.WriteString(activity.item_list); // max length 64 in these clients buf.WriteUInt32(activity.goal_count); buf.WriteInt32(activity.skill_id); buf.WriteInt32(activity.spell_id); buf.WriteInt32(activity.zone_ids.empty() ? 0 : activity.zone_ids.front()); buf.WriteString(activity.description_override); } } auto outapp = new EQApplicationPacket(OP_OpenNewTasksWindow, buf); client->QueuePacket(outapp); safe_delete(outapp); } void TaskManager::SendTaskSelectorNew(Client *client, Mob *mob, int task_count, int *task_list) { LogTasks("SendTaskSelectorNew for [{}] Tasks", task_count); int player_level = client->GetLevel(); // Check if any of the tasks exist for (int i = 0; i < task_count; i++) { if (p_task_data[task_list[i]] != nullptr) { break; } } int valid_tasks_count = 0; for (int task_index = 0; task_index < task_count; task_index++) { if (!ValidateLevel(task_list[task_index], player_level)) { continue; } if (client->IsTaskActive(task_list[task_index])) { continue; } if (!IsTaskRepeatable(task_list[task_index]) && client->IsTaskCompleted(task_list[task_index])) { continue; } valid_tasks_count++; } if (valid_tasks_count == 0) { return; } SerializeBuffer buf(50 * valid_tasks_count); buf.WriteUInt32(valid_tasks_count); // TaskCount buf.WriteUInt32(2); // Type, valid values: 0-3. 0 = Task, 1 = Shared Task, 2 = Quest, 3 = ??? -- should fix maybe some day, but we let more than 1 type through :P // so I guess an NPC can only offer one type of quests or we can only open a selection with one type :P (so quest call can tell us I guess) // this is also sent in OP_TaskDescription buf.WriteUInt32(mob->GetID()); // TaskGiver for (int i = 0; i < task_count; i++) { // max 40 if (!ValidateLevel(task_list[i], player_level)) { continue; } if (client->IsTaskActive(task_list[i])) { continue; } if (!IsTaskRepeatable(task_list[i]) && client->IsTaskCompleted(task_list[i])) { continue; } buf.WriteUInt32(task_list[i]); // task_id buf.WriteFloat(1.0f); // affects color, difficulty? buf.WriteUInt32(p_task_data[task_list[i]]->duration); buf.WriteUInt32(static_cast(p_task_data[task_list[i]]->duration_code)); // 1 = Short, 2 = Medium, 3 = Long, anything else Unlimited buf.WriteString(p_task_data[task_list[i]]->title); // max 64 with null buf.WriteString(p_task_data[task_list[i]]->description); // max 4000 with null buf.WriteUInt8(0); // Has reward set flag buf.WriteUInt32(p_task_data[task_list[i]]->activity_count); // activity_count for (int j = 0; j < p_task_data[task_list[i]]->activity_count; ++j) { buf.WriteUInt32(j); // ActivityNumber auto &activity = p_task_data[task_list[i]]->activity_information[j]; buf.WriteUInt32(activity.activity_type); // ActivityType buf.WriteUInt32(0); // solo, group, raid? buf.WriteString(activity.target_name); // max length 64, "target name" so like loot x foo from bar (this is bar) // this string is item names buf.WriteLengthString(activity.item_list); buf.WriteUInt32(activity.goal_count); // GoalCount // this string is skill IDs? probably one of the "use on" tasks buf.WriteLengthString(activity.skill_list); // this string is spell IDs? probably one of the "use on" tasks buf.WriteLengthString(activity.spell_list); //buf.WriteString(itoa(Tasks[TaskList[i]]->activity_information[activity_id].ZoneID)); buf.WriteString(activity.zones); // Zone number in ascii max length 64, can be multiple with separated by ; buf.WriteString(activity.description_override); // max length 128 -- overrides the automatic descriptions // this doesn't appear to be shown to the client at all and isn't the same as zones ... defaults to '0' though buf.WriteString(activity.zones); // Zone number in ascii max length 64, probably can be separated by ; too, haven't found it used } } auto outapp = new EQApplicationPacket(OP_OpenNewTasksWindow, buf); client->QueuePacket(outapp); safe_delete(outapp); } int TaskManager::GetActivityCount(int task_id) { if ((task_id > 0) && (task_id < MAXTASKS)) { if (p_task_data[task_id]) { return p_task_data[task_id]->activity_count; } } return 0; } void TaskManager::ExplainTask(Client *client, int task_id) { // TODO: This method is not finished (hardly started). It was intended to // explain in English, what each activity_information did, conditions for step unlocking, etc. // return; if (!client) { return; } if ((task_id <= 0) || (task_id >= MAXTASKS)) { client->Message(Chat::White, "task_id out-of-range."); return; } if (p_task_data[task_id] == nullptr) { client->Message(Chat::White, "Task does not exist."); return; } char explanation[1000], *ptr; client->Message(Chat::White, "Task %4i: title: %s", task_id, p_task_data[task_id]->description.c_str()); client->Message(Chat::White, "%3i Activities", p_task_data[task_id]->activity_count); ptr = explanation; for (int i = 0; i < p_task_data[task_id]->activity_count; i++) { sprintf(ptr, "Act: %3i: ", i); ptr = ptr + strlen(ptr); switch (p_task_data[task_id]->activity_information[i].activity_type) { case ActivityDeliver: sprintf(ptr, "Deliver"); break; } } } bool TaskManager::IsTaskRepeatable(int task_id) { if ((task_id <= 0) || (task_id >= MAXTASKS)) { return false; } TaskInformation *task_data = p_task_manager->p_task_data[task_id]; if (task_data == nullptr) { return false; } return task_data->repeatable; } void TaskManager::SendCompletedTasksToClient(Client *c, ClientTaskState *client_task_state) { int packet_length = 4; //vector::const_iterator iterator; // The client only display the first 50 Completed Tasks send, so send the 50 most recent int first_task_to_send = 0; int last_task_to_send = client_task_state->completed_tasks.size(); if (client_task_state->completed_tasks.size() > 50) { first_task_to_send = client_task_state->completed_tasks.size() - 50; } LogTasks( "[SendCompletedTasksToClient] completed task count [{}] first tank to send is [{}] last is [{}]", client_task_state->completed_tasks.size(), first_task_to_send, last_task_to_send ); /* for(iterator=activity_state->CompletedTasks.begin(); iterator!=activity_state->CompletedTasks.end(); iterator++) { int task_id = (*iterator).task_id; if(Tasks[task_id] == nullptr) continue; PacketLength = PacketLength + 8 + strlen(Tasks[task_id]->title) + 1; } */ for (int i = first_task_to_send; i < last_task_to_send; i++) { int TaskID = client_task_state->completed_tasks[i].task_id; if (p_task_data[TaskID] == nullptr) { continue; } packet_length = packet_length + 8 + p_task_data[TaskID]->title.size() + 1; } auto outapp = new EQApplicationPacket(OP_CompletedTasks, packet_length); char *buf = (char *) outapp->pBuffer; //*(uint32 *)buf = activity_state->CompletedTasks.size(); *(uint32 *) buf = last_task_to_send - first_task_to_send; buf = buf + 4; //for(iterator=activity_state->CompletedTasks.begin(); iterator!=activity_state->CompletedTasks.end(); iterator++) { // int task_id = (*iterator).task_id; for (int i = first_task_to_send; i < last_task_to_send; i++) { int task_id = client_task_state->completed_tasks[i].task_id; if (p_task_data[task_id] == nullptr) { continue; } *(uint32 *) buf = task_id; buf = buf + 4; sprintf(buf, "%s", p_task_data[task_id]->title.c_str()); buf = buf + strlen(buf) + 1; //*(uint32 *)buf = (*iterator).CompletedTime; *(uint32 *) buf = client_task_state->completed_tasks[i].completed_time; buf = buf + 4; } c->QueuePacket(outapp); safe_delete(outapp); } void TaskManager::SendTaskActivityShort(Client *client, int task_id, int activity_id, int client_task_index) { // This activity_information Packet is sent for activities that have not yet been unlocked and appear as ??? // in the client. TaskActivityShort_Struct *task_activity_short; if (client->ClientVersionBit() & EQ::versions::maskRoFAndLater) { auto outapp = new EQApplicationPacket(OP_TaskActivity, 25); outapp->WriteUInt32(client_task_index); outapp->WriteUInt32(static_cast(p_task_data[task_id]->type)); outapp->WriteUInt32(task_id); outapp->WriteUInt32(activity_id); outapp->WriteUInt32(0); outapp->WriteUInt32(0xffffffff); outapp->WriteUInt8(0); client->FastQueuePacket(&outapp); return; } auto outapp = new EQApplicationPacket(OP_TaskActivity, sizeof(TaskActivityShort_Struct)); task_activity_short = (TaskActivityShort_Struct *) outapp->pBuffer; task_activity_short->TaskSequenceNumber = client_task_index; task_activity_short->unknown2 = static_cast(p_task_data[task_id]->type); task_activity_short->TaskID = task_id; task_activity_short->ActivityID = activity_id; task_activity_short->unknown3 = 0x000000; task_activity_short->ActivityType = 0xffffffff; task_activity_short->unknown4 = 0x00000000; client->QueuePacket(outapp); safe_delete(outapp); } void TaskManager::SendTaskActivityLong( Client *client, int task_id, int activity_id, int client_task_index, bool optional, bool task_complete ) { if (client->ClientVersion() >= EQ::versions::ClientVersion::RoF) { SendTaskActivityNew(client, task_id, activity_id, client_task_index, optional, task_complete); return; } SerializeBuffer buf(100); buf.WriteUInt32(client_task_index); buf.WriteUInt32(static_cast(p_task_data[task_id]->type)); buf.WriteUInt32(task_id); buf.WriteUInt32(activity_id); buf.WriteUInt32(0); // unknown3 // We send our 'internal' types as ActivityCastOn. text3 should be set to the activity_information description, so it makes // no difference to the client. All activity_information updates will be done based on our interal activity_information types. if ((p_task_data[task_id]->activity_information[activity_id].activity_type > 0) && p_task_data[task_id]->activity_information[activity_id].activity_type < 100) { buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].activity_type); } else { buf.WriteUInt32(ActivityCastOn); } // w/e! buf.WriteUInt32(optional); buf.WriteUInt32(0); // solo, group, raid buf.WriteString(p_task_data[task_id]->activity_information[activity_id].target_name); // target name string buf.WriteString(p_task_data[task_id]->activity_information[activity_id].item_list); // item name list if (p_task_data[task_id]->activity_information[activity_id].activity_type != ActivityGiveCash) { buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].goal_count); } else { // For our internal type GiveCash, where the goal count has the amount of cash that must be given, // we don't want the donecount and goalcount fields cluttered up with potentially large numbers, so we just // send a goalcount of 1, and a bit further down, a donecount of 1 if the activity_information is complete, 0 otherwise. // The text3 field should decribe the exact activity_information goal, e.g. give 3500gp to Hasten Bootstrutter. buf.WriteUInt32(1); } buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].skill_id); buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].spell_id); buf.WriteUInt32( p_task_data[task_id]->activity_information[activity_id].zone_ids.empty() ? 0 : p_task_data[task_id]->activity_information[activity_id].zone_ids.front()); buf.WriteUInt32(0); buf.WriteString(p_task_data[task_id]->activity_information[activity_id].description_override); if (p_task_data[task_id]->activity_information[activity_id].activity_type != ActivityGiveCash) { buf.WriteUInt32(client->GetTaskActivityDoneCount(p_task_data[task_id]->type, client_task_index, activity_id)); } else { // For internal activity_information types, done_count is either 1 if the activity_information is complete, 0 otherwise. buf.WriteUInt32((client->GetTaskActivityDoneCount(p_task_data[task_id]->type, client_task_index, activity_id) >= p_task_data[task_id]->activity_information[activity_id].goal_count)); } buf.WriteUInt32(1); // unknown auto outapp = new EQApplicationPacket(OP_TaskActivity, buf); client->QueuePacket(outapp); safe_delete(outapp); } // Used only by RoF+ Clients void TaskManager::SendTaskActivityNew( Client *client, int task_id, int activity_id, int client_task_index, bool optional, bool task_complete ) { SerializeBuffer buf(100); buf.WriteUInt32(client_task_index); // TaskSequenceNumber buf.WriteUInt32(static_cast(p_task_data[task_id]->type)); // task type buf.WriteUInt32(task_id); buf.WriteUInt32(activity_id); buf.WriteUInt32(0); // unknown3 // We send our 'internal' types as ActivityCastOn. text3 should be set to the activity_information description, so it makes // no difference to the client. All activity_information updates will be done based on our interal activity_information types. if ((p_task_data[task_id]->activity_information[activity_id].activity_type > 0) && p_task_data[task_id]->activity_information[activity_id].activity_type < 100) { buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].activity_type); } else { buf.WriteUInt32(ActivityCastOn); } // w/e! buf.WriteUInt8(optional); buf.WriteUInt32(0); // solo, group, raid // One of these unknown fields maybe related to the 'Use On' activity_information types buf.WriteString(p_task_data[task_id]->activity_information[activity_id].target_name); // target name string buf.WriteLengthString(p_task_data[task_id]->activity_information[activity_id].item_list); // item name list // Goal Count if (p_task_data[task_id]->activity_information[activity_id].activity_type != ActivityGiveCash) { buf.WriteUInt32(p_task_data[task_id]->activity_information[activity_id].goal_count); } else { buf.WriteUInt32(1); } // GoalCount // skill ID list ; separated buf.WriteLengthString(p_task_data[task_id]->activity_information[activity_id].skill_list); // spelll ID list ; separated -- unsure wtf we're doing here buf.WriteLengthString(p_task_data[task_id]->activity_information[activity_id].spell_list); buf.WriteString(p_task_data[task_id]->activity_information[activity_id].zones); buf.WriteUInt32(0); // unknown7 buf.WriteString(p_task_data[task_id]->activity_information[activity_id].description_override); // description override if (p_task_data[task_id]->activity_information[activity_id].activity_type != ActivityGiveCash) { buf.WriteUInt32( client->GetTaskActivityDoneCount( p_task_data[task_id]->type, client_task_index, activity_id )); // done_count } else { // For internal activity_information types, done_count is either 1 if the activity_information is complete, 0 otherwise. buf.WriteUInt32((client->GetTaskActivityDoneCount(p_task_data[task_id]->type, client_task_index, activity_id) >= p_task_data[task_id]->activity_information[activity_id].goal_count)); } buf.WriteUInt8(1); // unknown9 buf.WriteString(p_task_data[task_id]->activity_information[activity_id].zones); auto outapp = new EQApplicationPacket(OP_TaskActivity, buf); client->QueuePacket(outapp); safe_delete(outapp); } void TaskManager::SendActiveTasksToClient(Client *client, bool task_complete) { auto state = client->GetTaskState(); if (!state) { return; } for (int task_index = 0; task_index < MAXACTIVEQUESTS + 1; task_index++) { int task_id = state->active_tasks[task_index].task_id; if ((task_id == 0) || (p_task_data[task_id] == 0)) { continue; } int start_time = state->active_tasks[task_index].accepted_time; SendActiveTaskDescription( client, task_id, state->active_tasks[task_index], start_time, p_task_data[task_id]->duration, false ); LogTasks("[SendActiveTasksToClient] task_id [{}] activity_count [{}]", task_id, GetActivityCount(task_id)); int sequence = 0; int fixed_index = p_task_data[task_id]->type == TaskType::Task ? 0 : task_index - 1; // hmmm fuck for (int activity_id = 0; activity_id < GetActivityCount(task_id); activity_id++) { if (client->GetTaskActivityState(p_task_data[task_id]->type, fixed_index, activity_id) != ActivityHidden) { LogTasks( "[SendActiveTasksToClient] (Long Update) task_id [{}] activity_id [{}] fixed_index [{}] task_complete [{}]", task_id, activity_id, fixed_index, task_complete ? "true" : "false" ); if (activity_id == GetActivityCount(task_id) - 1) { SendTaskActivityLong( client, task_id, activity_id, fixed_index, p_task_data[task_id]->activity_information[activity_id].optional, task_complete ); } else { SendTaskActivityLong( client, task_id, activity_id, fixed_index, p_task_data[task_id]->activity_information[activity_id].optional, 0 ); } } else { LogTasks( "[SendActiveTasksToClient] (Short Update) task_id [{}] activity_id [{}] fixed_index [{}]", task_id, activity_id, fixed_index ); SendTaskActivityShort(client, task_id, activity_id, fixed_index); } sequence++; } } } void TaskManager::SendSingleActiveTaskToClient( Client *client, ClientTaskInformation &task_info, bool task_complete, bool bring_up_task_journal ) { int task_id = task_info.task_id; if (task_id == 0 || p_task_data[task_id] == nullptr) { return; } int start_time = task_info.accepted_time; SendActiveTaskDescription( client, task_id, task_info, start_time, p_task_data[task_id]->duration, bring_up_task_journal ); Log(Logs::General, Logs::Tasks, "[UPDATE] SendSingleActiveTasksToClient: Task %i, Activities: %i", task_id, GetActivityCount(task_id)); for (int activity_id = 0; activity_id < GetActivityCount(task_id); activity_id++) { if (task_info.activity[activity_id].activity_state != ActivityHidden) { LogTasks("[SendSingleActiveTaskToClient] Long [{}] [{}] complete [{}]", task_id, activity_id, task_complete); if (activity_id == GetActivityCount(task_id) - 1) { SendTaskActivityLong( client, task_id, activity_id, task_info.slot, p_task_data[task_id]->activity_information[activity_id].optional, task_complete ); } else { SendTaskActivityLong( client, task_id, activity_id, task_info.slot, p_task_data[task_id]->activity_information[activity_id].optional, 0 ); } } else { LogTasks("[SendSingleActiveTaskToClient] Short [{}] [{}]", task_id, activity_id); SendTaskActivityShort(client, task_id, activity_id, task_info.slot); } } } void TaskManager::SendActiveTaskDescription( Client *client, int task_id, ClientTaskInformation &task_info, int start_time, int duration, bool bring_up_task_journal ) { if ((task_id < 1) || (task_id >= MAXTASKS) || !p_task_data[task_id]) { return; } int packet_length = sizeof(TaskDescriptionHeader_Struct) + p_task_data[task_id]->title.length() + 1 + sizeof(TaskDescriptionData1_Struct) + p_task_data[task_id]->description.length() + 1 + sizeof(TaskDescriptionData2_Struct) + 1 + sizeof(TaskDescriptionTrailer_Struct); // If there is an item make the reward text into a link to the item (only the first item if a list // is specified). I have been unable to get multiple item links to work. // if (p_task_data[task_id]->reward_id && p_task_data[task_id]->item_link.empty()) { int item_id = 0; // If the reward is a list of items, and the first entry on the list is valid if (p_task_data[task_id]->reward_method == METHODSINGLEID) { item_id = p_task_data[task_id]->reward_id; } else if (p_task_data[task_id]->reward_method == METHODLIST) { item_id = goal_list_manager.GetFirstEntry(p_task_data[task_id]->reward_id); if (item_id < 0) { item_id = 0; } } if (item_id) { const EQ::ItemData *reward_item = database.GetItem(item_id); EQ::SayLinkEngine linker; linker.SetLinkType(EQ::saylink::SayLinkItemData); linker.SetItemData(reward_item); linker.SetTaskUse(); p_task_data[task_id]->item_link = linker.GenerateLink(); } } packet_length += p_task_data[task_id]->reward.length() + 1 + p_task_data[task_id]->item_link.length() + 1; char *Ptr; TaskDescriptionHeader_Struct *task_description_header; TaskDescriptionData1_Struct *tdd1; TaskDescriptionData2_Struct *tdd2; TaskDescriptionTrailer_Struct *tdt; auto outapp = new EQApplicationPacket(OP_TaskDescription, packet_length); task_description_header = (TaskDescriptionHeader_Struct *) outapp->pBuffer; task_description_header->SequenceNumber = task_info.slot; task_description_header->TaskID = task_id; task_description_header->open_window = bring_up_task_journal; task_description_header->task_type = static_cast(p_task_data[task_id]->type); task_description_header->reward_type = 0; // TODO: 4 says Radiant Crystals else Ebon Crystals when shared task Ptr = (char *) task_description_header + sizeof(TaskDescriptionHeader_Struct); sprintf(Ptr, "%s", p_task_data[task_id]->title.c_str()); Ptr += p_task_data[task_id]->title.length() + 1; tdd1 = (TaskDescriptionData1_Struct *) Ptr; tdd1->Duration = duration; tdd1->dur_code = static_cast(p_task_data[task_id]->duration_code); tdd1->StartTime = start_time; Ptr = (char *) tdd1 + sizeof(TaskDescriptionData1_Struct); sprintf(Ptr, "%s", p_task_data[task_id]->description.c_str()); Ptr += p_task_data[task_id]->description.length() + 1; tdd2 = (TaskDescriptionData2_Struct *) Ptr; // we have this reward stuff! // if we ever don't hardcode this, TaskDescriptionTrailer_Struct will need to be fixed since // "has_reward_selection" is after this bool! Smaller packet when this is 0 tdd2->has_rewards = 1; tdd2->coin_reward = p_task_data[task_id]->cash_reward; tdd2->xp_reward = p_task_data[task_id]->experience_reward ? 1 : 0; // just booled tdd2->faction_reward = p_task_data[task_id]->faction_reward ? 1 : 0; // faction booled Ptr = (char *) tdd2 + sizeof(TaskDescriptionData2_Struct); // we actually have 2 strings here. One is max length 96 and not parsed for item links // We actually skipped past that string incorrectly before, so TODO: fix item link string sprintf(Ptr, "%s", p_task_data[task_id]->reward.c_str()); Ptr += p_task_data[task_id]->reward.length() + 1; // second string is parsed for item links sprintf(Ptr, "%s", p_task_data[task_id]->item_link.c_str()); Ptr += p_task_data[task_id]->item_link.length() + 1; tdt = (TaskDescriptionTrailer_Struct *) Ptr; tdt->Points = 0x00000000; // Points Count TODO: this does have a visible affect on the client ... tdt->has_reward_selection = 0; // TODO: new rewards window client->QueuePacket(outapp); safe_delete(outapp); } bool TaskManager::LoadClientState(Client *client, ClientTaskState *client_task_state) { if (!client || !client_task_state) { return false; } int character_id = client->CharacterID(); client_task_state->active_task_count = 0; LogTasks("[LoadClientState] for character_id [{}]", character_id); auto character_tasks = CharacterTasksRepository::GetWhere( database, fmt::format("charid = {} ORDER BY acceptedtime", character_id) ); for (auto &character_task: character_tasks) { int task_id = character_task.taskid; int slot = character_task.slot; auto type = static_cast(character_task.type); if ((task_id < 0) || (task_id >= MAXTASKS)) { LogTasks("[LoadClientState] Error: task_id [{}] out of range while loading character tasks from database", task_id); continue; } auto task_info = client_task_state->GetClientTaskInfo(type, slot); if (task_info == nullptr) { LogTasks("[LoadClientState] Error: slot [{}] out of range while loading character tasks from database", slot); continue; } if (task_info->task_id != TASKSLOTEMPTY) { LogTasks("[LoadClientState] Error: slot [{}] for task [{}] is already occupied", slot, task_id); continue; } task_info->task_id = task_id; task_info->current_step = -1; task_info->accepted_time = character_task.acceptedtime; task_info->updated = false; for (auto &i : task_info->activity) { i.activity_id = -1; } if (type == TaskType::Quest) { ++client_task_state->active_task_count; } LogTasks( "[LoadClientState] character_id [{}] task_id [{}] accepted_time [{}]", character_id, task_id, character_task.acceptedtime ); } // Load Activities LogTasks("[LoadClientState] Loading activities for character_id [{}]", character_id); auto character_activities = CharacterActivitiesRepository::GetWhere( database, fmt::format("charid = {} ORDER BY `taskid` ASC, `activityid` ASC", character_id) ); for (auto &character_activity: character_activities) { int task_id = character_activity.taskid; if ((task_id < 0) || (task_id >= MAXTASKS)) { LogTasks( "[LoadClientState] Error: task_id [{}] out of range while loading character activities from database character_id [{}]", task_id, character_id ); continue; } int activity_id = character_activity.activityid; if ((activity_id < 0) || (activity_id >= MAXACTIVITIESPERTASK)) { LogTasks( "[LoadClientState] Error: activity_id [{}] out of range while loading character activities from database character_id [{}]", activity_id, character_id ); continue; } ClientTaskInformation *task_info = nullptr; if (client_task_state->active_task.task_id == task_id) { task_info = &client_task_state->active_task; } // wasn't task if (task_info == nullptr) { for (auto &active_quest : client_task_state->active_quests) { if (active_quest.task_id == task_id) { task_info = &active_quest; } } } if (task_info == nullptr) { LogTasks( "[LoadClientState] Error: activity_id [{}] found for task_id [{}] which client does not have character_id [{}]", activity_id, task_id, character_id ); continue; } task_info->activity[activity_id].activity_id = activity_id; task_info->activity[activity_id].done_count = character_activity.donecount; if (character_activity.completed) { task_info->activity[activity_id].activity_state = ActivityCompleted; } else { task_info->activity[activity_id].activity_state = ActivityHidden; } task_info->activity[activity_id].updated = false; LogTasks( "[LoadClientState] character_id [{}] task_id [{}] activity_id [{}] done_count [{}] completed [{}]", character_id, task_id, activity_id, character_activity.donecount, character_activity.completed ); } if (RuleB(TaskSystem, RecordCompletedTasks)) { CompletedTaskInformation completed_task_information{}; for (bool &i : completed_task_information.activity_done) { i = false; } int previous_task_id = -1; int previous_completed_time = -1; auto character_completed_tasks = CompletedTasksRepository::GetWhere( database, fmt::format("charid = {} ORDER BY completedtime, taskid, activityid", character_id) ); for (auto &character_completed_task: character_completed_tasks) { int task_id = character_completed_task.taskid; if ((task_id <= 0) || (task_id >= MAXTASKS)) { LogError("[TASKS]Task ID [{}] out of range while loading completed tasks from database", task_id); continue; } // An activity_id of -1 means mark all the none optional activities in the // task as complete. If the Rule to record optional activities is enabled, // subsequent records for this task will flag any optional tasks that were // completed. int activity_id = character_completed_task.activityid; if ((activity_id < -1) || (activity_id >= MAXACTIVITIESPERTASK)) { LogError("[TASKS]activity_information ID [{}] out of range while loading completed tasks from database", activity_id); continue; } int completed_time = character_completed_task.completedtime; if ((previous_task_id != -1) && ((task_id != previous_task_id) || (completed_time != previous_completed_time))) { client_task_state->completed_tasks.push_back(completed_task_information); for (bool &activity_done : completed_task_information.activity_done) { activity_done = false; } } completed_task_information.task_id = previous_task_id = task_id; completed_task_information.completed_time = previous_completed_time = completed_time; // If activity_id is -1, Mark all the non-optional tasks as completed. if (activity_id < 0) { TaskInformation *task = p_task_data[task_id]; if (task == nullptr) { continue; } for (int i = 0; i < task->activity_count; i++) { if (!task->activity_information[i].optional) { completed_task_information.activity_done[i] = true; } } } else { completed_task_information.activity_done[activity_id] = true; } } if (previous_task_id != -1) { client_task_state->completed_tasks.push_back(completed_task_information); } client_task_state->last_completed_task_loaded = client_task_state->completed_tasks.size(); } std::string query = StringFormat( "SELECT `taskid` FROM character_enabledtasks " "WHERE `charid` = %i AND `taskid` >0 AND `taskid` < %i " "ORDER BY `taskid` ASC", character_id, MAXTASKS ); auto results = database.QueryDatabase(query); if (results.Success()) { for (auto row = results.begin(); row != results.end(); ++row) { int task_id = atoi(row[0]); client_task_state->enabled_tasks.push_back(task_id); LogTasksDetail("[LoadClientState] Adding task_id [{}] to enabled tasks", task_id); } } // Check that there is an entry in the client task state for every activity_information in each task // This should only break if a ServerOP adds or deletes activites for a task that players already // have active, or due to a bug. for (int task_index = 0; task_index < MAXACTIVEQUESTS + 1; task_index++) { int task_id = client_task_state->active_tasks[task_index].task_id; if (task_id == TASKSLOTEMPTY) { continue; } if (!p_task_data[task_id]) { client->Message( Chat::Red, "Active Task Slot %i, references a task (%i), that does not exist. " "Removing from memory. Contact a GM to resolve this.", task_index, task_id ); LogError("[LoadClientState] Character [{}] has task [{}] which does not exist", character_id, task_id); client_task_state->active_tasks[task_index].task_id = TASKSLOTEMPTY; continue; } for (int activity_index = 0; activity_index < p_task_data[task_id]->activity_count; activity_index++) { if (client_task_state->active_tasks[task_index].activity[activity_index].activity_id != activity_index) { client->Message( Chat::Red, "Active Task %i, %s. activity_information count does not match expected value." "Removing from memory. Contact a GM to resolve this.", task_id, p_task_data[task_id]->title.c_str() ); LogTasks( "[LoadClientState] Fatal error in character [{}] task state. activity_information [{}] for Task [{}] either missing from client state or from task", character_id, activity_index, task_id ); client_task_state->active_tasks[task_index].task_id = TASKSLOTEMPTY; break; } } } if (client_task_state->active_task.task_id != TASKSLOTEMPTY) { client_task_state->UnlockActivities(character_id, client_task_state->active_task); } // TODO: shared for (auto &active_quest : client_task_state->active_quests) { if (active_quest.task_id != TASKSLOTEMPTY) { client_task_state->UnlockActivities(character_id, active_quest); } } LogTasks("[LoadClientState] for Character ID [{}] DONE!", character_id); return true; }