Browse Source

Fix #448 - Scroll/Recipe scribing fixes
* Spells now have level and class checks
* Recipes now have class checks (already had level checks)
* Recipes now fallback to the item recipebook info to populate players recipes from a recipe book if the recipe book is not properly configured by book name
Fix #396 - Implemented subspawn tables so we can update specific types of spawns, in this case collectors when we remove the red book
Fix #205 - ground spawns now can have random heading assigned or forced heading
alter table spawn_ground add column randomize_heading tinyint(3) unsigned default 1;
randomize_heading = 0 uses base heading in the spawn location
randomize_heading = 1 means all spawns will have a random heading 0-360 degrees
Fix #118 - Knowledge sorting / spell book updates now correctly work.
* Down/Across patterns now supported (zig-zag was default)
Fixed a crash when a spawn spell is interrupted and then the spawn dies to a melee attack could cause memory corruption removing spell timer pointers
Startup opcode warnings are now grouped by versioning to allow easier troubleshooting, DoF uses 546 and AoM 60085, thus most opcodes warnings are not relevant to supported cases
Fixed a regex crash on /findspawn

Emagi 1 year ago
parent
commit
60928d2e0c

+ 1 - 0
DB/updates/spawn_ground_randomheading_aug_11_2022.sql

@@ -0,0 +1 @@
+alter table spawn_ground add column randomize_heading tinyint(3) unsigned default 1;

+ 3 - 3
EQ2/source/WorldServer/Combat.cpp

@@ -1060,7 +1060,7 @@ bool Entity::DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_
 	}
 	}
 
 
 	if (victim->GetHP() <= 0)
 	if (victim->GetHP() <= 0)
-		KillSpawn(victim, damage_type, blow_type);
+		KillSpawn(victim, type, damage_type, blow_type);
 	else {
 	else {
 		victim->CheckProcs(PROC_TYPE_DEFENSIVE, this);
 		victim->CheckProcs(PROC_TYPE_DEFENSIVE, this);
 		if (spell_name)
 		if (spell_name)
@@ -1257,7 +1257,7 @@ bool Entity::CheckInterruptSpell(Entity* attacker) {
 	return false;
 	return false;
 }
 }
 
 
-void Entity::KillSpawn(Spawn* dead, int8 damage_type, int16 kill_blow_type) {
+void Entity::KillSpawn(Spawn* dead, int8 type, int8 damage_type, int16 kill_blow_type) {
 	if(!dead)
 	if(!dead)
 		return;
 		return;
 
 
@@ -1303,7 +1303,7 @@ void Entity::KillSpawn(Spawn* dead, int8 damage_type, int16 kill_blow_type) {
 	dead->ClearRunningLocations();
 	dead->ClearRunningLocations();
 	dead->CalculateRunningLocation(true);
 	dead->CalculateRunningLocation(true);
 
 
-	GetZone()->KillSpawn(true, dead, this, true, damage_type, kill_blow_type);
+	GetZone()->KillSpawn(true, dead, this, true, type, damage_type, kill_blow_type);
 }
 }
 
 
 void Entity::HandleDeathExperienceDebt(Spawn* killer)
 void Entity::HandleDeathExperienceDebt(Spawn* killer)

+ 23 - 25
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -2187,7 +2187,10 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						Spell* spell = master_spell_list.GetSpell(item->skill_info->spell_id, item->skill_info->spell_tier);
 						Spell* spell = master_spell_list.GetSpell(item->skill_info->spell_id, item->skill_info->spell_tier);
 						int8 old_slot = 0;
 						int8 old_slot = 0;
 						if (spell) {
 						if (spell) {
-
+							if(!spell->ScribeAllowed(client->GetPlayer())) {
+								client->SimpleMessage(CHANNEL_COLOR_RED, "You do not meet one of the requirements to scribe, due to class or level.");
+								break;
+							}
 							int16 tier_up = player->GetTierUp(spell->GetSpellTier());
 							int16 tier_up = player->GetTierUp(spell->GetSpellTier());
 							if (rule_manager.GetGlobalRule(R_Spells, RequirePreviousTierScribe)->GetInt8() && !player->HasSpell(spell->GetSpellID(), tier_up, false, true))
 							if (rule_manager.GetGlobalRule(R_Spells, RequirePreviousTierScribe)->GetInt8() && !player->HasSpell(spell->GetSpellID(), tier_up, false, true))
 								client->SimpleMessage(CHANNEL_COLOR_RED, "You have not scribed the required previous version of this ability.");
 								client->SimpleMessage(CHANNEL_COLOR_RED, "You have not scribed the required previous version of this ability.");
@@ -2205,7 +2208,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 								// force purge client cache and display updated spell for hover over
 								// force purge client cache and display updated spell for hover over
 								EQ2Packet* app = spell->SerializeSpell(client, false, false);
 								EQ2Packet* app = spell->SerializeSpell(client, false, false);
 								client->QueuePacket(app);
 								client->QueuePacket(app);
-													}
+							}
 						}
 						}
 						else
 						else
 							LogWrite(COMMAND__ERROR, 0, "Command", "Unknown spell ID: %u and tier: %u", item->skill_info->spell_id, item->skill_info->spell_tier);
 							LogWrite(COMMAND__ERROR, 0, "Command", "Unknown spell ID: %u and tier: %u", item->skill_info->spell_id, item->skill_info->spell_tier);
@@ -2218,6 +2221,10 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 							client->Message(CHANNEL_NARRATIVE, "Your tradeskill level is not high enough to scribe this book.");
 							client->Message(CHANNEL_NARRATIVE, "Your tradeskill level is not high enough to scribe this book.");
 							safe_delete(recipe_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->details.item_id))) {
 						else if (recipe_book && !(client->GetPlayer()->GetRecipeBookList()->HasRecipeBook(item->details.item_id))) {
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Valid recipe book that the player doesn't have");
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Valid recipe book that the player doesn't have");
 							// Add recipe book to the players list
 							// Add recipe book to the players list
@@ -2226,43 +2233,34 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 							// Get a list of all recipes this book contains
 							// Get a list of all recipes this book contains
 							vector<Recipe*>* book_recipes = master_recipe_list.GetRecipes(recipe_book->GetBookName());
 							vector<Recipe*>* book_recipes = master_recipe_list.GetRecipes(recipe_book->GetBookName());
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "%i recipes found for %s book", book_recipes->size(), recipe_book->GetBookName());
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "%i recipes found for %s book", book_recipes->size(), recipe_book->GetBookName());
-
+							
+							int16 i = 0;
 							// Create the packet to send to update the players recipe list
 							// Create the packet to send to update the players recipe list
 							PacketStruct* packet = 0;
 							PacketStruct* packet = 0;
 							if (client->GetRecipeListSent()) {
 							if (client->GetRecipeListSent()) {
 								packet = configReader.getStruct("WS_RecipeList", client->GetVersion());
 								packet = configReader.getStruct("WS_RecipeList", client->GetVersion());
-
-								if (packet)
+								if(packet && book_recipes->size() < 1 && item->recipebook_info) {
+									packet->setArrayLengthByName("num_recipes", item->recipebook_info->recipes.size());
+									for(int32 r = 0; r < item->recipebook_info->recipes.size(); r++){
+										Recipe* recipe = master_recipe_list.GetRecipeByName(item->recipebook_info->recipes.at(r).c_str());
+										if(recipe) {
+											Recipe* player_recipe = new Recipe(recipe);
+											client->AddRecipeToPlayer(recipe, packet, &i);
+										}
+									}
+								}
+								else if (packet)
 									packet->setArrayLengthByName("num_recipes", book_recipes->size());
 									packet->setArrayLengthByName("num_recipes", book_recipes->size());
 							}
 							}
 
 
 							// loop through the list
 							// loop through the list
 							vector<Recipe*>::iterator itr;
 							vector<Recipe*>::iterator itr;
-							int16 i = 0;
 							for (itr = book_recipes->begin(); itr != book_recipes->end(); itr++) {
 							for (itr = book_recipes->begin(); itr != book_recipes->end(); itr++) {
 								// check to see if the player already has this recipe some how
 								// check to see if the player already has this recipe some how
 								if (!client->GetPlayer()->GetRecipeList()->GetRecipe((*itr)->GetID())) {
 								if (!client->GetPlayer()->GetRecipeList()->GetRecipe((*itr)->GetID())) {
 									// Player doesn't already have this recipe so lets add it
 									// Player doesn't already have this recipe so lets add it
 									Recipe* recipe = new Recipe(master_recipe_list.GetRecipe((*itr)->GetID()));
 									Recipe* recipe = new Recipe(master_recipe_list.GetRecipe((*itr)->GetID()));
-									client->GetPlayer()->GetRecipeList()->AddRecipe(recipe);
-									database.SavePlayerRecipe(client->GetPlayer(), recipe->GetID());
-									client->Message(CHANNEL_NARRATIVE, "Recipe: \"%s\" put in recipe book.", recipe->GetName());
-
-									if (packet && client->GetRecipeListSent()) {
-										packet->setArrayDataByName("id", recipe->GetID(), i);
-										packet->setArrayDataByName("tier", recipe->GetTier(), i);
-										packet->setArrayDataByName("level", recipe->GetLevel(), i);
-										packet->setArrayDataByName("icon", recipe->GetIcon(), i);
-										packet->setArrayDataByName("classes", recipe->GetClasses(), i);
-										packet->setArrayDataByName("skill", recipe->GetSkill(), i);
-										packet->setArrayDataByName("technique", recipe->GetTechnique(), i);
-										packet->setArrayDataByName("knowledge", recipe->GetKnowledge(), i);
-										packet->setArrayDataByName("unknown2", recipe->GetUnknown2(), i);
-										packet->setArrayDataByName("recipe_name", recipe->GetName(), i);
-										packet->setArrayDataByName("recipe_book", recipe->GetBook(), i);
-										packet->setArrayDataByName("unknown3", recipe->GetUnknown3(), i);
-										i++;
-									}
+									client->AddRecipeToPlayer(recipe, packet, &i);
 								}
 								}
 							}
 							}
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Done adding recipes");
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Done adding recipes");

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

@@ -1379,7 +1379,7 @@ public:
 	void			AddHate(Entity* attacker, sint32 hate);
 	void			AddHate(Entity* attacker, sint32 hate);
 	bool			CheckInterruptSpell(Entity* attacker);
 	bool			CheckInterruptSpell(Entity* attacker);
 	bool			CheckFizzleSpell(LuaSpell* spell);
 	bool			CheckFizzleSpell(LuaSpell* spell);
-	void			KillSpawn(Spawn* dead, int8 damage_type = 0, int16 kill_blow_type = 0);
+	void			KillSpawn(Spawn* dead, int8 type = 0, int8 damage_type = 0, int16 kill_blow_type = 0);
 	void			HandleDeathExperienceDebt(Spawn* killer);
 	void			HandleDeathExperienceDebt(Spawn* killer);
 	void            SetAttackDelay(bool primary = false, bool ranged = false);
 	void            SetAttackDelay(bool primary = false, bool ranged = false);
 	float           CalculateAttackSpeedMod();
 	float           CalculateAttackSpeedMod();

+ 1 - 0
EQ2/source/WorldServer/GroundSpawn.cpp

@@ -37,6 +37,7 @@ GroundSpawn::GroundSpawn(){
 	groundspawn_id = 0;
 	groundspawn_id = 0;
 	MHarvest.SetName("GroundSpawn::MHarvest");
 	MHarvest.SetName("GroundSpawn::MHarvest");
 	MHarvestUse.SetName("GroundSpawn::MHarvestUse");
 	MHarvestUse.SetName("GroundSpawn::MHarvestUse");
+	randomize_heading = true; // we by default randomize heading of groundspawns DB overrides
 }
 }
 
 
 GroundSpawn::~GroundSpawn(){
 GroundSpawn::~GroundSpawn(){

+ 5 - 0
EQ2/source/WorldServer/GroundSpawn.h

@@ -52,6 +52,7 @@ public:
 		new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag());
 		new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag());
 		new_spawn->SetLootTier(GetLootTier());
 		new_spawn->SetLootTier(GetLootTier());
 		new_spawn->SetLootDropType(GetLootDropType());
 		new_spawn->SetLootDropType(GetLootDropType());
+		new_spawn->SetRandomizeHeading(GetRandomizeHeading());
 		return new_spawn;
 		return new_spawn;
 	}
 	}
 	bool IsGroundSpawn(){ return true; }
 	bool IsGroundSpawn(){ return true; }
@@ -69,6 +70,9 @@ public:
 	string GetHarvestSpellType();
 	string GetHarvestSpellType();
 	string GetHarvestSpellName();
 	string GetHarvestSpellName();
 	void HandleUse(Client* client, string type);
 	void HandleUse(Client* client, string type);
+	
+	void SetRandomizeHeading(bool val) { randomize_heading = val; }
+	bool GetRandomizeHeading() { return randomize_heading; }
 private:
 private:
 	int8	number_harvests;
 	int8	number_harvests;
 	int8	num_attempts_per_harvest;
 	int8	num_attempts_per_harvest;
@@ -76,6 +80,7 @@ private:
 	string	collection_skill;
 	string	collection_skill;
 	Mutex	MHarvest;
 	Mutex	MHarvest;
 	Mutex	MHarvestUse;
 	Mutex	MHarvestUse;
+	bool 	randomize_heading;
 };
 };
 #endif
 #endif
 
 

+ 80 - 17
EQ2/source/WorldServer/Player.cpp

@@ -2555,40 +2555,46 @@ void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 ma
 	//pattern : 0 - zigzag, 1 - down, 2 - across
 	//pattern : 0 - zigzag, 1 - down, 2 - across
 	MSpellsBook.lock();
 	MSpellsBook.lock();
 
 
+	std::vector<SpellBookEntry*> sort_spells(spells);
+	
 	if (!maxlvl_only)
 	if (!maxlvl_only)
 	{
 	{
 		switch (sort_by)
 		switch (sort_by)
 		{
 		{
 		case 0:
 		case 0:
 			if (!order)
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByName);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByName);
 			else
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByNameReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByNameReverse);
 			break;
 			break;
 		case 1:
 		case 1:
 			if (!order)
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByLevel);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevel);
 			else
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByLevelReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevelReverse);
 			break;
 			break;
 		case 2:
 		case 2:
 			if (!order)
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByCategory);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategory);
 			else
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByCategoryReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategoryReverse);
 			break;
 			break;
 		}
 		}
 	}
 	}
 
 
 	vector<SpellBookEntry*>::iterator itr;
 	vector<SpellBookEntry*>::iterator itr;
 	SpellBookEntry* spell = 0;
 	SpellBookEntry* spell = 0;
-	int i = 0;
 	map<string, SpellBookEntry*> tmpSpells;
 	map<string, SpellBookEntry*> tmpSpells;
 	vector<SpellBookEntry*> resultSpells;
 	vector<SpellBookEntry*> resultSpells;
-	for (itr = spells.begin(); itr != spells.end(); itr++) {
+	
+	int32 i = 0;
+	int8 page_book_count = 0;
+	int32 last_start_point = 0;
+	
+	for (itr = sort_spells.begin(); itr != sort_spells.end(); itr++) {
 		spell = *itr;
 		spell = *itr;
 
 
-		if (spell->type != book_type || spell->slot == -1)
+		if (spell->type != book_type)
 			continue;
 			continue;
 
 
 		if (maxlvl_only)
 		if (maxlvl_only)
@@ -2627,9 +2633,12 @@ void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 ma
 			resultSpells.push_back(spell);
 			resultSpells.push_back(spell);
 		}
 		}
 		spell->slot = i;
 		spell->slot = i;
-		i++;
+		
+			Spell* tmpspell = 0;
+			tmpspell = master_spell_list.GetSpell(spell->spell_id, spell->tier);
+		GetSpellBookSlotSort(pattern, &i, &page_book_count, &last_start_point);
 	} // end for loop for setting slots
 	} // end for loop for setting slots
-
+	
 	if (maxlvl_only)
 	if (maxlvl_only)
 	{
 	{
 		switch (sort_by)
 		switch (sort_by)
@@ -2655,13 +2664,15 @@ void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 ma
 		}
 		}
 
 
 		i = 0;
 		i = 0;
+		page_book_count = 0;
+		last_start_point = 0;
 		vector<SpellBookEntry*>::iterator tmpItr;
 		vector<SpellBookEntry*>::iterator tmpItr;
 		for (tmpItr = resultSpells.begin(); tmpItr != resultSpells.end(); tmpItr++) {
 		for (tmpItr = resultSpells.begin(); tmpItr != resultSpells.end(); tmpItr++) {
 			((SpellBookEntry*)*tmpItr)->slot = i;
 			((SpellBookEntry*)*tmpItr)->slot = i;
-			i++;
+			GetSpellBookSlotSort(pattern, &i, &page_book_count, &last_start_point);
 		}
 		}
 	}
 	}
-
+	
 	MSpellsBook.unlock();
 	MSpellsBook.unlock();
 }
 }
 
 
@@ -3162,8 +3173,8 @@ EQ2Packet* Player::GetSpellBookUpdatePacket(int16 version) {
 				}
 				}
 				spell_xor_packet = new uchar[count * total_bytes];
 				spell_xor_packet = new uchar[count * total_bytes];
 				memset(spell_xor_packet, 0, count * total_bytes);
 				memset(spell_xor_packet, 0, count * total_bytes);
-				spell_count = count;
 			}
 			}
+			spell_count = count;
 			MSpellsBook.lock();
 			MSpellsBook.lock();
 			for (int32 i = 0; i < spells.size(); i++) {
 			for (int32 i = 0; i < spells.size(); i++) {
 				spell_entry = (SpellBookEntry*)spells[i];
 				spell_entry = (SpellBookEntry*)spells[i];
@@ -4936,7 +4947,10 @@ void Player::RemoveQuest(int32 id, bool delete_quest){
 	if(delete_quest){
 	if(delete_quest){
 		safe_delete(player_quests[id]);
 		safe_delete(player_quests[id]);
 	}
 	}
-	player_quests.erase(id);
+	map<int32, Quest*>::iterator itr = player_quests.find(id);
+	if(itr != player_quests.end()) {
+		player_quests.erase(itr);
+	}
 	MPlayerQuests.releasewritelock(__FUNCTION__, __LINE__);
 	MPlayerQuests.releasewritelock(__FUNCTION__, __LINE__);
 	SendQuestRequiredSpawns(id);
 	SendQuestRequiredSpawns(id);
 }
 }
@@ -5021,7 +5035,9 @@ int16 Player::GetTaskGroupStep(int32 quest_id){
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	if(player_quests.count(quest_id) > 0){
 	if(player_quests.count(quest_id) > 0){
 		quest = player_quests[quest_id];
 		quest = player_quests[quest_id];
-		step = quest->GetTaskGroupStep();
+		if(quest) {
+			step = quest->GetTaskGroupStep();
+		}
 	}
 	}
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	return step;
 	return step;
@@ -5045,7 +5061,9 @@ int16 Player::GetQuestStep(int32 quest_id){
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	if(player_quests.count(quest_id) > 0){
 	if(player_quests.count(quest_id) > 0){
 		quest = player_quests[quest_id];
 		quest = player_quests[quest_id];
-		step = quest->GetQuestStep();
+		if(quest) {
+			step = quest->GetQuestStep();
+		}
 	}
 	}
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	return step;
 	return step;
@@ -7263,4 +7281,49 @@ int Player::GetPVPAlignment(){
 		return alignment;
 		return alignment;
 	}
 	}
 	return -1; //error
 	return -1; //error
+}
+
+void Player::GetSpellBookSlotSort(int32 pattern, int32* i, int8* page_book_count, int32* last_start_point) {
+	switch(pattern) {
+		case 1: { // down
+			*i = (*i) + 2;
+			(*page_book_count)++;
+			if(*page_book_count > 3) {
+				if(((*i) % 2) == 0) {
+					(*i) = (*last_start_point) + 1;
+				}
+				else {
+					(*last_start_point) = (*last_start_point) + 8;
+					(*i) = (*last_start_point);
+				}
+				(*page_book_count) = 0;
+			}
+			break;
+		}
+		case 2: { // across
+			(*page_book_count)++;
+			switch(*page_book_count) {
+				case 1:
+				case 3:	{
+					(*i)++;
+					break;
+				}
+				case 2: {
+					(*i) = (*i) + 7;
+					break;
+				}
+				case 4: {
+					(*last_start_point) = (*last_start_point) + 2;
+					(*i) = (*last_start_point);
+					(*page_book_count) = 0;
+					break;
+				}
+			}
+			break;
+		}
+		default: { // zig-zag
+			(*i)++;
+			break;
+		}
+	}
 }
 }

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

@@ -849,7 +849,7 @@ public:
 	}
 	}
 	void RemoveSpellBookEntry(int32 spell_id, bool remove_passives_from_list = true);
 	void RemoveSpellBookEntry(int32 spell_id, bool remove_passives_from_list = true);
 	void ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 maxlvl_only, int32 book_type);
 	void ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 maxlvl_only, int32 book_type);
-
+	void GetSpellBookSlotSort(int32 pattern, int32* i, int8* page_book_count, int32* last_start_point);
 	static bool SortSpellEntryByName(SpellBookEntry* s1, SpellBookEntry* s2);
 	static bool SortSpellEntryByName(SpellBookEntry* s1, SpellBookEntry* s2);
 	static bool SortSpellEntryByCategory(SpellBookEntry* s1, SpellBookEntry* s2);
 	static bool SortSpellEntryByCategory(SpellBookEntry* s1, SpellBookEntry* s2);
 	static bool SortSpellEntryByLevel(SpellBookEntry* s1, SpellBookEntry* s2);
 	static bool SortSpellEntryByLevel(SpellBookEntry* s1, SpellBookEntry* s2);

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

@@ -22,9 +22,12 @@
 
 
 #include "../../common/types.h"
 #include "../../common/types.h"
 #include "../../common/Mutex.h"
 #include "../../common/Mutex.h"
+#include "../classes.h"
+
 #include <string.h>
 #include <string.h>
 #include <map>
 #include <map>
 
 
+class Item;
 using namespace std;
 using namespace std;
 
 
 struct RecipeProducts {
 struct RecipeProducts {
@@ -85,6 +88,16 @@ public:
 	int32 GetTechnique() {return technique;}
 	int32 GetTechnique() {return technique;}
 	int32 GetKnowledge() {return knowledge;}
 	int32 GetKnowledge() {return knowledge;}
 	int32 GetClasses() {return classes;}
 	int32 GetClasses() {return classes;}
+	//class_id = classes.GetTSBaseClass(spawn->GetTradeskillClass())  bit-match on class ids 1-13
+	//secondary_class_id = classes.GetSecondaryTSBaseClass(spawn->GetTradeskillClass()) bit-match on class ids 1-13
+	//tertiary_class_id = spawn->GetTradeskillClass() (direct match)
+	bool CanUseRecipeByClass(Item* item, int8 class_id) {
+    /* any can use bit combination of 1+2
+	   adornments = 1
+	   artisan = 2	
+	*/
+	return item->generic_info.tradeskill_classes < 4 || (1 << class_id) & item->generic_info.tradeskill_classes;
+	}
 	int32 GetUnknown2() {return unknown2;}
 	int32 GetUnknown2() {return unknown2;}
 	int32 GetUnknown3() {return unknown3;}
 	int32 GetUnknown3() {return unknown3;}
 	int32 GetUnknown4() {return unknown4;}
 	int32 GetUnknown4() {return unknown4;}

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

@@ -2084,8 +2084,6 @@ void Spawn::InitializePosPacketData(Player* player, PacketStruct* packet, bool b
 		include_heading = false;
 		include_heading = false;
 	else if (IsSign() && ((Sign*)this)->GetIncludeHeading() == false)
 	else if (IsSign() && ((Sign*)this)->GetIncludeHeading() == false)
 		include_heading = false;
 		include_heading = false;
-	else if (IsGroundSpawn())
-		include_heading = false;
 
 
 	if (include_heading){
 	if (include_heading){
 		packet->setDataByName("pos_heading1", appearance.pos.Dir1);
 		packet->setDataByName("pos_heading1", appearance.pos.Dir1);

+ 1 - 1
EQ2/source/WorldServer/SpellProcess.cpp

@@ -1998,7 +1998,7 @@ void SpellProcess::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, boo
 			while(itr.Next()){
 			while(itr.Next()){
 				interrupt = itr->value;
 				interrupt = itr->value;
 				if(interrupt && interrupt->interrupted == spawn){
 				if(interrupt && interrupt->interrupted == spawn){
-					interrupt_list.Remove(interrupt);
+					interrupt_list.Remove(interrupt, true);
 				}
 				}
 			}
 			}
 		}
 		}

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

@@ -6699,7 +6699,7 @@ bool WorldDatabase::LoadGroundSpawn(ZoneServer* zone, int32 spawn_id) {
 	int32 id = 0;
 	int32 id = 0;
 	DatabaseResult result;
 	DatabaseResult result;
 
 
-	database_new.Select(&result, "SELECT sg.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, sg.number_harvests, sg.num_attempts_per_harvest, sg.groundspawn_id, sg.collection_skill, s.size_offset, s.disable_sounds, s.aaxp_rewards, s.loot_tier, s.loot_drop_type\n"
+	database_new.Select(&result, "SELECT sg.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, sg.number_harvests, sg.num_attempts_per_harvest, sg.groundspawn_id, sg.collection_skill, s.size_offset, s.disable_sounds, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, sg.randomize_heading\n"
 								 "FROM spawn s\n"
 								 "FROM spawn s\n"
 								 "INNER JOIN spawn_ground sg\n"
 								 "INNER JOIN spawn_ground sg\n"
 								 "ON sg.spawn_id = s.id\n"
 								 "ON sg.spawn_id = s.id\n"
@@ -6746,6 +6746,8 @@ bool WorldDatabase::LoadGroundSpawn(ZoneServer* zone, int32 spawn_id) {
 
 
 		spawn->SetLootDropType(result.GetInt32(24));
 		spawn->SetLootDropType(result.GetInt32(24));
 		
 		
+		spawn->SetRandomizeHeading(result.GetInt32(25));
+		
 		zone->AddGroundSpawn(id, spawn);
 		zone->AddGroundSpawn(id, spawn);
 
 
 		if (!zone->GetGroundSpawnEntries(spawn->GetGroundSpawnEntryID()))
 		if (!zone->GetGroundSpawnEntries(spawn->GetGroundSpawnEntryID()))

+ 28 - 0
EQ2/source/WorldServer/client.cpp

@@ -9738,6 +9738,7 @@ void Client::AcceptCollectionRewards(Collection* collection, int32 selectable_it
 	
 	
 	HandInCollections();
 	HandInCollections();
 
 
+	GetPlayer()->GetZone()->SendSubSpawnUpdates(SUBSPAWN_TYPES::COLLECTOR);
 }
 }
 
 
 void Client::SendRecipeList() {
 void Client::SendRecipeList() {
@@ -11224,3 +11225,30 @@ void Client::UpdateCharacterRewardData(QuestRewardData* data) {
 			data->is_temporary, data->is_collection, data->has_displayed, data->tmp_coin, data->tmp_status, database.getSafeEscapeString(data->description.c_str()).c_str(), GetCharacterID(), data->db_index, data->quest_id);
 			data->is_temporary, data->is_collection, data->has_displayed, data->tmp_coin, data->tmp_status, database.getSafeEscapeString(data->description.c_str()).c_str(), GetCharacterID(), data->db_index, data->quest_id);
 	}
 	}
 }
 }
+
+void Client::AddRecipeToPlayer(Recipe* recipe, PacketStruct* packet, int16* i) {
+	if(recipe == nullptr)
+		return;
+	
+	GetPlayer()->GetRecipeList()->AddRecipe(recipe);
+	database.SavePlayerRecipe(GetPlayer(), recipe->GetID());
+	Message(CHANNEL_NARRATIVE, "Recipe: \"%s\" put in recipe book.", recipe->GetName());
+
+	if (packet && GetRecipeListSent()) {
+		packet->setArrayDataByName("id", recipe->GetID(), *i);
+		packet->setArrayDataByName("tier", recipe->GetTier(), *i);
+		packet->setArrayDataByName("level", recipe->GetLevel(), *i);
+		packet->setArrayDataByName("icon", recipe->GetIcon(), *i);
+		packet->setArrayDataByName("classes", recipe->GetClasses(), *i);
+		packet->setArrayDataByName("skill", recipe->GetSkill(), *i);
+		packet->setArrayDataByName("technique", recipe->GetTechnique(), *i);
+		packet->setArrayDataByName("knowledge", recipe->GetKnowledge(), *i);
+		packet->setArrayDataByName("unknown2", recipe->GetUnknown2(), *i);
+		packet->setArrayDataByName("recipe_name", recipe->GetName(), *i);
+		packet->setArrayDataByName("recipe_book", recipe->GetBook(), *i);
+		packet->setArrayDataByName("unknown3", recipe->GetUnknown3(), *i);
+		if(i) {
+			(*i)++;
+		}
+	}
+}

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

@@ -561,6 +561,9 @@ public:
 	void	SaveQuestRewardData(bool force_refresh = false);
 	void	SaveQuestRewardData(bool force_refresh = false);
 	void	UpdateCharacterRewardData(QuestRewardData* data);
 	void	UpdateCharacterRewardData(QuestRewardData* data);
 	void	SetQuestUpdateState(bool val) { quest_updates = val; }
 	void	SetQuestUpdateState(bool val) { quest_updates = val; }
+	
+	void	AddRecipeToPlayer(Recipe* recipe, PacketStruct* packet, int16* i);
+	
 private:
 private:
 	void    SavePlayerImages();
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);

+ 28 - 2
EQ2/source/WorldServer/net.cpp

@@ -196,14 +196,40 @@ int main(int argc, char** argv) {
 	EQOpcodeVersions = database.GetVersions();
 	EQOpcodeVersions = database.GetVersions();
 	map<int16,int16>::iterator version_itr;
 	map<int16,int16>::iterator version_itr;
 	int16 version1 = 0;
 	int16 version1 = 0;
+	int16 prevVersion = 0;
+	std::string prevString = std::string("");
+	std::string builtString = std::string("");
 	for (version_itr = EQOpcodeVersions.begin(); version_itr != EQOpcodeVersions.end(); version_itr++) {
 	for (version_itr = EQOpcodeVersions.begin(); version_itr != EQOpcodeVersions.end(); version_itr++) {
 		version1 = version_itr->first;
 		version1 = version_itr->first;
 		EQOpcodeManager[version1] = new RegularOpcodeManager();
 		EQOpcodeManager[version1] = new RegularOpcodeManager();
 		map<string, uint16> eq = database.GetOpcodes(version1);
 		map<string, uint16> eq = database.GetOpcodes(version1);
-		if(!EQOpcodeManager[version1]->LoadOpcodes(&eq)) {
+		std::string missingOpcodesList = std::string("");
+		if(!EQOpcodeManager[version1]->LoadOpcodes(&eq, &missingOpcodesList)) {
 			LogWrite(INIT__ERROR, 0, "Init", "Loading opcodes failed. Make sure you have sourced the opcodes.sql file!");
 			LogWrite(INIT__ERROR, 0, "Init", "Loading opcodes failed. Make sure you have sourced the opcodes.sql file!");
 			return false;
 			return false;
 		}
 		}
+		
+		if(version1 == 0) // we don't need to display version 0
+			continue;
+		
+		if(prevString.size() > 0) {
+			if(prevString == missingOpcodesList) {
+				builtString += ", " + std::to_string(version1);
+			}
+			else {
+					LogWrite(OPCODE__WARNING, 1, "Opcode", "Opcodes %s.", builtString.c_str());
+					builtString = std::string("");
+					prevString = std::string("");
+			}
+		}
+		if(prevString.size() < 1) {
+			prevString = std::string(missingOpcodesList);
+			builtString = std::string(missingOpcodesList + " are missing from the opcodes table for version(s) " + std::to_string(version1));
+		}
+	}
+
+	if(builtString.size() > 0) {
+		LogWrite(OPCODE__WARNING, 1, "Opcode", "Opcodes %s.", builtString.c_str());
 	}
 	}
 
 
 	LogWrite(WORLD__DEBUG, 1, "World", "-Loading structs...");
 	LogWrite(WORLD__DEBUG, 1, "World", "-Loading structs...");
@@ -956,7 +982,7 @@ void NetConnection::WelcomeHeader()
 #ifdef _WIN32
 #ifdef _WIN32
 	SetConsoleTextAttribute(console, FOREGROUND_YELLOW_BOLD);
 	SetConsoleTextAttribute(console, FOREGROUND_YELLOW_BOLD);
 #endif
 #endif
-	printf("\n\nCopyright (C) 2007-2021 EQ2Emulator. https://www.eq2emu.com \n\n");
+	printf("\n\nCopyright (C) 2007-2022 EQ2Emulator. https://www.eq2emu.com \n\n");
 	printf("EQ2Emulator is free software: you can redistribute it and/or modify\n");
 	printf("EQ2Emulator is free software: you can redistribute it and/or modify\n");
 	printf("it under the terms of the GNU General Public License as published by\n");
 	printf("it under the terms of the GNU General Public License as published by\n");
 	printf("the Free Software Foundation, either version 3 of the License, or\n");
 	printf("the Free Software Foundation, either version 3 of the License, or\n");

+ 54 - 7
EQ2/source/WorldServer/zoneserver.cpp

@@ -178,6 +178,7 @@ ZoneServer::ZoneServer(const char* name, bool incoming_client) {
 ZoneServer::~ZoneServer() {
 ZoneServer::~ZoneServer() {
 	zoneShuttingDown = true;  //ensure other threads shut down too
 	zoneShuttingDown = true;  //ensure other threads shut down too
 	//allow other threads to properly shut down
 	//allow other threads to properly shut down
+	LogWrite(ZONE__INFO, 0, "Zone", "Initiating zone shutdown of '%s'", zone_name);
 	while (spawnthread_active || initial_spawn_threads_active > 0){
 	while (spawnthread_active || initial_spawn_threads_active > 0){
 		if (spawnthread_active)
 		if (spawnthread_active)
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on spawn thread");
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on spawn thread");
@@ -185,7 +186,6 @@ ZoneServer::~ZoneServer() {
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on initial spawn thread");
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on initial spawn thread");
 		Sleep(10);
 		Sleep(10);
 	}
 	}
-	LogWrite(ZONE__INFO, 0, "Zone", "Initiating zone shutdown of '%s'", zone_name);
 	changed_spawns.clear(true);
 	changed_spawns.clear(true);
 	transport_spawns.clear();
 	transport_spawns.clear();
 	safe_delete(tradeskillMgr);
 	safe_delete(tradeskillMgr);
@@ -1273,6 +1273,13 @@ void ZoneServer::DeleteSpawns(bool delete_all) {
 				if(sitr != spawn_list.end()) {
 				if(sitr != spawn_list.end()) {
 					spawn_list.erase(sitr);
 					spawn_list.erase(sitr);
 				}
 				}
+				
+				if(spawn->IsCollector()) {
+					std::map<int32, Spawn*>::iterator subitr = subspawn_list[SUBSPAWN_TYPES::COLLECTOR].find(spawn->GetID());
+					if(subitr != subspawn_list[SUBSPAWN_TYPES::COLLECTOR].end()) {
+						subspawn_list[SUBSPAWN_TYPES::COLLECTOR].erase(subitr);
+					}
+				}
 				MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
 				MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
 			
 			
 				safe_delete(spawn);
 				safe_delete(spawn);
@@ -1625,8 +1632,10 @@ bool ZoneServer::SpawnProcess(){
 		if (pending_spawn_list_remove.size() > 0) {
 		if (pending_spawn_list_remove.size() > 0) {
 			MSpawnList.writelock(__FUNCTION__, __LINE__);
 			MSpawnList.writelock(__FUNCTION__, __LINE__);
 			vector<int32>::iterator itr2;
 			vector<int32>::iterator itr2;
-			for (itr2 = pending_spawn_list_remove.begin(); itr2 != pending_spawn_list_remove.end(); itr2++) 
+			for (itr2 = pending_spawn_list_remove.begin(); itr2 != pending_spawn_list_remove.end(); itr2++) {
 				spawn_list.erase(*itr2);
 				spawn_list.erase(*itr2);
+				subspawn_list[SUBSPAWN_TYPES::COLLECTOR].erase(*itr2);
+			}
 
 
 			pending_spawn_list_remove.clear();
 			pending_spawn_list_remove.clear();
 			MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
 			MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
@@ -1645,6 +1654,10 @@ bool ZoneServer::SpawnProcess(){
 				Spawn* spawn = *itr2;
 				Spawn* spawn = *itr2;
 				if (spawn)
 				if (spawn)
 					spawn_list[spawn->GetID()] = spawn;
 					spawn_list[spawn->GetID()] = spawn;
+				
+				if(spawn->IsCollector()) {
+					subspawn_list[SUBSPAWN_TYPES::COLLECTOR].insert(make_pair(spawn->GetID(),spawn));
+				}
 			}
 			}
 
 
 			pending_spawn_list_add.clear();
 			pending_spawn_list_add.clear();
@@ -3090,6 +3103,15 @@ GroundSpawn* ZoneServer::AddGroundSpawn(SpawnLocation* spawnlocation, SpawnEntry
 		spawn->SetSpawnEntryID(spawnentry->spawn_entry_id);
 		spawn->SetSpawnEntryID(spawnentry->spawn_entry_id);
 		spawn->SetRespawnTime(spawnentry->respawn);
 		spawn->SetRespawnTime(spawnentry->respawn);
 		spawn->SetExpireTime(spawnentry->expire_time);
 		spawn->SetExpireTime(spawnentry->expire_time);
+		
+		if(spawn->GetRandomizeHeading()) {
+			float rand_heading = MakeRandomFloat(0.0f, 360.0f);
+			spawn->SetHeading(rand_heading);
+		}
+		else {
+			spawn->SetHeading(spawnlocation->heading);
+		}
+		
 		if (spawnentry->expire_time > 0)
 		if (spawnentry->expire_time > 0)
 			AddSpawnExpireTimer(spawn, spawnentry->expire_time, spawnentry->expire_offset);
 			AddSpawnExpireTimer(spawn, spawnentry->expire_time, spawnentry->expire_offset);
 
 
@@ -4482,8 +4504,12 @@ void ZoneServer::Despawn(Spawn* spawn, int32 timer){
 	AddDeadSpawn(spawn, timer);
 	AddDeadSpawn(spawn, timer);
 }
 }
 
 
-void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, bool send_packet, int8 damage_type, int16 kill_blow_type)
+void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, bool send_packet, int8 type, int8 damage_type, int16 kill_blow_type)
 {
 {
+	bool isSpell = (type == DAMAGE_PACKET_TYPE_SIPHON_SPELL || type == DAMAGE_PACKET_TYPE_SIPHON_SPELL2 ||
+					type == DAMAGE_PACKET_TYPE_SPELL_DAMAGE || type == DAMAGE_PACKET_TYPE_SPELL_CRIT_DMG || 
+					type == DAMAGE_PACKET_TYPE_SPELL_DAMAGE2 || type == DAMAGE_PACKET_TYPE_SPELL_DAMAGE3);
+	
 	MDeadSpawns.readlock(__FUNCTION__, __LINE__);
 	MDeadSpawns.readlock(__FUNCTION__, __LINE__);
 	if(!dead || this->dead_spawns.count(dead->GetID()) > 0) {
 	if(!dead || this->dead_spawns.count(dead->GetID()) > 0) {
 		MDeadSpawns.releasereadlock(__FUNCTION__, __LINE__);
 		MDeadSpawns.releasereadlock(__FUNCTION__, __LINE__);
@@ -4524,7 +4550,7 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 		if (dead->Alive())
 		if (dead->Alive())
 			return;
 			return;
 
 
-		RemoveSpellTimersFromSpawn(dead, true, !dead->IsPlayer());
+		RemoveSpellTimersFromSpawn(dead, true, !dead->IsPlayer(), true, !isSpell);
 
 
 		if(dead->IsPlayer()) 
 		if(dead->IsPlayer()) 
 		{
 		{
@@ -4670,7 +4696,7 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 
 
 
 
 	// Remove the support functions for the dead spawn
 	// Remove the support functions for the dead spawn
-	RemoveSpawnSupportFunctions(dead);
+	RemoveSpawnSupportFunctions(dead, !isSpell);
 
 
 	// Erase the expire timer if it has one
 	// Erase the expire timer if it has one
 	if (spawn_expire_timers.count(dead->GetID()) > 0)
 	if (spawn_expire_timers.count(dead->GetID()) > 0)
@@ -6294,13 +6320,21 @@ void ZoneServer::FindSpawn(Client* client, char* regSearchStr)
 		client->SimpleMessage(CHANNEL_COLOR_RED, "Try/Catch ZoneServer::FindSpawn(Client*, char* regSearchStr) failure.");
 		client->SimpleMessage(CHANNEL_COLOR_RED, "Try/Catch ZoneServer::FindSpawn(Client*, char* regSearchStr) failure.");
 		return;
 		return;
 	}
 	}
+	std::regex re;
+	try {
+		re = std::regex(resString, std::regex_constants::icase);
+	}
+	catch(...) {
+		client->SimpleMessage(CHANNEL_COLOR_RED, "Invalid regex for FindSpawn.");
+		return;
+	}
+	
 	client->Message(CHANNEL_NARRATIVE, "RegEx Search Spawn List: %s", regSearchStr);
 	client->Message(CHANNEL_NARRATIVE, "RegEx Search Spawn List: %s", regSearchStr);
 	client->Message(CHANNEL_NARRATIVE, "Database ID | Spawn Name | X , Y , Z");
 	client->Message(CHANNEL_NARRATIVE, "Database ID | Spawn Name | X , Y , Z");
 	client->Message(CHANNEL_NARRATIVE, "========================");
 	client->Message(CHANNEL_NARRATIVE, "========================");
 	map<int32, Spawn*>::iterator itr;
 	map<int32, Spawn*>::iterator itr;
 	MSpawnList.readlock(__FUNCTION__, __LINE__);
 	MSpawnList.readlock(__FUNCTION__, __LINE__);
 	int32 spawnsFound = 0;
 	int32 spawnsFound = 0;
-	std::regex re(resString, std::regex_constants::icase);
 	for (itr = spawn_list.begin(); itr != spawn_list.end(); itr++) {
 	for (itr = spawn_list.begin(); itr != spawn_list.end(); itr++) {
 		Spawn* spawn = itr->second;
 		Spawn* spawn = itr->second;
 		if (!spawn || !spawn->GetName())
 		if (!spawn || !spawn->GetName())
@@ -8202,8 +8236,10 @@ void ZoneServer::ProcessSpawnRemovals()
 	MPendingSpawnRemoval.writelock(__FUNCTION__, __LINE__);
 	MPendingSpawnRemoval.writelock(__FUNCTION__, __LINE__);
 	if (m_pendingSpawnRemove.size() > 0) {
 	if (m_pendingSpawnRemove.size() > 0) {
 		map<int32,bool>::iterator itr2;
 		map<int32,bool>::iterator itr2;
-		for (itr2 = m_pendingSpawnRemove.begin(); itr2 != m_pendingSpawnRemove.end(); itr2++) 
+		for (itr2 = m_pendingSpawnRemove.begin(); itr2 != m_pendingSpawnRemove.end(); itr2++) {
 			spawn_list.erase(itr2->first);
 			spawn_list.erase(itr2->first);
+			subspawn_list[SUBSPAWN_TYPES::COLLECTOR].erase(itr2->first);
+		}
 
 
 		m_pendingSpawnRemove.clear();
 		m_pendingSpawnRemove.clear();
 	}
 	}
@@ -8325,3 +8361,14 @@ void ZoneServer::RemoveClientsFromZone(ZoneServer* zone) {
 	}
 	}
 	MClientList.releasereadlock(__FUNCTION__, __LINE__);
 	MClientList.releasereadlock(__FUNCTION__, __LINE__);
 }
 }
+
+void ZoneServer::SendSubSpawnUpdates(SUBSPAWN_TYPES subtype) {
+	std::map<int32, Spawn*>::iterator subitr;
+	MSpawnList.readlock(__FUNCTION__, __LINE__);
+	for(subitr = subspawn_list[subtype].begin(); subitr !=  subspawn_list[subtype].end(); subitr++) {
+		subitr->second->changed = true;
+		subitr->second->info_changed = true;
+		AddChangedSpawn(subitr->second);
+	}
+	MSpawnList.releasereadlock(__FUNCTION__, __LINE__);
+}

+ 11 - 1
EQ2/source/WorldServer/zoneserver.h

@@ -257,6 +257,11 @@ struct ZoneInfoSlideStruct {
 	vector<ZoneInfoSlideStructTransitionInfo*> slide_transition_info;
 	vector<ZoneInfoSlideStructTransitionInfo*> slide_transition_info;
 };
 };
 
 
+enum SUBSPAWN_TYPES {
+	COLLECTOR = 0,
+	MAX_SUBSPAWN_TYPE = 20
+};
+
 // need to attempt to clean this up and add xml comments, remove unused code, find a logical way to sort the functions maybe by get/set/process/add etc...
 // need to attempt to clean this up and add xml comments, remove unused code, find a logical way to sort the functions maybe by get/set/process/add etc...
 class ZoneServer {
 class ZoneServer {
 public:
 public:
@@ -334,7 +339,7 @@ public:
 	
 	
 	vector<Entity*> GetPlayers();
 	vector<Entity*> GetPlayers();
 	
 	
-	void	KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, bool send_packet = true, int8 damage_type = 0, int16 kill_blow_type = 0);
+	void	KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, bool send_packet = true, int8 type = 0, int8 damage_type = 0, int16 kill_blow_type = 0);
 	
 	
 	void	SendDamagePacket(Spawn* attacker, Spawn* victim, int8 type1, int8 type2, int8 damage_type, int16 damage, const char* spell_name);
 	void	SendDamagePacket(Spawn* attacker, Spawn* victim, int8 type1, int8 type2, int8 damage_type, int16 damage, const char* spell_name);
 	void    SendHealPacket(Spawn* caster, Spawn* target, int16 type, int32 heal_amt, const char* spell_name);
 	void    SendHealPacket(Spawn* caster, Spawn* target, int16 type, int32 heal_amt, const char* spell_name);
@@ -690,6 +695,8 @@ public:
 	void	StopSpawnScriptTimer(Spawn* spawn, std::string functionName);
 	void	StopSpawnScriptTimer(Spawn* spawn, std::string functionName);
 
 
 	Client*	RemoveZoneServerFromClient(ZoneServer* zone);
 	Client*	RemoveZoneServerFromClient(ZoneServer* zone);
+	
+	void	SendSubSpawnUpdates(SUBSPAWN_TYPES subtype);
 private:
 private:
 #ifndef WIN32
 #ifndef WIN32
 	pthread_t ZoneThread;
 	pthread_t ZoneThread;
@@ -835,6 +842,9 @@ private:
 	/* Lists */
 	/* Lists */
 	list<Spawn*>	pending_spawn_list_add;
 	list<Spawn*>	pending_spawn_list_add;
 	
 	
+	/* Specialized Lists to update specific scenarios */
+	std::map<int32, Spawn*>	subspawn_list[SUBSPAWN_TYPES::MAX_SUBSPAWN_TYPE];
+	
 	/* Vectors */
 	/* Vectors */
 	vector<RevivePoint*>*	revive_points;
 	vector<RevivePoint*>*	revive_points;
 	vector<int32>			transport_spawns;
 	vector<int32>			transport_spawns;

+ 16 - 6
EQ2/source/common/opcodemgr.cpp

@@ -41,7 +41,7 @@ using namespace std;
 OpcodeManager::OpcodeManager() {
 OpcodeManager::OpcodeManager() {
 	loaded = false;
 	loaded = false;
 }
 }
-bool OpcodeManager::LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s){
+bool OpcodeManager::LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s, std::string* missingOpcodes){
 	//do the mapping and store them in the shared memory array
 	//do the mapping and store them in the shared memory array
 	bool ret = true;
 	bool ret = true;
 	EmuOpcode emu_op;
 	EmuOpcode emu_op;
@@ -59,7 +59,17 @@ bool OpcodeManager::LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s
 		//find the opcode in the file
 		//find the opcode in the file
 		res = eq->find(op_name);
 		res = eq->find(op_name);
 		if(res == eq->end()) {
 		if(res == eq->end()) {
-			LogWrite(OPCODE__WARNING, 1, "Opcode", "Opcode %s is missing from the opcodes table.", op_name);
+			if(missingOpcodes) {
+				if(missingOpcodes->size() < 1) {
+					missingOpcodes->append(op_name);
+				}
+				else {
+					missingOpcodes->append(", " + std::string(op_name));
+				}
+			}
+			else {
+				LogWrite(OPCODE__WARNING, 1, "Opcode", "Opcode %s is missing from the opcodes table.", op_name);
+			}
 			s->Set(emu_op, 0xFFFF);
 			s->Set(emu_op, 0xFFFF);
 			continue;	//continue to give them a list of all missing opcodes
 			continue;	//continue to give them a list of all missing opcodes
 		}
 		}
@@ -156,7 +166,7 @@ RegularOpcodeManager::~RegularOpcodeManager() {
 	safe_delete_array(eq_to_emu);
 	safe_delete_array(eq_to_emu);
 }
 }
 
 
-bool RegularOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
+bool RegularOpcodeManager::LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes) {
 	NormalMemStrategy s;
 	NormalMemStrategy s;
 	s.it = this;
 	s.it = this;
 	MOpcodes.lock();
 	MOpcodes.lock();
@@ -170,7 +180,7 @@ bool RegularOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
 	//dont need to set eq_to_emu cause every element should get a value
 	//dont need to set eq_to_emu cause every element should get a value
 	memset(eq_to_emu, 0, sizeof(EmuOpcode)*MAX_EQ_OPCODE);
 	memset(eq_to_emu, 0, sizeof(EmuOpcode)*MAX_EQ_OPCODE);
 	
 	
-	bool ret = LoadOpcodesMap(eq, &s);
+	bool ret = LoadOpcodesMap(eq, &s, missingOpcodes);
 	MOpcodes.unlock();
 	MOpcodes.unlock();
 	return ret;
 	return ret;
 }
 }
@@ -263,7 +273,7 @@ NullOpcodeManager::NullOpcodeManager()
 : MutableOpcodeManager() {
 : MutableOpcodeManager() {
 }
 }
 
 
-bool NullOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
+bool NullOpcodeManager::LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes) {
 	return(true);
 	return(true);
 }
 }
 
 
@@ -292,7 +302,7 @@ bool EmptyOpcodeManager::LoadOpcodes(const char *filename) {
 	return(true);
 	return(true);
 }
 }
 
 
-bool EmptyOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
+bool EmptyOpcodeManager::LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes) {
 	return(true);
 	return(true);
 }
 }
 
 

+ 6 - 6
EQ2/source/common/opcodemgr.h

@@ -34,7 +34,7 @@ public:
 	
 	
 	virtual bool Mutable() { return(false); }
 	virtual bool Mutable() { return(false); }
 	virtual bool LoadOpcodes(const char *filename) = 0;
 	virtual bool LoadOpcodes(const char *filename) = 0;
-	virtual bool LoadOpcodes(map<string, uint16>* eq) = 0;
+	virtual bool LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes = nullptr) = 0;
 	virtual bool ReloadOpcodes(const char *filename) = 0;
 	virtual bool ReloadOpcodes(const char *filename) = 0;
 	
 	
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op) = 0;
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op) = 0;
@@ -57,7 +57,7 @@ protected:
 					//in a shared manager, this dosent protect others
 					//in a shared manager, this dosent protect others
 	
 	
 	static bool LoadOpcodesFile(const char *filename, OpcodeSetStrategy *s);
 	static bool LoadOpcodesFile(const char *filename, OpcodeSetStrategy *s);
-	static bool LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s);
+	static bool LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s, std::string* missingOpcodes = nullptr);
 };
 };
 
 
 class MutableOpcodeManager : public OpcodeManager {
 class MutableOpcodeManager : public OpcodeManager {
@@ -74,7 +74,7 @@ public:
 	virtual ~SharedOpcodeManager() {}
 	virtual ~SharedOpcodeManager() {}
 	
 	
 	virtual bool LoadOpcodes(const char *filename);
 	virtual bool LoadOpcodes(const char *filename);
-	virtual bool LoadOpcodes(map<string, uint16>* eq);
+	virtual bool LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes = nullptr);
 	virtual bool ReloadOpcodes(const char *filename);
 	virtual bool ReloadOpcodes(const char *filename);
 	
 	
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -97,7 +97,7 @@ public:
 	
 	
 	virtual bool Editable() { return(true); }
 	virtual bool Editable() { return(true); }
 	virtual bool LoadOpcodes(const char *filename);
 	virtual bool LoadOpcodes(const char *filename);
-	virtual bool LoadOpcodes(map<string, uint16>* eq);
+	virtual bool LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes = nullptr);
 	virtual bool ReloadOpcodes(const char *filename);
 	virtual bool ReloadOpcodes(const char *filename);
 	
 	
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -126,7 +126,7 @@ public:
 	NullOpcodeManager();
 	NullOpcodeManager();
 	
 	
 	virtual bool LoadOpcodes(const char *filename);
 	virtual bool LoadOpcodes(const char *filename);
-	virtual bool LoadOpcodes(map<string, uint16>* eq);
+	virtual bool LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes = nullptr);
 	virtual bool ReloadOpcodes(const char *filename);
 	virtual bool ReloadOpcodes(const char *filename);
 	
 	
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -144,7 +144,7 @@ public:
 	EmptyOpcodeManager();
 	EmptyOpcodeManager();
 	
 	
 	virtual bool LoadOpcodes(const char *filename);
 	virtual bool LoadOpcodes(const char *filename);
-	virtual bool LoadOpcodes(map<string, uint16>* eq);
+	virtual bool LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes = nullptr);
 	virtual bool ReloadOpcodes(const char *filename);
 	virtual bool ReloadOpcodes(const char *filename);
 	
 	
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);
 	virtual uint16 EmuToEQ(const EmuOpcode emu_op);