Browse Source

DoF Recipe/Tradeskill Stage 1 of 2 for Issue #532
- /info command now has recipe_product support
- /delete_quest crash fix
- knowledge sorting for DoF client now supported (ability book cannot be resorted in DoF)
- fixed copying of Recipe::Recipe class
- OP_RequestRecipeDetailsMsg and WS_RecipeDetailList support for earlier clients (DoF and probably similar classic)
- fixed HandleExamineInfoRequest to properly map the "id" for DoF client
- Quest mutex updated for pending quests to avoid crashes
- PacketStruct::serializeCountPacket better defined for larger sizes

Emagi 8 months ago
parent
commit
f720513876

+ 49 - 7
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -2194,6 +2194,30 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 					else
 						LogWrite(COMMAND__ERROR, 0, "Command", "Unknown Recipe ID: %u", recipe_id);
 				}
+				else if (strcmp(sep->arg[0], "recipe_product") == 0) {
+					sint32 recipe_id = atol(sep->arg[1]);
+									
+					PlayerRecipeList* prl = client->GetPlayer()->GetRecipeList();
+					Recipe* recipe = nullptr;
+					if ((recipe = prl->GetRecipe(recipe_id)) && recipe->GetProductID()) {
+						RecipeProducts* rp = recipe->products[1];
+						if (recipe->GetProductID() > 0) {
+							Item* item = master_item_list.GetItem(recipe->GetProductID());
+							if(item){
+								EQ2Packet* app = item->serialize(client->GetVersion(), true, client->GetPlayer());
+								client->QueuePacket(app);
+							}
+							else
+								LogWrite(COMMAND__ERROR, 0, "Command", "Unknown Item ID: %u", recipe->GetProductID());
+						}
+						else {
+								LogWrite(COMMAND__ERROR, 0, "Command", "recipe_product recipe->GetProductID() has value 0 for recipe id %u.", recipe_id);
+						}
+					}
+					else {
+							LogWrite(COMMAND__ERROR, 0, "Command", "recipe_product with recipe id %u not found (recipe missing or no product in stage 1 assigned).", recipe_id);
+					}
+				}
 				else if (strcmp(sep->arg[0], "maintained") ==0) {
 					int32 slot = atol(sep->arg[1]);
 					int32 spell_id = atol(sep->arg[2]);
@@ -3048,14 +3072,19 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 			if(sep && sep->arg[0] && sep->IsNumber(0))
 				quest_id = atoul(sep->arg[0]);
 			if(quest_id > 0){
+				client->GetPlayer()->MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 				if(lua_interface && client->GetPlayer()->player_quests.count(quest_id) > 0) {
 					Quest* quest = client->GetPlayer()->player_quests[quest_id];
+					client->GetPlayer()->MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 					if (quest && quest->CanDeleteQuest()) {
 						lua_interface->CallQuestFunction(quest, "Deleted", client->GetPlayer());
 						client->RemovePlayerQuest(quest_id);
 						client->GetCurrentZone()->SendQuestUpdates(client);
 					}
 				}
+				else {
+					client->GetPlayer()->MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
+				}
 			}
 			break;
 								  }
@@ -3625,16 +3654,23 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 			break;
 		}
 		case COMMAND_KNOWLEDGEWINDOWSORT: {
-			if (client->GetVersion() <= 546)
-				break;
 
-			if (sep && sep->GetArgNumber() == 4)
+			if (sep && (client->GetVersion() <= 546 && sep->GetArgNumber() == 3) || sep->GetArgNumber() == 4)
 			{
 				int32 book = atoul(sep->arg[0]); // 0 - spells, 1 - combat, 2 - abilities, 3 - tradeskill
+				
+				// cannot sort the ability book in this client it is greyed out
+				if (client->GetVersion() <= 546 && book == SPELL_BOOK_TYPE_ABILITY)
+					break;
+			
 				int32 sort_by = atoul(sep->arg[1]); // 0 - alpha, 1 - level, 2 - category
 				int32 order = atoul(sep->arg[2]); // 0 - ascending, 1 - descending
 				int32 pattern = atoul(sep->arg[3]); // 0 - zigzag, 1 - down, 2 - across
-				int32 maxlvlonly = atoul(sep->arg[4]); // 0 - zigzag, 1 - down, 2 - across
+				int32 maxlvlonly = 0;
+				
+				if(client->GetVersion() > 546 && sep->arg[4][0]) {
+					maxlvlonly = atoul(sep->arg[4]); // checkbox for newer clients
+				}
 				client->GetPlayer()->ResortSpellBook(sort_by, order, pattern, maxlvlonly, book);
 				ClientPacketFunctions::SendSkillSlotMappings(client);
 			}
@@ -5844,7 +5880,6 @@ void Commands::Command_CancelMaintained(Client* client, Seperator* sep)
 void Commands::Command_Create(Client* client, Seperator* sep)
 {
 	PrintSep(sep, "COMMAND_CREATE");
-	client->SendRecipeList();
 	client->ShowRecipeBook();
 }
 
@@ -7687,7 +7722,9 @@ void Commands::Command_ModifyQuest(Client* client, Seperator* sep)
 				for (itr = quests->begin(); itr != quests->end(); itr++)
 				{
 					Quest* quest = itr->second;
-					client->Message(CHANNEL_COLOR_YELLOW, "%u) %s", itr->first, quest->GetName());
+					if(quest) {
+						client->Message(CHANNEL_COLOR_YELLOW, "%u) %s", itr->first, quest->GetName());
+					}
 				}
 			}
 
@@ -7706,7 +7743,9 @@ void Commands::Command_ModifyQuest(Client* client, Seperator* sep)
 				for (itr = quests->begin(); itr != quests->end(); itr++)
 				{
 					Quest* quest = itr->second;
-					client->Message(CHANNEL_COLOR_YELLOW, "%u) %s", itr->first, quest->GetName());
+					if(quest) {
+						client->Message(CHANNEL_COLOR_YELLOW, "%u) %s", itr->first, quest->GetName());
+					}
 				}
 			}
 
@@ -10577,6 +10616,9 @@ void Commands::Command_Test(Client* client, EQ2_16BitString* command_parms) {
 				safe_delete(packet);
 			}
 		}
+		else if (atoi(sep->arg[0]) == 31) {
+			client->SendRecipeList();
+		}
 	}
 	else {
 			PacketStruct* packet2 = configReader.getStruct("WS_ExamineSpellInfo", client->GetVersion());

+ 71 - 14
EQ2/source/WorldServer/Recipes/Recipe.cpp

@@ -24,9 +24,11 @@
 #include "Recipe.h"
 #include "../../common/ConfigReader.h"
 #include "../Items/Items.h"
+#include "../World.h"
 
 extern ConfigReader configReader;
 extern MasterItemList master_item_list;
+extern World world;
 
 
 Recipe::Recipe() {
@@ -57,28 +59,63 @@ Recipe::~Recipe() {
 Recipe::Recipe(Recipe *in){
 	assert(in);
 	id = in->GetID();
+	soe_id = in->GetSoeID();
 	book_id = in->GetBookID();
 	strncpy(name, in->GetName(), sizeof(name));
+	strncpy(description, in->GetDescription(), sizeof(description));
 	strncpy(book_name, in->GetBookName(), sizeof(book_name));
 	strncpy(book, in->GetBook(), sizeof(book));
 	strncpy(device, in->GetDevice(), sizeof(device));
-	strncpy(product_name, in->product_name, sizeof(product_name));
-	strncpy(primary_build_comp_title, in->primary_build_comp_title, sizeof(primary_build_comp_title));
-	strncpy(build1_comp_title, in->build1_comp_title, sizeof(build1_comp_title));
-	strncpy(build2_comp_title, in->build2_comp_title, sizeof(build2_comp_title));
-	strncpy(build3_comp_title, in->build3_comp_title, sizeof(build3_comp_title));
-	strncpy(build4_comp_title, in->build4_comp_title, sizeof(build4_comp_title));
+	
 	level = in->GetLevel();
 	tier = in->GetTier();
 	icon = in->GetIcon();
 	skill = in->GetSkill();
 	technique = in->GetTechnique();
 	knowledge = in->GetKnowledge();
+	device_sub_type = in->GetDevice_Sub_Type();
 	classes = in->GetClasses();
-	device_sub_type = in-> GetDevice_Sub_Type();
+	unknown1 = in->GetUnknown1();
 	unknown2 = in->GetUnknown2();
 	unknown3 = in->GetUnknown3();
 	unknown4 = in->GetUnknown4();
+	
+	product_item_id = in->GetProductID();
+	strncpy(product_name, in->product_name, sizeof(product_name));
+	product_qty = in->GetProductQuantity();
+	
+	strncpy(primary_build_comp_title, in->primary_build_comp_title, sizeof(primary_build_comp_title));
+	strncpy(build1_comp_title, in->build1_comp_title, sizeof(build1_comp_title));
+	strncpy(build2_comp_title, in->build2_comp_title, sizeof(build2_comp_title));
+	strncpy(build3_comp_title, in->build3_comp_title, sizeof(build3_comp_title));
+	strncpy(build4_comp_title, in->build4_comp_title, sizeof(build4_comp_title));
+	
+	strncpy(fuel_comp_title, in->fuel_comp_title, sizeof(fuel_comp_title));
+	build1_comp_qty = in->GetBuild1ComponentQuantity();
+	build2_comp_qty = in->GetBuild2ComponentQuantity();
+	build3_comp_qty = in->GetBuild3ComponentQuantity();
+	build4_comp_qty = in->GetBuild4ComponentQuantity();
+	fuel_comp_qty = in->GetFuelComponentQuantity();
+	primary_comp_qty = in->GetPrimaryComponentQuantity();
+	highestStage = in->GetHighestStage();
+	
+	std::map<int8, RecipeProducts*>::iterator itr;
+	for (itr = in->products.begin(); itr != in->products.end(); itr++) {
+		RecipeProducts* rp = new RecipeProducts;
+		rp->product_id = itr->second->product_id;
+		rp->byproduct_id = itr->second->byproduct_id;
+		rp->product_qty = itr->second->product_qty;
+		rp->byproduct_qty = itr->second->byproduct_qty;
+		products.insert(make_pair(itr->first, rp));
+	}
+	
+	std::map<int8, vector<int32>>::iterator itr2;
+	for (itr2 = in->components.begin(); itr2 != in->components.end(); itr2++) {
+		std::vector<int32> recipe_component;
+		 std::copy(itr2->second.begin(), itr2->second.end(),
+              std::back_inserter(recipe_component));
+		components.insert(make_pair(itr2->first, recipe_component));
+	}
 }
 
 MasterRecipeList::MasterRecipeList() {
@@ -224,6 +261,12 @@ bool PlayerRecipeList::RemoveRecipe(int32 recipe_id) {
 	return ret;
 }
 
+
+int32 PlayerRecipeList::Size() {
+    std::unique_lock lock(player_recipe_mutex);
+	return recipes.size();
+}
+
 MasterRecipeBookList::MasterRecipeBookList(){
 	m_recipeBooks.SetName("MasterRecipeBookList::recipeBooks");
 }
@@ -344,7 +387,11 @@ EQ2Packet * Recipe::SerializeRecipe(Client *client, Recipe *recipe, bool display
 		packet->setSubstructDataByName("info_header", "show_name", 1);
 	else
 		packet->setSubstructDataByName("info_header", "show_popup", 1);
-	if(packet_type > 0)
+	
+	if(client->GetVersion() <= 546) {
+		packet->setSubstructDataByName("info_header", "packettype", 0x02);
+	}
+	else if(packet_type > 0)
 		packet->setSubstructDataByName("info_header", "packettype", GetItemPacketType(packet->GetVersion()));
 	else
 		if(version == 1096)
@@ -473,6 +520,10 @@ EQ2Packet * Recipe::SerializeRecipe(Client *client, Recipe *recipe, bool display
 	// Check to see if we have a primary component (slot = 0)
 	vector<Item*> itemss;
 	if (recipe->components.count(0) > 0) {
+		if(client->GetVersion() <= 546) {
+			packet->setSubstructDataByName("recipe_info", "primary_count", 1);
+		}	
+		
 		int16 have_qty = 0;
 		vector<int32> rc = recipe->components[0];
 		for (itr = rc.begin(); itr != rc.end(); itr++, i++) {
@@ -504,20 +555,20 @@ EQ2Packet * Recipe::SerializeRecipe(Client *client, Recipe *recipe, bool display
 	if (recipe->components.count(4) > 0)
 		total_build_components++;
 
-	
+	int8 index = 0;
+	int8 count = 0;
 	if (total_build_components > 0) {
 		packet->setSubstructArrayLengthByName("recipe_info", "num_comps", total_build_components);
-		for (int8 index = 0; index < 4; index++) {
+		for (index = 0; index < 4; index++) {
 			if (recipe->components.count(index + 1) == 0)
 				continue;
-
+			
+			count++;
 			vector<int32> rc = recipe->components[index + 1];
 			int16 have_qty = 0;
 			string comp_title;
 			int8 comp_qty;
 			for (itr = rc.begin(); itr != rc.end(); itr++, i++) {
-
-				
 				if (index == 0) {
 					comp_title = recipe->build1_comp_title;
 					comp_qty = recipe->build1_comp_qty;
@@ -546,8 +597,14 @@ EQ2Packet * Recipe::SerializeRecipe(Client *client, Recipe *recipe, bool display
 		}
 		
 	}
+	
+	if(client->GetVersion() <= 546) {
+		packet->setSubstructDataByName("recipe_info", "fuel_count", 1);
+		packet->setSubstructDataByName("recipe_info", "fuel_comp", recipe->fuel_comp_title);
+		packet->setSubstructDataByName("recipe_info", "fuel_comp_qty", recipe->fuel_comp_qty);
+	}	
 	// Check to see if we have a fuel component (slot = 5)
-	if (recipe->components.count(5) > 0) {
+	else if (recipe->components.count(5) > 0) {
 		vector<int32> rc = recipe->components[5];
 		for (itr = rc.begin(); itr != rc.end(); itr++, i++) {
 			item = master_item_list.GetItem(*itr);

+ 14 - 3
EQ2/source/WorldServer/Tradeskills/TradeskillsPackets.cpp

@@ -27,10 +27,12 @@
 #include "../../common/Log.h"
 #include "../Spells.h"
 #include "../../common/MiscFunctions.h"
+#include "../World.h"
 
 extern ConfigReader configReader;
 extern MasterRecipeList master_recipe_list;
 extern MasterSpellList master_spell_list;
+extern World world;
 
 void ClientPacketFunctions::SendCreateFromRecipe(Client* client, int32 recipeID) {
 
@@ -214,7 +216,7 @@ void ClientPacketFunctions::SendCreateFromRecipe(Client* client, int32 recipeID)
 	//--------------------------------------------------------------Start Build Components-------------------------------------------------------------
 	if (total_build_components > 0) {
 		packet->setArrayLengthByName("num_build_components", total_build_components);
-		LogWrite(TRADESKILL__ERROR, 0, "Recipes", "num_build_components ", total_build_components);
+		LogWrite(TRADESKILL__INFO, 0, "Recipes", "num_build_components %u", total_build_components);
 		for (int8 index = 0; index < 4; index++) {
 			if (recipe->components.count(index + 1) == 0)
 				continue;
@@ -344,9 +346,15 @@ void ClientPacketFunctions::SendCreateFromRecipe(Client* client, int32 recipeID)
 			item_player = 0;
 			item_player = client->GetPlayer()->item_list.GetItemFromID((*itr));
 
+			if(client->GetVersion() <= 546) {
+				packet->setDataByName("fuel_qty", item->details.count);
+				packet->setDataByName("fuel_icon", item->details.icon);
+			}
+			
 			itemss = client->GetPlayer()->item_list.GetAllItemsFromID((*itr));
 			packet->setArrayLengthByName("num_fuel_choices", itemss.size());
 			if (itemss.size() > 0) {
+				
 				int16 needed_qty = recipe->GetFuelComponentQuantity();
 				int16 have_qty = 0;
 				if (firstID == 0)
@@ -403,9 +411,11 @@ void ClientPacketFunctions::SendCreateFromRecipe(Client* client, int32 recipeID)
 
 	packet->setDataByName("recipe_id", recipeID);
 	
-	packet->PrintPacket();
+	//packet->PrintPacket();
+	EQ2Packet* outapp = packet->serialize();
+	//DumpPacket(outapp);
 	// Send the packet
-	client->QueuePacket(packet->serialize());
+	client->QueuePacket(outapp);
 	safe_delete(packet);
 }
 
@@ -567,6 +577,7 @@ void ClientPacketFunctions::SendItemCreationUI(Client* client, Recipe* recipe) {
 	}
 
 
+	//packet->PrintPacket();
 	EQ2Packet* outapp = packet->serialize();
 	//DumpPacket(outapp);
 	client->QueuePacket(outapp);

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

@@ -2650,7 +2650,7 @@ void WorldDatabase::SaveCharacterQuests(Client* client){
 			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()){
+		if(itr->second && itr->second->GetSaveNeeded()){
 			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);
@@ -2661,7 +2661,7 @@ void WorldDatabase::SaveCharacterQuests(Client* client){
 		LogWrite(WORLD__ERROR, 0, "World", "Error in SaveCharacterQuests query '%s': %s", query.GetQuery(), query.GetError());
 	quests = client->GetPlayer()->GetCompletedPlayerQuests();
 	for(itr = quests->begin(); itr != quests->end(); itr++){
-		if(itr->second->GetSaveNeeded()){
+		if(itr->second && itr->second->GetSaveNeeded()){
 			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

+ 199 - 23
EQ2/source/WorldServer/client.cpp

@@ -173,7 +173,6 @@ Client::Client(EQStream* ieqs) : underworld_cooldown_timer(5000), pos_update(125
 	lua_debug_timer.Disable();
 	transport_spawn = 0;
 	MBuyBack.SetName("Client::MBuyBack");
-	MPendingQuestAccept.SetName("Client::MPendingQuestAccept");
 	MDeletePlayer.SetName("Client::MDeletePlayer");
 	MQuestPendingUpdates.SetName("Client::MQuestPendingUpdates");
 	search_items = 0;
@@ -219,6 +218,10 @@ Client::Client(EQStream* ieqs) : underworld_cooldown_timer(5000), pos_update(125
 	underworld_cooldown_timer.Disable();
 	player_pos_change_count = 0;
 	pov_ghost_spawn_id = 0;
+	recipe_orig_packet = nullptr;
+	recipe_xor_packet = nullptr;
+	recipe_packet_count = 0;
+	recipe_orig_packet_size = 0;
 }
 
 Client::~Client() {
@@ -1633,6 +1636,32 @@ bool Client::HandlePacket(EQApplicationPacket* app) {
 		QueuePacket(app25);
 		break;
 	}
+	case OP_RequestRecipeDetailsMsg: {
+		PacketStruct* packet = configReader.getStruct("WS_RequestRecipeDetail", GetVersion());
+		if (packet) {
+			DumpPacket(app->pBuffer, app->size);
+			packet->LoadPacketData(app->pBuffer, app->size);
+			packet->PrintPacket();
+			int32 recipe_id = 0;
+			char recipe_prop_name[30];
+			int32 num_recipes = packet->getType_int32_ByName("num_recipes");
+			// WS_RecipeDetails
+			vector<int32> recipes;
+			for (int32 i = 0; i < num_recipes; i++) {
+				memset(recipe_prop_name, 0, 30);
+				snprintf(recipe_prop_name, 30, "recipe_id_%i", i);
+				recipe_id = packet->getType_int32_ByName(recipe_prop_name);
+				if(recipe_id > 0) {
+					recipes.push_back(recipe_id);
+				}
+			}
+			
+			safe_delete(packet);
+
+			SendRecipeDetails(&recipes);
+		}
+		break;
+	}
 	case OP_ShowCreateFromRecipeUIMsg: {
 		LogWrite(OPCODE__DEBUG, 1, "Opcode", "Opcode 0x%X (%i): OP_ShowCreateFromRecipeUIMsg", opcode, opcode);
 		break;
@@ -3062,7 +3091,17 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) {
 		if (!request)
 			return;
 		request->LoadPacketData(app->pBuffer, app->size);
-		int32 id = request->getType_int32_ByName("unknown_id");
+		
+		int32 id = 0;
+		if(GetVersion() < 546) {
+			id = request->getType_int32_ByName("id");
+		}
+		if(GetVersion() == 546) {
+			id = request->getType_int32_ByName("unique_id");
+		}
+		else {
+			id = request->getType_int32_ByName("unknown_id");
+		}
 		Recipe* recipe = master_recipe_list.GetRecipe(id);
 		if (recipe) {
 			EQ2Packet* app = recipe->SerializeRecipe(this, recipe, false, GetItemPacketType(GetVersion()), 0x02);
@@ -5746,9 +5785,8 @@ void Client::BankDeposit(int64 amount) {
 
 void Client::AddPendingQuestAcceptReward(Quest* quest)
 {
-	MPendingQuestAccept.lock();
+	std::unique_lock lock(MPendingQuestAccept);
 	pending_quest_accept.push_back(quest->GetQuestID());
-	MPendingQuestAccept.unlock();
 }
 
 void Client::AddPendingQuestReward(Quest* quest, bool update, bool is_temporary, std::string description) {
@@ -6031,27 +6069,34 @@ void Client::AddPendingQuest(Quest* quest, bool forced) {
 }
 
 void Client::AcceptQuest(int32 quest_id) {
-	MPendingQuestAccept.lock();
+	MPendingQuestAccept.lock_shared();
 	if (player->pending_quests.count(quest_id) > 0) {
-		Quest* quest = player->pending_quests[quest_id];	
+		Quest* quest = player->pending_quests[quest_id];
 		if(quest) {
+			MPendingQuestAccept.unlock_shared();
+			MPendingQuestAccept.lock();
 			player->pending_quests.erase(quest->GetQuestID());
+			MPendingQuestAccept.unlock();
 			AddPlayerQuest(quest);
 			GetCurrentZone()->SendQuestUpdates(this);
 
 			GetPlayer()->UpdateQuestCompleteCount(quest_id);
+			return; // already unlocked mutex
 		}
 	}
-	MPendingQuestAccept.unlock();
+	MPendingQuestAccept.unlock_shared();
 }
 
 void Client::RemovePendingQuest(int32 quest_id) {
 	bool send_updates = false;
-	MPendingQuestAccept.lock();
+	MPendingQuestAccept.lock_shared();
 	
 	if (player->pending_quests.count(quest_id) > 0) {
-		Quest* quest = player->pending_quests[quest_id];	
+		Quest* quest = player->pending_quests[quest_id];
+		MPendingQuestAccept.unlock_shared();
+		MPendingQuestAccept.lock();
 		player->pending_quests.erase(quest_id);
+		MPendingQuestAccept.unlock();
 		
 		if(lua_interface) {
 			lua_interface->CallQuestFunction(quest, "Declined", GetPlayer());
@@ -6062,7 +6107,9 @@ void Client::RemovePendingQuest(int32 quest_id) {
 		
 		send_updates = true;
 	}
-	MPendingQuestAccept.unlock();
+	else {
+		MPendingQuestAccept.unlock_shared();
+	}
 
 	if(send_updates) {
 		GetCurrentZone()->SendQuestUpdates(this);
@@ -6276,11 +6323,11 @@ void Client::ReloadQuests() {
 }
 
 Quest* Client::GetPendingQuestAcceptance(int32 item_id) {
+	std::unique_lock lock(MPendingQuestAccept);
 	bool found_quest = false;
 	vector<int32>::iterator itr;
 	int32 questID = 0;
 	Quest* quest = nullptr;
-	MPendingQuestAccept.lock();
 	for (itr = pending_quest_accept.begin(); itr != pending_quest_accept.end();) {
 		questID = *itr;
 		
@@ -6300,7 +6347,6 @@ Quest* Client::GetPendingQuestAcceptance(int32 item_id) {
 		
 		itr++;
 	}
-	MPendingQuestAccept.unlock();
 
 	return quest;
 }
@@ -9275,6 +9321,10 @@ void Client::SetReadyForUpdates() {
 		database.loadCharacterProperties(this);
 
 	ready_for_updates = true;
+	
+	if(GetVersion() <= 546) {
+		SendRecipeList();
+	}
 }
 
 void Client::SetReadyForSpawns(bool val) {
@@ -10202,13 +10252,35 @@ void Client::SendRecipeList() {
 	}
 	
 	PacketStruct* packet = 0;
-	if (!(packet = configReader.getStruct("WS_RecipeList", version))) {
-		return;
-	}
 	map<int32, Recipe*>* recipes = player->GetRecipeList()->GetRecipes();
 	map<int32, Recipe*>::iterator itr;
-	Recipe* recipe;
 	int16 i = 0;
+	Recipe* recipe;
+	
+	if(version <= 546) {
+		PacketStruct* packet = 0;
+		if (!(packet = configReader.getStruct("WS_UpdateRecipeBook", GetVersion()))) {
+			return;
+		}
+		packet->setArrayLengthByName("recipe_count", recipes->size());
+		for (itr = recipes->begin(); itr != recipes->end(); itr++) {
+			recipe = itr->second;
+			int32 recipe_id = recipe->GetID();
+			packet->setArrayDataByName("recipe_id", recipe_id, i);
+			packet->setArrayDataByName("recipe_data_crc", GetRecipeCRC(recipe), i);
+			packet->setArrayDataByName("unknown", 0x7005BE3, i); //0x7005BE3
+			i++;
+		}
+		packet->PrintPacket();
+		EQ2Packet* ret = packet->serializeCountPacket(GetVersion(), 0, nullptr, nullptr);
+		QueuePacket(ret);
+		safe_delete(packet);
+		SetRecipeListSent(true);
+		return;
+	}
+	else if (!(packet = configReader.getStruct("WS_RecipeList", version))) {
+		return;
+	}
 	int8 level = player->GetTSLevel();
 	int index = 0;
 	for (itr = recipes->begin(); itr != recipes->end(); itr++) {
@@ -10223,6 +10295,7 @@ void Client::SendRecipeList() {
 
 	packet->setDataByName("command_type", 0);
 	packet->setArrayLengthByName("num_recipes", recipes->size());
+	int stringsize = 0;
 	for (itr = recipes->begin(); itr != recipes->end(); itr++) {
 		recipe = itr->second;
 		int32 myid = recipe->GetID();
@@ -10264,8 +10337,8 @@ void Client::SendRecipeList() {
 		i++;
 	}
 	//packet->PrintPacket();
-	//DumpPacket(packet->serialize());
-	QueuePacket(packet->serialize());
+	EQ2Packet* pack = packet->serialize();
+	QueuePacket(pack);
 	safe_delete(packet);
 	SetRecipeListSent(true);
 }
@@ -11979,17 +12052,15 @@ bool Client::AddRecipeBookToPlayer(int32 recipe_book_id, Item* item) {
 			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");
+			LogWrite(PLAYER__DEBUG, 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);
 			}
 
+			std::vector<Recipe*> recipes;
 			// 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) {
+			if (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.GetRecipeByCRC(itr);   //GetRecipeByName(itr.c_str());
@@ -11997,6 +12068,10 @@ bool Client::AddRecipeBookToPlayer(int32 recipe_book_id, Item* item) {
 						recipes.push_back(r);
 					}
 				}
+				LogWrite(PLAYER__DEBUG, 0, "Recipe", "%i recipes found for %s book", recipes.size(), recipe_book->GetBookName());
+			}
+			else {
+				LogWrite(PLAYER__ERROR, 0, "Recipe", "no recipes found for %s book", recipe_book->GetBookName());
 			}
 			
 			//Filter out duplicate recipes the player already has
@@ -12144,4 +12219,105 @@ void Client::ProcessZoneIgnoreWidgets() {
 		SendReplaceWidget(itr->first, true);
 	}
 	GetPlayer()->MIgnoredWidgets.unlock_shared();
+}
+
+void Client::PopulateRecipeData(Recipe* recipe, PacketStruct* packet, int i) {
+	if(!recipe || !packet)
+		return;
+	
+		int8 level = player->GetTSLevel();
+		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, i);
+		else if ((rlevel <= level) & (rlevel >= even))
+			packet->setArrayDataByName("tier", 3, i);
+		else if ((rlevel <= even) & (rlevel >= easymin))
+			packet->setArrayDataByName("tier", 2, i);
+		else if ((rlevel <= easymin) & (rlevel >= veasymin))
+			packet->setArrayDataByName("tier", 1, i);
+		else if ((rlevel <= veasymin) & (rlevel >= 0))
+			packet->setArrayDataByName("tier", 0, i);
+		if (rlevel == 2)
+			int xxx = 1;
+		packet->setArrayDataByName("recipe_id", myid, i);
+		packet->setArrayDataByName("level", recipe->GetLevel(), i);
+		packet->setArrayDataByName("icon", recipe->GetIcon(), i);
+		packet->setArrayDataByName("classes", recipe->GetClasses(), i);
+		packet->setArrayDataByName("technique", recipe->GetTechnique(), i);
+		packet->setArrayDataByName("knowledge", recipe->GetKnowledge(), i);
+		packet->setArrayDataByName("device", recipe->GetDevice(), i);
+		packet->setArrayDataByName("device_sub_type", recipe->GetDevice_Sub_Type(), i);
+		packet->setArrayDataByName("recipe_name", recipe->GetName(), i);
+		packet->setArrayDataByName("recipe_book", recipe->GetBook(), i);
+		packet->setArrayDataByName("unknown3", recipe->GetUnknown3(), i);
+		
+		packet->setArrayDataByName("book_volume", 0x01, i);
+		packet->setArrayDataByName("device_id", 0x01, i);
+}
+
+int32 Client::GetRecipeCRC(Recipe* recipe) {
+	
+	PacketStruct* packet = 0;
+	if (!(packet = configReader.getStruct("WS_RecipeDetailList", GetVersion()))) {
+		return 0;
+	}
+	packet->setArrayLengthByName("num_recipes", 1);
+	
+	PopulateRecipeData(recipe, packet);
+	
+	string* generic_string_data = packet->serializeString();
+	int32 size = generic_string_data->length();
+	
+	if(size < 5)
+		return 0;
+	
+	uchar* out_data = new uchar[size+1];
+	uchar* out_ptr = out_data;
+	memcpy(out_ptr, (uchar*)generic_string_data->c_str()+4, generic_string_data->length()-4);
+	uint32 out_crc = GenerateCRCRecipe(0, (void*)out_ptr, size-4);
+	safe_delete(packet);
+	safe_delete_array(out_data);
+	
+	return out_crc;
+}
+
+void Client::SendRecipeDetails(vector<int32>* recipes) {
+	if(!recipes || recipes->size() == 0)
+		return;
+	
+	PacketStruct* packet = 0;
+	if (!(packet = configReader.getStruct("WS_RecipeDetailList", GetVersion()))) {
+		return;
+	}
+	int32 recipe_size = player->GetRecipeList()->Size();
+	packet->setArrayLengthByName("num_recipes", recipe_size > 100 ? 100 : recipe_size);
+	int16 i = 0;
+	int32 count = 0;
+	vector<int32>::iterator recipe_itr;
+	for(recipe_itr = recipes->begin(); recipe_itr != recipes->end(); recipe_itr++) {
+		Recipe* recipe = player->GetRecipeList()->GetRecipe(*recipe_itr);
+		if(!recipe) {
+			continue;
+		}
+		else if(i > 99) {
+			QueuePacket(packet->serialize());
+			safe_delete(packet);
+			packet = configReader.getStruct("WS_RecipeDetailList", GetVersion());
+			recipe_size -= i;
+			packet->setArrayLengthByName("num_recipes", recipe_size > 100 ? 100 : recipe_size);
+			i = 0;
+		}
+		count++;
+		PopulateRecipeData(recipe, packet, i);
+		i++;
+	}
+	
+	//packet->PrintPacket();
+	
+	QueuePacket(packet->serialize());
+	safe_delete(packet);
 }

+ 9 - 1
EQ2/source/WorldServer/client.h

@@ -380,6 +380,9 @@ public:
 	void	HandInCollections();
 	void	AcceptCollectionRewards(Collection *collection, int32 selectable_item_id = 0);
 	void	SendRecipeList();
+	void	PopulateRecipeData(Recipe* recipe, PacketStruct* packet, int i=0);
+	int32	GetRecipeCRC(Recipe* recipe);
+	void	SendRecipeDetails(vector<int32>* recipes);
 	void	SendTitleUpdate();
 	void	SendUpdateTitles(sint32 prefix, sint32 suffix);
 	void	SendLanguagesUpdate(int32 id, bool setlang = 1);
@@ -608,7 +611,7 @@ private:
 	deque<BuyBackItem*> buy_back_items;
 	Spawn*	merchant_transaction;
 	Spawn*	mail_transaction;
-	Mutex	MPendingQuestAccept;
+	mutable std::shared_mutex MPendingQuestAccept;
 	vector<int32> pending_quest_accept;	
 	bool	lua_debug;
 	bool	should_target;
@@ -728,6 +731,11 @@ private:
 	
 	std::atomic<int32> pov_ghost_spawn_id;
 	Timer delay_msg_timer;
+	
+	uchar* recipe_orig_packet;
+	uchar* recipe_xor_packet;
+	int	recipe_packet_count;
+	int recipe_orig_packet_size;
 };
 
 class ClientList {

+ 4 - 4
EQ2/source/common/PacketStruct.cpp

@@ -2242,15 +2242,15 @@ EQ2Packet* PacketStruct::serializeCountPacket(int16 version, int8 offset, uchar*
 	string* packet_data = serializeString();
 	uchar* data = (uchar*)packet_data->c_str();
 	int32 size = packet_data->size();
-	uchar* packed_data = new uchar[size + 20];
-	memset(packed_data, 0, size + 20);
+	uchar* packed_data = new uchar[size + 1000]; // this size + 20 is poorly defined, depending on the packet data, we could use a additional length of 1K+
+	memset(packed_data, 0, size + 1000);
 	if (orig_packet && xor_packet) {
 		memcpy(xor_packet, data + 6, size - 6 - offset);
 		Encode(xor_packet, orig_packet, size - 6 - offset);
-		size = Pack(packed_data, xor_packet, size - 6 - offset, size + 20, version);
+		size = Pack(packed_data, xor_packet, size - 6 - offset, size + 1000, version);
 	}
 	else
-		size = Pack(packed_data, data + 6, packet_data->size() - 6 - offset, packet_data->size() + 20, version);
+		size = Pack(packed_data, data + 6, packet_data->size() - 6 - offset, packet_data->size() + 1000, version);
 	uchar* combined = new uchar[size + sizeof(int16) + offset];
 	memset(combined, 0, size + sizeof(int16) + offset);
 	uchar* ptr = combined;

+ 269 - 0
EQ2/source/common/packet_functions.cpp

@@ -266,3 +266,272 @@ uint32 GenerateCRC(int32 b, int32 bufsize, uchar *buf) {
 	
 	return (htonl (CRC ^ 0xFFFFFFFF));
 }
+
+long int CRCArray[] = {
+	0,
+1996959894,
+3993919788,
+2567524794,
+124634137,
+1886057615,
+3915621685,
+2657392035,
+249268274,
+2044508324,
+3772115230,
+2547177864,
+162941995,
+2125561021,
+3887607047,
+2428444049,
+498536548,
+1789927666,
+4089016648,
+2227061214,
+450548861,
+1843258603,
+4107580753,
+2211677639,
+325883990,
+1684777152,
+4251122042,
+2321926636,
+335633487,
+1661365465,
+4195302755,
+2366115317,
+997073096,
+1281953886,
+3579855332,
+2724688242,
+1006888145,
+1258607687,
+3524101629,
+2768942443,
+901097722,
+1119000684,
+3686517206,
+2898065728,
+853044451,
+1172266101,
+3705015759,
+2882616665,
+651767980,
+1373503546,
+3369554304,
+3218104598,
+565507253,
+1454621731,
+3485111705,
+3099436303,
+671266974,
+1594198024,
+3322730930,
+2970347812,
+795835527,
+1483230225,
+3244367275,
+3060149565,
+1994146192,
+31158534,
+2563907772,
+4023717930,
+1907459465,
+112637215,
+2680153253,
+3904427059,
+2013776290,
+251722036,
+2517215374,
+3775830040,
+2137656763,
+141376813,
+2439277719,
+3865271297,
+1802195444,
+476864866,
+2238001368,
+4066508878,
+1812370925,
+453092731,
+2181625025,
+4111451223,
+1706088902,
+314042704,
+2344532202,
+4240017532,
+1658658271,
+366619977,
+2362670323,
+4224994405,
+1303535960,
+984961486,
+2747007092,
+3569037538,
+1256170817,
+1037604311,
+2765210733,
+3554079995,
+1131014506,
+879679996,
+2909243462,
+3663771856,
+1141124467,
+855842277,
+2852801631,
+3708648649,
+1342533948,
+654459306,
+3188396048,
+3373015174,
+1466479909,
+544179635,
+3110523913,
+3462522015,
+1591671054,
+702138776,
+2966460450,
+3352799412,
+1504918807,
+783551873,
+3082640443,
+3233442989,
+3988292384,
+2596254646,
+62317068,
+1957810842,
+3939845945,
+2647816111,
+81470997,
+1943803523,
+3814918930,
+2489596804,
+225274430,
+2053790376,
+3826175755,
+2466906013,
+167816743,
+2097651377,
+4027552580,
+2265490386,
+503444072,
+1762050814,
+4150417245,
+2154129355,
+426522225,
+1852507879,
+4275313526,
+2312317920,
+282753626,
+1742555852,
+4189708143,
+2394877945,
+397917763,
+1622183637,
+3604390888,
+2714866558,
+953729732,
+1340076626,
+3518719985,
+2797360999,
+1068828381,
+1219638859,
+3624741850,
+2936675148,
+906185462,
+1090812512,
+3747672003,
+2825379669,
+829329135,
+1181335161,
+3412177804,
+3160834842,
+628085408,
+1382605366,
+3423369109,
+3138078467,
+570562233,
+1426400815,
+3317316542,
+2998733608,
+733239954,
+1555261956,
+3268935591,
+3050360625,
+752459403,
+1541320221,
+2607071920,
+3965973030,
+1969922972,
+40735498,
+2617837225,
+3943577151,
+1913087877,
+83908371,
+2512341634,
+3803740692,
+2075208622,
+213261112,
+2463272603,
+3855990285,
+2094854071,
+198958881,
+2262029012,
+4057260610,
+1759359992,
+534414190,
+2176718541,
+4139329115,
+1873836001,
+414664567,
+2282248934,
+4279200368,
+1711684554,
+285281116,
+2405801727,
+4167216745,
+1634467795,
+376229701,
+2685067896,
+3608007406,
+1308918612,
+956543938,
+2808555105,
+3495958263,
+1231636301,
+1047427035,
+2932959818,
+3654703836,
+1088359270,
+936918000,
+2847714899,
+3736837829,
+1202900863,
+817233897,
+3183342108,
+3401237130,
+1404277552,
+615818150,
+3134207493,
+3453421203,
+1423857449,
+601450431,
+3009837614,
+3294710456,
+1567103746,
+711928724,
+3020668471,
+3272380065,
+1510334235,
+755167117};
+
+uint32 GenerateCRCRecipe(uint32 initial, void* buf, uint32 len)
+{
+	uint32 c = 0xFFFFFFFF;
+	sint8* u = static_cast<sint8*>(buf);
+	for (size_t i = 0; i < len; ++i) 
+	{
+		c = CRCArray[(c ^ u[i]) & 0xFF] ^ (c >> 8);
+	}
+	return c;
+}

+ 1 - 0
EQ2/source/common/packet_functions.h

@@ -40,5 +40,6 @@ void EncryptZoneSpawnPacket(uchar* pBuffer, int32 size);
 int DeflatePacket(unsigned char* in_data, int in_length, unsigned char* out_data, int max_out_length);
 uint32 InflatePacket(uchar* indata, uint32 indatalen, uchar* outdata, uint32 outdatalen, bool iQuiet = false);
 uint32 GenerateCRC(int32 b, int32 bufsize, uchar *buf);
+uint32 GenerateCRCRecipe(uint32 b, void* buf, uint32 bufsize);
 
 #endif

+ 170 - 0
server/WorldStructs.xml

@@ -5747,6 +5747,33 @@ to zero and treated like placeholders." />
 <Data ElementName="recipe_name" Type="EQ2_8Bit_String" />
 <Data ElementName="recipe_description" Type="EQ2_16Bit_String" />
 </Struct>
+<Struct Name="WS_RecipeInfo" ClientVersion="546" >
+<Data ElementName="id" Type="int32" Size="1" />
+<Data ElementName="unknown" Type="int8" Size="1" />
+<Data ElementName="level" Type="int8" />
+<Data ElementName="technique" Type="int32" Size="1" />
+<Data ElementName="knowledge" Type="int32" Size="1" />
+<Data ElementName="skill_level" Type="int8" Size="1" />
+<Data ElementName="device" Type="EQ2_8Bit_String" />
+<Data ElementName="unknown3" Type="int8" Size="1" />
+<Data ElementName="adventure_id" Type="int8" />
+<Data ElementName="tradeskill_id" Type="int8" />
+<Data ElementName="unknownz" Type="int8" size="13"/>
+<Data ElementName="product_icon" Type="int16" />
+<Data ElementName="product_name" Type="EQ2_8Bit_String" />
+<Data ElementName="unknowny" Type="int8" size="26"/>
+<Data ElementName="primary_count" Type="int8" />
+<Data ElementName="primary_comp" Type="EQ2_8Bit_String" />
+<Data ElementName="num_comps" Type="int8" />
+<Data ElementName="comp_array" Type="Array" ArraySizeVariable="num_comps">
+  <Data ElementName="build_comp" Type="EQ2_8Bit_String" />
+  <Data ElementName="build_comp_qty" Type="int8" />
+</Data>
+<Data ElementName="fuel_comp" Type="EQ2_8Bit_String" />
+<Data ElementName="fuel_comp_qty" Type="int8" />
+<Data ElementName="recipe_name" Type="EQ2_8Bit_String" />
+<Data ElementName="recipe_description" Type="EQ2_16Bit_String" />
+</Struct>
 <Struct Name="WS_RecipeInfo" ClientVersion="908" >
 <Data ElementName="id" Type="int32" Size="1" />
 <Data ElementName="unknown" Type="int8" Size="1" />
@@ -6325,6 +6352,10 @@ to zero and treated like placeholders." />
 <Data ElementName="info_header" Substruct="WS_ExamineInfoHeader" Size="1" />
 <Data ElementName="recipe_info" Substruct="WS_RecipeInfo" Size="1" />
 </Struct>
+<Struct Name="WS_ExamineRecipeInfo" ClientVersion="546" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
+<Data ElementName="info_header" Substruct="WS_ExamineInfoHeader" Size="1" />
+<Data ElementName="recipe_info" Substruct="WS_RecipeInfo" Size="1" />
+</Struct>
 <Struct Name="WS_ExamineRecipeInfo" ClientVersion="908" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
 <Data ElementName="info_header" Substruct="WS_ExamineInfoHeader" Size="1" />
 <Data ElementName="recipe_info" Substruct="WS_RecipeInfo" Size="1" />
@@ -15904,6 +15935,38 @@ to zero and treated like placeholders." />
 <Data ElementName="packetsubtype" Type="int8" />
 <Data ElementName="skill_id" Type="int32" Size="6"/>
 </Struct>
+<Struct Name="WS_ShowItemCreation" ClientVersion="546" OpcodeName="OP_ShowItemCreationProcessUIMsg">
+<!-- starting durability maybe?-->
+<Data ElementName="max_possible_durability" Type="int32" Size="1" />
+<Data ElementName="max_possible_progress" Type="int32" Size="1" />
+<Data ElementName="unknown2" Type="int32" Size="2" />
+<Data ElementName="progress_levels_known" Type="int8" Size="1" />
+<Data ElementName="num_process" Type="int8" Size="1" />
+<Data ElementName="process_array" Type="Array" ArraySizeVariable="num_process">
+  <Data ElementName="progress_needed" Type="int32" Size="1" />
+  <Data ElementName="unknown3" Type="int8" Size="1" IfVariableNotSet="progress_needed"/>
+  <Data ElementName="item_name" Type="EQ2_16Bit_String" />
+  <Data ElementName="item_icon" Type="int16" />
+  <Data ElementName="item" Type="EQ2_Item" />
+  <Data ElementName="item_byproduct_name" Type="EQ2_16Bit_String" />
+  <Data ElementName="item_byproduct_icon" Type="int16" />
+  <!-- Another EQ2_Item? Does subtype set to FF prevent the rest of the packet?-->
+  <!-- If not an EQ2_item this unknown *might* be quantity-->
+  <Data ElementName="item_byproduct_unknown" Type="int8" />
+  <Data ElementName="packettype" Type="int16" />
+  <Data ElementName="packetsubtype" Type="int8" />
+</Data>
+<Data ElementName="product_progress_needed" Type="int32" Size="1" />
+<Data ElementName="product_item_name" Type="EQ2_16Bit_String" />
+<Data ElementName="product_item_icon" Type="int16" />
+<Data ElementName="product_item" Type="EQ2_Item" />
+<Data ElementName="product_byproduct_name" Type="EQ2_16Bit_String" />
+<Data ElementName="product_byproduct_icon" Type="int16" />
+<Data ElementName="product_byproduct_unknown" Type="int8" />
+<Data ElementName="packettype" Type="int16" />
+<Data ElementName="packetsubtype" Type="int8" />
+<Data ElementName="skill_id" Type="int32" Size="6"/>
+</Struct>
 <Struct Name="WS_ShowItemCreation" ClientVersion="60085" OpcodeName="OP_ShowItemCreationProcessUIMsg">
 <!-- starting durability maybe?-->
 <Data ElementName="max_possible_durability" Type="int32" Size="1" />
@@ -16017,6 +16080,63 @@ to zero and treated like placeholders." />
 <Data ElementName="fuel_unknown6" Type="int32" Size="1" />
 <Data ElementName="unknown8" Type="int8" Size="1" />
 </Struct>
+<Struct Name="WS_CreateFromRecipe" ClientVersion="546" OpcodeName="OP_ShowCreateFromRecipeUIMsg">
+<Data ElementName="crafting_station" Type="EQ2_16Bit_String" />
+<Data ElementName="unknown1" Type="int32" Size="1" />
+<Data ElementName="recipe_name" Type="EQ2_16Bit_String" />
+<Data ElementName="tier" Type="int8" Size="1" />
+<Data ElementName="unknown3" Type="int32" Size="1" />
+<Data ElementName="product_name" Type="EQ2_16Bit_String" />
+<Data ElementName="icon" Type="int16" />
+<Data ElementName="product_qty" Type="int8" Size="1" />
+<Data ElementName="unknown5" Type="int16" Size="1" />
+<Data ElementName="unknown6" Type="int8" Size="1" />
+<Data ElementName="unknown7" Type="int16" Size="1" />
+<Data ElementName="primary_title" Type="EQ2_16Bit_String" />
+<Data ElementName="num_primary_choices" Type="int8" />
+<Data ElementName="primary_array" Type="Array" ArraySizeVariable="num_primary_choices">
+  <Data ElementName="primary_component" Type="EQ2_16Bit_String" />
+  <Data ElementName="primary_item_id" Type="int32" Size="1" />
+  <Data ElementName="primary_icon" Type="int16" Size="1" />
+  <Data ElementName="primary_total_quantity" Type="int8" Size="1" />
+</Data>
+<Data ElementName="primary_default_selected_id" Type="int32" Size="1" />
+<Data ElementName="primary_unknown5" Type="int32" Size="1" />
+<Data ElementName="num_build_components" Type="int8" />
+<Data ElementName="build_array" Type="Array" ArraySizeVariable="num_build_components">
+  <Data ElementName="build_slot" Type="int32" />
+  <Data ElementName="build_title" Type="EQ2_16Bit_String" />
+  <Data ElementName="build_qty_needed" Type="int8" />
+  <Data ElementName="build_unknown1" Type="int32" Size="1" />
+  <Data ElementName="num_build_choices" Type="int8" Size="1" />
+  <Data ElementName="build_components_array" Type="Array" ArraySizeVariable="num_build_choices">
+    <Data ElementName="build_component" Type="EQ2_16Bit_String" />
+    <Data ElementName="build_item_id" Type="int32" Size="1" />
+    <Data ElementName="build_icon" Type="int16" Size="1" />
+    <Data ElementName="build_total_quantity" Type="int8" Size="1" />
+  </Data>
+  <Data ElementName="num_build_items_selected" Type="int8" />
+	<Data ElementName="build_items_selected_array" Type="Array" ArraySizeVariable="num_build_items_selected">
+		<Data ElementName="build_selected_item_id" Type="int32" Size="1" />
+		<Data ElementName="build_selected_item_qty" Type="int8" Size="1" />
+	</Data>
+</Data>
+<Data ElementName="fuel_title" Type="EQ2_16Bit_String" />
+<Data ElementName="fuel_qty_needed" Type="int8" />
+<Data ElementName="num_fuel_choices" Type="int8" Size="1" />
+<Data ElementName="fuel_component_array" Type="Array" ArraySizeVariable="num_fuel_choices">
+  <Data ElementName="fuel_component" Type="EQ2_16Bit_String" />
+  <Data ElementName="fuel_item_id" Type="int32" Size="1" />
+  <Data ElementName="fuel_icon" Type="int16" />
+  <Data ElementName="fuel_total_quantity" Type="int8" Size="1" />
+  <Data ElementName="fuel_unknown4" Type="int8" Size="2" />
+</Data>
+<Data ElementName="num_fuel_items_selected" Type="int8" />
+<Data ElementName="fuel_items_selected_array" Type="Array" ArraySizeVariable="num_fuel_items_selected">
+	<Data ElementName="fuel_selected_item_id" Type="int32" Size="1" />
+	<Data ElementName="fuel_selected_item_qty" Type="int8" Size="1" />
+</Data>
+</Struct>
 <Struct Name="WS_CreateFromRecipe" ClientVersion="1096" OpcodeName="OP_ShowCreateFromRecipeUIMsg">
 <Data ElementName="crafting_station" Type="EQ2_16Bit_String" />
 <Data ElementName="unknown1" Type="int32" Size="1" />
@@ -16435,6 +16555,32 @@ to zero and treated like placeholders." />
   <Data ElementName="unknown3" Type="int8" Size="1" />
 </Data>
 </Struct>
+<Struct Name="WS_RecipeDetailList" ClientVersion="546" OpcodeName="OP_RecipeDetailsMsg">
+<Data ElementName="num_recipes" Type="int32" Size="1" />
+<Data ElementName="recipe_array" Type="array" ArraySizeVariable="num_recipes">
+  <Data ElementName="recipe_id" Type="int32" Size="1" />
+  <Data ElementName="icon" Type="int16" Size="1" />
+  <Data ElementName="recipe_name" Type="char" Size="200" />
+  <Data ElementName="recipe_desc" Type="char" Size="256" />
+  <Data ElementName="book_volume" Type="int32" Size="1" /> <!-- volume # of book -->
+  <Data ElementName="unknownx" Type="int32" Size="1" /> <!-- result item? -->
+  <Data ElementName="technique" Type="int32" Size="1" />
+  <Data ElementName="knowledge" Type="int32" Size="1" />
+  <Data ElementName="level" Type="int32" Size="1" />
+  <Data ElementName="recipe_book" Type="char" Size="200" />
+  <Data ElementName="device" Type="char" Size="40" />
+  <Data ElementName="device_id" Type="int32" Size="1" />
+</Data>
+</Struct>
+<!--<Struct Name="WS_RecipeList" ClientVersion="546" OpcodeName="OP_UpdateRecipeBookMsg">
+<Data ElementName="recipe_count" Type="int16" />
+<Data ElementName="packed_size" Type="int32" />
+<Data ElementName="recipe_array" Type="Array" ArrayVariableSize="recipe_count">
+  <Data ElementName="recipe_id" Type="int32" Size="1" />
+  <Data ElementName="unknown" Type="int32" Size="2" />
+</Data>
+<Data ElementName="unknown1" Type="int32" Size="1" />
+</Struct>-->
 <Struct Name="WS_RecipeList" ClientVersion="60085" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_RecipeList">
 <Data ElementName="command_type" Type="int8" Size="1" />
 <Data ElementName="num_recipes" Type="int16" Size="1" />
@@ -16461,6 +16607,15 @@ to zero and treated like placeholders." />
 <Data ElementName="unknown2" Type="int32" />
   <Data ElementName="unknown3" Type="int32" IfVariableSet="unknown2" />
 </Struct>
+<Struct Name="WS_ShowRecipeBook" ClientVersion="546" OpcodeName="OP_ShowRecipeBookMsg">
+<Data ElementName="device" Type="char" Size="42" />
+</Struct>
+<Struct Name="WS_ShowRecipeBook" ClientVersion="547" OpcodeName="OP_ShowRecipeBookMsg">
+<Data ElementName="device" Type="char" Size="42" />
+<Data ElementName="unknown1" Type="int8" Size="1" />
+<Data ElementName="unknown2" Type="int32" />
+  <Data ElementName="unknown3" Type="int32" IfVariableSet="unknown2" />
+</Struct>
 <Struct Name="WS_RecipeDetails" ClientVersion="1" OpcodeName="OP_RecipeDetailsMsg">
 <Data ElementName="num_recipes" Type="int32" Size="1" />
 <Data ElementName="recipe_array" Type="array" ArraySizeVariable="num_recipes">
@@ -16488,12 +16643,27 @@ to zero and treated like placeholders." />
 </Data>
 <Data ElementName="unknown1" Type="int32" Size="1" />
 </Struct>
+<Struct Name="WS_UpdateRecipeBook" ClientVersion="546" OpcodeName="OP_UpdateRecipeBookMsg">
+<Data ElementName="recipe_count" Type="int16" /> 
+<Data ElementName="packed_size" Type="int32" /> 
+<Data ElementName="skill_array" Type="Array" ArraySizeVariable="recipe_count"> 
+  <Data ElementName="recipe_id" Type="int32" Size="1" />
+  <Data ElementName="recipe_data_crc" Type="int32" Size="1" />
+  <Data ElementName="unknown" Type="int32" Size="1" />
+</Data>
+</Struct>
 <Struct Name="WS_RequestRecipeDetail" ClientVersion="1" OpcodeName="OP_RequestRecipeDetailsMsg">
 <Data ElementName="num_recipes" Type="int32" Size="1" />
 <Data ElementName="recipe_array" Type="Array" ArrayVariableSize="num_recipes">
   <Data ElementName="recipe_id" Type="int32" Size="1" />
 </Data>
 </Struct>
+<Struct Name="WS_RequestRecipeDetail" ClientVersion="546" OpcodeName="OP_RequestRecipeDetailsMsg">
+  <Data ElementName="num_recipes" Type="int32" Size="1" />
+  <Data ElementName="recipes_array" Type="Array" ArraySizeVariable="num_recipes">
+    <Data ElementName="recipe_id" Type="int32" Size="1" />
+  </Data>
+</Struct>
 <Struct Name="WS_DisplayHouseStatus" ClientVersion="1096" OpcodeName="OP_PlayerHouseDisplayStatusMsg">
 <Data ElementName="unknown1" Type="int32" Size="1" />
 <Data ElementName="unknown2" Type="int32" Size="1" />