rathena/src/map/quest.cpp
Aleos 2f326bf04a
Converted Quest Database to YAML (#4064)
* Changes TimeLimit format to be human readable.
* Converts title to string.
* Converts objectives and drop items to vectors.
* General clean ups and optimizations.
* Got rid of dummy_quest.
* Added TXT to YAML converter.
Thanks to @Atemo and @Normynator!
Co-authored-by: atemo <capucrath@gmail.com>
2020-03-30 13:17:19 -04:00

742 lines
19 KiB
C++

// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL
// For more information, see LICENCE in the main folder
#include "quest.hpp"
#include <stdlib.h>
#include "../common/cbasetypes.hpp"
#include "../common/malloc.hpp"
#include "../common/nullpo.hpp"
#include "../common/random.hpp"
#include "../common/showmsg.hpp"
#include "../common/socket.hpp"
#include "../common/strlib.hpp"
#include "../common/utilities.hpp"
#include "../common/utils.hpp"
#include "battle.hpp"
#include "chrif.hpp"
#include "clif.hpp"
#include "intif.hpp"
#include "itemdb.hpp"
#include "log.hpp"
#include "map.hpp"
#include "mob.hpp"
#include "party.hpp"
#include "pc.hpp"
static int split_exact_quest_time(char* modif_p, int* day, int* hour, int* minute, int *second);
const std::string QuestDatabase::getDefaultLocation() {
return std::string(db_path) + "/quest_db.yml";
}
/**
* Reads and parses an entry from the quest_db.
* @param node: YAML node containing the entry.
* @return count of successfully parsed rows
*/
uint64 QuestDatabase::parseBodyNode(const YAML::Node &node) {
uint32 quest_id;
if (!this->asUInt32(node, "Id", quest_id))
return 0;
std::shared_ptr<s_quest_db> quest = this->find(quest_id);
bool exists = quest != nullptr;
if (!exists) {
if (!this->nodesExist(node, { "Title" }))
return 0;
quest = std::make_shared<s_quest_db>();
quest->id = quest_id;
}
if (this->nodeExists(node, "Title")) {
std::string name;
if (!this->asString(node, "Title", name))
return 0;
quest->name = name;
}
if (this->nodeExists(node, "TimeLimit")) {
std::string time;
if (!this->asString(node, "TimeLimit", time))
return 0;
if (time.find("+") != std::string::npos) {
double timediff = solve_time(const_cast<char *>(time.c_str()));
if (timediff <= 0) {
this->invalidWarning(node["TimeLimit"], "Incorrect TimeLimit format %s given, skipping.\n", time.c_str());
return 0;
}
quest->time = static_cast<time_t>(timediff);
}
else {// '+' not found, set to specific time
int32 day, hour, minute, second;
if (split_exact_quest_time(const_cast<char *>(time.c_str()), &day, &hour, &minute, &second) == 0) {
this->invalidWarning(node["TimeLimit"], "Incorrect TimeLimit format %s given, skipping.\n", time.c_str());
return 0;
}
quest->time = day * 86400 + hour * 3600 + minute * 60 + second;
quest->time_at = true;
}
} else {
if (!exists) {
quest->time = 0;
quest->time_at = false;
}
}
if (this->nodeExists(node, "Targets")) {
const YAML::Node &targets = node["Targets"];
for (const YAML::Node &targetNode : targets) {
if (quest->objectives.size() >= MAX_QUEST_OBJECTIVES) {
this->invalidWarning(targetNode, "Targets list exceeds the maximum of %d, skipping.\n", MAX_QUEST_OBJECTIVES);
return 0;
}
if (!this->nodeExists(targetNode, "Mob"))
continue;
std::string mob_name;
if (!this->asString(targetNode, "Mob", mob_name))
return 0;
struct mob_db *mob = mobdb_search_aegisname(mob_name.c_str());
if (!mob) {
this->invalidWarning(targetNode["Mob"], "Mob %s does not exist, skipping.\n", mob_name.c_str());
continue;
}
//std::shared_ptr<s_quest_objective> target = util::vector_find(quest->objectives, mob->vd.class_);
std::shared_ptr<s_quest_objective> target;
std::vector<std::shared_ptr<s_quest_objective>>::iterator it = std::find_if(quest->objectives.begin(), quest->objectives.end(), [&](std::shared_ptr<s_quest_objective> const &v) {
return (*v).mob_id == mob->vd.class_;
});
if (it != quest->objectives.end())
target = (*it);
else
target = nullptr;
bool targetExists = target != nullptr;
if (!targetExists) {
if (!this->nodeExists(targetNode, "Count")) {
this->invalidWarning(targetNode["Count"], "Targets has no Count value specified, skipping.\n");
continue;
}
target = std::make_shared<s_quest_objective>();
target->mob_id = mob->vd.class_;
}
if (this->nodeExists(targetNode, "Count")) {
uint16 count;
if (!this->asUInt16(targetNode, "Count", count))
return 0;
target->count = count;
}
quest->objectives.push_back(target);
}
}
if (this->nodeExists(node, "Drops")) {
const YAML::Node &drops = node["Drops"];
for (const YAML::Node &dropNode : drops) {
if (quest->objectives.size() >= MAX_QUEST_OBJECTIVES) {
this->invalidWarning(dropNode, "Drops list exceeds the maximum of %d, skipping.\n", MAX_QUEST_OBJECTIVES);
return 0;
}
uint32 mob_id = 0; // Can be 0 which means all monsters
if (this->nodeExists(dropNode, "Mob")) {
std::string mob_name;
if (!this->asString(dropNode, "Mob", mob_name))
return 0;
struct mob_db *mob = mobdb_search_aegisname(mob_name.c_str());
if (!mob) {
this->invalidWarning(dropNode["Mob"], "Mob %s does not exist, skipping.\n", mob_name.c_str());
continue;
}
mob_id = mob->vd.class_;
}
//std::shared_ptr<s_quest_dropitem> target = util::vector_find(quest->dropitem, mob_id);
std::shared_ptr<s_quest_dropitem> target;
std::vector<std::shared_ptr<s_quest_dropitem>>::iterator it = std::find_if(quest->dropitem.begin(), quest->dropitem.end(), [&](std::shared_ptr<s_quest_dropitem> const &v) {
return (*v).mob_id == mob_id;
});
if (it != quest->dropitem.end())
target = (*it);
else
target = nullptr;
bool targetExists = target != nullptr;
if (!targetExists) {
if (!this->nodeExists(dropNode, "Item")) {
this->invalidWarning(dropNode["Item"], "Drops has no Item value specified, skipping.\n");
continue;
}
if (!this->nodeExists(dropNode, "Rate")) {
this->invalidWarning(dropNode["Item"], "Drops has no Rate value specified, skipping.\n");
continue;
}
target = std::make_shared<s_quest_dropitem>();
target->mob_id = mob_id;
}
if (this->nodeExists(dropNode, "Item")) {
std::string item_name;
if (!this->asString(dropNode, "Item", item_name))
return 0;
struct item_data *item = itemdb_search_aegisname(item_name.c_str());
if (!item) {
this->invalidWarning(dropNode["Item"], "Item %s does not exist, skipping.\n", item_name.c_str());
continue;
}
target->nameid = item->nameid;
}
if (this->nodeExists(dropNode, "Count")) {
uint16 count;
if (!this->asUInt16(dropNode, "Count", count))
return 0;
if (!itemdb_isstackable(target->nameid)) {
this->invalidWarning(dropNode["Count"], "Item %s is not stackable, capping to 1.\n", itemdb_name(target->nameid));
count = 1;
}
target->count = count;
} else {
if (!targetExists)
target->count = 1;
}
if (this->nodeExists(dropNode, "Rate")) {
uint16 rate;
if (!this->asUInt16(dropNode, "Rate", rate))
return 0;
target->rate = rate;
}
quest->dropitem.push_back(target);
}
}
if (!exists)
this->put(quest_id, quest);
return 1;
}
static int split_exact_quest_time(char* modif_p, int* day, int* hour, int* minute, int *second) {
int d = -1, h = -1, mn = -1, s = -1;
nullpo_retr(0, modif_p);
while (modif_p[0] != '\0') {
int value = atoi(modif_p);
if (modif_p[0] == '-' || modif_p[0] == '+')
modif_p++;
while (modif_p[0] >= '0' && modif_p[0] <= '9')
modif_p++;
if (modif_p[0] == 's') {
s = value;
modif_p++;
} else if (modif_p[0] == 'm' && modif_p[1] == 'n') {
mn = value;
modif_p = modif_p + 2;
} else if (modif_p[0] == 'h') {
h = value;
modif_p++;
} else if (modif_p[0] == 'd' || modif_p[0] == 'j') {
d = value;
modif_p++;
} else if (modif_p[0] != '\0') {
modif_p++;
}
}
if (h < 0 || h > 23 || mn > 59 || s > 59) // hour is required
return 0;
*day = max(0,d);
*hour = h;
*minute = max(0,mn);
*second = max(0,s);
return 1;
}
/**
* Searches a quest by ID.
* @param quest_id : ID to lookup
* @return Quest entry or nullptr on failure
*/
std::shared_ptr<s_quest_db> quest_search(int quest_id)
{
auto quest = quest_db.find(quest_id);
if (!quest)
return nullptr;
return quest;
}
/**
* Sends quest info to the player on login.
* @param sd : Player's data
* @return 0 in case of success, nonzero otherwise (i.e. the player has no quests)
*/
int quest_pc_login(struct map_session_data *sd)
{
if (!sd->avail_quests)
return 1;
clif_quest_send_list(sd);
#if PACKETVER < 20141022
clif_quest_send_mission(sd);
//@TODO[Haru]: Is this necessary? Does quest_send_mission not take care of this?
for (int i = 0; i < sd->avail_quests; i++)
clif_quest_update_objective(sd, &sd->quest_log[i], 0);
#endif
return 0;
}
/**
* Determine a quest's time limit.
* @param qi: Quest data
* @return Time limit value
*/
static time_t quest_time(std::shared_ptr<s_quest_db> qi)
{
if (!qi || qi->time < 0)
return 0;
if (!qi->time_at && qi->time > 0)
return time(nullptr) + qi->time;
else if (qi->time_at) {
time_t t = time(nullptr);
struct tm *lt = localtime(&t);
uint32 time_today = lt->tm_hour * 3600 + lt->tm_min * 60 + lt->tm_sec;
if (time_today < (qi->time % 86400))
return static_cast<time_t>(t + qi->time - time_today);
else // Carry over to the next day
return static_cast<time_t>(t + 86400 + qi->time - time_today);
}
return 0;
}
/**
* Adds a quest to the player's list.
* New quest will be added as Q_ACTIVE.
* @param sd : Player's data
* @param quest_id : ID of the quest to add.
* @return 0 in case of success, nonzero otherwise
*/
int quest_add(struct map_session_data *sd, int quest_id)
{
std::shared_ptr<s_quest_db> qi = quest_search(quest_id);
if (!qi) {
ShowError("quest_add: quest %d not found in DB.\n", quest_id);
return -1;
}
if (quest_check(sd, quest_id, HAVEQUEST) >= 0) {
ShowError("quest_add: Character %d already has quest %d.\n", sd->status.char_id, quest_id);
return -1;
}
int n = sd->avail_quests; //Insertion point
sd->num_quests++;
sd->avail_quests++;
RECREATE(sd->quest_log, struct quest, sd->num_quests);
//The character has some completed quests, make room before them so that they will stay at the end of the array
if (sd->avail_quests != sd->num_quests)
memmove(&sd->quest_log[n + 1], &sd->quest_log[n], sizeof(struct quest) * (sd->num_quests-sd->avail_quests));
sd->quest_log[n] = {};
sd->quest_log[n].quest_id = qi->id;
sd->quest_log[n].time = (uint32)quest_time(qi);
sd->quest_log[n].state = Q_ACTIVE;
sd->save_quest = true;
clif_quest_add(sd, &sd->quest_log[n]);
clif_quest_update_objective(sd, &sd->quest_log[n], 0);
if( save_settings&CHARSAVE_QUEST )
chrif_save(sd, CSAVE_NORMAL);
return 0;
}
/**
* Replaces a quest in a player's list with another one.
* @param sd : Player's data
* @param qid1 : Current quest to replace
* @param qid2 : New quest to add
* @return 0 in case of success, nonzero otherwise
*/
int quest_change(struct map_session_data *sd, int qid1, int qid2)
{
std::shared_ptr<s_quest_db> qi = quest_search(qid2);
if (!qi) {
ShowError("quest_change: quest %d not found in DB.\n", qid2);
return -1;
}
if (quest_check(sd, qid2, HAVEQUEST) >= 0) {
ShowError("quest_change: Character %d already has quest %d.\n", sd->status.char_id, qid2);
return -1;
}
if (quest_check(sd, qid1, HAVEQUEST) < 0) {
ShowError("quest_change: Character %d doesn't have quest %d.\n", sd->status.char_id, qid1);
return -1;
}
int i;
ARR_FIND(0, sd->avail_quests, i, sd->quest_log[i].quest_id == qid1);
if (i == sd->avail_quests) {
ShowError("quest_change: Character %d has completed quest %d.\n", sd->status.char_id, qid1);
return -1;
}
sd->quest_log[i] = {};
sd->quest_log[i].quest_id = qi->id;
sd->quest_log[i].time = (uint32)quest_time(qi);
sd->quest_log[i].state = Q_ACTIVE;
sd->save_quest = true;
clif_quest_delete(sd, qid1);
clif_quest_add(sd, &sd->quest_log[i]);
clif_quest_update_objective(sd, &sd->quest_log[i], 0);
if( save_settings&CHARSAVE_QUEST )
chrif_save(sd, CSAVE_NORMAL);
return 0;
}
/**
* Removes a quest from a player's list
* @param sd : Player's data
* @param quest_id : ID of the quest to remove
* @return 0 in case of success, nonzero otherwise
*/
int quest_delete(struct map_session_data *sd, int quest_id)
{
int i;
//Search for quest
ARR_FIND(0, sd->num_quests, i, sd->quest_log[i].quest_id == quest_id);
if (i == sd->num_quests) {
ShowError("quest_delete: Character %d doesn't have quest %d.\n", sd->status.char_id, quest_id);
return -1;
}
if (sd->quest_log[i].state != Q_COMPLETE)
sd->avail_quests--;
if (i < --sd->num_quests) //Compact the array
memmove(&sd->quest_log[i], &sd->quest_log[i + 1], sizeof(struct quest) * (sd->num_quests - i));
if (sd->num_quests == 0) {
aFree(sd->quest_log);
sd->quest_log = NULL;
} else
RECREATE(sd->quest_log, struct quest, sd->num_quests);
sd->save_quest = true;
clif_quest_delete(sd, quest_id);
if( save_settings&CHARSAVE_QUEST )
chrif_save(sd, CSAVE_NORMAL);
return 0;
}
/**
* Map iterator subroutine to update quest objectives for a party after killing a monster.
* @see map_foreachinrange
* @param ap : Argument list, expecting:
* int Party ID
* int Mob ID
*/
int quest_update_objective_sub(struct block_list *bl, va_list ap)
{
struct map_session_data *sd;
int mob_id, party_id;
nullpo_ret(bl);
nullpo_ret(sd = (struct map_session_data *)bl);
party_id = va_arg(ap,int);
mob_id = va_arg(ap,int);
if( !sd->avail_quests )
return 0;
if( sd->status.party_id != party_id )
return 0;
quest_update_objective(sd, mob_id);
return 1;
}
/**
* Updates the quest objectives for a character after killing a monster, including the handling of quest-granted drops.
* @param sd : Character's data
* @param mob_id : Monster ID
*/
void quest_update_objective(struct map_session_data *sd, int mob_id)
{
for (int i = 0; i < sd->avail_quests; i++) {
if (sd->quest_log[i].state == Q_COMPLETE) // Skip complete quests
continue;
std::shared_ptr<s_quest_db> qi = quest_search(sd->quest_log[i].quest_id);
// Process quest objectives
for (int j = 0; j < qi->objectives.size(); j++) {
if (qi->objectives[j]->mob_id == mob_id && sd->quest_log[i].count[j] < qi->objectives[j]->count) {
sd->quest_log[i].count[j]++;
sd->save_quest = true;
clif_quest_update_objective(sd, &sd->quest_log[i], mob_id);
}
}
// Process quest-granted extra drop bonuses
for (const auto &it : qi->dropitem) {
if (it->mob_id != 0 && it->mob_id != mob_id)
continue;
if (it->rate < 10000 && rnd()%10000 >= it->rate)
continue; // TODO: Should this be affected by server rates?
if (!itemdb_exists(it->nameid))
continue;
struct item item = {};
item.nameid = it->nameid;
item.identify = itemdb_isidentified(it->nameid);
item.amount = it->count;
//#ifdef BOUND_ITEMS
// item.bound = it.bound;
//#endif
// if (it.isGUID)
// item.unique_id = pc_generate_unique_id(sd);
char temp;
if ((temp = pc_additem(sd, &item, 1, LOG_TYPE_QUEST)) != ADDITEM_SUCCESS) // Failed to obtain the item
clif_additem(sd, 0, 0, temp);
// else if (it.isAnnounced || itemdb_exists(it.nameid)->flag.broadcast)
// intif_broadcast_obtain_special_item(sd, it.nameid, it.mob_id, ITEMOBTAIN_TYPE_MONSTER_ITEM);
}
}
pc_show_questinfo(sd);
}
/**
* Updates a quest's state.
* Only status of active and inactive quests can be updated. Completed quests can't (for now).
* @param sd : Character's data
* @param quest_id : Quest ID to update
* @param qs : New quest state
* @return 0 in case of success, nonzero otherwise
* @author [Inkfish]
*/
int quest_update_status(struct map_session_data *sd, int quest_id, e_quest_state status)
{
int i;
ARR_FIND(0, sd->avail_quests, i, sd->quest_log[i].quest_id == quest_id);
if (i == sd->avail_quests) {
ShowError("quest_update_status: Character %d doesn't have quest %d.\n", sd->status.char_id, quest_id);
return -1;
}
sd->quest_log[i].state = status;
sd->save_quest = true;
if (status < Q_COMPLETE) {
clif_quest_update_status(sd, quest_id, status == Q_ACTIVE ? true : false);
return 0;
}
// The quest is complete, so it needs to be moved to the completed quests block at the end of the array.
if (i < (--sd->avail_quests)) {
struct quest tmp_quest;
memcpy(&tmp_quest, &sd->quest_log[i], sizeof(struct quest));
memcpy(&sd->quest_log[i], &sd->quest_log[sd->avail_quests], sizeof(struct quest));
memcpy(&sd->quest_log[sd->avail_quests], &tmp_quest, sizeof(struct quest));
}
clif_quest_delete(sd, quest_id);
if (save_settings&CHARSAVE_QUEST)
chrif_save(sd, CSAVE_NORMAL);
return 0;
}
/**
* Queries quest information for a character.
* @param sd : Character's data
* @param quest_id : Quest ID
* @param type : Check type
* @return -1 if the quest was not found, otherwise it depends on the type:
* HAVEQUEST: The quest's state
* PLAYTIME: 2 if the quest's timeout has expired
* 1 if the quest was completed
* 0 otherwise
* HUNTING: 2 if the quest has not been marked as completed yet, and its objectives have been fulfilled
* 1 if the quest's timeout has expired
* 0 otherwise
*/
int quest_check(struct map_session_data *sd, int quest_id, e_quest_check_type type)
{
int i;
ARR_FIND(0, sd->num_quests, i, sd->quest_log[i].quest_id == quest_id);
if (i == sd->num_quests)
return -1;
switch (type) {
case HAVEQUEST:
if (sd->quest_log[i].state == Q_INACTIVE) // Player has the quest but it's in the inactive state; send it as Q_ACTIVE.
return 1;
return sd->quest_log[i].state;
case PLAYTIME:
return (sd->quest_log[i].time < (unsigned int)time(nullptr) ? 2 : sd->quest_log[i].state == Q_COMPLETE ? 1 : 0);
case HUNTING:
if (sd->quest_log[i].state == Q_INACTIVE || sd->quest_log[i].state == Q_ACTIVE) {
int j;
std::shared_ptr<s_quest_db> qi = quest_search(sd->quest_log[i].quest_id);
ARR_FIND(0, qi->objectives.size(), j, sd->quest_log[i].count[j] < qi->objectives[j]->count);
if (j == qi->objectives.size())
return 2;
if (sd->quest_log[i].time < (unsigned int)time(nullptr))
return 1;
}
return 0;
default:
ShowError("quest_check_quest: Unknown parameter %d",type);
break;
}
return -1;
}
/**
* Map iterator to ensures a player has no invalid quest log entries.
* Any entries that are no longer in the db are removed.
* @see map_foreachpc
* @param sd : Character's data
* @param ap : Ignored
*/
static int quest_reload_check_sub(struct map_session_data *sd, va_list ap)
{
nullpo_ret(sd);
int i, j = 0;
for (i = 0; i < sd->num_quests; i++) {
std::shared_ptr<s_quest_db> qi = quest_search(sd->quest_log[i].quest_id);
if (!qi) { //Remove no longer existing entries
if (sd->quest_log[i].state != Q_COMPLETE) //And inform the client if necessary
clif_quest_delete(sd, sd->quest_log[i].quest_id);
continue;
}
if (i != j) {
//Move entries if there's a gap to fill
memcpy(&sd->quest_log[j], &sd->quest_log[i], sizeof(struct quest));
}
j++;
}
sd->num_quests = j;
ARR_FIND(0, sd->num_quests, i, sd->quest_log[i].state == Q_COMPLETE);
sd->avail_quests = i;
return 1;
}
bool QuestDatabase::reload() {
if (!TypesafeYamlDatabase::reload())
return false;
// Update quest data for players, to ensure no entries about removed quests are left over.
map_foreachpc(&quest_reload_check_sub);
return true;
}
QuestDatabase quest_db;
/**
* Initializes the quest interface.
*/
void do_init_quest(void)
{
quest_db.load();
}
/**
* Finalizes the quest interface before shutdown.
*/
void do_final_quest(void)
{
}