Browse Source

Character saves needed to be async, too slow to run synchronous in client process

Fixes #44

Database class now has the ability to asynchronously run queries that don't tie up the process threads.

This is being utilized right now for character saves as that seems to be the slowest process.  However query id's (and potentially extension of such) can allow anything to be asynchronous -- and later on even add return vars.
Image 4 years ago
parent
commit
cb0d07c5fa

+ 2 - 2
EQ2/source/WorldServer/Collections/CollectionsDB.cpp

@@ -255,7 +255,7 @@ void WorldDatabase::SavePlayerCollection(Client *client, Collection *collection)
 	assert(client);
 	assert(collection);
 
-	query.RunQuery2(Q_UPDATE,	"INSERT INTO `character_collections` (`char_id`,`collection_id`,`completed`)\n"
+	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE,	"INSERT INTO `character_collections` (`char_id`,`collection_id`,`completed`)\n"
 								"VALUES (%u,%u,0)\n"
 								"ON DUPLICATE KEY UPDATE `completed`=%i",
 								client->GetPlayer()->GetCharacterID(), collection->GetID(),
@@ -288,7 +288,7 @@ void WorldDatabase::SavePlayerCollectionItem(Client *client, Collection *collect
 	assert(collection);
 	//assert(item);
 
-	query.RunQuery2(Q_INSERT,	"INSERT IGNORE INTO `character_collection_items` (`char_id`,`collection_id`,`collection_item_id`)\n"
+	query.AddQueryAsync(client->GetCharacterID(), this, Q_INSERT,	"INSERT IGNORE INTO `character_collection_items` (`char_id`,`collection_id`,`collection_item_id`)\n"
 								"VALUES (%u,%u,%u)",
 								client->GetPlayer()->GetCharacterID(), collection->GetID(), item_id);
 }

+ 1 - 1
EQ2/source/WorldServer/Items/ItemsDB.cpp

@@ -1103,7 +1103,7 @@ void WorldDatabase::SaveItem(int32 account_id, int32 char_id, Item* item, const
 
 	Query query;
 	string update_item = string("REPLACE INTO character_items (id, type, char_id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, account_id, login_checksum) VALUES (%u, '%s', %u, %i, %u, '%s', %i, %i, %i, %i, %i, %i, %i, %u, %u, 0)");
-	query.RunQuery2(Q_REPLACE, update_item.c_str(), item->details.unique_id, type, char_id, item->details.slot_id, item->details.item_id, 
+	query.AddQueryAsync(char_id, this, Q_REPLACE, update_item.c_str(), item->details.unique_id, type, char_id, item->details.slot_id, item->details.item_id,
 		getSafeEscapeString(item->creator.c_str()).c_str(),item->adorn0,item->adorn1,item->adorn2, item->generic_info.condition, item->CheckFlag(ATTUNED) ? 1 : 0, item->details.inv_slot_id, item->details.count, item->GetMaxSellValue(), account_id);
 }
 

+ 27 - 24
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -163,7 +163,7 @@ void WorldDatabase::SaveBuyBack(int32 char_id, int32 item_id, int16 quantity, in
 
 	Query query;
 	string insert = string("INSERT INTO character_buyback (char_id, item_id, quantity, price) VALUES (%u, %u, %i, %u) ");
-	query.RunQuery2(Q_INSERT, insert.c_str(), char_id, item_id, quantity, price);
+	query.AddQueryAsync(char_id, this, Q_INSERT, insert.c_str(), char_id, item_id, quantity, price);
 }
 
 int32 WorldDatabase::LoadCharacterSpells(int32 char_id, Player* player)
@@ -215,7 +215,7 @@ void WorldDatabase::SavePlayerSpells(Client* client)
 			spell = *itr;
 			Query query;
 			LogWrite(SPELL__DEBUG, 5, "Spells", "\tSaving SpellID: %u, tier: %i, slot: %i", spell->spell_id, spell->tier, spell->slot);
-			query.RunQuery2(Q_INSERT, "INSERT INTO character_spells (char_id, spell_id, tier) SELECT %u, %u, %i ON DUPLICATE KEY UPDATE tier = %i",
+			query.AddQueryAsync(client->GetCharacterID(), this, Q_INSERT, "INSERT INTO character_spells (char_id, spell_id, tier) SELECT %u, %u, %i ON DUPLICATE KEY UPDATE tier = %i",
 				client->GetPlayer()->GetCharacterID(), spell->spell_id, spell->tier, spell->tier);
 			spell->save_needed = false;
 		}
@@ -2206,7 +2206,7 @@ void WorldDatabase::SaveCharacterSkills(Client* client){
 			Skill* skill = 0;
 			for(int32 i=0;i<skills->size();i++){
 				skill = skills->at(i);
-				query.RunQuery2(Q_REPLACE, "replace into character_skills (char_id, skill_id, current_val, max_val) values(%u, %u, %i, %i)", client->GetCharacterID(), skill->skill_id, skill->current_val, skill->max_val);
+				query.AddQueryAsync(client->GetCharacterID(),this,Q_REPLACE, "replace into character_skills (char_id, skill_id, current_val, max_val) values(%u, %u, %i, %i)", client->GetCharacterID(), skill->skill_id, skill->current_val, skill->max_val);
 			}
 			if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF)
 				LogWrite(WORLD__ERROR, 0, "World", "Error in SaveCharacterSkills query '%s': %s", query.GetQuery(), query.GetError());
@@ -2223,12 +2223,12 @@ void WorldDatabase::SaveCharacterQuests(Client* client){
 	map<int32, Quest*>* quests = client->GetPlayer()->GetPlayerQuests();
 	for(itr = quests->begin(); itr != quests->end(); itr++){
 		if(client->GetCurrentQuestID() == itr->first){
-			query.RunQuery2(Q_UPDATE, "update character_quests set current_quest = 0 where char_id = %u", client->GetCharacterID());
-			query.RunQuery2(Q_UPDATE, "update character_quests set current_quest = 1 where char_id = %u and quest_id = %u", client->GetCharacterID(), itr->first);
+			query.AddQueryAsync(client->GetCharacterID(),this,Q_UPDATE, "update character_quests set current_quest = 0 where char_id = %u", client->GetCharacterID());
+			query.AddQueryAsync(client->GetCharacterID(), this,Q_UPDATE, "update character_quests set current_quest = 1 where char_id = %u and quest_id = %u", client->GetCharacterID(), itr->first);
 		}
 		if(itr->second->GetSaveNeeded()){
-			query.RunQuery2(Q_INSERT, "insert ignore into character_quests (char_id, quest_id, given_date, quest_giver) values(%u, %u, now(), %u)", client->GetCharacterID(), itr->first, itr->second->GetQuestGiver());
-			query.RunQuery2(Q_UPDATE, "update character_quests set tracked = %i, quest_flags = %u, hidden = %i, complete_count = %u where char_id = %u and quest_id = %u", itr->second->IsTracked() ? 1 : 0, itr->second->GetQuestFlags(), itr->second->IsHidden() ? 1 : 0, itr->second->GetCompleteCount(), client->GetCharacterID(), itr->first);
+			query.AddQueryAsync(client->GetCharacterID(), this,Q_INSERT, "insert ignore into character_quests (char_id, quest_id, given_date, quest_giver) values(%u, %u, now(), %u)", client->GetCharacterID(), itr->first, itr->second->GetQuestGiver());
+			query.AddQueryAsync(client->GetCharacterID(), this,Q_UPDATE, "update character_quests set tracked = %i, quest_flags = %u, hidden = %i, complete_count = %u where char_id = %u and quest_id = %u", itr->second->IsTracked() ? 1 : 0, itr->second->GetQuestFlags(), itr->second->IsHidden() ? 1 : 0, itr->second->GetCompleteCount(), client->GetCharacterID(), itr->first);
 			SaveCharacterQuestProgress(client, itr->second);
 			itr->second->SetSaveNeeded(false);
 		}
@@ -2238,11 +2238,11 @@ void WorldDatabase::SaveCharacterQuests(Client* client){
 	quests = client->GetPlayer()->GetCompletedPlayerQuests();
 	for(itr = quests->begin(); itr != quests->end(); itr++){
 		if(itr->second->GetSaveNeeded()){
-			query.RunQuery2(Q_DELETE, "delete FROM character_quest_progress where char_id = %u and quest_id = %u", client->GetCharacterID(), itr->first);
+			query.AddQueryAsync(client->GetCharacterID(), this,Q_DELETE, "delete FROM character_quest_progress where char_id = %u and quest_id = %u", client->GetCharacterID(), itr->first);
 
 			/* incase the quest is completed before the quest could be inserted in the PlayerQuests loop, we first try to insert it.  If it already exists then we can just update
 			 * the completed_date */
-			query.RunQuery2(Q_INSERT, "INSERT INTO character_quests (char_id, quest_id, quest_giver, current_quest, given_date, completed_date, complete_count) values (%u,%u,%u,0, now(),now(), %u) ON DUPLICATE KEY UPDATE completed_date = now(), complete_count = %u, current_quest = 0", client->GetCharacterID(), itr->first, itr->second->GetQuestGiver(), itr->second->GetCompleteCount(), itr->second->GetCompleteCount());
+			query.AddQueryAsync(client->GetCharacterID(), this,Q_INSERT, "INSERT INTO character_quests (char_id, quest_id, quest_giver, current_quest, given_date, completed_date, complete_count) values (%u,%u,%u,0, now(),now(), %u) ON DUPLICATE KEY UPDATE completed_date = now(), complete_count = %u, current_quest = 0", client->GetCharacterID(), itr->first, itr->second->GetQuestGiver(), itr->second->GetCompleteCount(), itr->second->GetCompleteCount());
 			itr->second->SetSaveNeeded(false);
 		}
 	}
@@ -3573,8 +3573,8 @@ void WorldDatabase::Save(Client* client){
 	int32 zone_id = 0;
 	if(client->GetCurrentZone())
 		zone_id = client->GetCurrentZone()->GetZoneID();
-	query.RunQuery2(Q_UPDATE, "update characters set current_zone_id=%u, x=%f, y=%f, z=%f, heading=%f, level=%i,instance_id=%i,last_saved=%i, `class`=%i, `tradeskill_level`=%i, `tradeskill_class`=%i where id = %u", zone_id, player->GetX(), player->GetY(), player->GetZ(), player->GetHeading(), player->GetLevel(), instance_id, client->GetLastSavedTimeStamp(), client->GetPlayer()->GetAdventureClass(), client->GetPlayer()->GetTSLevel(), client->GetPlayer()->GetTradeskillClass(), client->GetCharacterID());
-	query.RunQuery2(Q_UPDATE, "update character_details set hp=%u, power=%u, str=%i, sta=%i, agi=%i, wis=%i, intel=%i, heat=%i, cold=%i, magic=%i, mental=%i, divine=%i, disease=%i, poison=%i, coin_copper=%u, coin_silver=%u, coin_gold=%u, coin_plat=%u, max_hp = %u, max_power=%u, xp = %u, xp_needed = %u, xp_debt = %u, xp_vitality = %f, tradeskill_xp = %u, tradeskill_xp_needed = %u, tradeskill_xp_vitality = %f, bank_copper = %u, bank_silver = %u, bank_gold = %u, bank_plat = %u, bind_zone_id=%u, bind_x = %f, bind_y = %f, bind_z = %f, bind_heading = %f, house_zone_id=%u, combat_voice = %i, emote_voice = %i, biography='%s', flags=%u, flags2=%u, last_name='%s' where char_id = %u",
+	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE, "update characters set current_zone_id=%u, x=%f, y=%f, z=%f, heading=%f, level=%i,instance_id=%i,last_saved=%i, `class`=%i, `tradeskill_level`=%i, `tradeskill_class`=%i where id = %u", zone_id, player->GetX(), player->GetY(), player->GetZ(), player->GetHeading(), player->GetLevel(), instance_id, client->GetLastSavedTimeStamp(), client->GetPlayer()->GetAdventureClass(), client->GetPlayer()->GetTSLevel(), client->GetPlayer()->GetTradeskillClass(), client->GetCharacterID());
+	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE, "update character_details set hp=%u, power=%u, str=%i, sta=%i, agi=%i, wis=%i, intel=%i, heat=%i, cold=%i, magic=%i, mental=%i, divine=%i, disease=%i, poison=%i, coin_copper=%u, coin_silver=%u, coin_gold=%u, coin_plat=%u, max_hp = %u, max_power=%u, xp = %u, xp_needed = %u, xp_debt = %u, xp_vitality = %f, tradeskill_xp = %u, tradeskill_xp_needed = %u, tradeskill_xp_vitality = %f, bank_copper = %u, bank_silver = %u, bank_gold = %u, bank_plat = %u, bind_zone_id=%u, bind_x = %f, bind_y = %f, bind_z = %f, bind_heading = %f, house_zone_id=%u, combat_voice = %i, emote_voice = %i, biography='%s', flags=%u, flags2=%u, last_name='%s' where char_id = %u",
 		player->GetHP(), player->GetPower(), player->GetStrBase(), player->GetStaBase(), player->GetAgiBase(), player->GetWisBase(), player->GetIntBase(), player->GetHeatResistanceBase(), player->GetColdResistanceBase(), player->GetMagicResistanceBase(),
 		player->GetMentalResistanceBase(), player->GetDivineResistanceBase(), player->GetDiseaseResistanceBase(), player->GetPoisonResistanceBase(), player->GetCoinsCopper(), player->GetCoinsSilver(), player->GetCoinsGold(), player->GetCoinsPlat(), player->GetTotalHPBase(), player->GetTotalPowerBase(), player->GetXP(), player->GetNeededXP(), player->GetXPDebt(), player->GetXPVitality(), player->GetTSXP(), player->GetNeededTSXP(), player->GetTSXPVitality(), player->GetBankCoinsCopper(),
 		player->GetBankCoinsSilver(), player->GetBankCoinsGold(), player->GetBankCoinsPlat(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneID(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneX(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneY(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneZ(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneHeading(), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), 
@@ -3584,11 +3584,11 @@ void WorldDatabase::Save(Client* client){
 	if(friends && friends->size() > 0){
 		for(itr = friends->begin(); itr != friends->end(); itr++){
 			if(itr->second == 1){
-				query.RunQuery2(Q_INSERT, "insert ignore into character_social (char_id, name, type) values(%u, '%s', 'FRIEND')", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
+				query.AddQueryAsync(client->GetCharacterID(), this, Q_INSERT, "insert ignore into character_social (char_id, name, type) values(%u, '%s', 'FRIEND')", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
 				itr->second = 0;
 			}
 			else if(itr->second == 2){
-				query.RunQuery2(Q_DELETE, "delete FROM character_social where char_id = %u and name = '%s'", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
+				query.AddQueryAsync(client->GetCharacterID(), this, Q_DELETE, "delete FROM character_social where char_id = %u and name = '%s'", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
 				itr->second = 3;
 			}
 		}
@@ -3597,11 +3597,11 @@ void WorldDatabase::Save(Client* client){
 	if(ignored && ignored->size() > 0){
 		for(itr = ignored->begin(); itr != ignored->end(); itr++){
 			if(itr->second == 1){
-				query.RunQuery2(Q_INSERT, "insert ignore into character_social (char_id, name, type) values(%u, '%s', 'IGNORE')", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
+				query.AddQueryAsync(client->GetCharacterID(), this, Q_INSERT, "insert ignore into character_social (char_id, name, type) values(%u, '%s', 'IGNORE')", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
 				itr->second = 0;
 			}
 			else if(itr->second == 2){
-				query.RunQuery2(Q_DELETE, "delete FROM character_social where char_id = %u and name = '%s'", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
+				query.AddQueryAsync(client->GetCharacterID(), this, Q_DELETE, "delete FROM character_social where char_id = %u and name = '%s'", client->GetCharacterID(), getSafeEscapeString(itr->first.c_str()).c_str());
 				itr->second = 3;
 			}
 		}
@@ -3857,7 +3857,7 @@ void WorldDatabase::SavePlayerFactions(Client* client){
 	map<int32, sint32>* factions = client->GetPlayer()->GetFactions()->GetFactionValues();
 	map<int32, sint32>::iterator itr;
 	for(itr = factions->begin(); itr != factions->end(); itr++)
-		query.RunQuery2(Q_INSERT, "insert into character_factions (char_id, faction_id, faction_level) values(%u, %u, %i) ON DUPLICATE KEY UPDATE faction_level=%i", client->GetCharacterID(), itr->first, itr->second, itr->second);	
+		query.AddQueryAsync(client->GetCharacterID(), this,Q_INSERT, "insert into character_factions (char_id, faction_id, faction_level) values(%u, %u, %i) ON DUPLICATE KEY UPDATE faction_level=%i", client->GetCharacterID(), itr->first, itr->second, itr->second);
 }
 
 bool WorldDatabase::LoadPlayerFactions(Client* client) {
@@ -3880,9 +3880,9 @@ void WorldDatabase::SavePlayerMail(Mail* mail) {
 	Query query_update;
 	Query query_insert;
 	if (mail) {
-		query_update.RunQuery2(Q_UPDATE, "UPDATE `character_mail` SET `already_read`=%u, `coin_copper`=%u, `coin_silver`=%u, `coin_gold`=%u, `coin_plat`=%u WHERE `id`=%u", mail->already_read, mail->coin_copper, mail->coin_silver, mail->coin_gold, mail->coin_plat, mail->mail_id);
+		query_update.AddQueryAsync(mail->player_to_id, this, Q_UPDATE, "UPDATE `character_mail` SET `already_read`=%u, `coin_copper`=%u, `coin_silver`=%u, `coin_gold`=%u, `coin_plat`=%u WHERE `id`=%u", mail->already_read, mail->coin_copper, mail->coin_silver, mail->coin_gold, mail->coin_plat, mail->mail_id);
 		if (query_update.GetAffectedRows() == 0)
-			query_insert.RunQuery2(Q_UPDATE, "INSERT INTO `character_mail` (`player_to_id`, `player_from`, `subject`, `mail_body`, `already_read`, `mail_type`, `coin_copper`, `coin_silver`, `coin_gold`, `coin_plat`, `stack`, `postage_cost`, `attachment_cost`, `char_item_id`, `time_sent`, `expire_time`) VALUES (%u, '%s', '%s', '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u)", mail->player_to_id, mail->player_from.c_str(), getSafeEscapeString(mail->subject.c_str()).c_str(), getSafeEscapeString(mail->mail_body.c_str()).c_str(), mail->already_read, mail->mail_type, mail->coin_copper, mail->coin_silver, mail->coin_gold, mail->coin_plat, mail->stack, mail->postage_cost, mail->attachment_cost, mail->char_item_id, mail->time_sent, mail->expire_time);
+			query_insert.AddQueryAsync(mail->player_to_id, this, Q_UPDATE, "INSERT INTO `character_mail` (`player_to_id`, `player_from`, `subject`, `mail_body`, `already_read`, `mail_type`, `coin_copper`, `coin_silver`, `coin_gold`, `coin_plat`, `stack`, `postage_cost`, `attachment_cost`, `char_item_id`, `time_sent`, `expire_time`) VALUES (%u, '%s', '%s', '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u)", mail->player_to_id, mail->player_from.c_str(), getSafeEscapeString(mail->subject.c_str()).c_str(), getSafeEscapeString(mail->mail_body.c_str()).c_str(), mail->already_read, mail->mail_type, mail->coin_copper, mail->coin_silver, mail->coin_gold, mail->coin_plat, mail->stack, mail->postage_cost, mail->attachment_cost, mail->char_item_id, mail->time_sent, mail->expire_time);
 		mail->save_needed = false;
 	}
 }
@@ -4096,17 +4096,17 @@ void WorldDatabase::SaveQuickBar(int32 char_id, vector<QuickBarItem*>* quickbar_
 		if(qbi->deleted == false){
 			Query query;
 			if(qbi->text.size > 0){
-				query.RunQuery2(Q_REPLACE, "replace into character_skillbar (id, hotbar, slot, char_id, spell_id, type, text_val, tier) values(%u, %u, %u, %u, %u, %i, '%s', %i)", 
+				query.AddQueryAsync(char_id, this, Q_REPLACE, "replace into character_skillbar (id, hotbar, slot, char_id, spell_id, type, text_val, tier) values(%u, %u, %u, %u, %u, %i, '%s', %i)", 
 					qbi->unique_id, qbi->hotbar, qbi->slot, char_id, qbi->id, qbi->type, getSafeEscapeString(qbi->text.data.c_str()).c_str(), qbi->tier);
 			}
 			else{
-				query.RunQuery2(Q_REPLACE, "replace into character_skillbar (id, hotbar, slot, char_id, spell_id, type, text_val, tier) values(%u, %u, %u, %u, %u, %i, 'Unused', %i)", 
+				query.AddQueryAsync(char_id, this, Q_REPLACE, "replace into character_skillbar (id, hotbar, slot, char_id, spell_id, type, text_val, tier) values(%u, %u, %u, %u, %u, %i, 'Unused', %i)",
 					qbi->unique_id, qbi->hotbar, qbi->slot, char_id, qbi->id, qbi->type, qbi->tier);
 			}
 		}
 		else{
 			Query query;
-			query.RunQuery2(Q_DELETE, "delete FROM character_skillbar where hotbar=%u and slot=%u and char_id=%u", qbi->hotbar, qbi->slot, char_id);
+			query.AddQueryAsync(char_id, this, Q_DELETE, "delete FROM character_skillbar where hotbar=%u and slot=%u and char_id=%u", qbi->hotbar, qbi->slot, char_id);
 		}
 	}
 }
@@ -5772,7 +5772,7 @@ void WorldDatabase::SaveCharacterHistory(Player* player, int8 type, int8 subtype
 	LogWrite(PLAYER__INFO, 1, "Player", "Saving character history, type = %s (%i) subtype = %s (%i)", (char*)str_type.c_str(), type, (char*)str_subtype.c_str(), subtype);
 
 	Query query;
-	query.RunQuery2(Q_REPLACE, "replace into character_history (char_id, type, subtype, value, value2, location, event_date) values (%u, '%s', '%s', %i, %i, '%s', %u)", 
+	query.AddQueryAsync(player->GetCharacterID(), this, Q_REPLACE, "replace into character_history (char_id, type, subtype, value, value2, location, event_date) values (%u, '%s', '%s', %i, %i, '%s', %u)", 
 		player->GetCharacterID(), str_type.c_str(), str_subtype.c_str(), value, value2, location, event_date);
 }
 
@@ -6554,8 +6554,11 @@ void WorldDatabase::LoadZoneFlightPathLocations(ZoneServer* zone) {
 }
 
 void WorldDatabase::SaveCharacterLUAHistory(Player* player, int32 event_id, int32 value, int32 value2) {
-	if (!database_new.Query("REPLACE INTO character_lua_history (char_id, event_id, value, value2) VALUES (%u, %u, %u, %u)", player->GetCharacterID(), event_id, value, value2))
-		LogWrite(DATABASE__ERROR, 0, "DBNew", "MySQL Error %u: %s", database_new.GetError(), database_new.GetErrorMsg());
+	Query query;
+	query.AddQueryAsync(player->GetCharacterID(), this, Q_REPLACE, "REPLACE INTO character_lua_history(char_id, event_id, value, value2) VALUES(% u, % u, % u, % u)", player->GetCharacterID(), event_id, value, value2);
+
+//	if (!database_new.Query("REPLACE INTO character_lua_history (char_id, event_id, value, value2) VALUES (%u, %u, %u, %u)", player->GetCharacterID(), event_id, value, value2))
+//		LogWrite(DATABASE__ERROR, 0, "DBNew", "MySQL Error %u: %s", database_new.GetError(), database_new.GetErrorMsg());
 }
 
 void WorldDatabase::LoadCharacterLUAHistory(int32 char_id, Player* player) {

+ 99 - 68
EQ2/source/WorldServer/client.cpp

@@ -123,7 +123,7 @@ extern ChestTrapList chest_trap_list;
 
 using namespace std;
 
-Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_debug_timer(30000), transmuteID(0) {
+Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_debug_timer(30000), delayTimer(500), transmuteID(0) {
 	eqs = ieqs;
 	ip = eqs->GetrIP();
 	port = ntohs(eqs->GetrPort());
@@ -187,6 +187,10 @@ Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_deb
 	on_auto_mount = false;
 	should_load_spells = true;
 	spawnPlacementMode = ServerSpawnPlacementMode::DEFAULT;
+	delayedLogin = false;
+	delayedAccountID = 0;
+	delayedAccessKey = 0;
+	delayTimer.Disable();
 }
 
 Client::~Client() {
@@ -892,71 +896,8 @@ bool Client::HandlePacket(EQApplicationPacket *app) {
 				int32 account_id = request->getType_int32_ByName("account_id");
 				int32 access_code = request->getType_int32_ByName("access_code");
 
-				ZoneAuthRequest* zar = zone_auth.GetAuth(account_id, access_code);
-
-				if(zar)
-				{
-					firstlogin = zar->isFirstLogin ( );
-					LogWrite(ZONE__INFO, 0, "ZoneAuth", "Access Key: %u, Character Name: %s, Account ID: %u, Client Data Version: %u", zar->GetAccessKey(), zar->GetCharacterName(), zar->GetAccountID(), version);
-					if(database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)){
-						bool pvp_allowed = rule_manager.GetGlobalRule(R_PVP, AllowPVP)->GetBool();
-						if (pvp_allowed)
-							this->GetPlayer()->SetAttackable(1);
-						version = request->getType_int16_ByName("version");
-						MDeletePlayer.writelock(__FUNCTION__, __LINE__);
-						Client* client = zone_list.GetClientByCharName(player->GetName());
-						if(client){
-							if(client->getConnection())
-								client->getConnection()->SendDisconnect();
-							if(client->GetCurrentZone() && !client->IsZoning()){
-								//swap players, allowing the client to resume his LD'd player (ONLY if same version of the client)
-								if(client->GetVersion() == version){
-									Player* current_player = GetPlayer();
-									SetPlayer(client->GetPlayer());
-									client->SetPlayer(current_player);
-									client->GetPlayer()->SetPendingDeletion(true);
-
-									GetPlayer()->SetPendingDeletion(false);
-									GetPlayer()->ResetSavedSpawns();
-									GetPlayer()->SetReturningFromLD(true);
-									GetPlayer()->GetZone()->RemoveDelayedSpawnRemove(GetPlayer());
-								}
-								client->GetCurrentZone()->RemoveClientImmediately(client);
-							}
-						}
-						MDeletePlayer.releasewritelock(__FUNCTION__, __LINE__);						
-						if(!GetCurrentZone()){
-							LogWrite(ZONE__ERROR, 0, "Zone", "Error loading zone for character: %s", player->GetName());
-							ClientPacketFunctions::SendLoginDenied( this );
-						}
-						else if(EQOpcodeManager.count(GetOpcodeVersion(version)) > 0 && getConnection()){
-							getConnection()->SetClientVersion(version);
-							connected_to_zone = true;
-							client_list.Remove(this); //remove from master client list
-							new_client_login = true;
-							GetCurrentZone()->AddClient(this); //add to zones client list
-							world.RejoinGroup(this);
-							zone_list.AddClientToMap(player->GetName(), this);
-							GetCurrentZone()->SetSpawnStructs(this);
-						}
-						else{
-							LogWrite(WORLD__ERROR, 0, "World", "Incompatible version: %i", version);
-							ClientPacketFunctions::SendLoginDenied( this );
-
-													return false;
-						}
-					}
-					else{
-						LogWrite(WORLD__ERROR, 0, "World", "Could not load character '%s' with account id of: %u", zar->GetCharacterName(), zar->GetAccountID());
-						ClientPacketFunctions::SendLoginDenied( this );
-					}
-					zone_auth.RemoveAuth(zar);
-				}
-				else
-				{
-					LogWrite(WORLD__ERROR, 0, "World", "Invalid ZoneAuthRequest, disconnecting client.");
-					Disconnect();
-				}
+				if (!HandleNewLogin(account_id, access_code))
+					return false;
 			}
 			safe_delete(request);
 			break;
@@ -2521,6 +2462,12 @@ bool Client::Process(bool zone_process) {
 		should_load_spells = false;
 	}
 
+	if (delayedLogin && delayTimer.Enabled() && delayTimer.Check())
+	{
+		if (!HandleNewLogin(delayedAccountID, delayedAccessKey))
+			return false;
+	}
+
 	if(quest_updates) {
 		LogWrite(CCLIENT__DEBUG, 1, "Client", "%s, ProcessQuestUpdates", __FUNCTION__, __LINE__);
 		ProcessQuestUpdates();
@@ -2616,8 +2563,9 @@ bool Client::Process(bool zone_process) {
 	if (!eqs || (eqs && !eqs->CheckActive()))
 		ret = false;
 
-	if(!ret)
-		Save();
+// redundant to client disconnect
+//	if(!ret)
+//		Save();
 
 	return ret;
 }
@@ -8203,3 +8151,86 @@ void Client::SetTransmuteID(int32 trans_id) {
 int32 Client::GetTransmuteID() {
 	return transmuteID;
 }
+
+bool Client::HandleNewLogin(int32 account_id, int32 access_code)
+{
+	ZoneAuthRequest* zar = zone_auth.GetAuth(account_id, access_code);
+
+	if (zar)
+	{
+		int32 charID = database.GetCharacterID(zar->GetCharacterName());
+		if (database.IsActiveQuery(charID))
+		{
+			delayedLogin = true;
+			delayedAccountID = account_id;
+			delayedAccessKey = access_code;
+			delayTimer.Start(500);
+			LogWrite(ZONE__INFO, 0, "ZoneAuth", "Attempt to Login must be delayed, async character save in progress! ... Access Key: %u, Character Name: %s, Account ID: %u, Client Data Version: %u", zar->GetAccessKey(), zar->GetCharacterName(), zar->GetAccountID(), version);
+			return true;
+		}
+
+		delayedLogin = false;
+		delayTimer.Disable();
+
+		firstlogin = zar->isFirstLogin();
+		LogWrite(ZONE__INFO, 0, "ZoneAuth", "Access Key: %u, Character Name: %s, Account ID: %u, Client Data Version: %u", zar->GetAccessKey(), zar->GetCharacterName(), zar->GetAccountID(), version);
+		if (database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)) {
+			bool pvp_allowed = rule_manager.GetGlobalRule(R_PVP, AllowPVP)->GetBool();
+			if (pvp_allowed)
+				this->GetPlayer()->SetAttackable(1);
+			MDeletePlayer.writelock(__FUNCTION__, __LINE__);
+			Client* client = zone_list.GetClientByCharName(player->GetName());
+			if (client) {
+				if (client->getConnection())
+					client->getConnection()->SendDisconnect();
+				if (client->GetCurrentZone() && !client->IsZoning()) {
+					//swap players, allowing the client to resume his LD'd player (ONLY if same version of the client)
+					if (client->GetVersion() == version) {
+						Player* current_player = GetPlayer();
+						SetPlayer(client->GetPlayer());
+						client->SetPlayer(current_player);
+						client->GetPlayer()->SetPendingDeletion(true);
+
+						GetPlayer()->SetPendingDeletion(false);
+						GetPlayer()->ResetSavedSpawns();
+						GetPlayer()->SetReturningFromLD(true);
+						GetPlayer()->GetZone()->RemoveDelayedSpawnRemove(GetPlayer());
+					}
+					client->GetCurrentZone()->RemoveClientImmediately(client);
+				}
+			}
+			MDeletePlayer.releasewritelock(__FUNCTION__, __LINE__);
+			if (!GetCurrentZone()) {
+				LogWrite(ZONE__ERROR, 0, "Zone", "Error loading zone for character: %s", player->GetName());
+				ClientPacketFunctions::SendLoginDenied(this);
+			}
+			else if (EQOpcodeManager.count(GetOpcodeVersion(version)) > 0 && getConnection()) {
+				getConnection()->SetClientVersion(version);
+				connected_to_zone = true;
+				client_list.Remove(this); //remove from master client list
+				new_client_login = true;
+				GetCurrentZone()->AddClient(this); //add to zones client list
+				world.RejoinGroup(this);
+				zone_list.AddClientToMap(player->GetName(), this);
+				GetCurrentZone()->SetSpawnStructs(this);
+			}
+			else {
+				LogWrite(WORLD__ERROR, 0, "World", "Incompatible version: %i", version);
+				ClientPacketFunctions::SendLoginDenied(this);
+				return false;
+			}
+		}
+		else {
+			LogWrite(WORLD__ERROR, 0, "World", "Could not load character '%s' with account id of: %u", zar->GetCharacterName(), zar->GetAccountID());
+			ClientPacketFunctions::SendLoginDenied(this);
+		}
+		zone_auth.RemoveAuth(zar);
+	}
+	else
+	{
+		LogWrite(WORLD__ERROR, 0, "World", "Invalid ZoneAuthRequest, disconnecting client.");
+		Disconnect();
+	}
+
+	return true;
+}

+ 6 - 0
EQ2/source/WorldServer/client.h

@@ -395,6 +395,8 @@ public:
 	enum ServerSpawnPlacementMode { DEFAULT, OPEN_HEADING, CLOSE_HEADING };
 	void SetSpawnPlacementMode(ServerSpawnPlacementMode mode) { spawnPlacementMode = mode; }
 	ServerSpawnPlacementMode GetSpawnPlacementMode() { return spawnPlacementMode; }
+
+	bool HandleNewLogin(int32 account_id, int32 access_code);
 private:
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
@@ -487,6 +489,10 @@ private:
 	ServerSpawnPlacementMode spawnPlacementMode;
 	bool on_auto_mount;
 	bool EntityCommandPrecheck(Spawn* spawn, const char* command);
+	bool delayedLogin;
+	int32 delayedAccountID;
+	int32 delayedAccessKey;
+	Timer delayTimer;
 };
 
 class ClientList {

+ 255 - 3
EQ2/source/common/database.cpp

@@ -68,13 +68,23 @@ using namespace std;
 #include "../common/packet_dump.h"
 #include "../common/Log.h"
 
+#ifdef WORLD
+ThreadReturnType DBAsyncQueries(void* str)
+{
+	// allow some buffer for multiple queries to collect
+	Sleep(10);
+	DBStruct* data = (DBStruct*)str;
+	database.RunAsyncQueries(data->queryid);
+	THREAD_RETURN(NULL);
+}
+#endif
+
 Database::Database()
 {
 	InitVars();
-	
 }
 
-bool Database::Init() {
+bool Database::Init(bool silentLoad) {
 	char host[200], user[200], passwd[200], database[200];
 	int32 port=0;
 	bool compression = false;
@@ -104,7 +114,8 @@ bool Database::Init() {
 	}
 	else
 	{
-		LogWrite(DATABASE__INFO, 0, "DB", "Using database '%s' at %s", database, host);
+		if (!silentLoad)
+			LogWrite(DATABASE__INFO, 0, "DB", "Using database '%s' at %s", database, host);
 	}
 
 	return true;
@@ -163,7 +174,74 @@ void Database::InitVars() {
 
 Database::~Database()
 {
+#ifdef WORLD
+	DBQueryMutex.writelock(__FUNCTION__, __LINE__);
+	activeQuerySessions.clear();
+	DBQueryMutex.releasewritelock(__FUNCTION__, __LINE__);
+
+	DBAsyncMutex.writelock();
+	continueAsync = false;
+	map<int32, deque<Query*>>::iterator itr;
+	for (itr = asyncQueries.begin(); itr != asyncQueries.end(); itr++)
+	{
+		asyncQueriesMutex[itr->first]->writelock();
+		deque<Query*> queries = itr->second;
+		while (queries.size() > 0)
+		{
+			Query* cur = queries.front();
+			queries.pop_front();
+			safe_delete(cur);
+		}
+		asyncQueriesMutex[itr->first]->releasewritelock();
+		Mutex* mutex = asyncQueriesMutex[itr->first];
+		asyncQueriesMutex.erase(itr->first);
+		safe_delete(mutex);
+	}
+	asyncQueries.clear();
+
+	asyncQueriesMutex.clear();
+	DBAsyncMutex.releasewritelock();
+
+	PurgeDBInstances();
+#endif
+}
+
+#ifdef WORLD
+void Query::AddQueryAsync(int32 queryID, Database* db, QUERY_TYPE type, const char* format, ...) {
+	in_type = type;
+	va_list args;
+	va_start(args, format);
+#ifdef WIN32
+	char* buffer;
+	int buf_len = _vscprintf(format, args) + 1;
+	buffer = new char[buf_len];
+	vsprintf(buffer, format, args);
+#else
+	char* buffer;
+	int buf_len;
+	va_list argcopy;
+	va_copy(argcopy, args);
+	buf_len = vsnprintf(NULL, 0, format, argcopy) + 1;
+	va_end(argcopy);
+
+	buffer = new char[buf_len];
+	vsnprintf(buffer, buf_len, format, args);
+#endif
+	va_end(args);
+	query = string(buffer);
+
+	Query* asyncQuery = new Query(this, queryID);
+
+	safe_delete_array(buffer);
+
+	db->AddAsyncQuery(asyncQuery);
+}
+
+void Query::RunQueryAsync(Database* db) {
+	db->RunQuery(query.c_str(), query.length(), errbuf, &result, affected_rows, last_insert_id, &errnum, retry);
 }
+#endif
+
 MYSQL_RES* Query::RunQuery2(QUERY_TYPE type, const char* format, ...){
 	va_list args;
 	va_start( args, format );
@@ -246,3 +324,177 @@ MYSQL_RES* Query::RunQuery2(string in_query, QUERY_TYPE type){
 	database.RunQuery(query.c_str(), query.length(), errbuf, &result, affected_rows, last_insert_id, &errnum, retry); 
 	return result;
 }
+
+#ifdef WORLD
+void Database::RunAsyncQueries(int32 queryid)
+{
+	Database* asyncdb = FindFreeInstance();
+	DBAsyncMutex.writelock();
+	map<int32, deque<Query*>>::iterator itr = asyncQueries.find(queryid);
+	if (itr == asyncQueries.end())
+	{
+		DBAsyncMutex.releasewritelock();
+		return;
+	}
+
+	asyncQueriesMutex[queryid]->writelock();
+	deque<Query*> queries;
+	while (itr->second.size())
+	{
+		Query* cur = itr->second.front();
+		queries.push_back(cur);
+		itr->second.pop_front();
+	}
+	itr->second.clear();
+	asyncQueries.erase(itr);
+	DBAsyncMutex.releasewritelock();
+
+	int32 count = 0;
+	while (queries.size() > 0)
+	{
+		Query* cur = queries.front();
+		cur->RunQueryAsync(asyncdb);
+		this->RemoveActiveQuery(cur);
+		queries.pop_front();
+		safe_delete(cur);
+	}
+	FreeDBInstance(asyncdb);
+
+	asyncQueriesMutex[queryid]->releasewritelock();
+}
+
+void Database::AddAsyncQuery(Query* query)
+{
+	DBAsyncMutex.writelock();
+	map<int32, Mutex*>::iterator mutexItr = asyncQueriesMutex.find(query->GetQueryID());
+	if (mutexItr == asyncQueriesMutex.end())
+	{
+		Mutex* queryMutex = new Mutex();
+		queryMutex->SetName("AsyncQuery" + query->GetQueryID());
+		asyncQueriesMutex.insert(make_pair(query->GetQueryID(), queryMutex));
+	}
+	map<int32, deque<Query*>>::iterator itr = asyncQueries.find(query->GetQueryID());
+	asyncQueriesMutex[query->GetQueryID()]->writelock();
+
+	if ( itr != asyncQueries.end())
+		itr->second.push_back(query);
+	else
+	{
+		deque<Query*> queue;
+		queue.push_back(query);
+		asyncQueries.insert(make_pair(query->GetQueryID(), queue));
+	}
+
+	AddActiveQuery(query);
+
+	asyncQueriesMutex[query->GetQueryID()]->releasewritelock();
+	DBAsyncMutex.releasewritelock();
+
+	bool isActive = IsActiveQuery(query->GetQueryID(), query);
+	if (!isActive)
+	{
+	continueAsync = true;
+	DBStruct* tmp = new DBStruct;
+	tmp->queryid = query->GetQueryID();
+#ifdef WIN32
+	_beginthread(DBAsyncQueries, 0, (void*)tmp);
+#else
+	pthread_create(&t1, NULL, DBAsyncQueries, (void*)tmp);
+	pthread_detach(t1);
+#endif
+	}
+}
+
+Database* Database::FindFreeInstance()
+{
+	Database* db_inst = 0;
+	map<Database*, bool>::iterator itr;
+	DBInstanceMutex.writelock(__FUNCTION__, __LINE__);
+	for (itr = dbInstances.begin(); itr != dbInstances.end(); itr++) {
+		if (!itr->second)
+		{
+			db_inst = itr->first;
+			itr->second = true;
+			break;
+		}
+	}
+
+	if (!db_inst)
+	{
+		WorldDatabase* tmp = new WorldDatabase();
+		db_inst = (Database*)tmp;
+		tmp->Init();
+		tmp->ConnectNewDatabase();
+		dbInstances.insert(make_pair(db_inst, true));
+	}
+	DBInstanceMutex.releasewritelock(__FUNCTION__, __LINE__);
+
+	return db_inst;
+}
+
+void Database::PurgeDBInstances()
+{
+	map<Database*, bool>::iterator itr;
+	DBInstanceMutex.writelock(__FUNCTION__, __LINE__);
+	for (itr = dbInstances.begin(); itr != dbInstances.end(); itr++) {
+		Database* tmpInst = itr->first;
+		safe_delete(tmpInst);
+	}
+	dbInstances.clear();
+	DBInstanceMutex.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+void Database::FreeDBInstance(Database* cur)
+{
+	DBInstanceMutex.writelock(__FUNCTION__, __LINE__);
+	dbInstances[cur] = false;
+	DBInstanceMutex.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+void Database::RemoveActiveQuery(Query* query)
+{
+	DBQueryMutex.writelock(__FUNCTION__, __LINE__);
+
+	vector<Query*>::iterator itr;
+	for (itr = activeQuerySessions.begin(); itr != activeQuerySessions.end(); itr++)
+	{
+		Query* curQuery = *itr;
+		if (query == curQuery)
+		{
+			activeQuerySessions.erase(itr);
+			break;
+		}
+	}
+	DBQueryMutex.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+void Database::AddActiveQuery(Query* query)
+{
+	DBQueryMutex.writelock(__FUNCTION__, __LINE__);
+	activeQuerySessions.push_back(query);
+	DBQueryMutex.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+bool Database::IsActiveQuery(int32 id, Query* skip)
+{
+	bool isActive = false;
+
+	DBQueryMutex.readlock(__FUNCTION__, __LINE__);
+	vector<Query*>::iterator itr;
+	for (itr = activeQuerySessions.begin(); itr != activeQuerySessions.end(); itr++)
+	{
+		Query* query = *itr;
+		if (query == skip)
+			continue;
+
+		if (query->GetQueryID() == id)
+		{
+			isActive = true;
+			break;
+		}
+	}
+	DBQueryMutex.releasereadlock(__FUNCTION__, __LINE__);
+
+	return isActive;
+}
+#endif

+ 59 - 3
EQ2/source/common/database.h

@@ -37,26 +37,53 @@
 #include <map>
 
 using namespace std;
+class Query;
 
 class Database : public DBcore
 {
 public:
 	Database();
 	~Database();
-	bool Init();
+	bool Init(bool silentLoad=false);
 	bool LoadVariables();
 	void HandleMysqlError(int32 errnum);
 	map<string, uint16> GetOpcodes(int16 version);
 	map<int16, int16> GetVersions();
 
+#ifdef WORLD
+	void AddAsyncQuery(Query* query);
+	void RunAsyncQueries(int32 queryid);
+	Database* FindFreeInstance();
+	void RemoveActiveQuery(Query* query);
+	void AddActiveQuery(Query* query);
+	bool IsActiveQuery(int32 id, Query* skip=0);
+#endif
 protected:
-	
+
 private:
 	void InitVars();
+
+#ifdef WORLD
+	void PurgeDBInstances();
+	void FreeDBInstance(Database* cur);
+	bool continueAsync;
+	map<int32, deque<Query*>> asyncQueries;
+	map<int32, Mutex*> asyncQueriesMutex;
+	map<Database*, bool> dbInstances;
+	vector<Query*> activeQuerySessions;
+	Mutex DBAsyncMutex;
+	Mutex DBInstanceMutex;
+	Mutex DBQueryMutex;
+#endif
 };
+
+typedef struct {
+	int32 queryid;
+}DBStruct;
+
 class Query{
 public:
-	Query(){
+	Query() {
 		result = 0;
 		affected_rows = 0;
 		last_insert_id = 0;
@@ -68,7 +95,25 @@ public:
 		escaped_data1 = 0;
 		multiple_results = 0;
 		memset(errbuf, 0, sizeof(errbuf));
+		queryID = 0;
 	}
+	Query(Query* queryPtr, int32 in_id) {
+		result = 0;
+		affected_rows = 0;
+		last_insert_id = 0;
+		errnum = 0;
+		row = 0;
+		retry = true;
+		escaped_name = 0;
+		escaped_pass = 0;
+		escaped_data1 = 0;
+		multiple_results = 0;
+		memset(errbuf, 0, sizeof(errbuf));
+		query = string(queryPtr->GetQuery());
+		in_type = queryPtr->GetQueryType();
+		queryID = in_id;
+	}
+
 	~Query(){
 		if(result)
 			mysql_free_result(result);
@@ -105,7 +150,16 @@ public:
 		if(result)
 			*row = mysql_fetch_row(result);
 	}
+	void AddQueryAsync(int32 queryID, Database* db, QUERY_TYPE type, const char* format, ...);
+	void RunQueryAsync(Database* db);
 	MYSQL_RES*	RunQuery2(QUERY_TYPE type, const char* format, ...);
+
+	QUERY_TYPE GetQueryType() {
+		return in_type;
+	}
+
+	int32 GetQueryID() { return queryID; }
+
 	char* escaped_name;
 	char* escaped_pass;
 	char* escaped_data1;
@@ -117,8 +171,10 @@ private:
 	int32* affected_rows;
 	int32* last_insert_id;
 	int32 errnum;
+	QUERY_TYPE in_type;
 	bool retry;
 	MYSQL_ROW* row;
 	MYSQL	mysql;
+	int32 queryID;
 };
 #endif