// Copyright (c) rAthena Dev Teams - Licensed under GNU GPL // For more information, see LICENCE in the main folder #include "cashshop.hpp" #include // atoi #include // memset #include "../common/cbasetypes.hpp" // uint16, uint32 #include "../common/malloc.hpp" // CREATE, RECREATE, aFree #include "../common/showmsg.hpp" // ShowWarning, ShowStatus #include "clif.hpp" #include "log.hpp" #include "pc.hpp" // s_map_session_data #include "pet.hpp" // pet_create_egg 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. * return * 0 = failure * 1 = success */ static bool cashshop_parse_dbrow(char* fields[], int columns, int current) { uint16 tab = atoi(fields[0]); t_itemid nameid = strtoul(fields[1], nullptr, 10); uint32 price = atoi(fields[2]); int j; struct cash_item_data* cid; if( !itemdb_exists( nameid ) ){ ShowWarning( "cashshop_parse_dbrow: Invalid ID %u in line '%d', skipping...\n", nameid, current ); return 0; } 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 ){ ShowWarning( "cashshop_parse_dbrow: Invalid price %d in line '%d', skipping...\n", price, current ); return 0; } ARR_FIND( 0, cash_shop_items[tab].count, j, nameid == cash_shop_items[tab].item[j]->nameid ); if( j == cash_shop_items[tab].count ){ RECREATE( cash_shop_items[tab].item, struct cash_item_data *, ++cash_shop_items[tab].count ); CREATE( cash_shop_items[tab].item[ cash_shop_items[tab].count - 1], struct cash_item_data, 1 ); cid = cash_shop_items[tab].item[ cash_shop_items[tab].count - 1]; }else{ cid = cash_shop_items[tab].item[j]; } cid->nameid = nameid; cid->price = price; cash_shop_defined = true; return 1; } /* * Reads database from TXT format, * parses lines and sends them to parse_dbrow. */ static void cashshop_read_db_txt( void ){ const char* dbsubpath[] = { "", "/" DBIMPORT, }; int fi; for( fi = 0; fi < ARRAYLENGTH( dbsubpath ); ++fi ){ uint8 n1 = (uint8)(strlen(db_path)+strlen(dbsubpath[fi])+1); uint8 n2 = (uint8)(strlen(db_path)+strlen(DBPATH)+strlen(dbsubpath[fi])+1); char* dbsubpath1 = (char*)aMalloc(n1+1); char* dbsubpath2 = (char*)aMalloc(n2+1); if(fi==0) { safesnprintf(dbsubpath1,n1,"%s%s",db_path,dbsubpath[fi]); safesnprintf(dbsubpath2,n2,"%s/%s%s",db_path,DBPATH,dbsubpath[fi]); } else { safesnprintf(dbsubpath1,n1,"%s%s",db_path,dbsubpath[fi]); safesnprintf(dbsubpath2,n1,"%s%s",db_path,dbsubpath[fi]); } sv_readdb(dbsubpath2, "item_cash_db.txt", ',', 3, 3, -1, &cashshop_parse_dbrow, fi > 0); aFree(dbsubpath1); aFree(dbsubpath2); } } /* * Reads database from SQL format, * parses line and sends them to parse_dbrow. */ static int cashshop_read_db_sql( void ){ const char* cash_db_name[] = { item_cash_table, item_cash2_table }; int fi; for( fi = 0; fi < ARRAYLENGTH( cash_db_name ); ++fi ){ uint32 lines = 0, count = 0; if( SQL_ERROR == Sql_Query( mmysql_handle, "SELECT `tab`, `item_id`, `price` FROM `%s`", cash_db_name[fi] ) ){ Sql_ShowDebug( mmysql_handle ); continue; } while( SQL_SUCCESS == Sql_NextRow( mmysql_handle ) ){ char* str[3]; char dummy[256] = ""; int i; ++lines; for( i = 0; i < 3; ++i ){ Sql_GetData( mmysql_handle, i, &str[i], NULL ); if( str[i] == NULL ){ str[i] = dummy; } } if( !cashshop_parse_dbrow( str, 3, lines ) ) { ShowError("cashshop_read_db_sql: Cannot process table '%s' at line '%d', skipping...\n", cash_db_name[fi], lines); continue; } ++count; } Sql_FreeResult( mmysql_handle ); ShowStatus( "Done reading '" CL_WHITE "%u" CL_RESET "' entries in '" CL_WHITE "%s" CL_RESET "'.\n", count, cash_db_name[fi] ); } return 0; } #if PACKETVER_SUPPORTS_SALES static bool sale_parse_dbrow( char* fields[], int columns, int current ){ t_itemid nameid = strtoul(fields[0], nullptr, 10); 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 %u 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 %u 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 %u 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 %u 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]; char dummy[256] = ""; int i; lines++; for( i = 0; i < 4; i++ ){ Sql_GetData( mmysql_handle, i, &str[i], NULL ); if( str[i] == NULL ){ str[i] = dummy; } } 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 "%u" CL_RESET "' entries in '" CL_WHITE "%s" CL_RESET "'.\n", count, sales_table ); } static TIMER_FUNC(sale_end_timer){ 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 TIMER_FUNC(sale_start_timer){ 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( t_itemid 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 ( '%u', 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( t_itemid nameid ){ struct sale_item_data* sale_item; int i; // Check if there is an entry for this item id sale_item = sale_find_item(nameid, false); if( sale_item == NULL ){ return false; } // Delete it from the database if( SQL_ERROR == Sql_Query(mmysql_handle, "DELETE FROM `%s` WHERE `nameid` = '%u'", sales_table, nameid ) ){ Sql_ShowDebug(mmysql_handle); return false; } if( sale_item->timer_start != INVALID_TIMER ){ delete_timer(sale_item->timer_start, sale_start_timer); sale_item->timer_start = INVALID_TIMER; } // Check if the sale is currently running if( sale_item->timer_end != INVALID_TIMER ){ delete_timer(sale_item->timer_end, sale_end_timer); sale_item->timer_end = INVALID_TIMER; // Notify all clients that the sale has ended clif_sale_end(sale_item, NULL, ALL_CLIENT); } // 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( t_itemid 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. * Checks if the transaction is valid and if the user has enough inventory space to receive the item. * If yes, take cashpoints and give items; else return clif_error. * @param sd Player that request to buy item(s) * @param kafrapoints * @param n Count of item list * @param item_list Array of item ID * @return true: success, false: fail */ bool cashshop_buylist( struct map_session_data* sd, uint32 kafrapoints, int n, struct PACKET_CZ_SE_PC_BUY_CASHITEM_LIST_sub* item_list ){ uint32 totalcash = 0; uint32 totalweight = 0; int i,new_; if( sd == NULL || item_list == NULL || !cash_shop_defined){ clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_UNKNOWN ); return false; }else if( sd->state.trading ){ clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_PC_STATE ); return false; } new_ = 0; for( i = 0; i < n; ++i ){ t_itemid nameid = item_list[i].itemId; uint32 quantity = item_list[i].amount; uint16 tab = item_list[i].tab; int j; if( tab >= CASHSHOP_TAB_MAX ){ clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKNOWN ); return false; } ARR_FIND( 0, cash_shop_items[tab].count, j, nameid == cash_shop_items[tab].item[j]->nameid || nameid == itemdb_viewid(cash_shop_items[tab].item[j]->nameid) ); if( j == cash_shop_items[tab].count ){ clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKONWN_ITEM ); return false; } nameid = item_list[i].itemId = cash_shop_items[tab].item[j]->nameid; //item_avail replacement std::shared_ptr id = item_db.find(nameid); if( !id ){ clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_UNKONWN_ITEM ); return false; } 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 ){ struct sale_item_data* 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; case CHKADDITEM_NEW: new_ += id->inventorySlotNeeded(quantity); break; case CHKADDITEM_OVERAMOUNT: clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_OVER_PRODUCT_TOTAL_CNT ); return false; } totalcash += cash_shop_items[tab].item[j]->price * quantity; totalweight += itemdb_weight( nameid ) * quantity; } if( ( totalweight + sd->weight ) > sd->max_weight ){ clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_INVENTORY_WEIGHT ); return false; }else if( pc_inventoryblank( sd ) < new_ ){ clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_INVENTORY_ITEMCNT ); return false; } if(pc_paycash( sd, totalcash, kafrapoints, LOG_TYPE_CASH ) <= 0){ clif_cashshop_result( sd, 0, CASHSHOP_RESULT_ERROR_SHORTTAGE_CASH ); return false; } for( i = 0; i < n; ++i ){ t_itemid nameid = item_list[i].itemId; uint32 quantity = item_list[i].amount; #if PACKETVER_SUPPORTS_SALES uint16 tab = item_list[i].tab; #endif struct item_data *id = itemdb_search(nameid); if (!id) continue; unsigned short get_amt = quantity; if (id->flag.guid || !itemdb_isstackable2(id)) get_amt = 1; #if PACKETVER_SUPPORTS_SALES struct sale_item_data* sale = nullptr; if( tab == CASHSHOP_TAB_SALE ){ sale = sale_find_item( nameid, true ); if( sale == nullptr ){ // 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 for (uint32 j = 0; j < quantity; j += get_amt) { if( !pet_create_egg( sd, nameid ) ){ struct item item_tmp = { 0 }; item_tmp.nameid = nameid; item_tmp.identify = 1; switch( pc_additem( sd, &item_tmp, get_amt, LOG_TYPE_CASH ) ){ case ADDITEM_OVERWEIGHT: clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_INVENTORY_WEIGHT ); return false; case ADDITEM_OVERITEM: clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_INVENTORY_ITEMCNT ); return false; case ADDITEM_OVERAMOUNT: clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_OVER_PRODUCT_TOTAL_CNT ); return false; case ADDITEM_STACKLIMIT: clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_ERROR_RUNE_OVERCOUNT ); return false; } } clif_cashshop_result( sd, nameid, CASHSHOP_RESULT_SUCCESS ); #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` = '%u'", sales_table, new_amount, nameid ) ){ Sql_ShowDebug(mmysql_handle); } sale->amount = new_amount; clif_sale_amount(sale, NULL, ALL_CLIENT); } } #endif } } return true; } /* * Reloads cashshop database by destroying it and reading it again. */ void cashshop_reloaddb( void ){ do_final_cashshop(); do_init_cashshop(); } /* * Destroys cashshop class. * Closes all and cleanup. */ void do_final_cashshop( void ){ int tab, i; 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 } /* * Initializes cashshop class. * return * 0 : success */ void do_init_cashshop( void ){ cash_shop_defined = false; cashshop_read_db(); }