ソースを参照

Initial release of the cash shop sales (#1825)

Added a permission for the cashshop sales

Thanks to @Angelic234 and everyone else who tested this feature while it was sleeping waiting in a pull request.
Thanks to @aleos89, @secretdataz and @lighta for reviewing and commenting.
Lemongrass3110 8 年 前
コミット
aaa4ea919e

+ 1 - 0
conf/groups.conf

@@ -293,6 +293,7 @@ groups: (
 		item_unconditional: false
 		bypass_stat_onclone: true
 		bypass_max_stat: true
+		cashshop_sale: true
 		/* all_permission: true */
 	}
 }

+ 1 - 0
conf/inter_athena.conf

@@ -143,6 +143,7 @@ renewal-mob_skill_table: mob_skill_db_re
 mob_skill2_table: mob_skill_db2
 renewal-mob_skill2_table: mob_skill_db2_re
 mapreg_table: mapreg
+sales_table: sales
 vending_table: vendings
 vending_items_table: vending_items
 market_table: market

+ 1 - 0
db/import-tmpl/item_cash_db.txt

@@ -13,6 +13,7 @@
 //    5: Buff
 //    6: Heal
 //    7: Other
+//    8: Sale
 //
 // Price:
 //    Item cost, in cash points (#CASHPOINTS).

+ 13 - 1
db/packet_db.txt

@@ -2322,7 +2322,6 @@ packet_keys: 0x631C511C,0x111C111C,0x111C111C // [Shakto]
 0x08A4,36,storagepassword,2:4:20
 //New Packets
 //0x097E,12 //ZC_UPDATE_RANKING_POINT
-0x09B4,6,dull,0 //Cash Shop - Special Tab
 0x09CE,102,itemmonster,2
 0x09D4,2,npcshopclosed,0
 //NPC Market
@@ -2336,6 +2335,19 @@ packet_keys: 0x631C511C,0x111C111C,0x111C111C // [Shakto]
 0x098A,-1
 0x098D,-1,clanchat,2:4
 0x098E,-1
+// Sale
+0x09AC,-1,salesearch,2:4:8
+0x09AD,8
+0x09AE,17,saleadd,2:6:8:12:16
+0x09AF,4
+0x09B0,8,saleremove,2:6
+0x09B1,4
+0x09B2,8
+0x09B3,4
+0x09B4,6,saleopen,2
+0x09BC,6,saleclose,2
+0x09C3,8,salerefresh,2:6
+0x09C4,8
 
 // New Packet
 0x097A,-1		// ZC_ALL_QUEST_LIST2

+ 1 - 0
db/pre-re/item_cash_db.txt

@@ -13,6 +13,7 @@
 //    5: Buff
 //    6: Heal
 //    7: Other
+//    8: Sale
 //
 // Price:
 //    Item cost, in cash points (#CASHPOINTS).

+ 1 - 0
db/re/item_cash_db.txt

@@ -13,6 +13,7 @@
 //    5: Buff
 //    6: Heal
 //    7: Other
+//    8: Sale
 //
 // Price:
 //    Item cost, in cash points (#CASHPOINTS).

+ 12 - 0
sql-files/main.sql

@@ -813,6 +813,18 @@ CREATE TABLE IF NOT EXISTS `mercenary_owner` (
   PRIMARY KEY  (`char_id`)
 ) ENGINE=MyISAM;
 
+-- ----------------------------
+-- Table structure for `sales`
+-- ----------------------------
+
+CREATE TABLE IF NOT EXISTS `sales` (
+  `nameid` smallint(5) unsigned NOT NULL,
+  `start` datetime NOT NULL,
+  `end` datetime NOT NULL,
+  `amount` int(11) NOT NULL,
+  PRIMARY KEY (`nameid`)
+) ENGINE=MyISAM;
+
 --
 -- Table structure for table `sc_data`
 --

+ 11 - 0
sql-files/upgrades/upgrade_20170215.sql

@@ -0,0 +1,11 @@
+-- ----------------------------
+-- Table structure for `sales`
+-- ----------------------------
+
+CREATE TABLE IF NOT EXISTS `sales` (
+  `nameid` smallint(5) unsigned NOT NULL,
+  `start` datetime NOT NULL,
+  `end` datetime NOT NULL,
+  `amount` int(11) NOT NULL,
+  PRIMARY KEY (`nameid`)
+) ENGINE=MyISAM;

+ 3 - 0
src/common/mmo.h

@@ -31,6 +31,9 @@
 /// Check if the client needs delete_date as remaining time and not the actual delete_date (actually it was tested for clients since 2013)
 #define PACKETVER_CHAR_DELETEDATE (PACKETVER > 20130000 && PACKETVER < 20141016) || PACKETVER >= 20150826
 
+// Check if the specified packetvresion supports the cashshop sale system
+#define PACKETVER_SUPPORTS_SALES PACKETVER>=20131223
+
 ///Remove/Comment this line to disable sc_data saving. [Skotlex]
 #define ENABLE_SC_SAVING
 /** Remove/Comment this line to disable server-side hot-key saving support [Skotlex]

+ 380 - 5
src/map/cashshop.c

@@ -11,11 +11,15 @@
 #include <string.h> // memset
 #include <stdlib.h> // atoi
 
-struct cash_item_db cash_shop_items[CASHSHOP_TAB_SEARCH];
+struct cash_item_db cash_shop_items[CASHSHOP_TAB_MAX];
+#if PACKETVER_SUPPORTS_SALES
+struct sale_item_db sale_items;
+#endif
 bool cash_shop_defined = false;
 
 extern char item_cash_table[32];
 extern char item_cash2_table[32];
+extern char sales_table[32];
 
 /*
  * Reads one line from database and assigns it to RAM.
@@ -35,7 +39,7 @@ static bool cashshop_parse_dbrow(char* fields[], int columns, int current) {
 		return 0;
 	}
 
-	if( tab > CASHSHOP_TAB_SEARCH ){
+	if( tab >= CASHSHOP_TAB_MAX ){
 		ShowWarning( "cashshop_parse_dbrow: Invalid tab %d in line '%d', skipping...\n", tab, current );
 		return 0;
 	}else if( price < 1 ){
@@ -139,16 +143,314 @@ static int cashshop_read_db_sql( void ){
 	return 0;
 }
 
+#if PACKETVER_SUPPORTS_SALES
+static bool sale_parse_dbrow( char* fields[], int columns, int current ){
+	unsigned short nameid = atoi(fields[0]);
+	int start = atoi(fields[1]), end = atoi(fields[2]), amount = atoi(fields[3]), i;
+	time_t now = time(NULL);
+	struct sale_item_data* sale_item = NULL;
+
+	if( !itemdb_exists(nameid) ){
+		ShowWarning( "sale_parse_dbrow: Invalid ID %hu in line '%d', skipping...\n", nameid, current );
+		return false;
+	}
+
+	ARR_FIND( 0, cash_shop_items[CASHSHOP_TAB_SALE].count, i, cash_shop_items[CASHSHOP_TAB_SALE].item[i]->nameid == nameid );
+
+	if( i == cash_shop_items[CASHSHOP_TAB_SALE].count ){
+		ShowWarning( "sale_parse_dbrow: ID %hu is not registered in the limited tab in line '%d', skipping...\n", nameid, current );
+		return false;
+	}
+
+	// Check if the end is after the start
+	if( start >= end ){
+		ShowWarning( "sale_parse_dbrow: Sale for item %hu was ignored, because the timespan was not correct.\n", nameid );
+		return false;
+	}
+
+	// Check if it is already in the past
+	if( end < now ){
+		ShowWarning( "sale_parse_dbrow: An outdated sale for item %hu was ignored.\n", nameid );
+		return false;
+	}
+
+	// Check if there is already an entry
+	sale_item = sale_find_item(nameid,false);
+
+	if( sale_item == NULL ){
+		RECREATE(sale_items.item, struct sale_item_data *, ++sale_items.count);
+		CREATE(sale_items.item[sale_items.count - 1], struct sale_item_data, 1);
+		sale_item = sale_items.item[sale_items.count - 1];
+	}
+
+	sale_item->nameid = nameid;
+	sale_item->start = start;
+	sale_item->end = end;
+	sale_item->amount = amount;
+	sale_item->timer_start = INVALID_TIMER;
+	sale_item->timer_end = INVALID_TIMER;
+
+	return true;
+}
+
+static void sale_read_db_sql( void ){
+	uint32 lines = 0, count = 0;
+
+	if( SQL_ERROR == Sql_Query( mmysql_handle, "SELECT `nameid`, UNIX_TIMESTAMP(`start`), UNIX_TIMESTAMP(`end`), `amount` FROM `%s` WHERE `end` > now()", sales_table ) ){
+		Sql_ShowDebug(mmysql_handle);
+		return;
+	}
+
+	while( SQL_SUCCESS == Sql_NextRow(mmysql_handle) ){
+		char* str[4];
+		int i;
+
+		lines++;
+
+		for( i = 0; i < 4; i++ ){
+			Sql_GetData( mmysql_handle, i, &str[i], NULL );
+
+			if( str[i] == NULL ){
+				str[i] = "";
+			}
+		}
+
+		if( !sale_parse_dbrow( str, 4, lines ) ){
+			ShowError( "sale_read_db_sql: Cannot process table '%s' at line '%d', skipping...\n", sales_table, lines );
+			continue;
+		}
+
+		count++;
+	}
+
+	Sql_FreeResult(mmysql_handle);
+
+	ShowStatus( "Done reading '"CL_WHITE"%lu"CL_RESET"' entries in '"CL_WHITE"%s"CL_RESET"'.\n", count, sales_table );
+}
+
+static int sale_end_timer( int tid, unsigned int tick, int id, intptr_t data ){
+	struct sale_item_data* sale_item = (struct sale_item_data*)data;
+
+	// Remove the timer so the sale end is not sent out again
+	delete_timer( sale_item->timer_end, sale_end_timer );
+	sale_item->timer_end = INVALID_TIMER;
+	
+	clif_sale_end( sale_item, NULL, ALL_CLIENT );
+
+	sale_remove_item( sale_item->nameid );
+
+	return 1;
+}
+
+static int sale_start_timer( int tid, unsigned int tick, int id, intptr_t data ){
+	struct sale_item_data* sale_item = (struct sale_item_data*)data;
+
+	clif_sale_start( sale_item, NULL, ALL_CLIENT );
+	clif_sale_amount( sale_item, NULL, ALL_CLIENT );
+
+	// Clear the start timer
+	if( sale_item->timer_start != INVALID_TIMER ){
+		delete_timer( sale_item->timer_start, sale_start_timer );
+		sale_item->timer_start = INVALID_TIMER;
+	}
+
+	// Init sale end
+	sale_item->timer_end = add_timer( gettick() + (unsigned int)( sale_item->end - time(NULL) ) * 1000, sale_end_timer, 0, (intptr_t)sale_item );
+
+	return 1;
+}
+
+enum e_sale_add_result sale_add_item( uint16 nameid, int32 count, time_t from, time_t to ){
+	int i;
+	struct sale_item_data* sale_item;
+
+	// Check if the item exists in the sales tab
+	ARR_FIND( 0, cash_shop_items[CASHSHOP_TAB_SALE].count, i, cash_shop_items[CASHSHOP_TAB_SALE].item[i]->nameid == nameid );
+
+	// Item does not exist in the sales tab
+	if( i == cash_shop_items[CASHSHOP_TAB_SALE].count ){
+		return SALE_ADD_FAILED;
+	}
+
+	// Adding a sale in the past is not possible
+	if( from < time(NULL) ){
+		return SALE_ADD_FAILED;
+	}
+
+	// The end has to be after the start
+	if( from >= to ){
+		return SALE_ADD_FAILED;
+	}
+
+	// Amount has to be positive - this should be limited from the client too
+	if( count == 0 ){
+		return SALE_ADD_FAILED;
+	}
+
+	// Check if a sale of this item already exists
+	if( sale_find_item(nameid, false) ){
+		return SALE_ADD_DUPLICATE;
+	}
+	
+	if( SQL_ERROR == Sql_Query(mmysql_handle, "INSERT INTO `%s`(`nameid`,`start`,`end`,`amount`) VALUES ( '%d', FROM_UNIXTIME(%d), FROM_UNIXTIME(%d), '%d' )", sales_table, nameid, (uint32)from, (uint32)to, count) ){
+		Sql_ShowDebug(mmysql_handle);
+		return SALE_ADD_FAILED;
+	}
+
+	RECREATE(sale_items.item, struct sale_item_data *, ++sale_items.count);
+	CREATE(sale_items.item[sale_items.count - 1], struct sale_item_data, 1);
+	sale_item = sale_items.item[sale_items.count - 1];
+
+	sale_item->nameid = nameid;
+	sale_item->start = from;
+	sale_item->end = to;
+	sale_item->amount = count;
+	sale_item->timer_start = add_timer( gettick() + (unsigned int)(from - time(NULL)) * 1000, sale_start_timer, 0, (intptr_t)sale_item );
+	sale_item->timer_end = INVALID_TIMER;
+
+	return SALE_ADD_SUCCESS;
+}
+
+bool sale_remove_item( uint16 nameid ){
+	struct sale_item_data* sale_item;
+	int i;
+
+	// Check if there is an entry for this item id
+	if( !sale_find_item(nameid, false) ){
+		return false;
+	}
+
+	// Delete it from the database
+	if( SQL_ERROR == Sql_Query(mmysql_handle, "DELETE FROM `%s` WHERE `nameid` = '%d'", sales_table, nameid ) ){
+		Sql_ShowDebug(mmysql_handle);
+		return false;
+	}
+
+	// Check if the sale is currently running
+	sale_item = sale_find_item(nameid, true);
+
+	if( sale_item != NULL && sale_item->timer_end != INVALID_TIMER ){
+		// Notify all clients that the sale has ended
+		clif_sale_end(sale_item, NULL, ALL_CLIENT);
+	}
+
+	if( sale_item->timer_start != INVALID_TIMER ){
+		delete_timer(sale_item->timer_start, sale_start_timer);
+		sale_item->timer_start = INVALID_TIMER;
+	}
+
+	if( sale_item->timer_end != INVALID_TIMER ){
+		delete_timer(sale_item->timer_end, sale_end_timer);
+		sale_item->timer_end = INVALID_TIMER;
+	}
+
+	// Find the original pointer in the array
+	ARR_FIND( 0, sale_items.count, i, sale_items.item[i] == sale_item );
+
+	// Is there still any entry left?
+	if( --sale_items.count > 0 ){
+		// fill the hole by moving the rest
+		for( ; i < sale_items.count; i++ ){
+			memcpy( sale_items.item[i], sale_items.item[i + 1], sizeof(struct sale_item_data) );
+		}
+
+		aFree(sale_items.item[i]);
+
+		RECREATE(sale_items.item, struct sale_item_data *, sale_items.count);
+	}else{
+		aFree(sale_items.item[0]);
+		aFree(sale_items.item);
+		sale_items.item = NULL;
+	}
+
+	return true;
+}
+
+struct sale_item_data* sale_find_item( uint16 nameid, bool onsale ){
+	int i;
+	struct sale_item_data* sale_item;
+	time_t now = time(NULL);
+
+	ARR_FIND( 0, sale_items.count, i, sale_items.item[i]->nameid == nameid );
+
+	// No item with the specified item id was found
+	if( i == sale_items.count ){
+		return NULL;
+	}
+
+	sale_item = sale_items.item[i];
+
+	// No need to check any further
+	if( !onsale ){
+		return sale_item;
+	}
+
+	// The sale is in the future
+	if( sale_items.item[i]->start > now ){
+		return NULL;
+	}
+
+	// The sale was in the past
+	if( sale_items.item[i]->end < now ){
+		return NULL;
+	}
+
+	// The amount has been used up already
+	if( sale_items.item[i]->amount == 0 ){
+		return NULL;
+	}
+
+	// Return the sale item
+	return sale_items.item[i];
+}
+
+void sale_notify_login( struct map_session_data* sd ){
+	int i;
+
+	for( i = 0; i < sale_items.count; i++ ){
+		if( sale_items.item[i]->timer_end != INVALID_TIMER ){
+			clif_sale_start( sale_items.item[i], &sd->bl, SELF );
+			clif_sale_amount( sale_items.item[i], &sd->bl, SELF );
+		}
+	}
+}
+#endif
+
 /*
  * Determines whether to read TXT or SQL database
  * based on 'db_use_sqldbs' in conf/map_athena.conf.
  */
 static void cashshop_read_db( void ){
+#if PACKETVER_SUPPORTS_SALES
+	int i;
+	time_t now = time(NULL);
+#endif
+
 	if( db_use_sqldbs ){
 		cashshop_read_db_sql();
 	} else {
 		cashshop_read_db_txt();
 	}
+
+#if PACKETVER_SUPPORTS_SALES
+	sale_read_db_sql();
+
+	// Clean outdated sales
+	if( SQL_ERROR == Sql_Query(mmysql_handle, "DELETE FROM `%s` WHERE `end` < FROM_UNIXTIME(%d)", sales_table, (uint32)now ) ){
+		Sql_ShowDebug(mmysql_handle);
+	}
+
+	// Init next sale start, if there is any
+	for( i = 0; i < sale_items.count; i++ ){
+		struct sale_item_data* it = sale_items.item[i];
+
+		if( it->start > now ){
+			it->timer_start = add_timer( gettick() + (unsigned int)( it->start - time(NULL) ) * 1000, sale_start_timer, 0, (intptr_t)it );
+		}else{
+			sale_start_timer( 0, gettick(), 0, (intptr_t)it );
+		}
+	}
+#endif
 }
 
 /** Attempts to purchase a cashshop item from the list.
@@ -164,6 +466,9 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 	uint32 totalcash = 0;
 	uint32 totalweight = 0;
 	int i,new_;
+#if PACKETVER_SUPPORTS_SALES
+	struct sale_item_data* sale;
+#endif
 
 	if( sd == NULL || item_list == NULL || !cash_shop_defined){
 		clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_UNKNOWN );
@@ -178,10 +483,10 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 	for( i = 0; i < n; ++i ){
 		unsigned short nameid = *( item_list + i * 5 );
 		uint32 quantity = *( item_list + i * 5 + 2 );
-		uint16 tab = *( item_list + i * 5 + 4 );
+		uint8 tab = (uint8)*( item_list + i * 5 + 4 );
 		int j;
 
-		if( tab > CASHSHOP_TAB_SEARCH ){
+		if( tab >= CASHSHOP_TAB_MAX ){
 			clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKNOWN );
 			return false;
 		}
@@ -203,6 +508,32 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 			quantity = *( item_list + i * 5 + 2 ) = 1;
 		}
 
+		if( quantity > 99 ){
+			// Client blocks buying more than 99 items of the same type at the same time, this means someone forged a packet with a higher quantity
+			clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKNOWN );
+			return false;
+		}
+
+#if PACKETVER_SUPPORTS_SALES
+		if( tab == CASHSHOP_TAB_SALE ){
+			sale = sale_find_item( nameid, true );
+
+			if( sale == NULL ){
+				// Client tried to buy an item from sale that was not even on sale
+				clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKNOWN );
+				return false;
+			}
+
+			if( sale->amount < quantity ){
+				// Client tried to buy a higher quantity than is available
+				clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKNOWN );
+				// Maybe he did not get refreshed in time -> do it now
+				clif_sale_amount( sale, &sd->bl, SELF );
+				return false;
+			}
+		}
+#endif
+
 		switch( pc_checkadditem( sd, nameid, quantity ) ){
 			case CHKADDITEM_EXIST:
 				break;
@@ -236,6 +567,7 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 	for( i = 0; i < n; ++i ){
 		unsigned short nameid = *( item_list + i * 5 );
 		uint32 quantity = *( item_list + i * 5 + 2 );
+		uint16 tab = *(item_list + i * 5 + 4);
 		struct item_data *id = itemdb_search(nameid);
 
 		if (!id)
@@ -270,6 +602,24 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 						clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_RUNE_OVERCOUNT );
 						return false;
 				}
+
+#if PACKETVER_SUPPORTS_SALES
+				if( tab == CASHSHOP_TAB_SALE ){
+					uint32 new_amount = sale->amount - get_amt;
+
+					if( new_amount == 0 ){
+						sale_remove_item(sale->nameid);
+					}else{
+						if( SQL_ERROR == Sql_Query( mmysql_handle, "UPDATE `%s` SET `amount` = '%d' WHERE `nameid` = '%d'", sales_table, new_amount, nameid ) ){
+							Sql_ShowDebug(mmysql_handle);
+						}
+
+						sale->amount = new_amount;
+
+						clif_sale_amount(sale, NULL, ALL_CLIENT);
+					}
+				}
+#endif
 			}
 		}
 	}
@@ -293,13 +643,38 @@ void cashshop_reloaddb( void ){
 void do_final_cashshop( void ){
 	int tab, i;
 
-	for( tab = CASHSHOP_TAB_NEW; tab < CASHSHOP_TAB_SEARCH; tab++ ){
+	for( tab = CASHSHOP_TAB_NEW; tab < CASHSHOP_TAB_MAX; tab++ ){
 		for( i = 0; i < cash_shop_items[tab].count; i++ ){
 			aFree( cash_shop_items[tab].item[i] );
 		}
 		aFree( cash_shop_items[tab].item );
 	}
 	memset( cash_shop_items, 0, sizeof( cash_shop_items ) );
+
+#if PACKETVER_SUPPORTS_SALES
+	if( sale_items.count > 0 ){
+		for( i = 0; i < sale_items.count; i++ ){
+			struct sale_item_data* it = sale_items.item[i];
+
+			if( it->timer_start != INVALID_TIMER ){
+				delete_timer( it->timer_start, sale_start_timer );
+				it->timer_start = INVALID_TIMER;
+			}
+
+			if( it->timer_end != INVALID_TIMER ){
+				delete_timer( it->timer_end, sale_end_timer );
+				it->timer_end = INVALID_TIMER;
+			}
+
+			aFree(it);
+		}
+
+		aFree(sale_items.item);
+
+		sale_items.item = NULL;
+		sale_items.count = 0;
+	}
+#endif
 }
 
 /*

+ 44 - 9
src/map/cashshop.h

@@ -16,14 +16,17 @@ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, u
 enum CASH_SHOP_TAB_CODE
 {
 	CASHSHOP_TAB_NEW =  0x0,
-	CASHSHOP_TAB_POPULAR =  0x1,
-	CASHSHOP_TAB_LIMITED =  0x2,
-	CASHSHOP_TAB_RENTAL =  0x3,
-	CASHSHOP_TAB_PERPETUITY =  0x4,
-	CASHSHOP_TAB_BUFF =  0x5,
-	CASHSHOP_TAB_RECOVERY =  0x6,
-	CASHSHOP_TAB_ETC =  0x7,
-	CASHSHOP_TAB_SEARCH =  0x8
+	CASHSHOP_TAB_POPULAR,
+	CASHSHOP_TAB_LIMITED,
+	CASHSHOP_TAB_RENTAL,
+	CASHSHOP_TAB_PERPETUITY,
+	CASHSHOP_TAB_BUFF,
+	CASHSHOP_TAB_RECOVERY,
+	CASHSHOP_TAB_ETC,
+#if PACKETVER_SUPPORTS_SALES
+	CASHSHOP_TAB_SALE,
+#endif
+	CASHSHOP_TAB_MAX
 };
 
 // PACKET_ZC_SE_PC_BUY_CASHITEM_RESULT
@@ -54,7 +57,39 @@ struct cash_item_db{
 	uint32 count;
 };
 
-extern struct cash_item_db cash_shop_items[CASHSHOP_TAB_SEARCH];
+extern struct cash_item_db cash_shop_items[CASHSHOP_TAB_MAX];
 extern bool cash_shop_defined;
 
+enum e_sale_add_result {
+	SALE_ADD_SUCCESS = 0,
+	SALE_ADD_FAILED = 1,
+	SALE_ADD_DUPLICATE = 2
+};
+
+struct sale_item_data{
+	// Data
+	uint16 nameid;
+	time_t start;
+	time_t end;
+	uint32 amount;
+
+	// Timers
+	int timer_start;
+	int timer_end;
+};
+
+struct sale_item_db{
+	struct sale_item_data** item;
+	uint32 count;
+};
+
+#if PACKETVER_SUPPORTS_SALES
+extern struct sale_item_db sale_items;
+
+struct sale_item_data* sale_find_item(uint16 nameid, bool onsale);
+enum e_sale_add_result sale_add_item(uint16 nameid, int32 count, time_t from, time_t to);
+bool sale_remove_item(uint16 nameid);
+void sale_notify_login( struct map_session_data* sd );
+#endif
+
 #endif /* _CASHSHOP_H_ */

+ 270 - 5
src/map/clif.c

@@ -15403,7 +15403,7 @@ void clif_parse_CashShopReqTab(int fd, struct map_session_data *sd) {
 	short tab = RFIFOW(fd, packet_db[sd->packet_ver][RFIFOW(fd,0)].pos[0]);
 	int j;
 
-	if( tab < 0 || tab > CASHSHOP_TAB_SEARCH )
+	if( tab < 0 || tab >= CASHSHOP_TAB_MAX )
 		return;
 
 	WFIFOHEAD(fd, 10 + ( cash_shop_items[tab].count * 6 ) );
@@ -15425,7 +15425,7 @@ void clif_parse_CashShopReqTab(int fd, struct map_session_data *sd) {
 void clif_cashshop_list( int fd ){
 	int tab;
 
-	for( tab = CASHSHOP_TAB_NEW; tab < CASHSHOP_TAB_SEARCH; tab++ ){
+	for( tab = CASHSHOP_TAB_NEW; tab < CASHSHOP_TAB_MAX; tab++ ){
 		int length = 8 + cash_shop_items[tab].count * 6;
 		int i, offset;
 
@@ -15448,6 +15448,9 @@ void clif_cashshop_list( int fd ){
 void clif_parse_cashshop_list_request( int fd, struct map_session_data* sd ){
 	if( !sd->status.cashshop_sent ) {
 		clif_cashshop_list( fd );
+#if PACKETVER_SUPPORTS_SALES
+		sale_notify_login(sd);
+#endif
 		sd->status.cashshop_sent = true;
 	}
 }
@@ -18872,6 +18875,261 @@ void clif_hat_effect_single( struct map_session_data* sd, uint16 effectId, bool
 #endif
 }
 
+
+/// Notify the client that a sale has started
+/// 09b2 <item id>.W <remaining time>.L (ZC_NOTIFY_BARGAIN_SALE_SELLING)
+void clif_sale_start( struct sale_item_data* sale_item, struct block_list* bl, enum send_target target ){
+#if PACKETVER_SUPPORTS_SALES
+	unsigned char buf[8];
+
+	WBUFW(buf, 0) = 0x9b2;
+	WBUFW(buf, 2) = sale_item->nameid;
+	WBUFL(buf, 4) = (uint32)(sale_item->end - time(NULL)); // time in S
+
+	clif_send(buf, 8, bl, target);
+#endif
+}
+
+/// Notify the clien that a sale has ended
+/// 09b3 <item id>.W (ZC_NOTIFY_BARGAIN_SALE_CLOSE)
+void clif_sale_end( struct sale_item_data* sale_item, struct block_list* bl, enum send_target target ){
+#if PACKETVER_SUPPORTS_SALES
+	unsigned char buf[4];
+
+	WBUFW(buf, 0) = 0x9b3;
+	WBUFW(buf, 2) = sale_item->nameid;
+
+	clif_send(buf, 4, bl, target);
+#endif
+}
+
+/// Update the remaining amount of a sale item.
+/// 09c4 <item id>.W <amount>.L (ZC_ACK_COUNT_BARGAIN_SALE_ITEM)
+void clif_sale_amount( struct sale_item_data* sale_item, struct block_list* bl, enum send_target target ){
+#if PACKETVER_SUPPORTS_SALES
+	unsigned char buf[8];
+
+	WBUFW(buf, 0) = 0x9c4;
+	WBUFW(buf, 2) = sale_item->nameid;
+	WBUFL(buf, 4) = sale_item->amount;
+
+	clif_send(buf, 8, bl, target);
+#endif
+}
+
+/// The client requested a refresh of the current remaining count of a sale item
+/// 09ac <account id>.L <item id>.W (CZ_REQ_CASH_BARGAIN_SALE_ITEM_INFO)
+void clif_parse_sale_refresh( int fd, struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	struct sale_item_data* sale;
+
+	if( RFIFOL(fd, 2) != sd->status.account_id ){
+		return;
+	}
+
+	sale = sale_find_item( RFIFOW(fd, 6), true );
+
+	if( sale == NULL ){
+		return;
+	}
+
+	clif_sale_amount(sale, &sd->bl, SELF);
+#endif
+}
+
+/// Opens the sale administration window on the client
+/// 09b5 (ZC_OPEN_BARGAIN_SALE_TOOL)
+void clif_sale_open( struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	int fd = sd->fd;
+
+	// TODO: do we want state tracking?
+
+	WFIFOHEAD(fd, 2);
+	WFIFOW(fd, 0) = 0x9b5;
+	WFIFOSET(fd, 2);
+#endif
+}
+
+/// Client request to open the sale administration window.
+/// This is sent by /limitedsale
+/// 09b4 <account id>.L (CZ_OPEN_BARGAIN_SALE_TOOL)
+void clif_parse_sale_open( int fd, struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	nullpo_retv(sd);
+
+	if( RFIFOL(fd, 2) != sd->status.account_id ){
+		return;
+	}
+
+	if( !pc_has_permission( sd, PC_PERM_CASHSHOP_SALE ) ){
+		return;
+	}
+
+	clif_sale_open(sd);
+#endif
+}
+
+/// Closes the sale administration window on the client.
+/// 09bd (ZC_CLOSE_BARGAIN_SALE_TOOL)
+void clif_sale_close(struct map_session_data* sd) {
+#if PACKETVER_SUPPORTS_SALES
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, 2);
+	WFIFOW(fd, 0) = 0x9bd;
+	WFIFOSET(fd, 2);
+#endif
+}
+
+/// Client request to close the sale administration window.
+/// 09bc (CZ_CLOSE_BARGAIN_SALE_TOOL)
+void clif_parse_sale_close(int fd, struct map_session_data* sd) {
+#if PACKETVER_SUPPORTS_SALES
+	nullpo_retv(sd);
+
+	if( RFIFOL(fd, 2) != sd->status.account_id ){
+		return;
+	}
+
+	// TODO: do we want state tracking?
+
+	clif_sale_close(sd);
+#endif
+}
+
+/// Reply to a item search request for item sale administration.
+/// 09ad <result>.W <item id>.W <price>.L (ZC_ACK_CASH_BARGAIN_SALE_ITEM_INFO)
+void clif_sale_search_reply( struct map_session_data* sd, struct cash_item_data* item ){
+#if PACKETVER_SUPPORTS_SALES
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, 10);
+	WFIFOW(fd, 0) = 0x9ad;
+	if( item != NULL ){
+		WFIFOW(fd, 2) = 0;
+		WFIFOW(fd, 4) = item->nameid;
+		WFIFOL(fd, 6) = item->price;
+	}else{
+		WFIFOW(fd, 2) = 1;
+		WFIFOW(fd, 4) = 0;
+		WFIFOL(fd, 6) = 0;
+	}
+	WFIFOSET(fd, 10);
+#endif
+}
+
+/// Search request for an item sale administration.
+/// 09ac <length>.W <account id>.L <item name>.?B (CZ_REQ_CASH_BARGAIN_SALE_ITEM_INFO)
+void clif_parse_sale_search( int fd, struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	char item_name[ITEM_NAME_LENGTH];
+	struct item_data *id = NULL;
+
+	nullpo_retv(sd);
+
+	if( RFIFOL(fd, 4) != sd->status.account_id ){
+		return;
+	}
+
+	if( !pc_has_permission( sd, PC_PERM_CASHSHOP_SALE ) ){
+		return;
+	}
+
+	safestrncpy( item_name, RFIFOCP(fd, 8), min(RFIFOW(fd, 2) - 7, ITEM_NAME_LENGTH) );
+
+	id = itemdb_searchname(item_name);
+
+	if( id ){
+		int i;
+
+		for( i = 0; i < cash_shop_items[CASHSHOP_TAB_SALE].count; i++ ){
+			if( cash_shop_items[CASHSHOP_TAB_SALE].item[i]->nameid == id->nameid ){
+				clif_sale_search_reply( sd, cash_shop_items[CASHSHOP_TAB_SALE].item[i] );
+				return;
+			}
+		}
+	}
+
+	// not found
+	clif_sale_search_reply( sd, NULL );
+#endif
+}
+
+/// Reply if an item was successfully put on sale or not.
+/// 09af <result>.W (ZC_ACK_APPLY_BARGAIN_SALE_ITEM)
+void clif_sale_add_reply( struct map_session_data* sd, enum e_sale_add_result result ){
+#if PACKETVER_SUPPORTS_SALES
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, 4);
+	WFIFOW(fd, 0) = 0x9af;
+	WFIFOW(fd, 2) = (uint16)result;
+	WFIFOSET(fd, 4);
+#endif
+}
+
+/// A client request to put an item on sale.
+/// 09ae <account id>.L <item id>.W <amount>.L <start time>.L <hours on sale>.B (CZ_REQ_APPLY_BARGAIN_SALE_ITEM)
+void clif_parse_sale_add( int fd, struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	int32 count;
+	int16 nameid;
+	int startTime;
+	int endTime;
+	uint8 sellingHours;
+
+	nullpo_retv(sd);
+
+	if( RFIFOL(fd, 2) != sd->status.account_id ){
+		return;
+	}
+
+	if( !pc_has_permission( sd, PC_PERM_CASHSHOP_SALE ) ){
+		return;
+	}
+
+	nameid = RFIFOW(fd, 6);
+	count = RFIFOL(fd, 8);
+	startTime = RFIFOL(fd, 12);
+	sellingHours = RFIFOB(fd, 16);
+	endTime = startTime + sellingHours * 60 * 60;
+
+	clif_sale_add_reply( sd, sale_add_item(nameid,count,startTime,endTime) );
+#endif
+}
+
+/// Reply to an item removal from sale.
+/// 09b1 <result>.W (ZC_ACK_REMOVE_BARGAIN_SALE_ITEM)
+void clif_sale_remove_reply( struct map_session_data* sd, bool failed ){
+#if PACKETVER_SUPPORTS_SALES
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, 4);
+	WFIFOW(fd, 0) = 0x9b1;
+	WFIFOW(fd, 2) = failed;
+	WFIFOSET(fd, 4);
+#endif
+}
+
+/// Request to remove an item from sale.
+/// 09b0 <account id>.L <item id>.W (CZ_REQ_REMOVE_BARGAIN_SALE_ITEM)
+void clif_parse_sale_remove( int fd, struct map_session_data* sd ){
+#if PACKETVER_SUPPORTS_SALES
+	nullpo_retv(sd);
+
+	if( RFIFOL(fd, 2) != sd->status.account_id ){
+		return;
+	}
+
+	if( !pc_has_permission( sd, PC_PERM_CASHSHOP_SALE ) ){
+		return;
+	}
+
+	clif_sale_remove_reply(sd, !sale_remove_item(RFIFOW(fd, 6)));
+#endif
+}
+
 /*==========================================
  * Main client packet processing function
  *------------------------------------------*/
@@ -19258,10 +19516,10 @@ void packetdb_readdb(bool reload)
 	//#0x0980
 		7,  0,  0, 29, 28,  0,  0,  0,  6,  2, -1,  0,  0, -1, -1,  0,
 		31, 0,  0,  0,  0,  0,  0, -1,  8, 11,  9,  8,  0,  0,  0, 22,
-		0,  0,  0,  0,  0,  0, 12, 10, 14, 10, 14,  6,  0,  0,  0,  0,
-		0,  0,  0,  0,  0,  0,  6,  4,  6,  4,  0,  0,  0,  0,  0,  0,
+		0,  0,  0,  0,  0,  0, 12, 10, 14, 10, 14,  6, -1,  8, 17,  4,
+		8,  4,  8,  4,  6,  0,  6,  4,  6,  4,  0,  0,  6,  0,  0,  0,
 	//#0x09C0
-		0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 23,  17,  0,  0,102,  0,
+		0,  0,  0,  8,  8,  0,  0,  0,  0,  0, 23,  17,  0,  0,102,  0,
 		0,  0,  0,  0,  2,  0, -1, -1,  2,  0,  0,  -1,  -1,  -1,  0,  7,
 		0,  0,  0,  0,  0,  18,  22,  3, 11,  0, 11, -1,  0,  3, 11,  0,
 		0, 11, 12, 11,  0,  0,  0,  75,  -1,143,  0,  0,  0,  -1,  -1,  -1,
@@ -19517,6 +19775,13 @@ void packetdb_readdb(bool reload)
 		{ clif_parse_SelectCart, "selectcart" },
 		// Clan System
 		{ clif_parse_clan_chat, "clanchat" },
+		// Sale
+		{ clif_parse_sale_search, "salesearch" },
+		{ clif_parse_sale_add, "saleadd" },
+		{ clif_parse_sale_remove, "saleremove" },
+		{ clif_parse_sale_open, "saleopen" },
+		{ clif_parse_sale_close, "saleclose" },
+		{ clif_parse_sale_refresh, "salerefresh" },
 		{NULL,NULL}
 	};
 	struct {

+ 6 - 0
src/map/clif.h

@@ -31,6 +31,7 @@ struct battleground_data;
 struct quest;
 struct party_booking_ad_info;
 enum e_party_member_withdraw;
+struct sale_item_data;
 #include <stdarg.h>
 
 enum { // packet DB
@@ -983,6 +984,11 @@ void clif_clan_message(struct clan *clan,const char *mes,int len);
 void clif_clan_onlinecount( struct clan* clan );
 void clif_clan_leave( struct map_session_data* sd );
 
+// Bargain Tool
+void clif_sale_start(struct sale_item_data* sale_item, struct block_list* bl, enum send_target target);
+void clif_sale_end(struct sale_item_data* sale_item, struct block_list* bl, enum send_target target);
+void clif_sale_amount(struct sale_item_data* sale_item, struct block_list* bl, enum send_target target);
+
 /**
  * Color Table
  **/

+ 3 - 0
src/map/map.c

@@ -74,6 +74,7 @@ char mob2_table[32] = "mob_db2";
 char mob_skill_table[32] = "mob_skill_db";
 char mob_skill2_table[32] = "mob_skill_db2";
 #endif
+char sales_table[32] = "sales";
 char vendings_table[32] = "vendings";
 char vending_items_table[32] = "vending_items";
 char market_table[32] = "market";
@@ -4022,6 +4023,8 @@ int inter_config_read(char *cfgName)
 			strcpy(roulette_table, w2);
 		else if (strcmpi(w1, "market_table") == 0)
 			strcpy(market_table, w2);
+		else if (strcmpi(w1, "sales_table") == 0)
+			strcpy(sales_table, w2);
 		else
 		//Map Server SQL DB
 		if(strcmpi(w1,"map_server_ip")==0)

+ 2 - 0
src/map/pc_groups.h

@@ -49,6 +49,7 @@ enum e_pc_permission {
 	PC_PERM_ENABLE_COMMAND      = 0x01000000,
 	PC_PERM_BYPASS_STAT_ONCLONE = 0x02000000,
 	PC_PERM_BYPASS_MAX_STAT     = 0x04000000,
+	PC_PERM_CASHSHOP_SALE		= 0x08000000,
 	//.. add other here
 	PC_PERM_ALLPERMISSION       = 0xFFFFFFFF,
 };
@@ -84,6 +85,7 @@ static const struct {
 	{ "command_enable",PC_PERM_ENABLE_COMMAND },
 	{ "bypass_stat_onclone",PC_PERM_BYPASS_STAT_ONCLONE },
 	{ "bypass_max_stat",PC_PERM_BYPASS_MAX_STAT },
+	{ "cashshop_sale", PC_PERM_CASHSHOP_SALE },
 	{ "all_permission", PC_PERM_ALLPERMISSION },
 };