Browse Source

NPC Market Shop support
* New shop script definition: `<map name>,<x>,<y>,<facing>%TAB%marketshop%TAB%<NPC Name>%TAB%<sprite id>,<itemid>:<price>:<quantity>{,<itemid>:<price>:<quantity>...}`
* Added script command to update shop NPC: 'npcshopupdate "<name>",<itemid>,<price>{,<stock>}'
* Added NPCMarketDB (DBMap) for market data persistance method.
* Added `market_table` definition for market table in conf/inter_athena.conf.
* Thank to @aleos89, @Lemongrass, @icxbb-xx, merged HerculesWS/Hercules@cf19b26.

NOTES:
* Minimum client needed 2013-12-23 (but this client has bugs there-and-there).
* There's new table, see `upgrade_20150327.sql`.
* Market shop doesn't support discount.
* Added items by script `npcshopitem` or `npchopadditem` will be assumed as persistance items, will be loaded on next script reload or server start even market_shop NPC does't list them (unless NPC is not exists, entries will be removed).

Signed-off-by: Cydh Ramdh <house.bad@gmail.com>

Cydh Ramdh 10 năm trước cách đây
mục cha
commit
7d929a06a3

+ 1 - 0
conf/inter_athena.conf

@@ -138,6 +138,7 @@ mob_skill_db2_db: mob_skill_db2
 mapreg_db: mapreg
 vending_db: vendings
 vending_items_db: vending_items
+market_table: market
 
 // Use SQL item_db, mob_db and mob_skill_db for the map server? (yes/no)
 use_sql_db: no

+ 1 - 1
conf/msg_conf/map_msg.conf

@@ -537,7 +537,7 @@
 532: Shadow Right Accessory, 
 533: Shadow Left Accessory, 
 
-//534: // Free
+534: Shop is out of stock! Please come back later.
 
 // Bot detect messages (currently unused)
 535: Possible use of BOT (99%% of chance) or modified client by '%s' (account: %d, char_id: %d). This player ask your name when you are hidden.

+ 5 - 3
db/packet_db.txt

@@ -2310,10 +2310,12 @@ packet_keys: 0x631C511C,0x111C111C,0x111C111C // [Shakto]
 //0x097E,12 //ZC_UPDATE_RANKING_POINT
 0x09B4,6,dull,0 //Cash Shop - Special Tab
 0x09CE,102,itemmonster,2
-0x09D4,2,dull,0 //npcshopclosed
+0x09D4,2,npcshopclosed,0
 //NPC Market
-0x09D6,-1,dull,0 //npcmarketpurchase
-0x09D8,2,dull,0 //npcmarketclosed
+0x09D5,-1
+0x09D6,-1,npcmarketpurchase,2:4:6
+0x09D7,-1
+0x09D8,2,npcmarketclosed,0
 0x09DF,7
 
 //Add new packets here

+ 21 - 2
doc/script_commands.txt

@@ -282,6 +282,8 @@ these floating NPC objects are for. More on that below.
 -%TAB%pointshop%TAB%<NPC Name>%TAB%<sprite id>,<costvariable>{:<discount>},<itemid>:<price>{,<itemid>:<price>...}
 <map name>,<x>,<y>,<facing>%TAB%pointshop%TAB%<NPC Name>%TAB%<sprite id>,<costvariable>{:<discount>},<itemid>:<price>{,<itemid>:<price>...}
 
+<map name>,<x>,<y>,<facing>%TAB%marketshop%TAB%<NPC Name>%TAB%<sprite id>,<itemid>:<price>:<quantity>{,<itemid>:<price>:<quantity>...}
+
 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 
@@ -6239,7 +6241,10 @@ specified will be for sale.
 
 The function returns 1 if shop was updated successfully, or 0 if not found.
 
-Note that you cannot use -1 to specify default selling price!
+NOTES:
+ - That you cannot use -1 to specify default selling price!
+ - If attached shop type is market shop, need an extra param after price, it's <qty>
+   and make sure don't add duplication item!
 
 ---------------------------------------
 
@@ -6251,7 +6256,10 @@ appear twice on the sell list.
 
 The function returns 1 if shop was updated successfully, or 0 if not found.
 
-Note that you cannot use -1 to specify default selling price!
+NOTES:
+ - That you cannot use -1 to specify default selling price!
+ - If attached shop type is market shop, need an extra param after price, it's <qty>
+   and make sure don't add duplication item!
 
 ---------------------------------------
 
@@ -6282,6 +6290,17 @@ override any other script that may be already attached.
 
 The function returns 0 if the shop was not found, 1 otherwise.
 
+NOTES:
+ - If attached shop type is market shop, will be default to call the 'buy' window.
+
+---------------------------------------
+
+*npcshopupdate "<name>",<item_id>,<price>{,<stock>}
+
+Update an entry from shop. If price is 0 means don't change the price, maybe used for
+marketshop to update the stock quantity. Except marketshop type, 'stock' value means
+nothing.
+
 ---------------------------------------
 
 *waitingroom "<chatroom name>",<limit>{,"<event label>"{,<trigger>{,<required zeny>{,<min lvl>{,<max lvl>}}}}};

+ 33 - 0
sql-files/main.sql

@@ -695,6 +695,10 @@ CREATE TABLE IF NOT EXISTS `storage` (
   KEY `account_id` (`account_id`)
 ) ENGINE=MyISAM;
 
+--
+-- Table structure for table `interreg`
+--
+
 CREATE TABLE IF NOT EXISTS `interreg` (
   `varname` varchar(11) NOT NULL,
   `value` varchar(20) NOT NULL,
@@ -714,6 +718,10 @@ CREATE TABLE IF NOT EXISTS `bonus_script` (
   `icon` SMALLINT(3) NOT NULL DEFAULT '-1'
 ) ENGINE=InnoDB;
 
+--
+-- Table structure for table `vending_items`
+--
+
 CREATE TABLE IF NOT EXISTS `vending_items` (
   `vending_id` int(10) unsigned NOT NULL,
   `index` smallint(5) unsigned NOT NULL,
@@ -722,6 +730,10 @@ CREATE TABLE IF NOT EXISTS `vending_items` (
   `price` int(10) unsigned NOT NULL
 ) ENGINE=MyISAM;
 
+--
+-- Table structure for table `vendings`
+--
+
 CREATE TABLE IF NOT EXISTS `vendings` (
   `id` int(10) unsigned NOT NULL,
   `account_id` int(11) unsigned NOT NULL,
@@ -738,6 +750,10 @@ CREATE TABLE IF NOT EXISTS `vendings` (
   PRIMARY KEY (`id`)
 ) ENGINE=MyISAM;
 
+--
+-- Table structure for table `buyingstore_items`
+--
+
 CREATE TABLE IF NOT EXISTS `buyingstore_items` (
   `buyingstore_id` int(10) unsigned NOT NULL,
   `index` smallint(5) unsigned NOT NULL,
@@ -746,6 +762,10 @@ CREATE TABLE IF NOT EXISTS `buyingstore_items` (
   `price` int(10) unsigned NOT NULL
 ) ENGINE=MyISAM;
 
+--
+-- Table structure for table `buyingstores`
+--
+
 CREATE TABLE IF NOT EXISTS `buyingstores` (
   `id` int(10) unsigned NOT NULL,
   `account_id` int(11) unsigned NOT NULL,
@@ -762,3 +782,16 @@ CREATE TABLE IF NOT EXISTS `buyingstores` (
   `autotrade` tinyint(4) NOT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=MyISAM;
+
+--
+-- Table `market` for market shop persistency
+--
+
+CREATE TABLE IF NOT EXISTS `market` (
+  `name` varchar(32) NOT NULL DEFAULT '',
+  `nameid` SMALLINT(5) UNSIGNED NOT NULL,
+  `price` INT(11) UNSIGNED NOT NULL,
+  `amount` SMALLINT(5) UNSIGNED NOT NULL,
+  `flag` TINYINT(2) UNSIGNED NOT NULL DEFAULT '0',
+  PRIMARY KEY  (`name`,`nameid`)
+) ENGINE = MyISAM;

+ 12 - 0
sql-files/upgrades/upgrade_20150327_market.sql

@@ -0,0 +1,12 @@
+--
+-- Table `market` for market shop persistency
+--
+
+CREATE TABLE IF NOT EXISTS `market` (
+  `name` varchar(32) NOT NULL DEFAULT '',
+  `nameid` SMALLINT(5) UNSIGNED NOT NULL,
+  `price` INT(11) UNSIGNED NOT NULL,
+  `amount` SMALLINT(5) UNSIGNED NOT NULL,
+  `flag` TINYINT(2) UNSIGNED NOT NULL DEFAULT '0',
+  PRIMARY KEY  (`name`,`nameid`)
+) ENGINE = MyISAM;

+ 133 - 5
src/map/clif.c

@@ -1856,6 +1856,133 @@ void clif_selllist(struct map_session_data *sd)
 }
 
 
+/**
+ * Presents list of items, that can be sold to a Market shop.
+ * @author: Ind and Yommy
+ **/
+void clif_npc_market_open(struct map_session_data *sd, struct npc_data *nd) {
+#if PACKETVER >= 20131223
+	struct npc_item_list *shop = nd->u.shop.shop_item;
+	unsigned short shop_size = nd->u.shop.count, i, c, cmd = 0x9d5;
+	struct item_data *id = NULL;
+	struct s_packet_db *info;
+	int fd;
+
+	nullpo_retv(sd);
+
+	if (sd->state.trading)
+		return;
+
+	info = &packet_db[sd->packet_ver][cmd];
+	if (!info || info->len == 0)
+		return;
+
+	fd = sd->fd;
+
+	WFIFOHEAD(fd, 4 + shop_size * 13);
+	WFIFOW(fd,0) = cmd;
+
+	for (i = 0, c = 0; i < shop_size; i++) {
+		if (shop[i].nameid && (id = itemdb_exists(shop[i].nameid))) {
+			WFIFOW(fd, 4+c*13) = shop[i].nameid;
+			WFIFOB(fd, 6+c*13) = itemtype(id->nameid);
+			WFIFOL(fd, 7+c*13) = shop[i].value;
+			WFIFOL(fd,11+c*13) = shop[i].qty;
+			WFIFOW(fd,15+c*13) = (id->view_id > 0) ? id->view_id : id->nameid;
+			c++;
+		}
+	}
+
+	WFIFOW(fd,2) = 4 + c*13;
+	WFIFOSET(fd,WFIFOW(fd,2));
+	sd->state.trading = 1;
+#endif
+}
+
+
+/// Closes the Market shop window.
+void clif_parse_NPCMarketClosed(int fd, struct map_session_data *sd) {
+	nullpo_retv(sd);
+	sd->npc_shopid = 0;
+	sd->state.trading = 0;
+}
+
+
+/// Purchase item from Market shop.
+void clif_npc_market_purchase_ack(struct map_session_data *sd, uint8 res, uint8 n, struct s_npc_buy_list *list) {
+#if PACKETVER >= 20131223
+	unsigned short cmd = 0x9d7, len = 0;
+	struct npc_data* nd;
+	uint8 result = (res == 0 ? 1 : 0);
+	int fd = 0;
+	struct s_packet_db *info;
+
+	nullpo_retv(sd);
+	nullpo_retv((nd = map_id2nd(sd->npc_shopid)));
+
+	info = &packet_db[sd->packet_ver][cmd];
+	if (!info || info->len == 0)
+		return;
+
+	fd = sd->fd;
+	len = 5 + 8*n;
+
+	WFIFOHEAD(fd, len);
+	WFIFOW(fd, 0) = cmd;
+	WFIFOW(fd, 2) = len;
+
+	if (result) {
+		uint8 i, j;
+		struct npc_item_list *shop = nd->u.shop.shop_item;
+		unsigned short count = nd->u.shop.count;
+
+		for (i = 0; i < n; i++) {
+			WFIFOW(fd, 5+i*8) = list[i].nameid;
+			WFIFOW(fd, 7+i*8) = list[i].qty;
+
+			ARR_FIND(0, count, j, list[i].nameid == shop[j].nameid);
+			WFIFOL(fd, 9+i*8) = (j != count) ? shop[j].value : 0;
+		}
+	}
+
+	WFIFOB(fd, 4) = n;
+	WFIFOSET(fd, len);
+#endif
+}
+
+
+/// Purchase item from Market shop.
+void clif_parse_NPCMarketPurchase(int fd, struct map_session_data *sd) {
+#if PACKETVER >= 20131223
+	struct s_packet_db* info;
+	struct s_npc_buy_list *item_list;
+	uint16 cmd = RFIFOW(fd,0), len = 0, i = 0;
+	uint8 res = 0, n = 0;
+
+	nullpo_retv(sd);
+
+	if (!sd->npc_shopid)
+		return;
+
+	info = &packet_db[sd->packet_ver][RFIFOW(fd,0)];
+	if (!info || info->len == 0)
+		return;
+	len = RFIFOW(fd,info->pos[0]);
+	n = (len-4) / 6;
+
+	CREATE(item_list, struct s_npc_buy_list, n);
+	for (i = 0; i < n; i++) {
+		item_list[i].nameid = RFIFOW(fd,info->pos[1]+i*6);
+		item_list[i].qty    = (uint16)min(RFIFOL(fd,info->pos[2]+i*6),USHRT_MAX);
+	}
+
+	res = npc_buylist(sd, n, item_list);
+	clif_npc_market_purchase_ack(sd, res, n, item_list);
+	aFree(item_list);
+#endif
+}
+
+
 /// Displays an NPC dialog message (ZC_SAY_DIALOG).
 /// 00b4 <packet len>.W <npc id>.L <message>.?B
 /// Client behavior (dialog window):
@@ -10930,17 +11057,15 @@ void clif_npc_buy_result(struct map_session_data* sd, unsigned char result)
 void clif_parse_NpcBuyListSend(int fd, struct map_session_data* sd)
 {
 	struct s_packet_db* info = &packet_db[sd->packet_ver][RFIFOW(fd,0)];
-	int n = (RFIFOW(fd,info->pos[0])-4) /4;
-	unsigned short* item_list = (unsigned short*)RFIFOP(fd,info->pos[1]);
+	uint16 n = (RFIFOW(fd,info->pos[0])-4) /4;
 	int result;
 
 	if( sd->state.trading || !sd->npc_shopid )
 		result = 1;
 	else
-		result = npc_buylist(sd,n,item_list);
+		result = npc_buylist(sd, n, (struct s_npc_buy_list*)RFIFOP(fd,info->pos[1]));
 
 	sd->npc_shopid = 0; //Clear shop data.
-
 	clif_npc_buy_result(sd, result);
 }
 
@@ -17989,7 +18114,7 @@ void packetdb_readdb(bool reload)
 		0,  0,  0,  0,  0,  0,  6,  4,  6,  4,  0,  0,  0,  0,  0,  0,
 	//#0x09C0
 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 23,  0,  0,  0,102,  0,
-		0,  0,  0,  0,  2,  0, -1,  0,  2,  0,  0,  0,  0,  0,  0,  7,
+		0,  0,  0,  0,  2,  0, -1, -1,  2,  0,  0,  0,  0,  0,  0,  7,
 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
 	};
@@ -18214,6 +18339,9 @@ void packetdb_readdb(bool reload)
 		{ clif_parse_client_version, "clientversion"},
 		{ clif_parse_blocking_playcancel, "booking_playcancel"},
 		{ clif_parse_ranklist, "ranklist"},
+		/* Market NPC */
+		{ clif_parse_NPCMarketClosed, "npcmarketclosed" },
+		{ clif_parse_NPCMarketPurchase, "npcmarketpurchase" },
 		{NULL,NULL}
 	};
 	struct {

+ 3 - 0
src/map/clif.h

@@ -455,6 +455,9 @@ void clif_fixpos(struct block_list *bl);	// area
 void clif_npcbuysell(struct map_session_data* sd, int id);	//self
 void clif_buylist(struct map_session_data *sd, struct npc_data *nd);	//self
 void clif_selllist(struct map_session_data *sd);	//self
+void clif_npc_market_open(struct map_session_data *sd, struct npc_data *nd);
+void clif_parse_NPCMarketClosed(int fd, struct map_session_data *sd);
+void clif_parse_NPCMarketPurchase(int fd, struct map_session_data *sd);
 void clif_scriptmes(struct map_session_data *sd, int npcid, const char *mes);	//self
 void clif_scriptnext(struct map_session_data *sd,int npcid);	//self
 void clif_scriptclose(struct map_session_data *sd, int npcid);	//self

+ 3 - 0
src/map/map.c

@@ -69,6 +69,7 @@ char mob_skill_db_re_db[32] = "mob_skill_db_re";
 char mob_skill_db2_db[32] = "mob_skill_db2";
 char vendings_db[32] = "vendings";
 char vending_items_db[32] = "vending_items";
+char market_table[32] = "market";
 
 // log database
 char log_db_ip[32] = "127.0.0.1";
@@ -3759,6 +3760,8 @@ int inter_config_read(char *cfgName)
 			strcpy( vendings_db, w2 );
 		else if( strcmpi( w1, "vending_items_db" ) == 0 )
 			strcpy( vending_items_db, w2 );
+		else if (strcmpi(w1, "market_table") == 0)
+			strcpy(market_table, w2);
 		else
 		//Map Server SQL DB
 		if(strcmpi(w1,"map_server_ip")==0)

+ 3 - 1
src/map/map.h

@@ -312,7 +312,8 @@ enum npc_subtype {
 	NPCTYPE_CASHSHOP, /// Cashshop
 	NPCTYPE_ITEMSHOP, /// Itemshop
 	NPCTYPE_POINTSHOP, /// Pointshop
-	NPCTYPE_TOMB /// Monster tomb
+	NPCTYPE_TOMB, /// Monster tomb
+	NPCTYPE_MARKETSHOP, /// Marketshop
 };
 
 enum e_race {
@@ -993,6 +994,7 @@ extern char mob_skill_db_re_db[32];
 extern char mob_skill_db2_db[32];
 extern char vendings_db[32];
 extern char vending_items_db[32];
+extern char market_table[32];
 
 void do_shutdown(void);
 

+ 366 - 59
src/map/npc.c

@@ -40,6 +40,20 @@ static int npc_mob=0;
 static int npc_delay_mob=0;
 static int npc_cache_mob=0;
 
+// Market Shop
+#if PACKETVER >= 20131223
+struct s_npc_market {
+	struct npc_item_list *list;
+	char exname[NAME_LENGTH+1];
+	uint16 count;
+};
+static DBMap *NPCMarketDB; /// Stock persistency! Temporary market stocks from `market` table. struct s_npc_market, key: NPC exname
+static void npc_market_checkall(void);
+static void npc_market_fromsql(void);
+#define npc_market_delfromsql(exname,nameid) (npc_market_delfromsql_((exname), (nameid), false))
+#define npc_market_clearfromsql(exname) (npc_market_delfromsql_((exname), 0, true))
+#endif
+
 /// Returns a new npc id that isn't being used in id_db.
 /// Fatal error if nothing is available.
 int npc_get_new_npc_id(void) {
@@ -1224,6 +1238,26 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd)
 		case NPCTYPE_CASHSHOP:
 			clif_cashshop_show(sd,nd);
 			break;
+		case NPCTYPE_MARKETSHOP:
+#if PACKETVER >= 20131223
+			 {
+				unsigned short i;
+
+				for (i = 0; i < nd->u.shop.count; i++) {
+					if (nd->u.shop.shop_item[i].qty)
+						break;
+				}
+
+				if (i == nd->u.shop.count) {
+					clif_colormes(sd, color_table[COLOR_RED], msg_txt(sd, 534));
+					return false;
+				}
+
+				sd->npc_shopid = nd->bl.id;
+				clif_npc_market_open(sd, nd);
+			}
+#endif
+			break;
 		case NPCTYPE_SCRIPT:
 			run_script(nd->u.scr.script,0,sd->bl.id,nd->bl.id);
 			break;
@@ -1318,7 +1352,7 @@ int npc_buysellsel(struct map_session_data* sd, int id, int type)
 	sd->state.callshop = 0;
 	sd->npc_shopid = id;
 
-	if (type==0) {
+	if (type == 0) {
 		clif_buylist(sd,nd);
 	} else {
 		clif_selllist(sd);
@@ -1410,9 +1444,14 @@ int npc_cashshop_buylist(struct map_session_data *sd, int points, int count, uns
 	return 0;
 }
 
-//npc_buylist for script-controlled shops.
-static int npc_buylist_sub(struct map_session_data* sd, int n, unsigned short* item_list, struct npc_data* nd)
-{
+/**
+ * npc_buylist for script-controlled shops.
+ * @param sd Player who bought
+ * @param n Number of item
+ * @param item_list List of item
+ * @param nd Attached NPC
+ **/
+static int npc_buylist_sub(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list, struct npc_data* nd) {
 	char npc_ev[EVENT_NAME_LENGTH];
 	int i;
 	int key_nameid = 0;
@@ -1423,10 +1462,9 @@ static int npc_buylist_sub(struct map_session_data* sd, int n, unsigned short* i
 	script_cleararray_pc(sd, "@bought_quantity", (void*)0);
 
 	// save list of bought items
-	for( i = 0; i < n; i++ )
-	{
-		script_setarray_pc(sd, "@bought_nameid", i, (void*)(intptr_t)item_list[i*2+1], &key_nameid);
-		script_setarray_pc(sd, "@bought_quantity", i, (void*)(intptr_t)item_list[i*2], &key_amount);
+	for (i = 0; i < n; i++) {
+		script_setarray_pc(sd, "@bought_nameid", i, (void*)(intptr_t)item_list[i].nameid, &key_nameid);
+		script_setarray_pc(sd, "@bought_quantity", i, (void*)(intptr_t)item_list[i].qty, &key_amount);
 	}
 
 	// invoke event
@@ -1519,16 +1557,20 @@ int npc_cashshop_buy(struct map_session_data *sd, unsigned short nameid, int amo
 	return 0;
 }
 
-/// Player item purchase from npc shop.
-///
-/// @param item_list 'n' pairs <amount,itemid>
-/// @return result code for clif_parse_NpcBuyListSend
-int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
-{
+/**
+ * Shop buylist
+ * @param sd Player who attempt to buy
+ * @param n Number of item will be bought
+ * @param *item_list List of item will be bought
+ * @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) {
 	struct npc_data* nd;
+	struct npc_item_list *shop = NULL;
 	double z;
 	int i,j,k,w,skill,new_,count = 0;
 	char output[CHAT_SIZE_MAX];
+	uint8 market_index[MAX_INVENTORY];
 
 	nullpo_retr(3, sd);
 	nullpo_retr(3, item_list);
@@ -1536,48 +1578,58 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 	nd = npc_checknear(sd,map_id2bl(sd->npc_shopid));
 	if( nd == NULL )
 		return 3;
-	if( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP )
+	if( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP )
+		return 3;
+	if (!item_list || !n)
 		return 3;
 
 	z = 0;
 	w = 0;
 	new_ = 0;
+
+	shop = nd->u.shop.shop_item;
+
+	memset(market_index, 0, sizeof(market_index));
 	// process entries in buy list, one by one
-	for( i = 0; i < n; ++i )
-	{
-		unsigned short nameid;
-		int amount, value;
+	for( i = 0; i < n; ++i ) {
+		unsigned short nameid, amount;
+		int value;
 
 		// find this entry in the shop's sell list
 		ARR_FIND( 0, nd->u.shop.count, j,
-			item_list[i*2+1] == nd->u.shop.shop_item[j].nameid || //Normal items
-			item_list[i*2+1] == itemdb_viewid(nd->u.shop.shop_item[j].nameid) //item_avail replacement
+			item_list[i].nameid == shop[j].nameid || //Normal items
+			item_list[i].nameid == itemdb_viewid(shop[j].nameid) //item_avail replacement
 		);
 
 		if( j == nd->u.shop.count )
 			return 3; // no such item in shop
 
-		amount = item_list[i*2+0];
-		nameid = item_list[i*2+1] = nd->u.shop.shop_item[j].nameid; //item_avail replacement
-		value = nd->u.shop.shop_item[j].value;
+#if PACKETVER >= 20131223
+		if (nd->subtype == NPCTYPE_MARKETSHOP) {
+			if (item_list[i].qty > shop[j].qty)
+				return 3;
+			market_index[i] = j;
+		}
+#endif
+
+		amount = item_list[i].qty;
+		nameid = item_list[i].nameid = shop[j].nameid; //item_avail replacement
+		value = shop[j].value;
 
 		if( !itemdb_exists(nameid) )
 			return 3; // item no longer in itemdb
 
-		if( !itemdb_isstackable(nameid) && amount > 1 )
-		{	//Exploit? You can't buy more than 1 of equipment types o.O
+		if( !itemdb_isstackable(nameid) && 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 %hu!\n",
 				sd->status.name, sd->status.account_id, sd->status.char_id, amount, nameid);
-			amount = item_list[i*2+0] = 1;
+			amount = item_list[i].qty = 1;
 		}
 
-		if( nd->master_nd )
-		{// Script-controlled shops decide by themselves, what can be bought and for what price.
+		if( nd->master_nd ) { // Script-controlled shops decide by themselves, what can be bought and for what price.
 			continue;
 		}
 
-		switch( pc_checkadditem(sd,nameid,amount) )
-		{
+		switch( pc_checkadditem(sd,nameid,amount) ) {
 			case CHKADDITEM_EXIST:
 				break;
 
@@ -1596,11 +1648,12 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 		w += itemdb_weight(nameid) * amount;
 	}
 
-	if( nd->master_nd != NULL ) //Script-based shops.
+	if (nd->master_nd) //Script-based shops.
 		return npc_buylist_sub(sd,n,item_list,nd->master_nd);
 
 	switch(nd->subtype) {
 		case NPCTYPE_SHOP:
+		case NPCTYPE_MARKETSHOP:
 			if (z > (double)sd->status.zeny)
 				return 1;	// Not enough Zeny
 			break;
@@ -1645,6 +1698,7 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 
 	switch(nd->subtype) {
 		case NPCTYPE_SHOP:
+		case NPCTYPE_MARKETSHOP:
 			pc_payzeny(sd, (int)z, LOG_TYPE_NPC, NULL);
 			break;
 		case NPCTYPE_ITEMSHOP:
@@ -1666,16 +1720,24 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 			break;
 	}
 
-	for( i = 0; i < n; ++i )
-	{
-		unsigned short nameid = item_list[i*2+1];
-		int amount = item_list[i*2+0];
+	for( i = 0; i < n; ++i ) {
+		unsigned short nameid = item_list[i].nameid;
+		unsigned short amount = item_list[i].qty;
 		struct item item_tmp;
 
+#if PACKETVER >= 20131223
+		if (nd->subtype == NPCTYPE_MARKETSHOP) {
+			j = market_index[i];
+			if (amount > shop[j].qty)
+				return 1;
+			shop[j].qty -= amount;
+			npc_market_tosql(nd->exname, &shop[j]);
+		}
+#endif
+
 		if (itemdb_type(nameid) == IT_PETEGG)
 			pet_create_egg(sd, nameid);
-		else
-		{
+		else {
 			memset(&item_tmp,0,sizeof(item_tmp));
 			item_tmp.nameid = nameid;
 			item_tmp.identify = 1;
@@ -1685,14 +1747,12 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 	}
 
 	// custom merchant shop exp bonus
-	if( battle_config.shop_exp > 0 && z > 0 && (skill = pc_checkskill(sd,MC_DISCOUNT)) > 0 )
-	{
+	if( battle_config.shop_exp > 0 && z > 0 && (skill = pc_checkskill(sd,MC_DISCOUNT)) > 0 ) {
 		uint16 sk_idx = skill_get_index(MC_DISCOUNT);
 		if( sd->status.skill[sk_idx].flag >= SKILL_FLAG_REPLACED_LV_0 )
 			skill = sd->status.skill[sk_idx].flag - SKILL_FLAG_REPLACED_LV_0;
 
-		if( skill > 0 )
-		{
+		if( skill > 0 ) {
 			z = z * (double)skill * (double)battle_config.shop_exp/10000.;
 			if( z < 1 )
 				z = 1;
@@ -1708,7 +1768,6 @@ int npc_buylist(struct map_session_data* sd, int n, unsigned short* item_list)
 	return 0;
 }
 
-
 /// npc_selllist for script-controlled shops
 static int npc_selllist_sub(struct map_session_data* sd, int n, unsigned short* item_list, struct npc_data* nd)
 {
@@ -1771,7 +1830,7 @@ static int npc_selllist_sub(struct map_session_data* sd, int n, unsigned short*
 ///
 /// @param item_list 'n' pairs <index,amount>
 /// @return result code for clif_parse_NpcSellListSend
-int npc_selllist(struct map_session_data* sd, int n, unsigned short* item_list)
+uint8 npc_selllist(struct map_session_data* sd, int n, unsigned short *item_list)
 {
 	double z;
 	int i,skill;
@@ -1957,7 +2016,7 @@ int npc_unload(struct npc_data* nd, bool single) {
 	if( single && nd->bl.m != -1 )
 		map_remove_questinfo(nd->bl.m, nd);
 
-	if( (nd->subtype == NPCTYPE_SHOP || nd->subtype == NPCTYPE_CASHSHOP || nd->subtype == NPCTYPE_ITEMSHOP || nd->subtype == NPCTYPE_POINTSHOP) && nd->src_id == 0) //src check for duplicate shops [Orcao]
+	if( (nd->subtype == NPCTYPE_SHOP || nd->subtype == NPCTYPE_CASHSHOP || nd->subtype == NPCTYPE_ITEMSHOP || nd->subtype == NPCTYPE_POINTSHOP || nd->subtype == NPCTYPE_MARKETSHOP) && nd->src_id == 0) //src check for duplicate shops [Orcao]
 		aFree(nd->u.shop.shop_item);
 	else if( nd->subtype == NPCTYPE_SCRIPT ) {
 		struct s_mapiterator* iter;
@@ -2375,8 +2434,9 @@ static const char* npc_parse_warp(char* w1, char* w2, char* w3, char* w4, const
  * <map name>,<x>,<y>,<facing>%TAB%cashshop%TAB%<NPC Name>%TAB%<sprite id>,<itemid>:<price>{,<itemid>:<price>...}
  * <map name>,<x>,<y>,<facing>%TAB%itemshop%TAB%<NPC Name>%TAB%<sprite id>,<costitemid>{:<discount>},<itemid>:<price>{,<itemid>:<price>...}
  * <map name>,<x>,<y>,<facing>%TAB%pointshop%TAB%<NPC Name>%TAB%<sprite id>,<costvariable>{:<discount>},<itemid>:<price>{,<itemid>:<price>...}
+ * <map name>,<x>,<y>,<facing>%TAB%marketshop%TAB%<NPC Name>%TAB%<sprite id>,<itemid>:<price>:<quantity>{,<itemid>:<price>:<quantity>...}
  * @param w1 : word 1 before tab (<from map name>,<x>,<y>,<facing>)
- * @param w2 : word 2 before tab (shop|cashshop|itemshop|pointshop), keyword that sent us in this parsing
+ * @param w2 : word 2 before tab (shop|cashshop|itemshop|pointshop|marketshop), keyword that sent us in this parsing
  * @param w3 : word 3 before tab (<NPC Name>)
  * @param w4 : word 4 before tab (<sprited id>,<shop definition...>)
  * @param start : index to start parsing
@@ -2420,6 +2480,8 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 		type = NPCTYPE_ITEMSHOP;
 	else if( !strcasecmp(w2,"pointshop") )
 		type = NPCTYPE_POINTSHOP;
+	else if( !strcasecmp(w2, "marketshop") )
+		type = NPCTYPE_MARKETSHOP;
 	else
 		type = NPCTYPE_SHOP;
 
@@ -2459,6 +2521,14 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 			p = strchr(p+1,',');
 			break;
 		}
+		case NPCTYPE_MARKETSHOP:
+#if PACKETVER < 20131223
+			ShowError("npc_parse_shop: (MARKETSHOP) Feature is disabled, need client 20131223 or newer. Ignoring file '%s', line '%d\n * w1=%s\n * w2=%s\n * w3=%s\n * w4=%s\n", filepath, strline(buffer, start - buffer), w1, w2, w3, w4);
+			return strchr(start, '\n'); // skip and continue
+#else
+			is_discount = 0;
+			break;
+#endif
 		default:
 			is_discount = 1;
 			break;
@@ -2468,25 +2538,43 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 
 	nd->u.shop.count = 0;
 	while ( p ) {
-		unsigned short nameid2;
-		int value;
+		unsigned short nameid2, qty = 0;
+		int value, i = 0;
 		struct item_data* id;
+		bool skip = false;
+
 		if( p == NULL )
 			break;
-		if( sscanf(p, ",%hu:%d", &nameid2, &value) != 2 ) {
-			ShowError("npc_parse_shop: Invalid item definition in file '%s', line '%d'. Ignoring the rest of the line...\n * w1=%s\n * w2=%s\n * w3=%s\n * w4=%s\n", filepath, strline(buffer,start-buffer), w1, w2, w3, w4);
-			break;
+		switch(type) {
+			case NPCTYPE_SHOP:
+				if (sscanf(p, ",%hu:%d", &nameid2, &value) != 2) {
+					ShowError("npc_parse_shop: (SHOP) Invalid item definition in file '%s', line '%d'. Ignoring the rest of the line...\n * w1=%s\n * w2=%s\n * w3=%s\n * w4=%s\n", filepath, strline(buffer, start - buffer), w1, w2, w3, w4);
+					skip = true;
+				}
+				break;
+			case NPCTYPE_MARKETSHOP:
+#if PACKETVER >= 20131223
+				if (sscanf(p, ",%hu:%d:%hu", &nameid2, &value, &qty) != 3) {
+					ShowError("npc_parse_shop: (MARKETSHOP) Invalid item definition in file '%s', line '%d'. Ignoring the rest of the line...\n * w1=%s\n * w2=%s\n * w3=%s\n * w4=%s\n", filepath, strline(buffer, start - buffer), w1, w2, w3, w4);
+					skip = true;
+				}
+#endif
+				break;
 		}
+
+		if (skip)
+			break;
+
 		if( (id = itemdb_exists(nameid2)) == NULL ) {
 			ShowWarning("npc_parse_shop: Invalid sell item in file '%s', line '%d' (id '%hu').\n", filepath, strline(buffer,start-buffer), nameid2);
 			p = strchr(p+1,',');
 			continue;
 		}
 		if( value < 0 ) {
-			if( type == NPCTYPE_SHOP ) value = id->value_buy;
+			if (type == NPCTYPE_SHOP || type == NPCTYPE_MARKETSHOP) value = id->value_buy;
 			else value = 0; // Cashshop doesn't have a "buy price" in the item_db
 		}
-		if( (type == NPCTYPE_SHOP || type == NPCTYPE_ITEMSHOP || type == NPCTYPE_POINTSHOP) && value == 0 ) { // NPC selling items for free!
+		if (value == 0 && (type == NPCTYPE_SHOP || type == NPCTYPE_ITEMSHOP || type == NPCTYPE_POINTSHOP || type == NPCTYPE_MARKETSHOP)) { // NPC selling items for free!
 			ShowWarning("npc_parse_shop: Item %s [%hu] is being sold for FREE in file '%s', line '%d'.\n",
 				id->name, nameid2, filepath, strline(buffer,start-buffer));
 		}
@@ -2494,17 +2582,37 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 			ShowWarning("npc_parse_shop: Item %s [%hu] discounted buying price (%d->%d) is less than overcharged selling price (%d->%d) at file '%s', line '%d'.\n",
 				id->name, nameid2, value, (int)(value*0.75), id->value_sell, (int)(id->value_sell*1.24), filepath, strline(buffer,start-buffer));
 		}
+		if (type == NPCTYPE_MARKETSHOP && (!qty || qty > UINT16_MAX)) {
+			ShowWarning("npc_parse_shop: Item %s [%hu] is stocked with invalid value %d, changed to 1. File '%s', line '%d'.\n",
+				id->name, nameid2, filepath, strline(buffer,start-buffer));
+			qty = 1;
+		}
 		//for logs filters, atcommands and iteminfo script command
 		if( id->maxchance == 0 )
 			id->maxchance = -1; // -1 would show that the item's sold in NPC Shop
+		
+#if PACKETVER >= 20131223
+		if (nd->u.shop.count && type == NPCTYPE_MARKETSHOP) {
+			// Duplicate entry? Replace the value
+			ARR_FIND(0, nd->u.shop.count, i, nd->u.shop.shop_item[i].nameid == nameid);
+			if (i != nd->u.shop.count) {
+				nd->u.shop.shop_item[i].qty = qty;
+				nd->u.shop.shop_item[i].value = value;
+				p = strchr(p+1,',');
+				continue;
+			}
+		}
+#endif
 
-		if (nd->u.shop.count > 0)
-			RECREATE(nd->u.shop.shop_item, struct npc_item_list,nd->u.shop.count+1);
-		else
-			CREATE(nd->u.shop.shop_item, struct npc_item_list,1);
+		RECREATE(nd->u.shop.shop_item, struct npc_item_list,nd->u.shop.count+1);
 
 		nd->u.shop.shop_item[nd->u.shop.count].nameid = nameid2;
 		nd->u.shop.shop_item[nd->u.shop.count].value = value;
+#if PACKETVER >= 20131223
+		nd->u.shop.shop_item[nd->u.shop.count].flag = 0;
+		if (type == NPCTYPE_MARKETSHOP)
+			nd->u.shop.shop_item[nd->u.shop.count].qty = qty;
+#endif
 		nd->u.shop.count++;
 		p = strchr(p+1,',');
 	}
@@ -2519,6 +2627,7 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 		else if (type == NPCTYPE_POINTSHOP) safestrncpy(nd->u.shop.pointshop_str,point_str,strlen(point_str)+1); // Point shop currency
 		nd->u.shop.discount = is_discount;
 	}
+
 	nd->bl.prev = nd->bl.next = NULL;
 	nd->bl.m = m;
 	nd->bl.x = x;
@@ -2531,6 +2640,14 @@ static const char* npc_parse_shop(char* w1, char* w2, char* w3, char* w4, const
 	++npc_shop;
 	nd->bl.type = BL_NPC;
 	nd->subtype = type;
+#if PACKETVER >= 20131223
+	// Insert market data to table
+	if (nd->subtype == NPCTYPE_MARKETSHOP) {
+		uint16 i;
+		for (i = 0; i < nd->u.shop.count; i++)
+			npc_market_tosql(nd->exname, &nd->u.shop.shop_item[i]);
+	}
+#endif
 	if( m >= 0 )
 	{// normal shop npc
 		map_addnpc(m,nd);
@@ -2857,7 +2974,7 @@ const char* npc_parse_duplicate(char* w1, char* w2, char* w3, char* w4, const ch
 	type = dnd->subtype;
 
 	// get placement
-	if( (type == NPCTYPE_SHOP || type == NPCTYPE_CASHSHOP || type == NPCTYPE_ITEMSHOP || type == NPCTYPE_POINTSHOP || type == NPCTYPE_SCRIPT) && strcmp(w1, "-") == 0 ) {// floating shop/chashshop/itemshop/pointshop/script
+	if ((type == NPCTYPE_SHOP || type == NPCTYPE_CASHSHOP || type == NPCTYPE_ITEMSHOP || type == NPCTYPE_POINTSHOP || type == NPCTYPE_SCRIPT || type == NPCTYPE_MARKETSHOP) && strcmp(w1, "-") == 0) {// floating shop/chashshop/itemshop/pointshop/script
 		x = y = dir = 0;
 		m = -1;
 	} else {
@@ -2908,6 +3025,7 @@ const char* npc_parse_duplicate(char* w1, char* w2, char* w3, char* w4, const ch
 		case NPCTYPE_CASHSHOP:
 		case NPCTYPE_ITEMSHOP:
 		case NPCTYPE_POINTSHOP:
+		case NPCTYPE_MARKETSHOP:
 			++npc_shop;
 			nd->u.shop.shop_item = dnd->u.shop.shop_item;
 			nd->u.shop.count = dnd->u.shop.count;
@@ -3059,6 +3177,176 @@ int npc_instanceinit(struct npc_data* nd)
 	return 0;
 }
 
+#if PACKETVER >= 20131223
+/**
+ * Saves persistent NPC Market Data into SQL
+ * @param exname NPC exname
+ * @param nameid Item ID
+ * @param qty Stock
+ **/
+void npc_market_tosql(const char *exname, struct npc_item_list *list) {
+	SqlStmt* stmt = SqlStmt_Malloc(mmysql_handle);
+	if (SQL_ERROR == SqlStmt_Prepare(stmt, "REPLACE INTO `%s` (`name`,`nameid`,`price`,`amount`,`flag`) VALUES ('%s','%hu','%d','%hu','%"PRIu8"')",
+		market_table, exname, list->nameid, list->value, list->qty, list->flag) ||
+		SQL_ERROR == SqlStmt_Execute(stmt))
+		SqlStmt_ShowDebug(stmt);
+	SqlStmt_Free(stmt);
+}
+
+/**
+ * Removes persistent NPC Market Data from SQL
+ * @param exname NPC exname
+ * @param nameid Item ID
+ * @param clear True: will removes all records related with the NPC
+ **/
+void npc_market_delfromsql_(const char *exname, unsigned short nameid, bool clear) {
+	SqlStmt* stmt = SqlStmt_Malloc(mmysql_handle);
+	if (clear) {
+		if( SQL_ERROR == SqlStmt_Prepare(stmt, "DELETE FROM `%s` WHERE `name`='%s'", market_table, exname) ||
+			SQL_ERROR == SqlStmt_Execute(stmt))
+			SqlStmt_ShowDebug(stmt);
+	} else {
+		if (SQL_ERROR == SqlStmt_Prepare(stmt, "DELETE FROM `%s` WHERE `name`='%s' AND `nameid`='%d' LIMIT 1", market_table, exname, nameid) ||
+			SQL_ERROR == SqlStmt_Execute(stmt))
+			SqlStmt_ShowDebug(stmt);
+	}
+	SqlStmt_Free(stmt);
+}
+
+/**
+ * Check NPC Market Shop for each entry
+ **/
+static int npc_market_checkall_sub(DBKey key, DBData *data, va_list ap) {
+	struct s_npc_market *market = (struct s_npc_market *)db_data2ptr(data);
+	struct npc_data *nd = NULL;
+	uint16 i;
+
+	if (!market)
+		return 1;
+
+	nd = npc_name2id(market->exname);
+	if (!nd) {
+		ShowInfo("npc_market_checkall_sub: NPC '%s' not found, removing...\n", market->exname);
+		npc_market_clearfromsql(market->exname);
+		return 1;
+	}
+	else if (nd->subtype != NPCTYPE_MARKETSHOP || !nd->u.shop.shop_item || !nd->u.shop.count ) {
+		ShowError("npc_market_checkall_sub: NPC '%s' is not proper for market, removing...\n", nd->exname);
+		npc_market_clearfromsql(nd->exname);
+		return 1;
+	}
+
+	if (!market->count || !market->list)
+		return 1;
+
+	for (i = 0; i < market->count; i++) {
+		struct npc_item_list *list = &market->list[i];
+		uint16 j;
+
+		if (!list->nameid || !itemdb_exists(list->nameid)) {
+			ShowError("npc_market_checkall_sub: NPC '%s' sells invalid item '%hu', deleting...\n", nd->exname, list->nameid);
+			npc_market_delfromsql(nd->exname, list->nameid);
+			continue;
+		}
+
+		// Reloading stock from `market` table
+		ARR_FIND(0, nd->u.shop.count, j, nd->u.shop.shop_item[j].nameid == list->nameid);
+		if (j != nd->u.shop.count) {
+			nd->u.shop.shop_item[j].value = list->value;
+			nd->u.shop.shop_item[j].qty = list->qty;
+			nd->u.shop.shop_item[j].flag = list->flag;
+			npc_market_tosql(nd->exname, &nd->u.shop.shop_item[j]);
+			continue;
+		}
+
+		if (list->flag&1) { // Item added by npcshopitem/npcshopadditem, add new entry
+			RECREATE(nd->u.shop.shop_item, struct npc_item_list, nd->u.shop.count+1);
+			nd->u.shop.shop_item[j].nameid = list->nameid;
+			nd->u.shop.shop_item[j].value = list->value;
+			nd->u.shop.shop_item[j].qty = list->qty;
+			nd->u.shop.shop_item[j].flag = list->flag;
+			nd->u.shop.count++;
+			npc_market_tosql(nd->exname, &nd->u.shop.shop_item[j]);
+		}
+		else { // Removing "out-of-date" entry
+			ShowError("npc_market_checkall_sub: NPC '%s' does not sell item %hu (qty %hu), deleting...\n", nd->exname, list->nameid, list->qty);
+			npc_market_delfromsql(nd->exname, list->nameid);
+		}
+	}
+
+	return 0;
+}
+
+/**
+ * Clear NPC market single entry
+ **/
+static int npc_market_free(DBKey key, DBData *data, va_list ap) {
+	struct s_npc_market *market = (struct s_npc_market *)db_data2ptr(data);
+	if (!market)
+		return 0;
+	if (market->list) {
+		aFree(market->list);
+		market->list = NULL;
+	}
+	aFree(market);
+	return 1;
+}
+
+/**
+ * Check all existing  NPC Market Shop after first loading map-server or after reloading scripts.
+ * Overwrite stocks from NPC by using stock entries from `market` table.
+ **/
+static void npc_market_checkall(void) {
+	if (!db_size(NPCMarketDB))
+		return;
+	else {
+		ShowInfo("Checking '"CL_WHITE"%d"CL_RESET"' NPC Markets...\n", db_size(NPCMarketDB));
+		NPCMarketDB->foreach(NPCMarketDB, npc_market_checkall_sub);
+		ShowStatus("Done checking '"CL_WHITE"%d"CL_RESET"' NPC Markets.\n", db_size(NPCMarketDB));
+		NPCMarketDB->clear(NPCMarketDB, npc_market_free);
+	}
+}
+
+/**
+ * Loads persistent NPC Market Data from SQL, use the records after NPCs init'd to reuse the stock values.
+ **/
+static void npc_market_fromsql(void) {
+	uint32 count = 0;
+
+	if (SQL_ERROR == Sql_Query(mmysql_handle, "SELECT `name`,`nameid`,`price`,`amount`,`flag` FROM `%s` ORDER BY `name`", market_table)) {
+		Sql_ShowDebug(mmysql_handle);
+		return;
+	}
+
+	while (SQL_SUCCESS == Sql_NextRow(mmysql_handle)) {
+		char *data;
+		struct s_npc_market *market;
+		struct npc_item_list list;
+
+		Sql_GetData(mmysql_handle, 0, &data, NULL);
+
+		if (!(market = (struct s_npc_market *)strdb_get(NPCMarketDB,data))) {
+			CREATE(market, struct s_npc_market, 1);
+			market->count = 0;
+			safestrncpy(market->exname, data, strlen(data)+1);
+			strdb_put(NPCMarketDB, market->exname, market);
+		}
+
+		Sql_GetData(mmysql_handle, 1, &data, NULL); list.nameid = atoi(data);
+		Sql_GetData(mmysql_handle, 2, &data, NULL); list.value = atoi(data);
+		Sql_GetData(mmysql_handle, 3, &data, NULL); list.qty = atoi(data);
+		Sql_GetData(mmysql_handle, 4, &data, NULL); list.flag = atoi(data);
+
+		RECREATE(market->list, struct npc_item_list, market->count+1);
+		market->list[market->count++] = list;
+		count++;
+	}
+	Sql_FreeResult(mmysql_handle);
+
+	ShowStatus("Done loading '"CL_WHITE"%d"CL_RESET"' entries for '"CL_WHITE"%d"CL_RESET"' NPC Markets from '"CL_WHITE"%s"CL_RESET"' table.\n", count, db_size(NPCMarketDB), market_table);
+}
+#endif
+
 //Set mapcell CELL_NPC to trigger event later
 void npc_setcells(struct npc_data* nd)
 {
@@ -3928,7 +4216,7 @@ int npc_parsesrcfile(const char* filepath, bool runOnInit)
 
 		if( strcasecmp(w2,"warp") == 0 && count > 3 )
 			p = npc_parse_warp(w1,w2,w3,w4, p, buffer, filepath);
-		else if( (!strcasecmp(w2,"shop") || !strcasecmp(w2,"cashshop") || !strcasecmp(w2,"itemshop") || !strcasecmp(w2,"pointshop")) && count > 3 )
+		else if( (!strcasecmp(w2,"shop") || !strcasecmp(w2,"cashshop") || !strcasecmp(w2,"itemshop") || !strcasecmp(w2,"pointshop") || !strcasecmp(w2,"marketshop") ) && count > 3 )
 			p = npc_parse_shop(w1,w2,w3,w4, p, buffer, filepath);
 		else if( strcasecmp(w2,"script") == 0 && count > 3 ) {
 			if( strcasecmp(w1,"function") == 0 )
@@ -4057,6 +4345,10 @@ int npc_reload(void) {
 
 	//Remove all npcs/mobs. [Skotlex]
 
+#if PACKETVER >= 20131223
+	npc_market_fromsql();
+#endif
+
 	iter = mapit_geteachiddb();
 	for( bl = (struct block_list*)mapit_first(iter); mapit_exists(iter); bl = (struct block_list*)mapit_next(iter) ) {
 		switch(bl->type) {
@@ -4130,6 +4422,10 @@ int npc_reload(void) {
 	if(!CheckForCharServer()){
 		ShowStatus("Event '"CL_WHITE"OnInterIfInit"CL_RESET"' executed with '"CL_WHITE"%d"CL_RESET"' NPCs.\n", npc_event_doall("OnInterIfInit"));
 	}
+
+#if PACKETVER >= 20131223
+	npc_market_checkall();
+#endif
 	return 0;
 }
 
@@ -4170,6 +4466,9 @@ void do_final_npc(void) {
 	ev_db->destroy(ev_db, NULL);
 	npcname_db->destroy(npcname_db, NULL);
 	npc_path_db->destroy(npc_path_db, NULL);
+#if PACKETVER >= 20131223
+	NPCMarketDB->destroy(NPCMarketDB, npc_market_free);
+#endif
 	ers_destroy(timer_event_ers);
 	npc_clearsrcfile();
 }
@@ -4226,6 +4525,10 @@ void do_init_npc(void){
 	ev_db = strdb_alloc((DBOptions)(DB_OPT_DUP_KEY|DB_OPT_RELEASE_DATA),2*NAME_LENGTH+2+1);
 	npcname_db = strdb_alloc(DB_OPT_BASE,NAME_LENGTH);
 	npc_path_db = strdb_alloc(DB_OPT_BASE|DB_OPT_DUP_KEY|DB_OPT_RELEASE_DATA,80);
+#if PACKETVER >= 20131223
+	NPCMarketDB = strdb_alloc(DB_OPT_BASE, NAME_LENGTH+1);
+	npc_market_fromsql();
+#endif
 
 	timer_event_ers = ers_new(sizeof(struct timer_event_data),"clif.c::timer_event_ers",ERS_OPT_NONE);
 
@@ -4248,6 +4551,10 @@ void do_init_npc(void){
 	memset(script_event, 0, sizeof(script_event));
 	npc_read_event_script();
 
+#if PACKETVER >= 20131223
+	npc_market_checkall();
+#endif
+
 	//Debug function to locate all endless loop warps.
 	if (battle_config.warp_point_debug)
 		npc_debug_warps();

+ 20 - 2
src/map/npc.h

@@ -15,13 +15,26 @@ struct view_data;
 struct npc_timerevent_list {
 	int timer,pos;
 };
+
 struct npc_label_list {
 	char name[NAME_LENGTH];
 	int pos;
 };
+
+/// Item list for NPC sell/buy list
 struct npc_item_list {
 	unsigned short nameid;
 	unsigned int value;
+#if PACKETVER >= 20131223
+	unsigned short qty; ///< Stock counter (Market shop)
+	uint8 flag; ///< 1: Item added by npcshopitem/npcshopadditem, force load! (Market shop)
+#endif
+};
+
+/// List of bought/sold item for NPC shops
+struct s_npc_buy_list {
+	unsigned short qty;		///< Amount of item will be bought
+	unsigned short nameid;	///< ID of item will be bought
 };
 
 struct npc_data {
@@ -126,8 +139,8 @@ int npc_click(struct map_session_data* sd, struct npc_data* nd);
 int 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);
-int npc_buylist(struct map_session_data* sd,int n, unsigned short* item_list);
-int npc_selllist(struct map_session_data* sd, int n, unsigned short* item_list);
+uint8 npc_buylist(struct map_session_data* sd, uint16 n, struct s_npc_buy_list *item_list);
+uint8 npc_selllist(struct map_session_data* sd, int n, unsigned short *item_list);
 void npc_parse_mob2(struct spawn_data* mob);
 bool npc_viewisid(const char * viewid);
 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);
@@ -179,6 +192,11 @@ extern struct npc_data* fake_nd;
 int npc_cashshop_buylist(struct map_session_data *sd, int points, int count, unsigned short* item_list);
 bool npc_shop_discount(enum npc_subtype type, bool discount);
 
+#if PACKETVER >= 20131223
+void npc_market_tosql(const char *exname, struct npc_item_list *list);
+void npc_market_delfromsql_(const char *exname, unsigned short nameid, bool clear);
+#endif
+
 #ifdef SECURE_NPCTIMEOUT
 	int npc_rr_secure_timeout_timer(int tid, unsigned int tick, int id, intptr_t data);
 #endif

+ 137 - 21
src/map/script.c

@@ -15628,28 +15628,45 @@ BUILDIN_FUNC(callshop)
 		return 0;
 	}
 	shopname = script_getstr(st, 2);
-	if( script_hasdata(st,3) )
+	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) )
-	{
+	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) ) {
 		ShowError("buildin_callshop: Shop [%s] not found (or NPC is not shop type)\n", shopname);
 		script_pushint(st,0);
 		return 1;
 	}
 
-	if( nd->subtype == NPCTYPE_SHOP || nd->subtype == NPCTYPE_ITEMSHOP || nd->subtype == NPCTYPE_POINTSHOP )
-	{
+	if (nd->subtype == NPCTYPE_SHOP || nd->subtype == NPCTYPE_ITEMSHOP || nd->subtype == NPCTYPE_POINTSHOP) {
 		// flag the user as using a valid script call for opening the shop (for floating NPCs)
 		sd->state.callshop = 1;
 
-		switch( flag )
-		{
+		switch (flag) {
 			case 1: npc_buysellsel(sd,nd->bl.id,0); break; //Buy window
 			case 2: npc_buysellsel(sd,nd->bl.id,1); break; //Sell window
 			default: clif_npcbuysell(sd,nd->bl.id); break; //Show menu
 		}
 	}
+#if PACKETVER >= 20131223
+	else if (nd->subtype == NPCTYPE_MARKETSHOP) {
+		unsigned short i;
+
+		for (i = 0; i < nd->u.shop.count; i++) {
+			if (nd->u.shop.shop_item[i].qty)
+				break;
+		}
+
+		if (i == nd->u.shop.count) {
+			clif_colormes(sd, color_table[COLOR_RED], msg_txt(sd, 534));
+			return false;
+		}
+
+		sd->npc_shopid = nd->bl.id;
+		clif_npc_market_open(sd, nd);
+		script_pushint(st,1);
+		return SCRIPT_CMD_SUCCESS;
+	}
+#endif
 	else
 		clif_cashshop_show(sd, nd);
 
@@ -15664,22 +15681,35 @@ BUILDIN_FUNC(npcshopitem)
 	struct npc_data* nd = npc_name2id(npcname);
 	int n, i;
 	int amount;
+	uint16 offs = 2;
 
-	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP ) )
-	{	//Not found.
+	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP ) ) { // Not found.
 		script_pushint(st,0);
 		return 0;
 	}
 
+#if PACKETVER >= 20131223
+	if (nd->subtype == NPCTYPE_MARKETSHOP) {
+		offs = 3;
+		npc_market_delfromsql_(nd->exname, 0, true);
+	}
+#endif
+
 	// get the count of new entries
-	amount = (script_lastdata(st)-2)/2;
+	amount = (script_lastdata(st)-2)/offs;
 
 	// generate new shop item list
 	RECREATE(nd->u.shop.shop_item, struct npc_item_list, amount);
-	for( n = 0, i = 3; n < amount; n++, i+=2 )
-	{
+	for (n = 0, i = 3; n < amount; n++, i+=offs) {
 		nd->u.shop.shop_item[n].nameid = script_getnum(st,i);
 		nd->u.shop.shop_item[n].value = script_getnum(st,i+1);
+#if PACKETVER >= 20131223
+		if (nd->subtype == NPCTYPE_MARKETSHOP) {
+			nd->u.shop.shop_item[n].qty = script_getnum(st,i+2);
+			nd->u.shop.shop_item[n].flag = 1;
+			npc_market_tosql(nd->exname, &nd->u.shop.shop_item[n]);
+		}
+#endif
 	}
 	nd->u.shop.count = n;
 
@@ -15693,19 +15723,47 @@ BUILDIN_FUNC(npcshopadditem)
 	struct npc_data* nd = npc_name2id(npcname);
 	int n, i;
 	int amount;
+	uint16 offs = 2;
 
-	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP ) )
-	{	//Not found.
+	if (!nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP)) { // Not found.
 		script_pushint(st,0);
 		return 0;
 	}
 
+	if (nd->subtype == NPCTYPE_MARKETSHOP)
+		offs = 3;
+
 	// get the count of new entries
-	amount = (script_lastdata(st)-2)/2;
+	amount = (script_lastdata(st)-2)/offs;
+
+#if PACKETVER >= 20131223
+	if (nd->subtype == NPCTYPE_MARKETSHOP) {
+		for (n = 0, i = 3; n < amount; n++, i += offs) {
+			uint16 nameid = script_getnum(st,i), j;
+
+			// Check existing entries
+			ARR_FIND(0, nd->u.shop.count, j, nd->u.shop.shop_item[j].nameid == nameid);
+			if (j == nd->u.shop.count) {
+				RECREATE(nd->u.shop.shop_item, struct npc_item_list, nd->u.shop.count+1);
+				j = nd->u.shop.count;
+				nd->u.shop.shop_item[j].flag = 1;
+				nd->u.shop.count++;
+			}
+
+			nd->u.shop.shop_item[j].nameid = nameid;
+			nd->u.shop.shop_item[j].value = script_getnum(st,i+1);
+			nd->u.shop.shop_item[j].qty = script_getnum(st,i+2);
+
+			npc_market_tosql(nd->exname, &nd->u.shop.shop_item[j]);
+		}
+		script_pushint(st,1);
+		return SCRIPT_CMD_SUCCESS;
+	}
+#endif
 
 	// append new items to existing shop item list
 	RECREATE(nd->u.shop.shop_item, struct npc_item_list, nd->u.shop.count+amount);
-	for( n = nd->u.shop.count, i = 3; n < nd->u.shop.count+amount; n++, i+=2 )
+	for (n = nd->u.shop.count, i = 3; n < nd->u.shop.count+amount; n++, i+=offs)
 	{
 		nd->u.shop.shop_item[n].nameid = script_getnum(st,i);
 		nd->u.shop.shop_item[n].value = script_getnum(st,i+1);
@@ -15724,7 +15782,7 @@ BUILDIN_FUNC(npcshopdelitem)
 	int amount;
 	int size;
 
-	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP ) ) { // Not found.
+	if (!nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP)) { // Not found.
 		script_pushint(st,0);
 		return 0;
 	}
@@ -15738,7 +15796,12 @@ BUILDIN_FUNC(npcshopdelitem)
 
 		ARR_FIND( 0, size, n, nd->u.shop.shop_item[n].nameid == nameid );
 		if( n < size ) {
-			memmove(&nd->u.shop.shop_item[n], &nd->u.shop.shop_item[n+1], sizeof(nd->u.shop.shop_item[0])*(size-n));
+			if (n+1 != size)
+				memmove(&nd->u.shop.shop_item[n], &nd->u.shop.shop_item[n+1], sizeof(nd->u.shop.shop_item[0])*(size-n));
+#if PACKETVER >= 20131223
+			if (nd->subtype == NPCTYPE_MARKETSHOP)
+				npc_market_delfromsql_(nd->exname, nameid, false);
+#endif
 			size--;
 		}
 	}
@@ -15750,7 +15813,10 @@ BUILDIN_FUNC(npcshopdelitem)
 	return SCRIPT_CMD_SUCCESS;
 }
 
-//Sets a script to attach to a shop npc.
+/**
+ * Sets a script to attach to a shop npc.
+ * npcshopattach "<npc_name>";
+ **/
 BUILDIN_FUNC(npcshopattach)
 {
 	const char* npcname = script_getstr(st,2);
@@ -15760,8 +15826,7 @@ BUILDIN_FUNC(npcshopattach)
 	if( script_hasdata(st,3) )
 		flag = script_getnum(st,3);
 
-	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP ) )
-	{	//Not found.
+	if (!nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP)) { // Not found.
 		script_pushint(st,0);
 		return 0;
 	}
@@ -19804,6 +19869,55 @@ BUILDIN_FUNC(mergeitem) {
 	return SCRIPT_CMD_SUCCESS;
 }
 
+/**
+ * Update an entry from NPC shop.
+ * npcshopupdate "<name>",<item_id>,<price>{,<stock>}
+ **/
+BUILDIN_FUNC(npcshopupdate) {
+	const char* npcname = script_getstr(st, 2);
+	struct npc_data* nd = npc_name2id(npcname);
+	uint16 nameid = script_getnum(st, 3);
+	int price = script_getnum(st, 4);
+	uint16 stock = 0;
+	int i;
+
+	if( !nd || ( nd->subtype != NPCTYPE_SHOP && nd->subtype != NPCTYPE_CASHSHOP && nd->subtype != NPCTYPE_ITEMSHOP && nd->subtype != NPCTYPE_POINTSHOP && nd->subtype != NPCTYPE_MARKETSHOP ) ) { // Not found.
+		script_pushint(st,0);
+		return SCRIPT_CMD_FAILURE;
+	}
+	
+	if (!nd->u.shop.count) {
+		ShowError("buildin_npcshopupdate: Attempt to update empty shop from '%s'.\n", nd->exname);
+		script_pushint(st,0);
+		return SCRIPT_CMD_FAILURE;
+	}
+
+	if (nd->subtype == NPCTYPE_MARKETSHOP) {
+		FETCH(5, stock);
+	}
+	else if ((price = cap_value(price, 0, INT_MAX)) == 0) { // Nothing to do here...
+		script_pushint(st,1);
+		return SCRIPT_CMD_SUCCESS;
+	}
+
+	for (i = 0; i < nd->u.shop.count; i++) {
+		if (nd->u.shop.shop_item[i].nameid == nameid) {
+
+			if (price != 0)
+				nd->u.shop.shop_item[i].value = price;
+#if PACKETVER >= 20131223
+			if (nd->subtype == NPCTYPE_MARKETSHOP) {
+				nd->u.shop.shop_item[i].qty = stock;
+				npc_market_tosql(nd->exname, &nd->u.shop.shop_item[i]);
+			}
+#endif
+		}
+	}
+
+	script_pushint(st,1);
+	return SCRIPT_CMD_SUCCESS;
+}
+
 #include "../custom/script.inc"
 
 // declarations that were supposed to be exported from npc_chat.c
@@ -20354,6 +20468,8 @@ struct script_function buildin_func[] = {
 	BUILDIN_DEF(countspiritball,"?"),
 	BUILDIN_DEF(mergeitem,"?"),
 
+	BUILDIN_DEF(npcshopupdate,"sii?"),
+
 #include "../custom/script_def.inc"
 
 	{NULL,NULL,NULL},