diff --git a/conf/inter_athena.conf b/conf/inter_athena.conf index 39032f0175..c2c89d1a33 100644 --- a/conf/inter_athena.conf +++ b/conf/inter_athena.conf @@ -127,6 +127,7 @@ clan_table: clan clan_alliance_table: clan_alliance // Map Database Tables +barter_table: barter buyingstore_table: buyingstores buyingstore_items_table: buyingstore_items item_table: item_db diff --git a/doc/script_commands.txt b/doc/script_commands.txt index 8a1fd3c84b..d85b9e2e0f 100644 --- a/doc/script_commands.txt +++ b/doc/script_commands.txt @@ -293,6 +293,8 @@ these floating NPC objects are for. More on that below. ,,,%TAB%marketshop%TAB%%TAB%,::{,::...} +Note: Additionally barter shops can be defined in npc/barters.yml + This will define a shop NPC, which, when triggered (which can only be done by clicking) will cause a shop window to come up. No code whatsoever runs in shop NPCs and you can't change the prices otherwise than by editing the script diff --git a/npc/barters.yml b/npc/barters.yml new file mode 100644 index 0000000000..66fd779908 --- /dev/null +++ b/npc/barters.yml @@ -0,0 +1,52 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program 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 . +# +########################################################################### +# Barter Database +########################################################################### +# +# Barter Settings +# +########################################################################### +# - Name NPC name. +# Map Map name. (Default: not on a map) +# X Map x coordinate. (Default: 0) +# Y Map y coordinate. (Default: 0) +# Direction Direction the NPC is looking. (Default: North) +# Sprite Sprite name of the NPC. (Default: FakeNpc) +# Items: List of sold items. +# - Index Index of the item inside the shop. (0-...) +# Maximum index depends on client. +# Item Aegis name of the item. +# Stock Amount of item in stock. 0 means unlimited. (Default: 0) +# Zeny Cost of them item in Zeny. (Default: 0) +# RequiredItems: List of required items (Optional) +# - Index Index of the required item. (0-4) +# Item Aegis name of required item. +# Amount Amount of required item. (Default: 1) +# Refine Refine level of required item. (Default: 0) +########################################################################### + +Header: + Type: BARTER_DB + Version: 1 + +Footer: + Imports: + - Path: npc/re/merchants/barters.yml + Mode: Renewal + - Path: npc/custom/barters.yml diff --git a/npc/custom/barters.yml b/npc/custom/barters.yml new file mode 100644 index 0000000000..cf74851141 --- /dev/null +++ b/npc/custom/barters.yml @@ -0,0 +1,46 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program 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 . +# +########################################################################### +# Barter Database +########################################################################### +# +# Barter Settings +# +########################################################################### +# - Name NPC name. +# Map Map name. (Default: not on a map) +# X Map x coordinate. (Default: 0) +# Y Map y coordinate. (Default: 0) +# Direction Direction the NPC is looking. (Default: North) +# Sprite Sprite name of the NPC. (Default: FakeNpc) +# Items: List of sold items. +# - Index Index of the item inside the shop. (0-...) +# Maximum index depends on client. +# Item Aegis name of the item. +# Stock Amount of item in stock. 0 means unlimited. (Default: 0) +# Zeny Cost of them item in Zeny. (Default: 0) +# RequiredItems: List of required items (Optional) +# - Index Index of the required item. (0-4) +# Item Aegis name of required item. +# Amount Amount of required item. (Default: 1) +# Refine Refine level of required item. (Default: 0) +########################################################################### + +Header: + Type: BARTER_DB + Version: 1 diff --git a/npc/re/merchants/barters.yml b/npc/re/merchants/barters.yml new file mode 100644 index 0000000000..cf74851141 --- /dev/null +++ b/npc/re/merchants/barters.yml @@ -0,0 +1,46 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program 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 . +# +########################################################################### +# Barter Database +########################################################################### +# +# Barter Settings +# +########################################################################### +# - Name NPC name. +# Map Map name. (Default: not on a map) +# X Map x coordinate. (Default: 0) +# Y Map y coordinate. (Default: 0) +# Direction Direction the NPC is looking. (Default: North) +# Sprite Sprite name of the NPC. (Default: FakeNpc) +# Items: List of sold items. +# - Index Index of the item inside the shop. (0-...) +# Maximum index depends on client. +# Item Aegis name of the item. +# Stock Amount of item in stock. 0 means unlimited. (Default: 0) +# Zeny Cost of them item in Zeny. (Default: 0) +# RequiredItems: List of required items (Optional) +# - Index Index of the required item. (0-4) +# Item Aegis name of required item. +# Amount Amount of required item. (Default: 1) +# Refine Refine level of required item. (Default: 0) +########################################################################### + +Header: + Type: BARTER_DB + Version: 1 diff --git a/sql-files/logs.sql b/sql-files/logs.sql index b62016af60..685ba16066 100644 --- a/sql-files/logs.sql +++ b/sql-files/logs.sql @@ -166,12 +166,13 @@ CREATE TABLE IF NOT EXISTS `npclog` ( # (Z) Merged Items # (Q)uest # Private Airs(H)ip +# Barter Shop (J) CREATE TABLE IF NOT EXISTS `picklog` ( `id` int(11) NOT NULL auto_increment, `time` datetime NOT NULL, `char_id` int(11) NOT NULL default '0', - `type` enum('M','P','L','T','V','S','N','C','A','R','G','E','B','O','I','X','D','U','$','F','Y','Z','Q','H') NOT NULL default 'P', + `type` enum('M','P','L','T','V','S','N','C','A','R','G','E','B','O','I','X','D','U','$','F','Y','Z','Q','H','J') NOT NULL default 'P', `nameid` int(10) unsigned NOT NULL default '0', `amount` int(11) NOT NULL default '1', `refine` tinyint(3) unsigned NOT NULL default '0', @@ -215,13 +216,14 @@ CREATE TABLE IF NOT EXISTS `picklog` ( # (E)Mail # (B)uying Store # Ban(K) Transactions +# Barter Shop (J) CREATE TABLE IF NOT EXISTS `zenylog` ( `id` int(11) NOT NULL auto_increment, `time` datetime NOT NULL, `char_id` int(11) NOT NULL default '0', `src_id` int(11) NOT NULL default '0', - `type` enum('T','V','P','M','S','N','D','C','A','E','I','B','K') NOT NULL default 'S', + `type` enum('T','V','P','M','S','N','D','C','A','E','I','B','K','J') NOT NULL default 'S', `amount` int(11) NOT NULL default '0', `map` varchar(11) NOT NULL default '', PRIMARY KEY (`id`), diff --git a/sql-files/main.sql b/sql-files/main.sql index 866e721131..e94ffb39d6 100644 --- a/sql-files/main.sql +++ b/sql-files/main.sql @@ -90,6 +90,17 @@ CREATE TABLE IF NOT EXISTS `auction` ( PRIMARY KEY (`auction_id`) ) ENGINE=MyISAM; +-- +-- Table `barter` for barter shop persistency +-- + +CREATE TABLE IF NOT EXISTS `barter` ( + `name` varchar(50) NOT NULL DEFAULT '', + `index` SMALLINT(5) UNSIGNED NOT NULL, + `amount` SMALLINT(5) UNSIGNED NOT NULL, + PRIMARY KEY (`name`,`index`) +) ENGINE=MyISAM; + -- -- Table structure for `db_roulette` -- diff --git a/sql-files/upgrades/upgrade_20220121.sql b/sql-files/upgrades/upgrade_20220121.sql new file mode 100644 index 0000000000..ed31ca4c78 --- /dev/null +++ b/sql-files/upgrades/upgrade_20220121.sql @@ -0,0 +1,10 @@ +-- +-- Table `barter` for barter shop persistency +-- + +CREATE TABLE IF NOT EXISTS `barter` ( + `name` varchar(50) NOT NULL DEFAULT '', + `index` SMALLINT(5) UNSIGNED NOT NULL, + `amount` SMALLINT(5) UNSIGNED NOT NULL, + PRIMARY KEY (`name`,`index`) +) ENGINE=MyISAM; diff --git a/sql-files/upgrades/upgrade_20220121_logs.sql b/sql-files/upgrades/upgrade_20220121_logs.sql new file mode 100644 index 0000000000..2ba388b8b5 --- /dev/null +++ b/sql-files/upgrades/upgrade_20220121_logs.sql @@ -0,0 +1,7 @@ +ALTER TABLE `picklog` + MODIFY `type` enum('M','P','L','T','V','S','N','C','A','R','G','E','B','O','I','X','D','U','$','F','Y','Z','Q','H','J') NOT NULL default 'P' +; + +ALTER TABLE `zenylog` + MODIFY `type` enum('T','V','P','M','S','N','D','C','A','E','I','B','K','J') NOT NULL default 'S' +; diff --git a/src/common/mmo.hpp b/src/common/mmo.hpp index 92fcfb8f5f..5a2414dc29 100644 --- a/src/common/mmo.hpp +++ b/src/common/mmo.hpp @@ -107,6 +107,9 @@ typedef uint32 t_itemid; #define DB_NAME_LEN 256 //max len of dbs #define MAX_CLAN 500 #define MAX_CLANALLIANCE 6 +#ifndef MAX_BARTER_REQUIREMENTS + #define MAX_BARTER_REQUIREMENTS 5 +#endif #ifdef RENEWAL #define MAX_WEAPON_LEVEL 5 diff --git a/src/map/clif.cpp b/src/map/clif.cpp index f864f8c1e5..6b486cfb16 100644 --- a/src/map/clif.cpp +++ b/src/map/clif.cpp @@ -2312,7 +2312,7 @@ void clif_parse_NPCMarketClosed(int fd, struct map_session_data *sd) { /// Purchase item from Market shop. /// 0x9d7 .W .B { .W .W .L }* (ZC_NPC_MARKET_PURCHASE_RESULT) -void clif_npc_market_purchase_ack(struct map_session_data *sd, uint8 res, uint8 n, struct s_npc_buy_list *list) { +void clif_npc_market_purchase_ack(struct map_session_data *sd, e_purchase_result res, uint8 n, struct s_npc_buy_list *list) { #if PACKETVER >= 20131223 nullpo_retv( sd ); nullpo_retv( list ); @@ -2328,9 +2328,9 @@ void clif_npc_market_purchase_ack(struct map_session_data *sd, uint8 res, uint8 p->PacketType = HEADER_ZC_NPC_MARKET_PURCHASE_RESULT; #if PACKETVER_MAIN_NUM >= 20190807 || PACKETVER_RE_NUM >= 20190807 || PACKETVER_ZERO_NUM >= 20190814 - p->result = ( res == 0 ? 0 : -1 ); + p->result = ( res == e_purchase_result::PURCHASE_SUCCEED ? 0 : -1 ); #else - p->result = ( res == 0 ? 1 : 0 ); + p->result = ( res == e_purchase_result::PURCHASE_SUCCEED ? 1 : 0 ); #endif int count = 0; @@ -2381,7 +2381,7 @@ void clif_parse_NPCMarketPurchase(int fd, struct map_session_data *sd) { list[i].qty = p->list[i].qty; } - uint8 res = npc_buylist( sd, count, list ); + e_purchase_result res = npc_buylist( sd, count, list ); clif_npc_market_purchase_ack( sd, res, count, list ); aFree( list ); @@ -12299,14 +12299,13 @@ void clif_parse_NpcBuySellSelected(int fd,struct map_session_data *sd) /// 12 = "The exchange was well done." /// 13 = "The item is already sold and out of stock." /// 14 = "There is not enough goods to exchange." -void clif_npc_buy_result(struct map_session_data* sd, unsigned char result) -{ - int fd = sd->fd; +void clif_npc_buy_result( struct map_session_data* sd, e_purchase_result result ){ + struct PACKET_ZC_PC_PURCHASE_RESULT p = {}; - WFIFOHEAD(fd,packet_len(0xca)); - WFIFOW(fd,0) = 0xca; - WFIFOB(fd,2) = result; - WFIFOSET(fd,packet_len(0xca)); + p.packetType = HEADER_ZC_PC_PURCHASE_RESULT; + p.result = (uint8)result; + + clif_send( &p, sizeof( p ), &sd->bl, SELF ); } @@ -12316,10 +12315,10 @@ void clif_parse_NpcBuyListSend( int fd, struct map_session_data* sd ){ const struct PACKET_CZ_PC_PURCHASE_ITEMLIST *p = (struct PACKET_CZ_PC_PURCHASE_ITEMLIST *)RFIFOP( fd, 0 ); uint16 n = ( p->packetLength - sizeof(struct PACKET_CZ_PC_PURCHASE_ITEMLIST) ) / sizeof( struct PACKET_CZ_PC_PURCHASE_ITEMLIST_sub ); - int result; + e_purchase_result result; if( sd->state.trading || !sd->npc_shopid ) - result = 1; + result = e_purchase_result::PURCHASE_FAIL_MONEY; else result = npc_buylist( sd, n, (struct s_npc_buy_list*)p->items ); @@ -22277,7 +22276,7 @@ void clif_parse_refineui_refine( int fd, struct map_session_data* sd ){ // Try to pay for the refine if( pc_payzeny( sd, cost->zeny, LOG_TYPE_CONSUME, NULL ) ){ - clif_npc_buy_result( sd, 1 ); // "You do not have enough zeny." + clif_npc_buy_result( sd, e_purchase_result::PURCHASE_FAIL_MONEY ); // "You do not have enough zeny." return; } @@ -22704,6 +22703,315 @@ void clif_parse_inventory_expansion_reject( int fd, struct map_session_data* sd #endif } +void clif_barter_open( struct map_session_data& sd, struct npc_data& nd ){ +#if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 + if( nd.subtype != NPCTYPE_BARTER || nd.u.barter.extended || sd.state.barter_open ){ + return; + } + + std::shared_ptr barter = barter_db.find( nd.exname ); + + if( barter == nullptr ){ + return; + } + + sd.state.barter_open = true; + + struct PACKET_ZC_NPC_BARTER_OPEN* p = (struct PACKET_ZC_NPC_BARTER_OPEN*)packet_buffer; + + p->packetType = HEADER_ZC_NPC_BARTER_OPEN; + p->packetLength = (int16)sizeof( struct PACKET_ZC_NPC_BARTER_OPEN ); + + int16 count = 0; + for( const auto& itemPair : barter->items ){ + struct PACKET_ZC_NPC_BARTER_OPEN_sub* item = &p->list[count]; + struct item_data* id = itemdb_exists( itemPair.second->nameid ); + + item->nameid = client_nameid( id->nameid ); + item->type = itemtype( id->nameid ); + if( itemPair.second->stockLimited ){ + item->amount = itemPair.second->stock; + }else{ + item->amount = -1; + } + item->weight = id->weight; + item->index = itemPair.second->index; +#if PACKETVER_MAIN_NUM >= 20210203 || PACKETVER_RE_NUM >= 20211103 + item->viewSprite = id->look; + item->location = pc_equippoint_sub( &sd, id ); +#endif + + // Use a loop if someone did not start with index 0 + for( const auto& requirementPair : itemPair.second->requirements ){ + std::shared_ptr requirement = requirementPair.second; + + item->currencyNameid = client_nameid( requirement->nameid ); + item->currencyAmount = requirement->amount; + + // It is a normal barter, cancel after first entry + break; + } + + p->packetLength += (int16)( sizeof( *item ) ); + count++; + } + + clif_send( p, p->packetLength, &sd.bl, SELF ); +#endif +} + +void clif_parse_barter_close( int fd, struct map_session_data* sd ){ +#if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 + if( sd->state.barter_open ){ + sd->npc_shopid = 0; + sd->state.barter_open = false; + } +#endif +} + +void clif_parse_barter_buy( int fd, struct map_session_data* sd ){ +#if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 + // No shop open + if( sd->npc_shopid == 0 || !sd->state.barter_open ){ + return; + } + + struct npc_data* nd = map_id2nd( sd->npc_shopid ); + + // Unknown shop + if( nd == nullptr ){ + return; + } + + // Not a barter + if( nd->subtype != NPCTYPE_BARTER ){ + return; + } + + // It is an extended barter + if( nd->u.barter.extended ){ + return; + } + + std::shared_ptr barter = barter_db.find( nd->exname ); + + if( barter == nullptr ){ + return; + } + + struct PACKET_CZ_NPC_BARTER_PURCHASE* p = (struct PACKET_CZ_NPC_BARTER_PURCHASE*)RFIFOP( fd, 0 ); + + uint16 entries = ( p->packetLength - sizeof( struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE ) ) / sizeof( struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE_sub ); + + // Empty purchase list + if( entries == 0 ){ + return; + } + + std::vector purchases; + + purchases.reserve( entries ); + + // Make sure each shop index and target item id is only used once + for( int i = 0; i < entries; i++ ){ + std::shared_ptr item = util::map_find( barter->items, (uint16)p->list[i].shopIndex ); + + // Invalid shop index + if( item == nullptr ){ + return; + } + + for( int j = i + 1; j < entries; j++ ){ + // Same shop index + if( p->list[i].shopIndex == p->list[j].shopIndex ){ + return; + } + + std::shared_ptr item2 = util::map_find( barter->items, (uint16)p->list[j].shopIndex ); + + // Invalid shop index + if( item2 == nullptr ){ + return; + } + + // Same target item id + if( item->nameid == item2->nameid ){ + return; + } + } + + s_barter_purchase purchase = {}; + + purchase.item = item; + purchase.amount = p->list[i].amount; + + purchases.push_back( purchase ); + } + + clif_npc_buy_result( sd, npc_barter_purchase( *sd, barter, purchases ) ); +#endif +} + +void clif_barter_extended_open( struct map_session_data& sd, struct npc_data& nd ){ +#if PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191106 || PACKETVER_ZERO_NUM >= 20191127 + if( nd.subtype != NPCTYPE_BARTER || !nd.u.barter.extended || sd.state.barter_extended_open ){ + return; + } + + std::shared_ptr barter = barter_db.find( nd.exname ); + + if( barter == nullptr ){ + return; + } + + sd.state.barter_extended_open = true; + + struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN* p = (struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN*)packet_buffer; + + p->packetType = HEADER_ZC_NPC_EXPANDED_BARTER_OPEN; + p->packetLength = (int16)sizeof( struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN ); + p->items_count = 0; + + for( const auto& itemPair : barter->items ){ + // Needs dynamic calculation, because of variable currencies + struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN_sub* item = (struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN_sub*)( ( (uint8*)p ) + p->packetLength ); + struct item_data* id = itemdb_exists( itemPair.second->nameid ); + + item->nameid = client_nameid( id->nameid ); + item->type = itemtype( id->nameid ); + if( itemPair.second->stockLimited ){ + item->amount = itemPair.second->stock; + }else{ + item->amount = -1; + } + item->weight = id->weight; + item->index = itemPair.second->index; + item->zeny = itemPair.second->price; +#if PACKETVER_MAIN_NUM >= 20210203 || PACKETVER_RE_NUM >= 20211103 + item->viewSprite = id->look; + item->location = pc_equippoint_sub( &sd, id ); +#endif + + p->packetLength += (int16)( sizeof( *item ) - sizeof( item->currencies ) ); + p->items_count++; + + item->currency_count = 0; + + for( const auto& requirementPair : itemPair.second->requirements ){ + // Needs dynamic calculation, because of variable currencies + struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN_sub2* req = (struct PACKET_ZC_NPC_EXPANDED_BARTER_OPEN_sub2*)( ( (uint8*)p ) + p->packetLength ); + std::shared_ptr requirement = requirementPair.second; + + req->nameid = requirement->nameid; + if( requirement->refine >= 0 ){ + req->refine_level = requirement->refine; + }else{ + req->refine_level = 0; + } + req->amount = requirement->amount; + req->type = itemtype( requirement->nameid ); + + p->packetLength += (int16)( sizeof( *req ) ); + item->currency_count++; + } + } + + clif_send( p, p->packetLength, &sd.bl, SELF ); +#endif +} + +void clif_parse_barter_extended_close( int fd, struct map_session_data* sd ){ +#if PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191106 || PACKETVER_ZERO_NUM >= 20191127 + if( sd->state.barter_extended_open ){ + sd->npc_shopid = 0; + sd->state.barter_extended_open = false; + } +#endif +} + +void clif_parse_barter_extended_buy( int fd, struct map_session_data* sd ){ +#if PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191106 || PACKETVER_ZERO_NUM >= 20191127 + // No shop open + if( sd->npc_shopid == 0 || !sd->state.barter_extended_open ){ + return; + } + + struct npc_data* nd = map_id2nd( sd->npc_shopid ); + + // Unknown shop + if( nd == nullptr ){ + return; + } + + // Not a barter + if( nd->subtype != NPCTYPE_BARTER ){ + return; + } + + // Not an extended barter + if( !nd->u.barter.extended ){ + return; + } + + std::shared_ptr barter = barter_db.find( nd->exname ); + + if( barter == nullptr ){ + return; + } + + struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE* p = (struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE*)RFIFOP( fd, 0 ); + + uint16 entries = ( p->packetLength - sizeof( struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE ) ) / sizeof( struct PACKET_CZ_NPC_EXPANDED_BARTER_PURCHASE_sub ); + + // Empty purchase list + if( entries == 0 ){ + return; + } + + std::vector purchases; + + purchases.reserve( entries ); + + // Make sure each shop index and target item id is only used once + for( int i = 0; i < entries; i++ ){ + std::shared_ptr item = util::map_find( barter->items, (uint16)p->list[i].shopIndex ); + + // Invalid shop index + if( item == nullptr ){ + return; + } + + for( int j = i + 1; j < entries; j++ ){ + // Same shop index + if( p->list[i].shopIndex == p->list[j].shopIndex ){ + return; + } + + std::shared_ptr item2 = util::map_find( barter->items, (uint16)p->list[j].shopIndex ); + + // Invalid shop index + if( item2 == nullptr ){ + return; + } + + // Same target item id + if( item->nameid == item2->nameid ){ + return; + } + } + + s_barter_purchase purchase = {}; + + purchase.item = item; + purchase.amount = p->list[i].amount; + + purchases.push_back( purchase ); + } + + clif_npc_buy_result( sd, npc_barter_purchase( *sd, barter, purchases ) ); +#endif +} + /*========================================== * Main client packet processing function *------------------------------------------*/ diff --git a/src/map/clif.hpp b/src/map/clif.hpp index 3fd7996825..b8c364fab0 100644 --- a/src/map/clif.hpp +++ b/src/map/clif.hpp @@ -186,6 +186,27 @@ enum e_bossmap_info { BOSS_INFO_DEAD, }; +enum class e_purchase_result : uint8{ + PURCHASE_SUCCEED = 0x0, + PURCHASE_FAIL_MONEY, + PURCHASE_FAIL_WEIGHT, + PURCHASE_FAIL_COUNT, + PURCHASE_FAIL_STOCK, + PURCHASE_FAIL_ITEM_EXCHANGING, + PURCHASE_FAIL_INVALID_MCSTORE, + PURCHASE_FAIL_OPEN_MCSTORE_ITEMLIST, + PURCHASE_FAIL_GIVE_MONEY, + PURCHASE_FAIL_EACHITEM_COUNT, + // Unknown names + PURCHASE_FAIL_RODEX, + PURCHASE_FAIL_EXCHANGE_FAILED, + PURCHASE_FAIL_EXCHANGE_DONE, + PURCHASE_FAIL_STOCK_EMPTY, + PURCHASE_FAIL_GOODS, + // End unknown names + PURCHASE_FAIL_ADD = 0xff, +}; + #define packet_len(cmd) packet_db[cmd].len extern struct s_packet_db packet_db[MAX_PACKET_DB+1]; extern int packet_db_ack[MAX_ACK_FUNC + 1]; @@ -1161,4 +1182,8 @@ void clif_parse_skill_toid( struct map_session_data* sd, uint16 skill_id, uint16 void clif_inventory_expansion_info( struct map_session_data* sd ); +// Barter System +void clif_barter_open( struct map_session_data& sd, struct npc_data& nd ); +void clif_barter_extended_open( struct map_session_data& sd, struct npc_data& nd ); + #endif /* CLIF_HPP */ diff --git a/src/map/clif_packetdb.hpp b/src/map/clif_packetdb.hpp index fbc8ed89ef..9faefe7d81 100644 --- a/src/map/clif_packetdb.hpp +++ b/src/map/clif_packetdb.hpp @@ -2407,6 +2407,11 @@ parseable_packet( HEADER_CZ_INVENTORY_EXPAND_REJECTED, sizeof( struct PACKET_CZ_INVENTORY_EXPAND_REJECTED ), clif_parse_inventory_expansion_reject, 0 ); #endif +#if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 + parseable_packet( HEADER_CZ_NPC_BARTER_PURCHASE, -1, clif_parse_barter_buy, 0 ); + parseable_packet( HEADER_CZ_NPC_BARTER_CLOSE, sizeof( struct PACKET_CZ_NPC_BARTER_CLOSE ), clif_parse_barter_close, 0 ); +#endif + #if PACKETVER_MAIN_NUM >= 20190227 || PACKETVER_RE_NUM >= 20190220 || PACKETVER_ZERO_NUM >= 20190220 parseable_packet( 0x0B1C, sizeof( struct PACKET_CZ_PING ), clif_parse_dull, 0 ); #endif @@ -2429,6 +2434,11 @@ parseable_packet( 0x0b4c, 2, clif_parse_dull, 0 ); #endif +#if PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191106 || PACKETVER_ZERO_NUM >= 20191127 + parseable_packet( HEADER_CZ_NPC_EXPANDED_BARTER_PURCHASE, -1, clif_parse_barter_extended_buy, 0 ); + parseable_packet( HEADER_CZ_NPC_EXPANDED_BARTER_CLOSE, sizeof( struct PACKET_CZ_NPC_EXPANDED_BARTER_CLOSE ), clif_parse_barter_extended_close, 0 ); +#endif + #if PACKETVER >= 20191224 parseable_packet( HEADER_CZ_SE_CASHSHOP_OPEN2, sizeof( struct PACKET_CZ_SE_CASHSHOP_OPEN2 ), clif_parse_cashshop_open_request, 0 ); parseable_packet( HEADER_CZ_REQ_ITEMREPAIR2, sizeof( struct PACKET_CZ_REQ_ITEMREPAIR2 ), clif_parse_RepairItem, 0 ); diff --git a/src/map/log.cpp b/src/map/log.cpp index 8c0ba2c8f7..5cf26d6deb 100644 --- a/src/map/log.cpp +++ b/src/map/log.cpp @@ -85,6 +85,7 @@ static char log_picktype2char(e_log_pick_type type) case LOG_TYPE_MERGE_ITEM: return 'Z'; // Merged Item case LOG_TYPE_QUEST: return 'Q'; // (Q)uest Item case LOG_TYPE_PRIVATE_AIRSHIP: return 'H'; // Private Airs(H)ip + case LOG_TYPE_BARTER: return 'J'; // Barter Shop } // should not get here, fallback diff --git a/src/map/log.hpp b/src/map/log.hpp index 86d262c05f..dc50bb2a06 100644 --- a/src/map/log.hpp +++ b/src/map/log.hpp @@ -26,35 +26,36 @@ enum e_log_chat_type : uint8 enum e_log_pick_type : uint32 { - LOG_TYPE_NONE = 0, - LOG_TYPE_TRADE = 0x000001, - LOG_TYPE_VENDING = 0x000002, - LOG_TYPE_PICKDROP_PLAYER = 0x000004, - LOG_TYPE_PICKDROP_MONSTER = 0x000008, - LOG_TYPE_NPC = 0x000010, - LOG_TYPE_SCRIPT = 0x000020, - LOG_TYPE_STEAL = 0x000040, - LOG_TYPE_CONSUME = 0x000080, - LOG_TYPE_PRODUCE = 0x000100, - LOG_TYPE_MVP = 0x000200, - LOG_TYPE_COMMAND = 0x000400, - LOG_TYPE_STORAGE = 0x000800, - LOG_TYPE_GSTORAGE = 0x001000, - LOG_TYPE_MAIL = 0x002000, - LOG_TYPE_AUCTION = 0x004000, - LOG_TYPE_BUYING_STORE = 0x008000, - LOG_TYPE_OTHER = 0x010000, - LOG_TYPE_CASH = 0x020000, - LOG_TYPE_BANK = 0x040000, - LOG_TYPE_BOUND_REMOVAL = 0x080000, - LOG_TYPE_ROULETTE = 0x100000, - LOG_TYPE_MERGE_ITEM = 0x200000, - LOG_TYPE_QUEST = 0x400000, - LOG_TYPE_PRIVATE_AIRSHIP = 0x800000, + LOG_TYPE_NONE = 0x0000000, + LOG_TYPE_TRADE = 0x0000001, + LOG_TYPE_VENDING = 0x0000002, + LOG_TYPE_PICKDROP_PLAYER = 0x0000004, + LOG_TYPE_PICKDROP_MONSTER = 0x0000008, + LOG_TYPE_NPC = 0x0000010, + LOG_TYPE_SCRIPT = 0x0000020, + LOG_TYPE_STEAL = 0x0000040, + LOG_TYPE_CONSUME = 0x0000080, + LOG_TYPE_PRODUCE = 0x0000100, + LOG_TYPE_MVP = 0x0000200, + LOG_TYPE_COMMAND = 0x0000400, + LOG_TYPE_STORAGE = 0x0000800, + LOG_TYPE_GSTORAGE = 0x0001000, + LOG_TYPE_MAIL = 0x0002000, + LOG_TYPE_AUCTION = 0x0004000, + LOG_TYPE_BUYING_STORE = 0x0008000, + LOG_TYPE_OTHER = 0x0010000, + LOG_TYPE_CASH = 0x0020000, + LOG_TYPE_BANK = 0x0040000, + LOG_TYPE_BOUND_REMOVAL = 0x0080000, + LOG_TYPE_ROULETTE = 0x0100000, + LOG_TYPE_MERGE_ITEM = 0x0200000, + LOG_TYPE_QUEST = 0x0400000, + LOG_TYPE_PRIVATE_AIRSHIP = 0x0800000, + LOG_TYPE_BARTER = 0x1000000, // combinations LOG_TYPE_LOOT = LOG_TYPE_PICKDROP_MONSTER|LOG_TYPE_CONSUME, // all - LOG_TYPE_ALL = 0xFFFFFF, + LOG_TYPE_ALL = 0xFFFFFFF, }; enum e_log_cash_type : uint8 diff --git a/src/map/map.cpp b/src/map/map.cpp index 735bfc6788..6fdc226f1e 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -63,6 +63,7 @@ Sql* mmysql_handle; Sql* qsmysql_handle; /// For query_sql int db_use_sqldbs = 0; +char barter_table[32] = "barter"; char buyingstores_table[32] = "buyingstores"; char buyingstore_items_table[32] = "buyingstore_items"; char item_cash_table[32] = "item_cash_db"; @@ -4186,7 +4187,9 @@ int inter_config_read(const char *cfgName) } #undef RENEWALPREFIX - if( strcmpi( w1, "buyingstore_db" ) == 0 ) + if( strcmpi( w1, "barter_table" ) == 0 ) + safestrncpy( barter_table, w2, sizeof(barter_table) ); + else if( strcmpi( w1, "buyingstore_db" ) == 0 ) safestrncpy( buyingstores_table, w2, sizeof(buyingstores_table) ); else if( strcmpi( w1, "buyingstore_items_table" ) == 0 ) safestrncpy( buyingstore_items_table, w2, sizeof(buyingstore_items_table) ); diff --git a/src/map/map.hpp b/src/map/map.hpp index 0712741093..97f1ee7523 100644 --- a/src/map/map.hpp +++ b/src/map/map.hpp @@ -295,6 +295,7 @@ enum npc_subtype : uint8{ NPCTYPE_POINTSHOP, /// Pointshop NPCTYPE_TOMB, /// Monster tomb NPCTYPE_MARKETSHOP, /// Marketshop + NPCTYPE_BARTER, /// Barter }; enum e_race : int8{ @@ -1220,6 +1221,7 @@ extern Sql* mmysql_handle; extern Sql* qsmysql_handle; extern Sql* logmysql_handle; +extern char barter_table[32]; extern char buyingstores_table[32]; extern char buyingstore_items_table[32]; extern char item_table[32]; diff --git a/src/map/npc.cpp b/src/map/npc.cpp index be55c72528..403bcd709c 100644 --- a/src/map/npc.cpp +++ b/src/map/npc.cpp @@ -121,6 +121,10 @@ struct script_event_s{ // Holds pointers to the commonly executed scripts for speedup. [Skotlex] std::map> script_event; +// Static functions +static struct npc_data* npc_create_npc( int16 m, int16 x, int16 y ); +static void npc_parsename( struct npc_data* nd, const char* name, const char* start, const char* buffer, const char* filepath ); + const std::string StylistDatabase::getDefaultLocation(){ return std::string(db_path) + "/stylist.yml"; } @@ -386,6 +390,451 @@ uint64 StylistDatabase::parseBodyNode( const YAML::Node &node ){ StylistDatabase stylist_db; +const std::string BarterDatabase::getDefaultLocation(){ + return "npc/barters.yml"; +} + +uint64 BarterDatabase::parseBodyNode( const YAML::Node& node ){ + std::string npcname; + + if( !this->asString( node, "Name", npcname ) ){ + return 0; + } + + std::shared_ptr barter = this->find( npcname ); + bool exists = barter != nullptr; + + if( !exists ){ + barter = std::make_shared(); + barter->name = npcname; + } + + if( this->nodeExists( node, "Map" ) ){ + std::string map; + + if( !this->asString( node, "Map", map ) ){ + return 0; + } + + uint16 index = mapindex_name2idx( map.c_str(), nullptr ); + + if( index == 0 ){ + this->invalidWarning( node["Map"], "barter_parseBodyNode: Unknown mapname %s, skipping.\n", map.c_str()); + return 0; + } + + barter->m = map_mapindex2mapid( index ); + + // Skip silently if the map is not on this map-server + if( barter->m < 0 ){ + return 1; + } + }else{ + if( !exists ){ + barter->m = -1; + } + } + + struct map_data* mapdata = nullptr; + + if( barter->m >= 0 ){ + mapdata = map_getmapdata( barter->m ); + } + + if( this->nodeExists( node, "X" ) ){ + uint16 x; + + if( !this->asUInt16( node, "X", x ) ){ + return 0; + } + + if( mapdata == nullptr ){ + this->invalidWarning( node["X"], "barter_parseBodyNode: Barter NPC is not on a map. Ignoring X coordinate...\n" ); + x = 0; + }else if( x >= mapdata->xs ){ + this->invalidWarning( node["X"], "barter_parseBodyNode: X coordinate %hu is out of bound %hu...\n", x, mapdata->xs ); + return 0; + } + + barter->x = x; + }else{ + if( !exists ){ + barter->x = 0; + } + } + + if( this->nodeExists( node, "Y" ) ){ + uint16 y; + + if( !this->asUInt16( node, "Y", y ) ){ + return 0; + } + + if( mapdata == nullptr ){ + this->invalidWarning( node["Y"], "barter_parseBodyNode: Barter NPC is not on a map. Ignoring Y coordinate...\n" ); + y = 0; + }else if( y >= mapdata->ys ){ + this->invalidWarning( node["Y"], "barter_parseBodyNode: Y coordinate %hu is out of bound %hu...\n", y, mapdata->ys ); + return 0; + } + + barter->y = y; + }else{ + if( !exists ){ + barter->y = 0; + } + } + + if( this->nodeExists( node, "Direction" ) ){ + std::string direction_name; + + if( !this->asString( node, "Direction", direction_name ) ){ + return 0; + } + + int64 constant; + + if( !script_get_constant( ( "DIR_" + direction_name ).c_str(), &constant ) ){ + this->invalidWarning( node["Direction"], "barter_parseBodyNode: Unknown direction %s, skipping.\n", direction_name.c_str() ); + return 0; + } + + if( constant < DIR_NORTH || constant >= DIR_MAX ){ + this->invalidWarning( node["Direction"], "barter_parseBodyNode: Invalid direction %s, defaulting to North.\n", direction_name.c_str() ); + constant = DIR_NORTH; + } + + barter->dir = (uint8)constant; + }else{ + if( !exists ){ + barter->dir = (uint8)DIR_NORTH; + } + } + + if( this->nodeExists( node, "Sprite" ) ){ + std::string sprite_name; + + if( !this->asString( node, "Sprite", sprite_name ) ){ + return 0; + } + + int64 constant; + + if( !script_get_constant( sprite_name.c_str(), &constant ) ){ + this->invalidWarning( node["Sprite"], "barter_parseBodyNode: Unknown sprite name %s, skipping.\n", sprite_name.c_str()); + return 0; + } + + if( constant != JT_FAKENPC && !npcdb_checkid( constant ) ){ + this->invalidWarning( node["Sprite"], "barter_parseBodyNode: Invalid sprite name %s, skipping.\n", sprite_name.c_str()); + return 0; + } + + barter->sprite = (int16)constant; + }else{ + if( !exists ){ + barter->sprite = JT_FAKENPC; + } + } + + if( this->nodeExists( node, "Items" ) ){ + for( const YAML::Node& itemNode : node["Items"] ){ + uint16 index; + + if( !this->asUInt16( itemNode, "Index", index ) ){ + return 0; + } + + std::shared_ptr item = util::map_find( barter->items, index ); + bool item_exists = item != nullptr; + + if( !item_exists ){ + if( !this->nodesExist( itemNode, { "Item" } ) ){ + return 0; + } + + item = std::make_shared(); + item->index = index; + } + + if( this->nodeExists( itemNode, "Item" ) ){ + std::string aegis_name; + + if( !this->asString( itemNode, "Item", aegis_name ) ){ + return 0; + } + + std::shared_ptr id = item_db.search_aegisname( aegis_name.c_str() ); + + if( id == nullptr ){ + this->invalidWarning( itemNode["Item"], "barter_parseBodyNode: Unknown item %s.\n", aegis_name.c_str() ); + return 0; + } + + item->nameid = id->nameid; + } + + if( this->nodeExists( itemNode, "Stock" ) ){ + uint32 stock; + + if( !this->asUInt32( itemNode, "Stock", stock ) ){ + return 0; + } + + item->stock = stock; + item->stockLimited = ( stock > 0 ); + }else{ + if( !item_exists ){ + item->stock = 0; + item->stockLimited = false; + } + } + + if( this->nodeExists( itemNode, "Zeny" ) ){ + uint32 zeny; + + if( !this->asUInt32( itemNode, "Zeny", zeny ) ){ + return 0; + } + + if( zeny > MAX_ZENY ){ + this->invalidWarning( itemNode["Zeny"], "barter_parseBodyNode: Zeny price %u is above MAX_ZENY (%u), capping...\n", zeny, MAX_ZENY ); + zeny = MAX_ZENY; + } + + item->price = zeny; + }else{ + if( !item_exists ){ + item->price = 0; + } + } + + if( this->nodeExists( itemNode, "RequiredItems" ) ){ + for( const YAML::Node& requiredItemNode : itemNode["RequiredItems"] ){ + uint16 requirement_index; + + if( !this->asUInt16( requiredItemNode, "Index", requirement_index ) ){ + return 0; + } + + if( requirement_index >= MAX_BARTER_REQUIREMENTS ){ + this->invalidWarning( requiredItemNode["Index"], "barter_parseBodyNode: Index %hu is out of bounds. Barters support up to %d requirements.\n", requirement_index, MAX_BARTER_REQUIREMENTS ); + return 0; + } + + std::shared_ptr requirement = util::map_find( item->requirements, requirement_index ); + bool requirement_exists = requirement != nullptr; + + if( !requirement_exists ){ + if( !this->nodesExist( requiredItemNode, { "Item" } ) ){ + return 0; + } + + requirement = std::make_shared(); + requirement->index = requirement_index; + } + + if( this->nodeExists( requiredItemNode, "Item" ) ){ + std::string aegis_name; + + if( !this->asString( requiredItemNode, "Item", aegis_name ) ){ + return 0; + } + + std::shared_ptr data = item_db.search_aegisname( aegis_name.c_str() ); + + if( data == nullptr ){ + this->invalidWarning( requiredItemNode["Item"], "barter_parseBodyNode: Unknown required item %s.\n", aegis_name.c_str() ); + return 0; + } + + requirement->nameid = data->nameid; + } + + if( this->nodeExists( requiredItemNode, "Amount" ) ){ + uint16 amount; + + if( !this->asUInt16( requiredItemNode, "Amount", amount ) ){ + return 0; + } + + if( amount > MAX_AMOUNT ){ + this->invalidWarning( requiredItemNode["Amount"], "barter_parseBodyNode: Amount %hu is too high, capping to %hu...\n", amount, MAX_AMOUNT ); + amount = MAX_AMOUNT; + } + + requirement->amount = amount; + }else{ + if( !requirement_exists ){ + requirement->amount = 1; + } + } + + if( this->nodeExists( requiredItemNode, "Refine" ) ){ + std::shared_ptr data = item_db.find( requirement->nameid ); + + if( data->flag.no_refine ){ + this->invalidWarning( requiredItemNode["Refine"], "barter_parseBodyNode: Item %s is not refineable.\n", data->name.c_str() ); + return 0; + } + + int16 refine; + + if( !this->asInt16( requiredItemNode, "Refine", refine ) ){ + return 0; + } + + if( refine > MAX_REFINE ){ + this->invalidWarning( requiredItemNode["Amount"], "barter_parseBodyNode: Refine %hd is too high, capping to %d.\n", refine, MAX_REFINE ); + refine = MAX_REFINE; + } + + requirement->refine = (int8)refine; + }else{ + if( !requirement_exists ){ + requirement->refine = -1; + } + } + + if( !requirement_exists ){ + item->requirements[requirement->index] = requirement; + } + } + } + + if( !item_exists ){ + barter->items[index] = item; + } + } + } + + if( !exists ){ + this->put( npcname, barter ); + } + + return 1; +} + +void BarterDatabase::loadingFinished(){ + for( const auto& pair : *this ){ +#if !( PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 ) + ShowError( "Barter system is not supported by your packet version.\n" ); + return; +#endif + + std::shared_ptr barter = pair.second; + + struct npc_data* nd = npc_create_npc( barter->m, barter->x, barter->y ); + + npc_parsename( nd, barter->name.c_str(), nullptr, nullptr, __FILE__ ":" QUOTE(__LINE__) ); + + nd->class_ = barter->sprite; + nd->speed = 200; + + nd->bl.type = BL_NPC; + nd->subtype = NPCTYPE_BARTER; + + nd->u.barter.extended = false; + + // Check if it has to use the extended barter feature or not + for( const auto& itemPair : barter->items ){ + // Normal barter cannot have zeny requirements + if( itemPair.second->price > 0 ){ + nd->u.barter.extended = true; + break; + } + + // Normal barter needs to have exchange items defined + if( itemPair.second->requirements.empty() ){ + nd->u.barter.extended = true; + break; + } + + // Normal barter can only exchange 1:1 + if( itemPair.second->requirements.size() > 1 ){ + nd->u.barter.extended = true; + break; + } + + // Normal barter cannot handle refine + for( const auto& requirement : itemPair.second->requirements ){ + if( requirement.second->refine >= 0 ){ + nd->u.barter.extended = true; + break; + } + } + + // Check if a refine requirement has been set in the loop above + if( nd->u.barter.extended ){ + break; + } + } + +#if !( PACKETVER_MAIN_NUM >= 20191120 || PACKETVER_RE_NUM >= 20191106 || PACKETVER_ZERO_NUM >= 20191127 ) + if( nd->u.barter.extended ){ + ShowError( "Barter %s uses extended mechanics but this is not supported by the current packet version.\n", nd->name ); + continue; + } +#endif + + if( nd->bl.m >= 0 ){ + map_addnpc( nd->bl.m, nd ); + npc_setcells( nd ); + // Couldn't add on map + if( map_addblock( &nd->bl ) ){ + continue; + } + + status_change_init( &nd->bl ); + unit_dataset( &nd->bl ); + nd->ud.dir = barter->dir; + + if( nd->class_ != JT_FAKENPC ){ + status_set_viewdata( &nd->bl, nd->class_ ); + + if( map_getmapdata( nd->bl.m )->users ){ + clif_spawn( &nd->bl ); + } + } + }else{ + map_addiddb( &nd->bl ); + } + + strdb_put( npcname_db, nd->exname, nd ); + + for( const auto& itemPair : barter->items ){ + if( itemPair.second->stockLimited ){ + if( Sql_Query( mmysql_handle, "SELECT `amount` FROM `%s` WHERE `name` = '%s' AND `index` = '%hu'", barter_table, barter->name.c_str(), itemPair.first ) != SQL_SUCCESS ){ + Sql_ShowDebug( mmysql_handle ); + continue; + } + + // Previous amount found + if( SQL_SUCCESS == Sql_NextRow( mmysql_handle ) ){ + char* data; + + Sql_GetData( mmysql_handle, 0, &data, nullptr ); + + itemPair.second->stock = strtoul( data, nullptr, 10 ); + } + + Sql_FreeResult( mmysql_handle ); + + // Save or refresh the amount + if( Sql_Query( mmysql_handle, "REPLACE INTO `%s` (`name`,`index`,`amount`) VALUES ( '%s', '%hu', '%hu' )", barter_table, barter->name.c_str(), itemPair.first, itemPair.second->stock ) != SQL_SUCCESS ){ + Sql_ShowDebug( mmysql_handle ); + } + }else{ + if( Sql_Query( mmysql_handle, "DELETE FROM `%s` WHERE `name` = '%s' AND `index` = '%hu'", barter_table, barter->name.c_str(), itemPair.first ) != SQL_SUCCESS ){ + Sql_ShowDebug( mmysql_handle ); + } + } + } + } +} + +BarterDatabase barter_db; + /** * Returns the viewdata for normal NPC classes. * @param class_: NPC class ID @@ -1735,6 +2184,14 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd) case NPCTYPE_TOMB: run_tomb(sd,nd); break; + case NPCTYPE_BARTER: + sd->npc_shopid = nd->bl.id; + if( nd->u.barter.extended ){ + clif_barter_extended_open( *sd, *nd ); + }else{ + clif_barter_open( *sd, *nd ); + } + break; } return 0; @@ -2232,23 +2689,23 @@ static int npc_buylist_sub(struct map_session_data* sd, uint16 n, struct s_npc_b * @param item_list: List of items * @return result code for clif_parse_NpcBuyListSend/clif_npc_market_purchase_ack */ -uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list) { +e_purchase_result npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list) { struct npc_data* nd; struct npc_item_list *shop = NULL; double z; int i,j,k,w,skill,new_; uint8 market_index[MAX_INVENTORY]; - nullpo_retr(3, sd); - nullpo_retr(3, item_list); + nullpo_retr(e_purchase_result::PURCHASE_FAIL_COUNT, sd); + nullpo_retr(e_purchase_result::PURCHASE_FAIL_COUNT, item_list); nd = npc_checknear(sd,map_id2bl(sd->npc_shopid)); if( nd == NULL ) - return 3; + return e_purchase_result::PURCHASE_FAIL_COUNT; if( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_MARKETSHOP ) - return 3; + return e_purchase_result::PURCHASE_FAIL_COUNT; if (!item_list || !n) - return 3; + return e_purchase_result::PURCHASE_FAIL_COUNT; z = 0; w = 0; @@ -2271,12 +2728,12 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * ); if( j == nd->u.shop.count ) - return 3; // no such item in shop + return e_purchase_result::PURCHASE_FAIL_COUNT; // no such item in shop #if PACKETVER >= 20131223 if (nd->subtype == NPCTYPE_MARKETSHOP) { if (item_list[i].qty > shop[j].qty) - return 3; + return e_purchase_result::PURCHASE_FAIL_COUNT; market_index[i] = j; } #endif @@ -2287,7 +2744,7 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * id = itemdb_exists(nameid); if( !id ) - return 3; // item no longer in itemdb + return e_purchase_result::PURCHASE_FAIL_COUNT; // item no longer in itemdb if( !itemdb_isstackable2(id) && amount > 1 ) { //Exploit? You can't buy more than 1 of equipment types o.O ShowWarning("Player %s (%d:%d) sent a hexed packet trying to buy %d of nonstackable item %u!\n", @@ -2308,7 +2765,7 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * break; case CHKADDITEM_OVERAMOUNT: - return 2; + return e_purchase_result::PURCHASE_FAIL_WEIGHT; } if (npc_shop_discount(nd)) @@ -2318,16 +2775,18 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * w += itemdb_weight(nameid) * amount; } - if (nd->master_nd) //Script-based shops. - return npc_buylist_sub(sd,n,item_list,nd->master_nd); + if (nd->master_nd){ //Script-based shops. + npc_buylist_sub(sd,n,item_list,nd->master_nd); + return e_purchase_result::PURCHASE_SUCCEED; + } if (z > (double)sd->status.zeny) - return 1; // Not enough Zeny + return e_purchase_result::PURCHASE_FAIL_MONEY; // Not enough Zeny if( w + sd->weight > sd->max_weight ) - return 2; // Too heavy + return e_purchase_result::PURCHASE_FAIL_WEIGHT; // Too heavy if( pc_inventoryblank(sd) < new_ ) - return 3; // Not enough space to store items + return e_purchase_result::PURCHASE_FAIL_COUNT; // Not enough space to store items pc_payzeny(sd, (int)z, LOG_TYPE_NPC, NULL); @@ -2339,7 +2798,7 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * if (nd->subtype == NPCTYPE_MARKETSHOP) { j = market_index[i]; if (amount > shop[j].qty) - return 1; + return e_purchase_result::PURCHASE_FAIL_MONEY; shop[j].qty -= amount; npc_market_tosql(nd->exname, &shop[j]); } @@ -2378,7 +2837,7 @@ uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list * } } - return 0; + return e_purchase_result::PURCHASE_SUCCEED; } /// npc_selllist for script-controlled shops @@ -2573,6 +3032,268 @@ uint8 npc_selllist(struct map_session_data* sd, int n, unsigned short *item_list return 0; } +e_purchase_result npc_barter_purchase( struct map_session_data& sd, std::shared_ptr barter, std::vector& purchases ){ + uint64 requiredZeny = 0; + uint32 requiredWeight = 0; + uint32 reducedWeight = 0; + uint16 requiredSlots = 0; + uint32 requiredItems[MAX_INVENTORY] = { 0 }; + + for( s_barter_purchase& purchase : purchases ){ + purchase.data = itemdb_exists( purchase.item->nameid ); + + if( purchase.data == nullptr ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + + uint32 amount = purchase.amount; + + if( purchase.item->stockLimited && purchase.item->stock < amount ){ + return e_purchase_result::PURCHASE_FAIL_STOCK_EMPTY; + } + + char result = pc_checkadditem( &sd, purchase.item->nameid, amount ); + + if( result == CHKADDITEM_OVERAMOUNT ){ + return e_purchase_result::PURCHASE_FAIL_COUNT; + }else if( result == CHKADDITEM_NEW ){ + requiredSlots += purchase.data->inventorySlotNeeded( amount ); + } + + requiredZeny += ( purchase.item->price * amount ); + requiredWeight += ( purchase.data->weight * amount ); + + for( const auto& requirementPair : purchase.item->requirements ){ + std::shared_ptr requirement = requirementPair.second; + + item_data* id = itemdb_exists( requirement->nameid ); + + if( id == nullptr ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + + if( itemdb_isstackable2( id ) ){ + int j; + + for( j = 0; j < MAX_INVENTORY; j++ ){ + if( sd.inventory.u.items_inventory[j].nameid == requirement->nameid ){ + // Equipped items are not taken into account + if( sd.inventory.u.items_inventory[j].equip != 0 ){ + continue; + } + + // Items in equip switch are not taken into account + if( sd.inventory.u.items_inventory[j].equipSwitch != 0 ){ + continue; + } + + // Server is configured to hide favorite items on selling + if( battle_config.hide_fav_sell && sd.inventory.u.items_inventory[j].favorite ){ + continue; + } + + // Actually stackable items should never be refinable, but who knows... + if( requirement->refine >= 0 && sd.inventory.u.items_inventory[j].refine != requirement->refine ){ + // Refine does not match, continue with next item + continue; + } + + // Found a match, accumulate required amount + requiredItems[j] += requirement->amount * amount; + + // Check if there are still enough items available + if( requiredItems[j] > sd.inventory.u.items_inventory[j].amount ){ + return e_purchase_result::PURCHASE_FAIL_GOODS; + } + + // Cancel the loop + break; + } + } + + // Required item not found + if( j == MAX_INVENTORY ){ + return e_purchase_result::PURCHASE_FAIL_GOODS; + } + }else{ + for( int i = 0; i < requirement->amount; i++ ){ + int j; + + for( j = 0; j < MAX_INVENTORY; j++ ){ + if( sd.inventory.u.items_inventory[j].nameid == requirement->nameid ){ + // Equipped items are not taken into account + if( sd.inventory.u.items_inventory[j].equip != 0 ){ + continue; + } + + // Items in equip switch are not taken into account + if( sd.inventory.u.items_inventory[j].equipSwitch != 0 ){ + continue; + } + + // Server is configured to hide favorite items on selling + if( battle_config.hide_fav_sell && sd.inventory.u.items_inventory[j].favorite ){ + continue; + } + + // If necessary, check if the refine rate matches + if( requirement->refine >= 0 && sd.inventory.u.items_inventory[j].refine != requirement->refine ){ + // Refine does not match, continue with next item + continue; + } + + // Found a match, since it is not stackable, check if it was already taken + if( requiredItems[j] > 0 ){ + // Item was already taken, try to find another match + continue; + } + + // Mark it as taken + requiredItems[j] = 1; + + // Cancel the loop + break; + } + } + + // Required item not found + if( j == MAX_INVENTORY ){ + // Maybe the refine level did not match + if( requirement->refine >= 0 ){ + int refine; + + // Try to find a higher refine level, going from the next lowest to the highest possible + for( refine = requirement->refine + 1; refine <= MAX_REFINE; refine++ ){ + for( j = 0; j < MAX_INVENTORY; j++ ){ + if( sd.inventory.u.items_inventory[j].nameid == requirement->nameid ){ + // Equipped items are not taken into account + if( sd.inventory.u.items_inventory[j].equip != 0 ){ + continue; + } + + // Items in equip switch are not taken into account + if( sd.inventory.u.items_inventory[j].equipSwitch != 0 ){ + continue; + } + + // Server is configured to hide favorite items on selling + if( battle_config.hide_fav_sell && sd.inventory.u.items_inventory[j].favorite ){ + continue; + } + + // If necessary, check if the refine rate matches + if( requirement->refine >= 0 && sd.inventory.u.items_inventory[j].refine != refine ){ + // Refine does not match, continue with next item + continue; + } + + // Found a match, since it is not stackable, check if it was already taken + if( requiredItems[j] > 0 ){ + // Item was already taken, try to find another match + continue; + } + + // Mark it as taken + requiredItems[j] = 1; + + // Cancel the loop + break; + } + } + + // If a match was found, make sure to cancel the loop + if( j < MAX_INVENTORY ){ + // Cancel the loop + break; + } + } + + // No matching entry found + if( refine > MAX_REFINE ){ + return e_purchase_result::PURCHASE_FAIL_GOODS; + } + }else{ + return e_purchase_result::PURCHASE_FAIL_GOODS; + } + } + } + } + + reducedWeight += ( purchase.amount * requirement->amount * id->weight ); + } + } + + // Check if there is enough Zeny + if( sd.status.zeny < requiredZeny ){ + return e_purchase_result::PURCHASE_FAIL_MONEY; + } + + // Check if there is enough Weight Limit + if( ( sd.weight + requiredWeight - reducedWeight ) > sd.max_weight ){ + return e_purchase_result::PURCHASE_FAIL_WEIGHT; + } + + if( pc_inventoryblank( &sd ) < requiredSlots ){ + return e_purchase_result::PURCHASE_FAIL_COUNT; + } + + for( int i = 0; i < MAX_INVENTORY; i++ ){ + if( requiredItems[i] > 0 ){ + if( pc_delitem( &sd, i, requiredItems[i], 0, 0, LOG_TYPE_BARTER ) != 0 ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + } + } + + if( pc_payzeny( &sd, (int)requiredZeny, LOG_TYPE_BARTER, nullptr ) != 0 ){ + return e_purchase_result::PURCHASE_FAIL_MONEY; + } + + for( s_barter_purchase& purchase : purchases ){ + if( purchase.item->stockLimited ){ + purchase.item->stock -= purchase.amount; + + if( Sql_Query( mmysql_handle, "REPLACE INTO `%s` (`name`,`index`,`amount`) VALUES ( '%s', '%hu', '%hu' )", barter_table, barter->name.c_str(), purchase.item->index, purchase.item->stock ) != SQL_SUCCESS ){ + Sql_ShowDebug( mmysql_handle ); + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + } + + if( itemdb_isstackable2( purchase.data ) ){ + struct item it = {}; + + it.nameid = purchase.item->nameid; + it.identify = true; + + if( pc_additem( &sd, &it, purchase.amount, LOG_TYPE_BARTER ) != ADDITEM_SUCCESS ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + }else{ + if( purchase.data->type == IT_PETEGG ){ + for( int i = 0; i < purchase.amount; i++ ){ + if( !pet_create_egg( &sd, purchase.item->nameid ) ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + } + }else{ + for( int i = 0; i < purchase.amount; i++ ){ + struct item it = {}; + + it.nameid = purchase.item->nameid; + it.identify = true; + + if( pc_additem( &sd, &it, 1, LOG_TYPE_BARTER ) != ADDITEM_SUCCESS ){ + return e_purchase_result::PURCHASE_FAIL_EXCHANGE_FAILED; + } + } + } + } + } + + return e_purchase_result::PURCHASE_SUCCEED; +} + + //Atempt to remove an npc from a map //This doesn't remove it from map_db int npc_remove_map(struct npc_data* nd) @@ -5051,6 +5772,7 @@ int npc_reload(void) { npc_id - npc_new_min, npc_warp, npc_shop, npc_script, npc_mob, npc_cache_mob, npc_delay_mob); stylist_db.reload(); + barter_db.reload(); //Re-read the NPC Script Events cache. npc_read_event_script(); @@ -5119,6 +5841,7 @@ void do_final_npc(void) { NPCMarketDB->destroy(NPCMarketDB, npc_market_free); #endif stylist_db.clear(); + barter_db.clear(); ers_destroy(timer_event_ers); ers_destroy(npc_sc_display_ers); npc_clearsrcfile(); @@ -5205,6 +5928,7 @@ void do_init_npc(void){ npc_id - START_NPC_NUM, npc_warp, npc_shop, npc_script, npc_mob, npc_cache_mob, npc_delay_mob); stylist_db.load(); + barter_db.load(); // set up the events cache npc_read_event_script(); diff --git a/src/map/npc.hpp b/src/map/npc.hpp index 6e7c50c936..a5704c4b5a 100644 --- a/src/map/npc.hpp +++ b/src/map/npc.hpp @@ -4,8 +4,13 @@ #ifndef NPC_HPP #define NPC_HPP +#include +#include + +#include "../common/database.hpp" #include "../common/timer.hpp" +#include "clif.hpp" // #include "map.hpp" // struct block_list #include "status.hpp" // struct status_change #include "unit.hpp" // struct unit_data @@ -85,6 +90,51 @@ public: extern StylistDatabase stylist_db; +struct s_npc_barter_requirement{ + uint16 index; + t_itemid nameid; + uint16 amount; + int8 refine; +}; + +struct s_npc_barter_item{ + uint16 index; + t_itemid nameid; + bool stockLimited; + uint32 stock; + uint32 price; + std::map> requirements; +}; + +struct s_npc_barter{ + std::string name; + int16 m; + uint16 x; + uint16 y; + uint8 dir; + int16 sprite; + std::map> items; +}; + +class BarterDatabase : public TypesafeYamlDatabase{ +public: + BarterDatabase() : TypesafeYamlDatabase( "BARTER_DB", 1 ){ + + } + + const std::string getDefaultLocation(); + uint64 parseBodyNode( const YAML::Node& node ); + void loadingFinished(); +}; + +extern BarterDatabase barter_db; + +struct s_barter_purchase{ + std::shared_ptr item; + uint32 amount; + item_data* data; +}; + struct s_questinfo { e_questinfo_types icon; e_questinfo_markcolor color; @@ -153,6 +203,9 @@ struct npc_data { char killer_name[NAME_LENGTH]; int spawn_timer; } tomb; + struct { + bool extended; + } barter; } u; struct sc_display_entry **sc_display; @@ -1414,9 +1467,10 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd); bool npc_scriptcont(struct map_session_data* sd, int id, bool closing); struct npc_data* npc_checknear(struct map_session_data* sd, struct block_list* bl); int npc_buysellsel(struct map_session_data* sd, int id, int type); -uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list); +e_purchase_result npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list); static int npc_buylist_sub(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list, struct npc_data* nd); uint8 npc_selllist(struct map_session_data* sd, int n, unsigned short *item_list); +e_purchase_result npc_barter_purchase( struct map_session_data& sd, std::shared_ptr barter, std::vector& purchases ); void npc_parse_mob2(struct spawn_data* mob); struct npc_data* npc_add_warp(char* name, short from_mapid, short from_x, short from_y, short xs, short ys, unsigned short to_mapindex, short to_x, short to_y); int npc_globalmessage(const char* name,const char* mes); diff --git a/src/map/packets.hpp b/src/map/packets.hpp index e291cb90e8..6ad98bab68 100644 --- a/src/map/packets.hpp +++ b/src/map/packets.hpp @@ -32,6 +32,11 @@ #pragma pack( push, 1 ) #endif +struct PACKET_ZC_PC_PURCHASE_RESULT{ + int16 packetType; + uint8 result; +} __attribute__((packed)); + struct PACKET_CZ_REQ_MAKINGARROW{ int16 packetType; #if PACKETVER_MAIN_NUM >= 20181121 || PACKETVER_RE_NUM >= 20180704 || PACKETVER_ZERO_NUM >= 20181114 @@ -235,6 +240,7 @@ struct PACKET_CZ_REQ_STYLE_CLOSE{ DEFINE_PACKET_HEADER(ZC_NOTIFY_CHAT, 0x8d) DEFINE_PACKET_HEADER(ZC_BROADCAST, 0x9a) DEFINE_PACKET_HEADER(ZC_ITEM_ENTRY, 0x9d) +DEFINE_PACKET_HEADER(ZC_PC_PURCHASE_RESULT, 0xca) DEFINE_PACKET_HEADER(ZC_MVP_GETTING_ITEM, 0x10a) DEFINE_PACKET_HEADER(ZC_ACK_TOUSESKILL, 0x110) DEFINE_PACKET_HEADER(CZ_REQMAKINGITEM, 0x18e) diff --git a/src/map/pc.hpp b/src/map/pc.hpp index 9aee11b47b..50721d3871 100644 --- a/src/map/pc.hpp +++ b/src/map/pc.hpp @@ -383,6 +383,8 @@ struct map_session_data { bool cashshop_open; bool sale_open; bool stylist_open; + bool barter_open; + bool barter_extended_open; unsigned int block_action : 10; bool refineui_open; t_itemid inventory_expansion_confirmation; @@ -1055,7 +1057,8 @@ extern JobDatabase job_db; static bool pc_cant_act2( struct map_session_data* sd ){ return sd->state.vending || sd->state.buyingstore || (sd->sc.opt1 && sd->sc.opt1 != OPT1_BURNING) || sd->state.trading || sd->state.storage_flag || sd->state.prevend || sd->state.refineui_open - || sd->state.stylist_open || sd->state.inventory_expansion_confirmation || sd->npc_shopid; + || sd->state.stylist_open || sd->state.inventory_expansion_confirmation || sd->npc_shopid + || sd->state.barter_open || sd->state.barter_extended_open; } // equals pc_cant_act2 and additionally checks for chat rooms and npcs static bool pc_cant_act( struct map_session_data* sd ){ diff --git a/src/map/script.cpp b/src/map/script.cpp index 15166527b6..cc7bc49c49 100644 --- a/src/map/script.cpp +++ b/src/map/script.cpp @@ -17511,7 +17511,7 @@ BUILDIN_FUNC(callshop) if (script_hasdata(st,3)) flag = script_getnum(st,3); nd = npc_name2id(shopname); - if( !nd || nd->bl.type != BL_NPC || (nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP) ) { + if( !nd || nd->bl.type != BL_NPC || (nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP && nd->subtype != NPCTYPE_BARTER) ) { ShowError("buildin_callshop: Shop [%s] not found (or NPC is not shop type)\n", shopname); script_pushint(st,0); return SCRIPT_CMD_FAILURE; @@ -17547,7 +17547,16 @@ BUILDIN_FUNC(callshop) return SCRIPT_CMD_SUCCESS; } #endif - else + else if( nd->subtype == NPCTYPE_BARTER ){ + // flag the user as using a valid script call for opening the shop (for floating NPCs) + sd->state.callshop = 1; + + if( nd->u.barter.extended ){ + clif_barter_extended_open( *sd, *nd ); + }else{ + clif_barter_open( *sd, *nd ); + } + }else clif_cashshop_show(sd, nd); sd->npc_shopid = nd->bl.id;