/** * This file is part of Hercules. * http://herc.ws - http://github.com/HerculesWS/Hercules * * Copyright (C) 2017 Hercules Dev Team * * Hercules is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define HERCULES_CORE #include "rodex.h" #include "map/battle.h" #include "map/date.h" #include "map/intif.h" #include "map/itemdb.h" #include "map/pc.h" #include "map/map.h" #include "common/nullpo.h" #include "common/sql.h" #include "common/memmgr.h" // NOTE : These values are hardcoded into the client // Cost of each Attached Item #define ATTACHITEM_COST 500 // Percent of Attached Zeny that will be paid as Tax #define ATTACHZENY_TAX 0 // Maximun number of messages that can be sent in one day #define DAILY_MAX_MAILS 3 static struct rodex_interface rodex_s; struct rodex_interface *rodex; /// Checks if RoDEX System is enabled in the server /// Returns true if it's enabled, false otherwise static bool rodex_isenabled(void) { if (battle_config.feature_rodex == 1) return true; return false; } /// Checks and refreshes the user daily number of Stamps /// @param sd : The player who's being checked static void rodex_refresh_stamps(struct map_session_data *sd) { int today = date_get_date(); nullpo_retv(sd); // Note : Weirdly, iRO starts this with maximum messages of the day and decrements // but our clients starts this at 0 and increments if (sd->sc.data[SC_DAILYSENDMAILCNT] == NULL) { sc_start2(NULL, &sd->bl, SC_DAILYSENDMAILCNT, 100, today, 0, INFINITE_DURATION); } else { int sc_date = sd->sc.data[SC_DAILYSENDMAILCNT]->val1; if (sc_date != today) { sc_start2(NULL, &sd->bl, SC_DAILYSENDMAILCNT, 100, today, 0, INFINITE_DURATION); } } } /// Attaches an item to a message being written /// @param sd : The player who's writting /// @param idx : the inventory idx of the item /// @param amount : Amount of the item to be attached static void rodex_add_item(struct map_session_data *sd, int16 idx, int16 amount) { int i; bool is_stack = false; nullpo_retv(sd); if (idx < 0 || idx >= sd->status.inventorySize) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_FATAL_ERROR); return; } if (amount < 0 || amount > sd->status.inventory[idx].amount) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_FATAL_ERROR); return; } if (!pc_can_give_items(sd) || sd->status.inventory[idx].expire_time || !itemdb_canmail(&sd->status.inventory[idx], pc_get_group_level(sd)) || (sd->status.inventory[idx].bound && !pc_can_give_bound_items(sd))) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_NOT_TRADEABLE); return; } if (itemdb->isstackable(sd->status.inventory[idx].nameid) == 1) { for (i = 0; i < RODEX_MAX_ITEM; ++i) { if (sd->rodex.tmp.items[i].idx == idx) { if (sd->status.inventory[idx].nameid == sd->rodex.tmp.items[i].item.nameid && sd->status.inventory[idx].unique_id == sd->rodex.tmp.items[i].item.unique_id) { is_stack = true; break; } } } if (i == RODEX_MAX_ITEM && sd->rodex.tmp.items_count < RODEX_MAX_ITEM) { ARR_FIND(0, RODEX_MAX_ITEM, i, sd->rodex.tmp.items[i].idx == 0); } } else if (sd->rodex.tmp.items_count < RODEX_MAX_ITEM) { ARR_FIND(0, RODEX_MAX_ITEM, i, sd->rodex.tmp.items[i].idx == 0); } else { i = RODEX_MAX_ITEM; } if (i == RODEX_MAX_ITEM) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_NO_SPACE); return; } if (sd->rodex.tmp.items[i].item.amount + amount > sd->status.inventory[idx].amount) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_FATAL_ERROR); return; } if (sd->rodex.tmp.weight + sd->inventory_data[idx]->weight * amount > RODEX_WEIGHT_LIMIT) { clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_FATAL_ERROR); return; } sd->rodex.tmp.items[i].idx = idx; sd->rodex.tmp.weight += sd->inventory_data[idx]->weight * amount; if (is_stack == false) { sd->rodex.tmp.items[i].item = sd->status.inventory[idx]; sd->rodex.tmp.items[i].item.amount = amount; sd->rodex.tmp.items_count++; } else { sd->rodex.tmp.items[i].item.amount += amount; } sd->rodex.tmp.type |= MAIL_TYPE_ITEM; clif->rodex_add_item_result(sd, idx, amount, RODEX_ADD_ITEM_SUCCESS); } /// Removes an item attached to a message being writen /// @param sd : The player who's writting the message /// @param idx : The index of the item /// @param amount : How much to remove static void rodex_remove_item(struct map_session_data *sd, int16 idx, int16 amount) { int i; struct item *it; struct item_data *itd; nullpo_retv(sd); Assert_retv(idx >= 0 && idx < sd->status.inventorySize); for (i = 0; i < RODEX_MAX_ITEM; ++i) { if (sd->rodex.tmp.items[i].idx == idx) break; } if (i == RODEX_MAX_ITEM) { clif->rodex_remove_item_result(sd, idx, -1); return; } it = &sd->rodex.tmp.items[i].item; if (amount <= 0 || amount > it->amount) { clif->rodex_remove_item_result(sd, idx, -1); return; } itd = itemdb->search(it->nameid); if (amount == it->amount) { sd->rodex.tmp.weight -= itd->weight * amount; sd->rodex.tmp.items_count--; if (sd->rodex.tmp.items_count < 1) { sd->rodex.tmp.type &= ~MAIL_TYPE_ITEM; } memset(&sd->rodex.tmp.items[i], 0x0, sizeof(sd->rodex.tmp.items[0])); clif->rodex_remove_item_result(sd, idx, 0); return; } it->amount -= amount; sd->rodex.tmp.weight -= itd->weight * amount; clif->rodex_remove_item_result(sd, idx, it->amount); } /// Request if character with given name exists and returns information about him /// @param sd : The player who's requesting /// @param name : The name of the character to check /// @param base_level : Reference to return the character base level, if he exists /// @param char_id : Reference to return the character id, if he exists /// @param class : Reference to return the character class id, if he exists static void rodex_check_player(struct map_session_data *sd, const char *name, int *base_level, int *char_id, int *class) { intif->rodex_checkname(sd, name); } /// Sends a Mail to an character /// @param sd : The player who's sending /// @param receiver_name : The name of the character who's receiving the message /// @param body : Mail message /// @param title : Mail Title /// @param zeny : Amount of zeny attached /// Returns result code: /// RODEX_SEND_MAIL_SUCCESS = 0, /// RODEX_SEND_MAIL_FATAL_ERROR = 1, /// RODEX_SEND_MAIL_COUNT_ERROR = 2, /// RODEX_SEND_MAIL_ITEM_ERROR = 3, /// RODEX_SEND_MAIL_RECEIVER_ERROR = 4 static int rodex_send_mail(struct map_session_data *sd, const char *receiver_name, const char *body, const char *title, int64 zeny) { int i; int64 total_zeny; nullpo_retr(RODEX_SEND_MAIL_FATAL_ERROR, sd); nullpo_retr(RODEX_SEND_MAIL_FATAL_ERROR, receiver_name); nullpo_retr(RODEX_SEND_MAIL_FATAL_ERROR, body); nullpo_retr(RODEX_SEND_MAIL_FATAL_ERROR, title); if (!rodex->isenabled() || sd->npc_id > 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_FATAL_ERROR; } if (!pc_has_permission(sd, PC_PERM_BYPASS_NOSTORAGE) && (map->list[sd->bl.m].flag.nostorage & 2)) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_FATAL_ERROR; } if (zeny < 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_FATAL_ERROR; } total_zeny = zeny + sd->rodex.tmp.items_count * ATTACHITEM_COST + (2 * zeny)/100; if (strcmp(receiver_name, sd->rodex.tmp.receiver_name) != 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_RECEIVER_ERROR; } if (total_zeny > sd->status.zeny || total_zeny < 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_FATAL_ERROR; } rodex_refresh_stamps(sd); if (sd->sc.data[SC_DAILYSENDMAILCNT] != NULL) { if (sd->sc.data[SC_DAILYSENDMAILCNT]->val2 >= DAILY_MAX_MAILS) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_COUNT_ERROR; } sc_start2(NULL, &sd->bl, SC_DAILYSENDMAILCNT, 100, sd->sc.data[SC_DAILYSENDMAILCNT]->val1, sd->sc.data[SC_DAILYSENDMAILCNT]->val2 + 1, INFINITE_DURATION); } else { sc_start2(NULL, &sd->bl, SC_DAILYSENDMAILCNT, 100, date_get_date(), 1, INFINITE_DURATION); } for (i = 0; i < RODEX_MAX_ITEM; i++) { int16 idx = sd->rodex.tmp.items[i].idx; int j; struct item *tmpItem = &sd->rodex.tmp.items[i].item; struct item *invItem = &sd->status.inventory[idx]; if (tmpItem->nameid == 0) continue; if (tmpItem->nameid != invItem->nameid || tmpItem->unique_id != invItem->unique_id || tmpItem->refine != invItem->refine || tmpItem->attribute != invItem->attribute || tmpItem->expire_time != invItem->expire_time || tmpItem->bound != invItem->bound || tmpItem->amount > invItem->amount || tmpItem->amount < 1) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_ITEM_ERROR; } for (j = 0; j < MAX_SLOTS; j++) { if (tmpItem->card[j] != invItem->card[j]) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_ITEM_ERROR; } } for (j = 0; j < MAX_ITEM_OPTIONS; j++) { if (tmpItem->option[j].index != invItem->option[j].index || tmpItem->option[j].value != invItem->option[j].value || tmpItem->option[j].param != invItem->option[j].param) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_ITEM_ERROR; } } } if (total_zeny > 0 && pc->payzeny(sd, (int)total_zeny, LOG_TYPE_MAIL, NULL) != 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_FATAL_ERROR; } for (i = 0; i < RODEX_MAX_ITEM; i++) { int16 idx = sd->rodex.tmp.items[i].idx; if (sd->rodex.tmp.items[i].item.nameid == 0) { continue; } if (pc->delitem(sd, idx, sd->rodex.tmp.items[i].item.amount, 0, DELITEM_NORMAL, LOG_TYPE_MAIL) != 0) { rodex->clean(sd, 1); return RODEX_SEND_MAIL_ITEM_ERROR; } } sd->rodex.tmp.zeny = zeny; sd->rodex.tmp.is_read = false; sd->rodex.tmp.is_deleted = false; sd->rodex.tmp.send_date = (int)time(NULL); sd->rodex.tmp.expire_date = (int)time(NULL) + RODEX_EXPIRE; if (strlen(sd->rodex.tmp.body) > 0) sd->rodex.tmp.type |= MAIL_TYPE_TEXT; if (sd->rodex.tmp.zeny > 0) sd->rodex.tmp.type |= MAIL_TYPE_ZENY; sd->rodex.tmp.sender_id = sd->status.char_id; safestrncpy(sd->rodex.tmp.sender_name, sd->status.name, NAME_LENGTH); safestrncpy(sd->rodex.tmp.title, title, RODEX_TITLE_LENGTH); safestrncpy(sd->rodex.tmp.body, body, RODEX_BODY_LENGTH); intif->rodex_sendmail(&sd->rodex.tmp); return RODEX_SEND_MAIL_SUCCESS; // this will not inform client of the success yet. (see rodex_send_mail_result) } /// The result of a message send, called by char-server /// @param ssd : Sender's sd /// @param rsd : Receiver's sd /// @param result : Message sent (true) or failed (false) static void rodex_send_mail_result(struct map_session_data *ssd, struct map_session_data *rsd, bool result) { if (ssd != NULL) { rodex->clean(ssd, 1); if (result == false) { clif->rodex_send_mail_result(ssd->fd, ssd, RODEX_SEND_MAIL_FATAL_ERROR); return; } clif->rodex_send_mail_result(ssd->fd, ssd, RODEX_SEND_MAIL_SUCCESS); } if (rsd != NULL) { clif->rodex_icon(rsd->fd, true); clif_disp_onlyself(rsd, "You've got a new mail!"); } return; } /// Retrieves one message from character /// @param sd : Character /// @param mail_id : Mail ID that's being retrieved /// Returns the message static struct rodex_message *rodex_get_mail(struct map_session_data *sd, int64 mail_id) { int i; struct rodex_message *msg; int char_id; nullpo_retr(NULL, sd); ARR_FIND(0, VECTOR_LENGTH(sd->rodex.messages), i, VECTOR_INDEX(sd->rodex.messages, i).id == mail_id); if (i == VECTOR_LENGTH(sd->rodex.messages)) return NULL; msg = &VECTOR_INDEX(sd->rodex.messages, i); char_id = sd->status.char_id; if ((msg->is_deleted == true) || (msg->expire_date < time(NULL) && ((msg->receiver_accountid > 0) || (msg->receiver_id == char_id && msg->sender_id != char_id))) || (msg->expire_date + RODEX_EXPIRE < time(NULL)) ) return NULL; return msg; } /// Request to read a mail by its ID /// @param sd : Who's reading /// @param mail_id : Mail ID to be read static void rodex_read_mail(struct map_session_data *sd, int64 mail_id) { struct rodex_message *msg; nullpo_retv(sd); msg = rodex->get_mail(sd, mail_id); nullpo_retv(msg); if (msg->opentype == RODEX_OPENTYPE_RETURN) { if (msg->sender_read == false) { intif->rodex_updatemail(sd, msg->id, 0, 4); msg->sender_read = true; } } else { if (msg->is_read == false) { intif->rodex_updatemail(sd, msg->id, 0, 0); msg->is_read = true; } } clif->rodex_read_mail(sd, msg->opentype, msg); } /// Deletes a mail /// @param sd : Who's deleting /// @param mail_id : Mail ID to be deleted static void rodex_delete_mail(struct map_session_data *sd, int64 mail_id) { struct rodex_message *msg; nullpo_retv(sd); msg = rodex->get_mail(sd, mail_id); nullpo_retv(msg); msg->is_deleted = true; intif->rodex_updatemail(sd, msg->id, 0, 3); clif->rodex_delete_mail(sd, msg->opentype, msg->id); } /// give requested zeny from message to player static void rodex_getZenyAck(struct map_session_data *sd, int64 mail_id, int8 opentype, int64 zeny) { nullpo_retv(sd); if (zeny <= 0) { clif->rodex_request_zeny(sd, opentype, mail_id, RODEX_GET_ZENY_FATAL_ERROR); return; } if (pc->getzeny(sd, (int)zeny, LOG_TYPE_MAIL, NULL) != 0) { clif->rodex_request_zeny(sd, opentype, mail_id, RODEX_GET_ZENY_FATAL_ERROR); return; } clif->rodex_request_zeny(sd, opentype, mail_id, RODEX_GET_ZENY_SUCCESS); } /// Gets attached zeny /// @param sd : Who's getting /// @param mail_id : Mail ID that we're getting zeny from static void rodex_get_zeny(struct map_session_data *sd, int8 opentype, int64 mail_id) { nullpo_retv(sd); struct rodex_message *msg = rodex->get_mail(sd, mail_id); if (msg == NULL) { clif->rodex_request_zeny(sd, opentype, mail_id, RODEX_GET_ZENY_FATAL_ERROR); return; } if ((int64)sd->status.zeny + msg->zeny > MAX_ZENY) { clif->rodex_request_zeny(sd, opentype, mail_id, RODEX_GET_ZENY_LIMIT_ERROR); return; } msg->type &= ~MAIL_TYPE_ZENY; msg->zeny = 0; intif->rodex_updatemail(sd, mail_id, opentype, 1); } // give requested items from message to player static void rodex_getItemsAck(struct map_session_data *sd, int64 mail_id, int8 opentype, int count, const struct rodex_item *items) { nullpo_retv(sd); nullpo_retv(items); for (int i = 0; i < count; ++i) { const struct item *it = &items[i].item; if (it->nameid == 0) { continue; } if (pc->additem(sd, it, it->amount, LOG_TYPE_MAIL) != 0) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FULL_ERROR); return; } } clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEMS_SUCCESS); } /// Gets attached item /// @param sd : Who's getting /// @param mail_id : Mail ID that we're getting items from static void rodex_get_items(struct map_session_data *sd, int8 opentype, int64 mail_id) { nullpo_retv(sd); int weight = 0; int empty_slots = 0; struct rodex_message *msg = rodex->get_mail(sd, mail_id); if (msg == NULL) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FATAL_ERROR); return; } if (msg->items_count < 1) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FATAL_ERROR); return; } for (int i = 0; i < RODEX_MAX_ITEM; ++i) { if (msg->items[i].item.nameid != 0) { weight += itemdb->search(msg->items[i].item.nameid)->weight * msg->items[i].item.amount; } } if ((sd->weight + weight > sd->max_weight)) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FULL_ERROR); return; } int required_slots = msg->items_count; for (int i = 0; i < sd->status.inventorySize; ++i) { if (sd->status.inventory[i].nameid == 0) { empty_slots++; } else if (itemdb->isstackable(sd->status.inventory[i].nameid) == 1) { int j; ARR_FIND(0, msg->items_count, j, sd->status.inventory[i].nameid == msg->items[j].item.nameid); if (j < msg->items_count) { struct item_data *idata = itemdb->search(sd->status.inventory[i].nameid); if ((idata->stack.inventory && sd->status.inventory[i].amount + msg->items[j].item.amount > idata->stack.amount) || sd->status.inventory[i].amount + msg->items[j].item.amount > MAX_AMOUNT) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FULL_ERROR); return; } required_slots--; } } } if (empty_slots < required_slots) { clif->rodex_request_items(sd, opentype, mail_id, RODEX_GET_ITEM_FULL_ERROR); return; } msg->type &= ~MAIL_TYPE_ITEM; msg->items_count = 0; intif->rodex_updatemail(sd, mail_id, opentype, 2); } /// Cleans user's RoDEX related data /// - should be called everytime we're going to stop using rodex in this character /// @param sd : Target to clean /// @param flag : /// 0 - clear everything /// 1 - clear tmp only static void rodex_clean(struct map_session_data *sd, int8 flag) { nullpo_retv(sd); if (flag == 0) VECTOR_CLEAR(sd->rodex.messages); memset(&sd->rodex.tmp, 0x0, sizeof(sd->rodex.tmp)); } /// User request to open rodex, load mails from char-server /// @param sd : Who's requesting /// @param open_type : Box Type (see RODEX_OPENTYPE) static void rodex_open(struct map_session_data *sd, int8 open_type, int64 first_mail_id) { #if PACKETVER >= 20170419 const int type = 1; #else const int type = 0; #endif nullpo_retv(sd); if (open_type == RODEX_OPENTYPE_ACCOUNT && battle_config.feature_rodex_use_accountmail == false) open_type = RODEX_OPENTYPE_MAIL; intif->rodex_requestinbox(sd->status.char_id, sd->status.account_id, type, open_type, first_mail_id); } /// User request to read next page of mails /// @param sd : Who's requesting /// @param open_type : Box Type (see RODEX_OPENTYPE) /// @param last_mail_id : The last mail from the current page static void rodex_next_page(struct map_session_data *sd, int8 open_type, int64 last_mail_id) { int64 msg_count, page_start = 0; nullpo_retv(sd); if (open_type == RODEX_OPENTYPE_ACCOUNT && battle_config.feature_rodex_use_accountmail == false) { // Should not happen open_type = RODEX_OPENTYPE_MAIL; rodex->open(sd, open_type, 0); return; } msg_count = VECTOR_LENGTH(sd->rodex.messages); if (last_mail_id > 0) { // Find where the page starts ARR_FIND(0, msg_count, page_start, VECTOR_INDEX(sd->rodex.messages, page_start).id == last_mail_id); if (page_start > 0 && page_start < msg_count) { --page_start; // Valid page, get first item of next page } else { page_start = msg_count - 1; // Should not happen, invalid lower_id given } clif->rodex_send_maillist(sd->fd, sd, open_type, page_start); } } /// User's request to refresh his mail box /// @param sd : Who's requesting /// @param open_type : Box Type (See RODEX_OPENTYPE) /// @param first_mail_id : The first mail id known by client, currently unused static void rodex_refresh(struct map_session_data *sd, int8 open_type, int64 first_mail_id) { nullpo_retv(sd); if (open_type == RODEX_OPENTYPE_ACCOUNT && battle_config.feature_rodex_use_accountmail == false) open_type = RODEX_OPENTYPE_MAIL; // Some clients sends the first mail id it currently has and expects to receive // a list of newer mails, other clients sends first mail id as 0 and expects // to receive the first page (as if opening the box) if (first_mail_id > 0) { intif->rodex_requestinbox(sd->status.char_id, sd->status.account_id, 1, open_type, first_mail_id); } else { intif->rodex_requestinbox(sd->status.char_id, sd->status.account_id, 0, open_type, first_mail_id); } } static void do_init_rodex(bool minimal) { if (minimal) return; } static void do_final_rodex(void) { } void rodex_defaults(void) { rodex = &rodex_s; rodex->init = do_init_rodex; rodex->final = do_final_rodex; rodex->open = rodex_open; rodex->next_page = rodex_next_page; rodex->refresh = rodex_refresh; rodex->isenabled = rodex_isenabled; rodex->add_item = rodex_add_item; rodex->remove_item = rodex_remove_item; rodex->check_player = rodex_check_player; rodex->send_mail = rodex_send_mail; rodex->send_mail_result = rodex_send_mail_result; rodex->get_mail = rodex_get_mail; rodex->read_mail = rodex_read_mail; rodex->delete_mail = rodex_delete_mail; rodex->get_zeny = rodex_get_zeny; rodex->get_items = rodex_get_items; rodex->clean = rodex_clean; rodex->getZenyAck = rodex_getZenyAck; rodex->getItemsAck = rodex_getItemsAck; }