From aaa4ea919e79293f33c7f4e7cdde8ceb00ed9f2f Mon Sep 17 00:00:00 2001 From: Lemongrass3110 Date: Wed, 15 Feb 2017 14:04:18 +0100 Subject: [PATCH] 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. --- conf/groups.conf | 1 + conf/inter_athena.conf | 1 + db/import-tmpl/item_cash_db.txt | 1 + db/packet_db.txt | 14 +- db/pre-re/item_cash_db.txt | 1 + db/re/item_cash_db.txt | 1 + sql-files/main.sql | 12 + sql-files/upgrades/upgrade_20170215.sql | 11 + src/common/mmo.h | 3 + src/map/cashshop.c | 385 +++++++++++++++++++++++- src/map/cashshop.h | 53 +++- src/map/clif.c | 275 ++++++++++++++++- src/map/clif.h | 6 + src/map/map.c | 3 + src/map/pc_groups.h | 2 + 15 files changed, 749 insertions(+), 20 deletions(-) create mode 100644 sql-files/upgrades/upgrade_20170215.sql diff --git a/conf/groups.conf b/conf/groups.conf index 627076370a..d42fb8a510 100644 --- a/conf/groups.conf +++ b/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 */ } } diff --git a/conf/inter_athena.conf b/conf/inter_athena.conf index 87de39ae4d..2f938ebfcf 100644 --- a/conf/inter_athena.conf +++ b/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 diff --git a/db/import-tmpl/item_cash_db.txt b/db/import-tmpl/item_cash_db.txt index f6a62d9b82..63489a8c90 100644 --- a/db/import-tmpl/item_cash_db.txt +++ b/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). diff --git a/db/packet_db.txt b/db/packet_db.txt index ff06821a92..4f04087c15 100644 --- a/db/packet_db.txt +++ b/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 diff --git a/db/pre-re/item_cash_db.txt b/db/pre-re/item_cash_db.txt index f6a62d9b82..63489a8c90 100644 --- a/db/pre-re/item_cash_db.txt +++ b/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). diff --git a/db/re/item_cash_db.txt b/db/re/item_cash_db.txt index f6a62d9b82..63489a8c90 100644 --- a/db/re/item_cash_db.txt +++ b/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). diff --git a/sql-files/main.sql b/sql-files/main.sql index 688db9013f..5809ae64e3 100644 --- a/sql-files/main.sql +++ b/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` -- diff --git a/sql-files/upgrades/upgrade_20170215.sql b/sql-files/upgrades/upgrade_20170215.sql new file mode 100644 index 0000000000..f2ff279286 --- /dev/null +++ b/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; diff --git a/src/common/mmo.h b/src/common/mmo.h index 0ed3885213..66623376ba 100644 --- a/src/common/mmo.h +++ b/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] diff --git a/src/map/cashshop.c b/src/map/cashshop.c index bd505cf54e..3b2136abc4 100644 --- a/src/map/cashshop.c +++ b/src/map/cashshop.c @@ -11,11 +11,15 @@ #include // memset #include // 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 } /* diff --git a/src/map/cashshop.h b/src/map/cashshop.h index 801f2852b4..5d0eea763d 100644 --- a/src/map/cashshop.h +++ b/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_ */ diff --git a/src/map/clif.c b/src/map/clif.c index ce7abc5b26..d83920d83e 100644 --- a/src/map/clif.c +++ b/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 .W .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 .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 .W .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 .L .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 .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 .W .W .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 .W .L .?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 .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 .L .W .L .L .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 .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 .L .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 { diff --git a/src/map/clif.h b/src/map/clif.h index e3f8ad6cf8..4beea15124 100644 --- a/src/map/clif.h +++ b/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 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 **/ diff --git a/src/map/map.c b/src/map/map.c index dad5e741d8..a385d5aae9 100644 --- a/src/map/map.c +++ b/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) diff --git a/src/map/pc_groups.h b/src/map/pc_groups.h index 12e6ecf9cc..ddaed0d031 100644 --- a/src/map/pc_groups.h +++ b/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 }, };