Browse Source

- Fixed popping non-existent items from overflow causing a crash
- Addressed spells being removed from character on /camp and disconnect
- LUA AddRecipeBookToPlayer(Player, BookID)
BookID = SELECT id, name, tradeskill_default_level FROM items WHERE item_type='Recipe'
- LUA RemoveRecipeFromPlayer(Player, RecipeID)
RecipeID = SELECT id from recipe where name='RecipeName';

Emagi 1 year ago
parent
commit
658658e1a6

+ 4 - 79
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -2302,90 +2302,15 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 					else if(item->generic_info.item_type == 7){
 							if(item->recipebook_info) {
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Scribing recipe book %s (%u) for player %s.", item->name.c_str(), item->recipebook_info->recipe_id, player->GetName());
-							Recipe* master_recipe = master_recipebook_list.GetRecipeBooks(item->recipebook_info->recipe_id);
-							
-							if(master_recipe) {
-								Recipe* recipe_book = new Recipe(master_recipe);//(item->details.item_id));
-								// if valid recipe book and the player doesn't have it
-								if (recipe_book && recipe_book->GetLevel() > client->GetPlayer()->GetTSLevel()) {
-									client->Message(CHANNEL_NARRATIVE, "Your tradeskill level is not high enough to scribe this book.");
-									safe_delete(recipe_book);
-								}
-								else if(recipe_book && !recipe_book->CanUseRecipeByClass(item, client->GetPlayer()->GetTradeskillClass())) {
-									client->Message(CHANNEL_NARRATIVE, "Your tradeskill class cannot use this recipe.");
-									safe_delete(recipe_book);
-								}
-								else if (recipe_book && !(client->GetPlayer()->GetRecipeBookList()->HasRecipeBook(item->recipebook_info->recipe_id))){// (item->details.item_id))) {
-									LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Valid recipe book that the player doesn't have");
-									// Add recipe book to the players list
-									client->GetPlayer()->GetRecipeBookList()->AddRecipeBook(recipe_book);
-
-									// Get a list of all recipes this book contains
-									vector<Recipe*> recipes = master_recipe_list.GetRecipes(recipe_book->GetBookName());
-									LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "%i recipes found for %s book", recipes.size(), recipe_book->GetBookName());
-
-									if (recipes.empty() && item->recipebook_info) {
-										//Backup I guess if the recipe book is empty for whatever reason?
-										for (auto& itr : item->recipebook_info->recipes) {
-											Recipe* r = master_recipe_list.GetRecipe(itr);   //GetRecipeByName(itr.c_str());
-											if (r) {
-												recipes.push_back(r);
-											}
-										}
-									}
-									
-									//Filter out duplicate recipes the player already has
-									for (auto itr = recipes.begin(); itr != recipes.end();) {
-										Recipe* recipe = *itr;
-										if (client->GetPlayer()->GetRecipeList()->GetRecipe(recipe->GetID())) {
-											itr = recipes.erase(itr);
-										}
-										else itr++;
-									}
-
-									int16 i = 0;
-									// Create the packet to send to update the players recipe list
-									PacketStruct* packet = 0;
-									if (!recipes.empty() && client->GetRecipeListSent()) {
-										packet = configReader.getStruct("WS_RecipeList", client->GetVersion());
-										if (packet) {
-											packet->setArrayLengthByName("num_recipes", recipes.size());
-										}
-									}
-
-									for (int32 r = 0; r < recipes.size(); r++) {
-										Recipe* recipe = recipes[r];
-										if (recipe) {
-											Recipe* player_recipe = new Recipe(recipe);
-											client->AddRecipeToPlayer(player_recipe, packet, &i);
-										}
-									}
-
-
-									LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Done adding recipes");
-									database.SavePlayerRecipeBook(client->GetPlayer(), recipe_book->GetBookID());
-									database.DeleteItem(client->GetCharacterID(), item, 0);
-									client->GetPlayer()->item_list.RemoveItem(item, true);
-									client->QueuePacket(client->GetPlayer()->SendInventoryUpdate(client->GetVersion()));
-									if (packet && client->GetRecipeListSent())
-										client->QueuePacket(packet->serialize());
-
-									safe_delete(packet);
-								}
-								else {
-									if (recipe_book)
-										client->Message(CHANNEL_NARRATIVE, "You have already learned all you can from this item.");
-									safe_delete(recipe_book);
-								}
+							client->AddRecipeBookToPlayer(item->recipebook_info->recipe_id, item);
 							}
 							else {
 									client->Message(CHANNEL_NARRATIVE, "Recipe book is unavailable! Report to admin recipe id %u could not be retrieved.", item->recipebook_info ? item->recipebook_info->recipe_id : 0);
 									LogWrite(COMMAND__ERROR, 0, "Command", "Recipe Book %u could not be retrieved for item %s.", item->recipebook_info ? item->recipebook_info->recipe_id : 0, item->name.c_str());
 							}
-						}
-						else {
-								LogWrite(COMMAND__ERROR, 0, "Command", "Recipe Book Info is not set for item %s.", item->name.c_str());
-						}
+					}
+					else {
+						LogWrite(COMMAND__ERROR, 0, "Command", "Recipe Book Info is not set for item %s.", item->name.c_str());
 					}
 				}
 				else

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

@@ -3706,13 +3706,19 @@ bool PlayerItemList::AddOverflowItem(Item* item) {
 }
 
 Item* PlayerItemList::GetOverflowItem() {
+	if(overflowItems.empty()) {
+		return nullptr;
+	}
+	
 	return overflowItems.at(0);
 }
 
 void PlayerItemList::RemoveOverflowItem(Item* item) {
 	MPlayerItems.writelock(__FUNCTION__, __LINE__);
 	vector<Item*>::iterator itr = std::find(overflowItems.begin(), overflowItems.end(), item);
-	overflowItems.erase(itr);
+	if(itr != overflowItems.end()) {
+		overflowItems.erase(itr);
+	}
 	MPlayerItems.releasewritelock(__FUNCTION__, __LINE__);
 }
 

+ 70 - 0
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -13410,9 +13410,11 @@ int EQ2Emu_lua_DamageEquippedItems(lua_State* state) {
 	
 	if (!spawn) {
 		lua_interface->LogError("%s: LUA DamageEquippedItems command error: spawn is not valid", lua_interface->GetScriptName(state));
+		lua_interface->ResetFunctionStack(state);
 		return 0;
 	}
 
+	lua_interface->ResetFunctionStack(state);
 	if (spawn->IsPlayer()) {
 		if (((Player*)spawn)->GetClient() && ((Player*)spawn)->DamageEquippedItems(damage_amount, ((Player*)spawn)->GetClient()))
 			lua_interface->SetBooleanValue(state, true);
@@ -13433,6 +13435,7 @@ int EQ2Emu_lua_CreateWidgetRegion(lua_State* state) {
 	RegionMap* region_map = world.GetRegionMap(std::string(zone->GetZoneFile()), version);
 	if(region_map == nullptr) {
 		lua_interface->LogError("%s: LUA CreateWidgetRegion command error: region map is not valid for version %u", lua_interface->GetScriptName(state), version);
+		lua_interface->ResetFunctionStack(state);
 		return 0;
 	}
 	string region_name = lua_interface->GetStringValue(state, 3);
@@ -13455,6 +13458,7 @@ int EQ2Emu_lua_RemoveRegion(lua_State* state) {
 	RegionMap* region_map = world.GetRegionMap(std::string(zone->GetZoneFile()), version);
 	if(region_map == nullptr) {
 		lua_interface->LogError("%s: LUA RemoveRegion command error: region map is not valid for version %u", lua_interface->GetScriptName(state), version);
+		lua_interface->ResetFunctionStack(state);
 		return 0;
 	}
 	string region_name = lua_interface->GetStringValue(state, 3);
@@ -13473,12 +13477,14 @@ int EQ2Emu_lua_SetPlayerPOVGhost(lua_State* state) {
 	if (!player) {
 		lua_interface->LogError("LUA SetPlayerPOVGhost command error: spawn is not valid");
 		lua_interface->SetBooleanValue(state, false);
+		lua_interface->ResetFunctionStack(state);
 		return 1;
 	}
 
 	if (!player->IsPlayer()) {
 		lua_interface->LogError("LUA SetPlayerPOVGhost command error: spawn is not a player");
 		lua_interface->SetBooleanValue(state, false);
+		lua_interface->ResetFunctionStack(state);
 		return 1;
 	}
 
@@ -13487,6 +13493,7 @@ int EQ2Emu_lua_SetPlayerPOVGhost(lua_State* state) {
 	if (!client) {
 		lua_interface->LogError("LUA SetPlayerPOVGhost command error: could not find client");
 		lua_interface->SetBooleanValue(state, false);
+		lua_interface->ResetFunctionStack(state);
 		return 1;
 	}
 	
@@ -13547,3 +13554,66 @@ int EQ2Emu_lua_IsCastOnAggroComplete(lua_State* state) {
 
 	return 1;
 }
+
+int EQ2Emu_lua_AddRecipeBookToPlayer(lua_State* state) {
+	Client* client = nullptr;
+	Spawn* player = lua_interface->GetSpawn(state);
+	int32 recipe_book_id = lua_interface->GetInt32Value(state, 2);
+	lua_interface->ResetFunctionStack(state);
+	if (!player) {
+		lua_interface->LogError("LUA AddRecipeBookToPlayer command error: spawn is not valid");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	if (!player->IsPlayer()) {
+		lua_interface->LogError("LUA AddRecipeBookToPlayer command error: spawn is not a player");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	client = player->GetClient();
+
+	if (!client) {
+		lua_interface->LogError("LUA AddRecipeBookToPlayer command error: could not find client");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	bool result = client->AddRecipeBookToPlayer(recipe_book_id);
+
+	lua_interface->SetBooleanValue(state, result);
+	return 1;
+}
+
+int EQ2Emu_lua_RemoveRecipeFromPlayer(lua_State* state) {
+	Client* client = nullptr;
+	Spawn* player = lua_interface->GetSpawn(state);
+	int32 recipe_id = lua_interface->GetInt32Value(state, 2);
+	lua_interface->ResetFunctionStack(state);
+	if (!player) {
+		lua_interface->LogError("LUA RemoveRecipeFromPlayer command error: spawn is not valid");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	if (!player->IsPlayer()) {
+		lua_interface->LogError("LUA RemoveRecipeFromPlayer command error: spawn is not a player");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	client = player->GetClient();
+
+	if (!client) {
+		lua_interface->LogError("LUA RemoveRecipeFromPlayer command error: could not find client");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	bool result = client->RemoveRecipeFromPlayer(recipe_id);
+
+	lua_interface->SetBooleanValue(state, result);
+	return 1;
+}
+

+ 3 - 0
EQ2/source/WorldServer/LuaFunctions.h

@@ -634,4 +634,7 @@ int EQ2Emu_lua_RemoveRegion(lua_State* state);
 int EQ2Emu_lua_SetPlayerPOVGhost(lua_State* state);
 int EQ2Emu_lua_SetCastOnAggroComplete(lua_State* state);
 int EQ2Emu_lua_IsCastOnAggroComplete(lua_State* state);
+
+int EQ2Emu_lua_AddRecipeBookToPlayer(lua_State* state);
+int EQ2Emu_lua_RemoveRecipeFromPlayer(lua_State* state);
 #endif

+ 3 - 0
EQ2/source/WorldServer/LuaInterface.cpp

@@ -1495,6 +1495,9 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	
 	lua_register(state, "SetCastOnAggroComplete", EQ2Emu_lua_SetCastOnAggroComplete);
 	lua_register(state, "IsCastOnAggroComplete", EQ2Emu_lua_IsCastOnAggroComplete);
+	
+	lua_register(state, "AddRecipeBookToPlayer", EQ2Emu_lua_AddRecipeBookToPlayer);
+	lua_register(state, "RemoveRecipeFromPlayer", EQ2Emu_lua_RemoveRecipeFromPlayer);
 }
 
 void LuaInterface::LogError(const char* error, ...)  {

+ 4 - 8
EQ2/source/WorldServer/Player.cpp

@@ -6625,8 +6625,7 @@ void Player::SaveSpellEffects()
 			if(caster_char_id == 0)
 				continue;
 			
-			Query effectSave;
-			effectSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
+			savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
 			"insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, custom_function) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s')", 
 			database.getSafeEscapeString(info->spell_effects[i].spell->spell->GetName()).c_str(), caster_char_id,
 			target_char_id,  0 /*no target_type for spell_effects*/, DB_TYPE_SPELLEFFECTS /* db_effect_type for spell_effects */, info->spell_effects[i].spell->spell->IsCopiedSpell() ? info->spell_effects[i].spell->spell->GetSpellData()->inherited_spell_id : info->spell_effects[i].spell_id, i, info->spell_effects[i].spell->slot_pos, 
@@ -6654,8 +6653,7 @@ void Player::SaveSpellEffects()
 			int32 timestamp = 0xFFFFFFFF;
 			if(!info->maintained_effects[i].spell->spell->GetSpellData()->duration_until_cancel)
 				timestamp = info->maintained_effects[i].expire_timestamp - Timer::GetCurrentTime2();
-			Query effectSave;
-			effectSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
+			savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
 			"insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, custom_function) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s')", 
 			database.getSafeEscapeString(info->maintained_effects[i].name).c_str(), caster_char_id, target_char_id,  info->maintained_effects[i].target_type, DB_TYPE_MAINTAINEDEFFECTS /* db_effect_type for maintained_effects */, info->maintained_effects[i].spell->spell->IsCopiedSpell() ? info->maintained_effects[i].spell->spell->GetSpellData()->inherited_spell_id : info->maintained_effects[i].spell_id, i, info->maintained_effects[i].slot_pos, 
 			info->maintained_effects[i].icon, info->maintained_effects[i].icon_backdrop, info->maintained_effects[i].conc_used, info->maintained_effects[i].tier, 
@@ -6720,10 +6718,8 @@ void Player::SaveSpellEffects()
 				firstTarget = false;
 			}
 			info->maintained_effects[i].spell->MSpellTargets.releasereadlock(__FUNCTION__, __LINE__);
-			if(!firstTarget)
-			{
-				Query targetSave;
-				targetSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, insertTargets.c_str());
+			if(!firstTarget) {
+				savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, insertTargets.c_str());
 			}
 		}
 	}

+ 2 - 2
EQ2/source/WorldServer/Quests.cpp

@@ -401,6 +401,7 @@ Quest::~Quest(){
 	for(int32 i=0;i<quest_steps.size();i++)
 		safe_delete(quest_steps[i]);
 	quest_steps.clear();
+	quest_step_map.clear();
 	MQuestSteps.unlock();
 	
 	for(int32 i=0;i<prereq_items.size();i++)
@@ -418,8 +419,7 @@ Quest::~Quest(){
 	for(int32 i=0;i<selectable_reward_items.size();i++)
 		safe_delete(selectable_reward_items[i]);
 	selectable_reward_items.clear();
-	
-	quest_step_map.clear();
+
 	task_group_order.clear();
 	task_group.clear();
 	complete_actions.clear();

+ 1 - 1
EQ2/source/WorldServer/Quests.h

@@ -328,9 +328,9 @@ public:
 	bool				CanDeleteQuest() { return can_delete_quest; }
 	
 	bool				CanShareQuestCriteria(Client* quest_sharer, bool display_client_msg = true);
+	Mutex				MQuestSteps;
 protected:
 	bool				needs_save;
-	Mutex				MQuestSteps;
 	Mutex				MCompletedActions;
 	Mutex               MProgressActions;
 	Mutex				MFailedActions;

+ 13 - 0
EQ2/source/WorldServer/Recipes/Recipe.cpp

@@ -175,6 +175,7 @@ PlayerRecipeList::~PlayerRecipeList(){
 }
 
 bool PlayerRecipeList::AddRecipe(Recipe *recipe){
+    std::unique_lock lock(player_recipe_mutex);
 	assert(recipe);
 
 	if(recipes.count(recipe->GetID()) == 0){
@@ -185,12 +186,14 @@ bool PlayerRecipeList::AddRecipe(Recipe *recipe){
 }
 
 Recipe * PlayerRecipeList::GetRecipe(int32 recipe_id){
+    std::shared_lock lock(player_recipe_mutex);
 	if (recipes.count(recipe_id) > 0)
 		return recipes[recipe_id];
 	return 0;
 }
 
 void PlayerRecipeList::ClearRecipes(){
+    std::unique_lock lock(player_recipe_mutex);
 	map<int32, Recipe *>::iterator itr;
 
 	for (itr = recipes.begin(); itr != recipes.end(); itr++)
@@ -198,6 +201,16 @@ void PlayerRecipeList::ClearRecipes(){
 	recipes.clear();
 }
 
+bool PlayerRecipeList::RemoveRecipe(int32 recipe_id) {
+    std::unique_lock lock(player_recipe_mutex);
+	bool ret = false;
+	if (recipes.count(recipe_id) > 0) {
+		recipes.erase(recipe_id);
+		ret = true;
+	}
+	return ret;
+}
+
 MasterRecipeBookList::MasterRecipeBookList(){
 	m_recipeBooks.SetName("MasterRecipeBookList::recipeBooks");
 }

+ 2 - 0
EQ2/source/WorldServer/Recipes/Recipe.h

@@ -237,12 +237,14 @@ public:
 	bool AddRecipe(Recipe *recipe);
 	Recipe * GetRecipe(int32 recipe_id);
 	void ClearRecipes();
+	bool RemoveRecipe(int32 recipe_id);
 	int32 Size();
 
 	map<int32, Recipe *> * GetRecipes() {return &recipes;}
 
 private:
 	map<int32, Recipe *> recipes;
+	mutable std::shared_mutex player_recipe_mutex;
 };
 
 class PlayerRecipeBookList {

+ 3 - 0
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -2680,6 +2680,7 @@ void WorldDatabase::SaveCharacterQuestProgress(Client* client, Quest* quest){
 	vector<QuestStep*>* steps = quest->GetQuestSteps();
 	vector<QuestStep*>::iterator itr;
 	QuestStep* step = 0;
+	quest->MQuestSteps.readlock(__FUNCTION__, __LINE__);
 	if(steps){
 		for(itr = steps->begin(); itr != steps->end(); itr++){
 			step = *itr;
@@ -2687,6 +2688,8 @@ void WorldDatabase::SaveCharacterQuestProgress(Client* client, Quest* quest){
 				query.RunQuery2(Q_REPLACE, "replace into character_quest_progress (char_id, quest_id, step_id, progress) values(%u, %u, %u, %i)", client->GetCharacterID(), quest->GetQuestID(), step->GetStepID(), step->GetQuestCurrentQuantity());
 		}
 	}
+	quest->MQuestSteps.releasereadlock(__FUNCTION__, __LINE__);
+	
 	if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF)
 		LogWrite(WORLD__ERROR, 0, "World", "Error in SaveCharacterQuestProgress query '%s': %s", query.GetQuery(), query.GetError());
 }

+ 166 - 10
EQ2/source/WorldServer/client.cpp

@@ -118,6 +118,7 @@ extern Chat chat;
 extern MasterAAList master_aa_list;
 extern MasterAAList master_tree_nodes;
 extern ChestTrapList chest_trap_list;
+extern MasterRecipeBookList master_recipebook_list;
 
 using namespace std;
 
@@ -4293,10 +4294,8 @@ void Client::Zone(ZoneServer* new_zone, bool set_coords, bool is_spell) {
 
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "%s: Removing player from fighting...", __FUNCTION__);
 	//GetCurrentZone()->GetCombat()->RemoveHate(player);
-	MSaveSpellStateMutex.lock();
-	player->SaveSpellEffects();
-	player->SetSaveSpellEffects(true);
-	MSaveSpellStateMutex.unlock();
+	SaveSpells();
+	
 	ResetSendMail();
 	// Remove players pet from zone if there is one
 	((Entity*)player)->DismissAllPets();
@@ -10070,11 +10069,11 @@ void Client::AcceptCollectionRewards(Collection* collection, int32 selectable_it
 }
 
 void Client::SendRecipeList() {
-	PacketStruct* packet = 0;
-	if (GetRecipeListSent()) {
+	if(GetRecipeListSent()) {
 		return;
 	}
-
+	
+	PacketStruct* packet = 0;
 	if (!(packet = configReader.getStruct("WS_RecipeList", version))) {
 		return;
 	}
@@ -10093,7 +10092,8 @@ void Client::SendRecipeList() {
 		else
 			devices.push_back(recipe->GetDevice());
 	}
-	
+
+	packet->setDataByName("command_type", 0);
 	packet->setArrayLengthByName("num_recipes", recipes->size());
 	for (itr = recipes->begin(); itr != recipes->end(); itr++) {
 		recipe = itr->second;
@@ -10139,7 +10139,6 @@ void Client::SendRecipeList() {
 	//DumpPacket(packet->serialize());
 	QueuePacket(packet->serialize());
 	safe_delete(packet);
-
 	SetRecipeListSent(true);
 }
 
@@ -11614,7 +11613,7 @@ void Client::UpdateCharacterRewardData(QuestRewardData* data) {
 	}
 }
 
-void Client::AddRecipeToPlayer(Recipe* recipe, PacketStruct* packet, int16* i) {
+void Client::AddRecipeToPlayerPack(Recipe* recipe, PacketStruct* packet, int16* i) {
 	
 	
 	int  index = 0;
@@ -11815,4 +11814,161 @@ void Client::SendNewTradeskillSpells() {
 	if(secondary_class != player->GetTradeskillClass()) { 
 		SendNewTSSpells(secondary_class);
 	}
+}
+
+bool Client::AddRecipeBookToPlayer(int32 recipe_book_id, Item* item) {
+	Recipe* master_recipe = master_recipebook_list.GetRecipeBooks(recipe_book_id);
+	
+	if(master_recipe) {
+		Recipe* recipe_book = new Recipe(master_recipe);
+		// if valid recipe book and the player doesn't have it
+		if (recipe_book && recipe_book->GetLevel() > GetPlayer()->GetTSLevel()) {
+			if(item) {
+				Message(CHANNEL_NARRATIVE, "Your tradeskill level is not high enough to scribe this book.");
+			}
+			safe_delete(recipe_book);
+		}
+		else if(recipe_book && item && !recipe_book->CanUseRecipeByClass(item, GetPlayer()->GetTradeskillClass())) {
+			if(item) {
+				Message(CHANNEL_NARRATIVE, "Your tradeskill class cannot use this recipe.");
+			}
+			safe_delete(recipe_book);
+		}
+		else if (recipe_book && (!item || !(GetPlayer()->GetRecipeBookList()->HasRecipeBook(recipe_book_id)))){
+			LogWrite(PLAYER__ERROR, 0, "Recipe", "Valid recipe book that the player doesn't have");
+			// Add recipe book to the players list
+			if(!GetPlayer()->GetRecipeBookList()->HasRecipeBook(recipe_book_id)) {
+				GetPlayer()->GetRecipeBookList()->AddRecipeBook(recipe_book);
+			}
+
+			// Get a list of all recipes this book contains
+			vector<Recipe*> recipes = master_recipe_list.GetRecipes(recipe_book->GetBookName());
+			LogWrite(PLAYER__ERROR, 0, "Recipe", "%i recipes found for %s book", recipes.size(), recipe_book->GetBookName());
+
+			if (recipes.empty() && item && item->recipebook_info) {
+				//Backup I guess if the recipe book is empty for whatever reason?
+				for (auto& itr : item->recipebook_info->recipes) {
+					Recipe* r = master_recipe_list.GetRecipe(itr);   //GetRecipeByName(itr.c_str());
+					if (r) {
+						recipes.push_back(r);
+					}
+				}
+			}
+			
+			//Filter out duplicate recipes the player already has
+			for (auto itr = recipes.begin(); itr != recipes.end();) {
+				Recipe* recipe = *itr;
+				if (GetPlayer()->GetRecipeList()->GetRecipe(recipe->GetID())) {
+					itr = recipes.erase(itr);
+				}
+				else itr++;
+			}
+
+			int16 i = 0;
+			// Create the packet to send to update the players recipe list
+			PacketStruct* packet = 0;
+			if (!recipes.empty() && GetRecipeListSent()) {
+				packet = configReader.getStruct("WS_RecipeList", GetVersion());
+				if (packet) {
+					packet->setArrayLengthByName("num_recipes", recipes.size());
+				}
+			}
+
+			for (int32 r = 0; r < recipes.size(); r++) {
+				Recipe* recipe = recipes[r];
+				if (recipe) {
+					Recipe* player_recipe = new Recipe(recipe);
+					AddRecipeToPlayerPack(player_recipe, packet, &i);
+				}
+			}
+
+			LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Done adding recipes");
+			database.SavePlayerRecipeBook(GetPlayer(), recipe_book->GetBookID());
+			if(item) {
+				database.DeleteItem(GetCharacterID(), item, 0);
+				GetPlayer()->item_list.RemoveItem(item, true);
+			}
+			QueuePacket(GetPlayer()->SendInventoryUpdate(GetVersion()));
+
+			SetRecipeListSent(false);
+			SendRecipeList();
+			safe_delete(packet);
+			return true;
+		}
+		else {
+			if (recipe_book && item) {
+				Message(CHANNEL_NARRATIVE, "You have already learned all you can from this item.");
+			}
+			safe_delete(recipe_book);
+		}
+	}
+	else {
+			LogWrite(PLAYER__ERROR, 0, "Player", "%u recipe book id does not exist.  Cannot AddRecipeToPlayer.", recipe_book_id);
+	}
+	return false;
+}
+
+
+bool Client::RemoveRecipeFromPlayer(int32 recipe_id) {
+	PlayerRecipeList* prl = GetPlayer()->GetRecipeList();
+	
+	PacketStruct* packet = configReader.getStruct("WS_RecipeList", version);
+	Recipe* recipe = prl->GetRecipe(recipe_id);
+	int8 level = player->GetTSLevel();
+	if(packet && recipe) {
+		packet->setDataByName("command_type", 1);
+		packet->setArrayLengthByName("num_recipes", 1);
+		int32 myid = recipe->GetID();
+		int8 rlevel = recipe->GetLevel();
+		int8 even = level - level * .05 + .5;
+		int8 easymin = level - level * .25 + .5;
+		int8 veasymin = level - level * .35 + .5;
+		if (rlevel > level )
+			packet->setArrayDataByName("tier", 4, 0);
+		else if ((rlevel <= level) & (rlevel >= even))
+			packet->setArrayDataByName("tier", 3, 0);
+		else if ((rlevel <= even) & (rlevel >= easymin))
+			packet->setArrayDataByName("tier", 2, 0);
+		else if ((rlevel <= easymin) & (rlevel >= veasymin))
+			packet->setArrayDataByName("tier", 1, 0);
+		else if ((rlevel <= veasymin) & (rlevel >= 0))
+			packet->setArrayDataByName("tier", 0, 0);
+		if (rlevel == 2)
+			int xxx = 1;
+		packet->setArrayDataByName("recipe_id", myid, 0);
+		packet->setArrayDataByName("level", recipe->GetLevel(), 0);
+		packet->setArrayDataByName("unknown1", recipe->GetLevel(), 0);
+		packet->setArrayDataByName("icon", recipe->GetIcon(), 0);
+		packet->setArrayDataByName("classes", recipe->GetClasses(), 0);
+		packet->setArrayDataByName("technique", recipe->GetTechnique(), 0);
+		packet->setArrayDataByName("knowledge", recipe->GetKnowledge(), 0);
+
+		
+		auto recipe_device = std::find(devices.begin(), devices.end(), recipe->GetDevice());
+		if (recipe_device != devices.end())
+			packet->setArrayDataByName("device_type", recipe_device - devices.begin(), 0);
+		else
+		{//TODO error should never get here
+		}
+		packet->setArrayDataByName("device_sub_type", recipe->GetDevice_Sub_Type(), 0);
+		packet->setArrayDataByName("recipe_name", recipe->GetName(), 0);
+		packet->setArrayDataByName("recipe_book", recipe->GetBook(), 0);
+		packet->setArrayDataByName("unknown3", recipe->GetUnknown3(), 0);
+	QueuePacket(packet->serialize());
+	}
+	safe_delete(packet);
+	
+	bool res = prl->RemoveRecipe(recipe_id);
+	if(res) {
+		Query query;
+		query.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "DELETE FROM character_recipes where char_id=%u and recipe_id=%u", GetCharacterID(), recipe_id);
+	}
+	return res;
+}
+
+void Client::SaveSpells() {
+	MSaveSpellStateMutex.lock();
+	player->SaveSpellEffects();
+	player->SetSaveSpellEffects(true);
+	MSaveSpellStateMutex.unlock();	
 }

+ 7 - 2
EQ2/source/WorldServer/client.h

@@ -571,7 +571,6 @@ public:
 	void	UpdateCharacterRewardData(QuestRewardData* data);
 	void	SetQuestUpdateState(bool val) { quest_updates = val; }
 	
-	void	AddRecipeToPlayer(Recipe* recipe, PacketStruct* packet, int16* i);
 	
 	bool	SetPlayerPOVGhost(Spawn* spawn);
 	
@@ -584,7 +583,13 @@ public:
 	
 	void	StartLinkdeadTimer();
 	bool	IsLinkdeadTimerEnabled();
+	
+	bool	AddRecipeBookToPlayer(int32 recipe_id, Item* item = nullptr);
+	bool	RemoveRecipeFromPlayer(int32 recipe_id);
+	
+	void	SaveSpells();
 private:
+	void	AddRecipeToPlayerPack(Recipe* recipe, PacketStruct* packet, int16* i);
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
 	void	GiveQuestReward(Quest* quest, bool has_displayed = false);
@@ -685,7 +690,7 @@ private:
 	IncomingPaperdollImage incoming_paperdoll;
 	int32 transmuteID;
 
-	bool m_recipeListSent;
+	std::atomic<bool> m_recipeListSent;
 	bool initial_spawns_sent;
 	bool should_load_spells;
 

+ 2 - 0
EQ2/source/WorldServer/zoneserver.cpp

@@ -3331,6 +3331,8 @@ void ZoneServer::RemoveClient(Client* client)
 		LogWrite(ZONE__INFO, 0, "Zone", "Scheduling client '%s' for removal.", client->GetPlayer()->GetName());
 		database.ToggleCharacterOnline(client, 0);
 		
+		client->SaveSpells();
+		
 		client->GetPlayer()->DeleteSpellEffects(true);
 		
 		RemoveSpawn(client->GetPlayer(), false, true, true, true, true);

+ 2 - 2
server/WorldStructs.xml

@@ -16309,7 +16309,7 @@ to zero and treated like placeholders." />
 	</Data>
 </Struct>
 <Struct Name="WS_RecipeList" ClientVersion="1" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_RecipeList">
-<Data ElementName="unknown" Type="int8" Size="1" />
+<Data ElementName="command_type" Type="int8" Size="1" />
 <Data ElementName="num_recipes" Type="int16" Size="1" />
 <Data ElementName="recipe_array" Type="Array" ArraySizeVariable="num_recipes">
   <Data ElementName="id" Type="int32" Size="1" />
@@ -16327,7 +16327,7 @@ to zero and treated like placeholders." />
 </Data>
 </Struct>
 <Struct Name="WS_RecipeList" ClientVersion="60085" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_RecipeList">
-<Data ElementName="unknown" Type="int8" Size="1" />
+<Data ElementName="command_type" Type="int8" Size="1" />
 <Data ElementName="num_recipes" Type="int16" Size="1" />
 <Data ElementName="recipe_array" Type="Array" ArraySizeVariable="num_recipes">
   <Data ElementName="recipe_id" Type="int32" Size="1" />