From 20c639c87271463bd716dc181d353e38f553d404 Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Sat, 16 Jul 2022 16:26:48 -0400 Subject: [PATCH] [Tasks] Reward clients on shared task completion sync (#2306) If a member is offline (or possibly during a race while zoning?) when the shared task is completed they will not receive the reward. On live the character receives their reward (with an updated replay timer) if they enter back into game while the shared task is still active. They keep the original replay timer if the shared task is no longer active and do not receive a reward. This makes it so clients are issued rewards (and a task completed event is dispatch) if the client's task state was out of sync with a completed shared task. To prevent characters being rewarded more than once in case of bad sync checks, a 'was_rewarded' field has been added to the character_tasks table and updated when rewards are assigned. This fixes a couple bugs so the character_activities table is correctly updated with shared task states to better detect when out of sync: - The character_activities table is now flagged to update after syncing shared task states. This table was not being updated if a client was offline or inaccessible for a shared task element update. - The character_activities table is now updated when a task element is completed. This was only being updated for activity increments and on completing the entire task. SaveClientState is now called at the end of ClientTaskState::IncrementDoneCount to cover all cases. This also has a cosmetic change to show replay timers before rewards like live, though this will not work for shared tasks until refactoring world code --- .../base/base_character_tasks_repository.h | 10 ++++ common/tasks.h | 1 + common/version.h | 2 +- utils/sql/db_update_manifest.txt | 1 + .../2022_07_10_character_task_rewarded.sql | 2 + zone/task_client_state.cpp | 60 +++++++++++-------- zone/task_client_state.h | 3 +- zone/task_manager.cpp | 41 +++++++++++-- zone/task_manager.h | 1 + 9 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 utils/sql/git/required/2022_07_10_character_task_rewarded.sql diff --git a/common/repositories/base/base_character_tasks_repository.h b/common/repositories/base/base_character_tasks_repository.h index 2f74b3ed6..013d2091c 100644 --- a/common/repositories/base/base_character_tasks_repository.h +++ b/common/repositories/base/base_character_tasks_repository.h @@ -24,6 +24,7 @@ public: int slot; int type; int acceptedtime; + int was_rewarded; }; static std::string PrimaryKey() @@ -39,6 +40,7 @@ public: "slot", "type", "acceptedtime", + "was_rewarded", }; } @@ -50,6 +52,7 @@ public: "slot", "type", "acceptedtime", + "was_rewarded", }; } @@ -95,6 +98,7 @@ public: entry.slot = 0; entry.type = 0; entry.acceptedtime = 0; + entry.was_rewarded = 0; return entry; } @@ -135,6 +139,7 @@ public: entry.slot = atoi(row[2]); entry.type = atoi(row[3]); entry.acceptedtime = atoi(row[4]); + entry.was_rewarded = atoi(row[5]); return entry; } @@ -173,6 +178,7 @@ public: update_values.push_back(columns[2] + " = " + std::to_string(character_tasks_entry.slot)); update_values.push_back(columns[3] + " = " + std::to_string(character_tasks_entry.type)); update_values.push_back(columns[4] + " = " + std::to_string(character_tasks_entry.acceptedtime)); + update_values.push_back(columns[5] + " = " + std::to_string(character_tasks_entry.was_rewarded)); auto results = db.QueryDatabase( fmt::format( @@ -199,6 +205,7 @@ public: insert_values.push_back(std::to_string(character_tasks_entry.slot)); insert_values.push_back(std::to_string(character_tasks_entry.type)); insert_values.push_back(std::to_string(character_tasks_entry.acceptedtime)); + insert_values.push_back(std::to_string(character_tasks_entry.was_rewarded)); auto results = db.QueryDatabase( fmt::format( @@ -233,6 +240,7 @@ public: insert_values.push_back(std::to_string(character_tasks_entry.slot)); insert_values.push_back(std::to_string(character_tasks_entry.type)); insert_values.push_back(std::to_string(character_tasks_entry.acceptedtime)); + insert_values.push_back(std::to_string(character_tasks_entry.was_rewarded)); insert_chunks.push_back("(" + Strings::Implode(",", insert_values) + ")"); } @@ -271,6 +279,7 @@ public: entry.slot = atoi(row[2]); entry.type = atoi(row[3]); entry.acceptedtime = atoi(row[4]); + entry.was_rewarded = atoi(row[5]); all_entries.push_back(entry); } @@ -300,6 +309,7 @@ public: entry.slot = atoi(row[2]); entry.type = atoi(row[3]); entry.acceptedtime = atoi(row[4]); + entry.was_rewarded = atoi(row[5]); all_entries.push_back(entry); } diff --git a/common/tasks.h b/common/tasks.h index d2777e151..d84e435b6 100644 --- a/common/tasks.h +++ b/common/tasks.h @@ -257,6 +257,7 @@ struct ClientTaskInformation { int current_step; int accepted_time; bool updated; + bool was_rewarded; // character has received reward for this task ClientActivityInformation activity[MAXACTIVITIESPERTASK]; }; diff --git a/common/version.h b/common/version.h index df34b1c65..c1b23ee11 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9188 +#define CURRENT_BINARY_DATABASE_VERSION 9189 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9029 diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 00e353cd1..a7eb2d592 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -442,6 +442,7 @@ 9186|2022_07_09_zone_expansion_deprecate.sql|SHOW COLUMNS FROM `zone` LIKE 'expansion'|notempty| 9187|2022_07_09_task_zone_version_matching.sql|SHOW COLUMNS FROM `task_activities` LIKE 'zone_version'|empty| 9188|2022_07_14_zone_expansion_revert.sql|SHOW COLUMNS FROM `zone` LIKE 'expansion'|empty| +9189|2022_07_10_character_task_rewarded.sql|SHOW COLUMNS FROM `character_tasks` LIKE 'was_rewarded'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2022_07_10_character_task_rewarded.sql b/utils/sql/git/required/2022_07_10_character_task_rewarded.sql new file mode 100644 index 000000000..3e70ef199 --- /dev/null +++ b/utils/sql/git/required/2022_07_10_character_task_rewarded.sql @@ -0,0 +1,2 @@ +ALTER TABLE `character_tasks` + ADD COLUMN `was_rewarded` TINYINT NOT NULL DEFAULT '0' AFTER `acceptedtime`; diff --git a/zone/task_client_state.cpp b/zone/task_client_state.cpp index 78c041794..b615e3812 100644 --- a/zone/task_client_state.cpp +++ b/zone/task_client_state.cpp @@ -1221,13 +1221,12 @@ void ClientTaskState::IncrementDoneCount( // 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) { - 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); + // world adds timers for shared tasks + if (task_information->type != TaskType::Shared) { + AddReplayTimer(client, *info, *task_information); + } + + DispatchEventTaskComplete(client, *info, activity_id); /* QS: PlayerLogTaskUpdates :: Complete */ if (RuleB(QueryServ, PlayerLogTaskUpdates)) { @@ -1242,25 +1241,18 @@ void ClientTaskState::IncrementDoneCount( } client->SendTaskActivityComplete(info->task_id, 0, task_index, task_information->type, 0); - task_manager->SaveClientState(client, this); // If Experience and/or cash rewards are set, reward them from the task even if reward_method is METHODQUEST - RewardTask(client, task_information); + RewardTask(client, task_information, *info); //RemoveTask(c, TaskIndex); - // add replay timer (world adds timers to shared task members) - AddReplayTimer(client, *info, *task_information); - // shared tasks linger at the completion step and do not get removed from the task window unlike quests/task - if (task_information->type == TaskType::Shared) { - return; + if (task_information->type != TaskType::Shared) { + task_manager->SendCompletedTasksToClient(client, this); + + client->CancelTask(task_index, task_information->type); } - - task_manager->SendCompletedTasksToClient(client, this); - - client->CancelTask(task_index, task_information->type); } - } else { // Send an updated packet for this single activity_information @@ -1270,17 +1262,32 @@ void ClientTaskState::IncrementDoneCount( activity_id, task_index ); - task_manager->SaveClientState(client, this); } + + task_manager->SaveClientState(client, this); } -void ClientTaskState::RewardTask(Client *client, TaskInformation *task_information) +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, TaskInformation *task_information, ClientTaskInformation& client_task) { - if (!task_information || !client) { + 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()); } @@ -2398,6 +2405,7 @@ void ClientTaskState::AcceptNewTask( active_slot->accepted_time = static_cast(accept_time); active_slot->updated = true; active_slot->current_step = -1; + active_slot->was_rewarded = false; for (int activity_id = 0; activity_id < task_manager->m_task_data[task_id]->activity_count; activity_id++) { active_slot->activity[activity_id].activity_id = activity_id; @@ -2631,8 +2639,7 @@ void ClientTaskState::ListTaskTimers(Client* client) void ClientTaskState::AddReplayTimer(Client* client, ClientTaskInformation& client_task, TaskInformation& task) { - // world adds timers for shared tasks and handles messages - if (task.type != TaskType::Shared && task.replay_timer_seconds > 0) + 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; @@ -2643,6 +2650,11 @@ void ClientTaskState::AddReplayTimer(Client* client, ClientTaskInformation& clie timer.expire_time = expire_time; timer.timer_type = static_cast(TaskTimerType::Replay); + // replace any existing replay timer + CharacterTaskTimersRepository::DeleteWhere(database, fmt::format( + "task_id = {} AND timer_type = {} AND character_id = {}", + client_task.task_id, static_cast(TaskTimerType::Replay), client->CharacterID())); + CharacterTaskTimersRepository::InsertOne(database, timer); client->Message(Chat::Yellow, fmt::format( diff --git a/zone/task_client_state.h b/zone/task_client_state.h index dfbdaaab3..5bd4e0b4a 100644 --- a/zone/task_client_state.h +++ b/zone/task_client_state.h @@ -43,7 +43,7 @@ public: bool TaskOutOfTime(TaskType task_type, int index); void TaskPeriodicChecks(Client *client); void SendTaskHistory(Client *client, int task_index); - void RewardTask(Client *client, TaskInformation *task_information); + void RewardTask(Client *client, TaskInformation *task_information, ClientTaskInformation& client_task); void EnableTask(int character_id, int task_count, int *task_list); void DisableTask(int character_id, int task_count, int *task_list); bool IsTaskEnabled(int task_id); @@ -75,6 +75,7 @@ public: private: void AddReplayTimer(Client *client, ClientTaskInformation& client_task, TaskInformation& task); + void DispatchEventTaskComplete(Client* client, ClientTaskInformation& client_task, int activity_id); void IncrementDoneCount( Client *client, diff --git a/zone/task_manager.cpp b/zone/task_manager.cpp index 501cb295e..00cdc4416 100644 --- a/zone/task_manager.cpp +++ b/zone/task_manager.cpp @@ -314,13 +314,14 @@ bool TaskManager::SaveClientState(Client *client, ClientTaskState *client_task_s ); std::string query = StringFormat( - "REPLACE INTO character_tasks (charid, taskid, slot, type, acceptedtime) " - "VALUES (%i, %i, %i, %i, %i)", + "REPLACE INTO character_tasks (charid, taskid, slot, type, acceptedtime, was_rewarded) " + "VALUES (%i, %i, %i, %i, %i, %d)", character_id, task_id, slot, static_cast(m_task_data[task_id]->type), - active_task.accepted_time + active_task.accepted_time, + active_task.was_rewarded ); auto results = database.QueryDatabase(query); @@ -1326,6 +1327,7 @@ bool TaskManager::LoadClientState(Client *client, ClientTaskState *client_task_s task_info->current_step = -1; task_info->accepted_time = character_task.acceptedtime; task_info->updated = false; + task_info->was_rewarded = character_task.was_rewarded; for (auto &i : task_info->activity) { i.activity_id = -1; @@ -1337,11 +1339,12 @@ bool TaskManager::LoadClientState(Client *client, ClientTaskState *client_task_s } LogTasks( - "[LoadClientState] character_id [{}] task_id [{}] slot [{}] accepted_time [{}]", + "[LoadClientState] character_id [{}] task_id [{}] slot [{}] accepted_time [{}] was_rewarded [{}]", character_id, task_id, slot, - character_task.acceptedtime + character_task.acceptedtime, + character_task.was_rewarded ); } @@ -1685,6 +1688,9 @@ void TaskManager::SyncClientSharedTaskWithPersistedState(Client *c, ClientTaskSt shared_task->activity[a.activity_id].activity_state = (a.completed_time > 0 ? ActivityCompleted : ActivityHidden); + // flag to update character_activities table entry on save + shared_task->activity[a.activity_id].updated = true; + // set flag to persist later fell_behind_state = true; } @@ -1692,6 +1698,17 @@ void TaskManager::SyncClientSharedTaskWithPersistedState(Client *c, ClientTaskSt // fell behind, force a save of client state if (fell_behind_state) { + // give reward if member was offline for shared task completion + // live does this as long as the shared task is still active when entering game + if (!shared_task->was_rewarded && IsActiveTaskComplete(*shared_task)) + { + LogTasksDetail("[LoadClientState] Syncing shared task completion for client [{}]", c->GetName()); + auto task_info = task_manager->m_task_data[shared_task->task_id]; + cts->AddReplayTimer(c, *shared_task, *task_info); // live updates a fresh timer + cts->DispatchEventTaskComplete(c, *shared_task, task_info->activity_count - 1); + cts->RewardTask(c, task_info, *shared_task); + } + SaveClientState(c, cts); } @@ -1915,3 +1932,17 @@ void TaskManager::HandleUpdateTasksOnKill(Client *client, uint32 npc_type_id, st } } } + +bool TaskManager::IsActiveTaskComplete(ClientTaskInformation& client_task) +{ + auto task_info = task_manager->m_task_data[client_task.task_id]; + for (int i = 0; i < task_info->activity_count; ++i) + { + if (client_task.activity[i].activity_state != ActivityCompleted && + !task_info->activity_information[i].optional) + { + return false; + } + } + return true; +} diff --git a/zone/task_manager.h b/zone/task_manager.h index 1acac00d3..0d54c8e37 100644 --- a/zone/task_manager.h +++ b/zone/task_manager.h @@ -66,6 +66,7 @@ public: int LastTaskInSet(int task_set); int NextTaskInSet(int task_set, int task_id); bool IsTaskRepeatable(int task_id); + bool IsActiveTaskComplete(ClientTaskInformation& client_task); friend class ClientTaskState;