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 3 months 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)
-		KillSpawn(victim, damage_type, blow_type);
+		KillSpawn(victim, type, damage_type, blow_type);
 	else {
 		victim->CheckProcs(PROC_TYPE_DEFENSIVE, this);
 		if (spell_name)
@@ -1257,7 +1257,7 @@ bool Entity::CheckInterruptSpell(Entity* attacker) {
 	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)
 		return;
 
@@ -1303,7 +1303,7 @@ void Entity::KillSpawn(Spawn* dead, int8 damage_type, int16 kill_blow_type) {
 	dead->ClearRunningLocations();
 	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)

+ 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);
 						int8 old_slot = 0;
 						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());
 							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.");
@@ -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
 								EQ2Packet* app = spell->SerializeSpell(client, false, false);
 								client->QueuePacket(app);
-													}
+							}
 						}
 						else
 							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.");
 							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))) {
 							LogWrite(TRADESKILL__DEBUG, 0, "Recipe", "Valid recipe book that the player doesn't have");
 							// 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
 							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());
-
+							
+							int16 i = 0;
 							// Create the packet to send to update the players recipe list
 							PacketStruct* packet = 0;
 							if (client->GetRecipeListSent()) {
 								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());
 							}
 
 							// loop through the list
 							vector<Recipe*>::iterator itr;
-							int16 i = 0;
 							for (itr = book_recipes->begin(); itr != book_recipes->end(); itr++) {
 								// check to see if the player already has this recipe some how
 								if (!client->GetPlayer()->GetRecipeList()->GetRecipe((*itr)->GetID())) {
 									// Player doesn't already have this recipe so lets add it
 									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");

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

@@ -1379,7 +1379,7 @@ public:
 	void			AddHate(Entity* attacker, sint32 hate);
 	bool			CheckInterruptSpell(Entity* attacker);
 	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            SetAttackDelay(bool primary = false, bool ranged = false);
 	float           CalculateAttackSpeedMod();

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

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

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

@@ -52,6 +52,7 @@ public:
 		new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag());
 		new_spawn->SetLootTier(GetLootTier());
 		new_spawn->SetLootDropType(GetLootDropType());
+		new_spawn->SetRandomizeHeading(GetRandomizeHeading());
 		return new_spawn;
 	}
 	bool IsGroundSpawn(){ return true; }
@@ -69,6 +70,9 @@ public:
 	string GetHarvestSpellType();
 	string GetHarvestSpellName();
 	void HandleUse(Client* client, string type);
+	
+	void SetRandomizeHeading(bool val) { randomize_heading = val; }
+	bool GetRandomizeHeading() { return randomize_heading; }
 private:
 	int8	number_harvests;
 	int8	num_attempts_per_harvest;
@@ -76,6 +80,7 @@ private:
 	string	collection_skill;
 	Mutex	MHarvest;
 	Mutex	MHarvestUse;
+	bool 	randomize_heading;
 };
 #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
 	MSpellsBook.lock();
 
+	std::vector<SpellBookEntry*> sort_spells(spells);
+	
 	if (!maxlvl_only)
 	{
 		switch (sort_by)
 		{
 		case 0:
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByName);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByName);
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByNameReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByNameReverse);
 			break;
 		case 1:
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByLevel);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevel);
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByLevelReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByLevelReverse);
 			break;
 		case 2:
 			if (!order)
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByCategory);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategory);
 			else
-				stable_sort(spells.begin(), spells.end(), SortSpellEntryByCategoryReverse);
+				stable_sort(sort_spells.begin(), sort_spells.end(), SortSpellEntryByCategoryReverse);
 			break;
 		}
 	}
 
 	vector<SpellBookEntry*>::iterator itr;
 	SpellBookEntry* spell = 0;
-	int i = 0;
 	map<string, SpellBookEntry*> tmpSpells;
 	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;
 
-		if (spell->type != book_type || spell->slot == -1)
+		if (spell->type != book_type)
 			continue;
 
 		if (maxlvl_only)
@@ -2627,9 +2633,12 @@ void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 ma
 			resultSpells.push_back(spell);
 		}
 		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
-
+	
 	if (maxlvl_only)
 	{
 		switch (sort_by)
@@ -2655,13 +2664,15 @@ void Player::ResortSpellBook(int32 sort_by, int32 order, int32 pattern, int32 ma
 		}
 
 		i = 0;
+		page_book_count = 0;
+		last_start_point = 0;
 		vector<SpellBookEntry*>::iterator tmpItr;
 		for (tmpItr = resultSpells.begin(); tmpItr != resultSpells.end(); tmpItr++) {
 			((SpellBookEntry*)*tmpItr)->slot = i;
-			i++;
+			GetSpellBookSlotSort(pattern, &i, &page_book_count, &last_start_point);
 		}
 	}
-
+	
 	MSpellsBook.unlock();
 }
 
@@ -3162,8 +3173,8 @@ EQ2Packet* Player::GetSpellBookUpdatePacket(int16 version) {
 				}
 				spell_xor_packet = new uchar[count * total_bytes];
 				memset(spell_xor_packet, 0, count * total_bytes);
-				spell_count = count;
 			}
+			spell_count = count;
 			MSpellsBook.lock();
 			for (int32 i = 0; i < spells.size(); i++) {
 				spell_entry = (SpellBookEntry*)spells[i];
@@ -4936,7 +4947,10 @@ void Player::RemoveQuest(int32 id, bool delete_quest){
 	if(delete_quest){
 		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__);
 	SendQuestRequiredSpawns(id);
 }
@@ -5021,7 +5035,9 @@ int16 Player::GetTaskGroupStep(int32 quest_id){
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	if(player_quests.count(quest_id) > 0){
 		quest = player_quests[quest_id];
-		step = quest->GetTaskGroupStep();
+		if(quest) {
+			step = quest->GetTaskGroupStep();
+		}
 	}
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	return step;
@@ -5045,7 +5061,9 @@ int16 Player::GetQuestStep(int32 quest_id){
 	MPlayerQuests.readlock(__FUNCTION__, __LINE__);
 	if(player_quests.count(quest_id) > 0){
 		quest = player_quests[quest_id];
-		step = quest->GetQuestStep();
+		if(quest) {
+			step = quest->GetQuestStep();
+		}
 	}
 	MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
 	return step;
@@ -7263,4 +7281,49 @@ int Player::GetPVPAlignment(){
 		return alignment;
 	}
 	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 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 SortSpellEntryByCategory(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/Mutex.h"
+#include "../classes.h"
+
 #include <string.h>
 #include <map>
 
+class Item;
 using namespace std;
 
 struct RecipeProducts {
@@ -85,6 +88,16 @@ public:
 	int32 GetTechnique() {return technique;}
 	int32 GetKnowledge() {return knowledge;}
 	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 GetUnknown3() {return unknown3;}
 	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;
 	else if (IsSign() && ((Sign*)this)->GetIncludeHeading() == false)
 		include_heading = false;
-	else if (IsGroundSpawn())
-		include_heading = false;
 
 	if (include_heading){
 		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()){
 				interrupt = itr->value;
 				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;
 	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"
 								 "INNER JOIN spawn_ground sg\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->SetRandomizeHeading(result.GetInt32(25));
+		
 		zone->AddGroundSpawn(id, spawn);
 
 		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();
 
+	GetPlayer()->GetZone()->SendSubSpawnUpdates(SUBSPAWN_TYPES::COLLECTOR);
 }
 
 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);
 	}
 }
+
+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	UpdateCharacterRewardData(QuestRewardData* data);
 	void	SetQuestUpdateState(bool val) { quest_updates = val; }
+	
+	void	AddRecipeToPlayer(Recipe* recipe, PacketStruct* packet, int16* i);
+	
 private:
 	void    SavePlayerImages();
 	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();
 	map<int16,int16>::iterator version_itr;
 	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++) {
 		version1 = version_itr->first;
 		EQOpcodeManager[version1] = new RegularOpcodeManager();
 		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!");
 			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...");
@@ -956,7 +982,7 @@ void NetConnection::WelcomeHeader()
 #ifdef _WIN32
 	SetConsoleTextAttribute(console, FOREGROUND_YELLOW_BOLD);
 #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("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");

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

@@ -178,6 +178,7 @@ ZoneServer::ZoneServer(const char* name, bool incoming_client) {
 ZoneServer::~ZoneServer() {
 	zoneShuttingDown = true;  //ensure other threads shut down too
 	//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){
 		if (spawnthread_active)
 			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");
 		Sleep(10);
 	}
-	LogWrite(ZONE__INFO, 0, "Zone", "Initiating zone shutdown of '%s'", zone_name);
 	changed_spawns.clear(true);
 	transport_spawns.clear();
 	safe_delete(tradeskillMgr);
@@ -1273,6 +1273,13 @@ void ZoneServer::DeleteSpawns(bool delete_all) {
 				if(sitr != spawn_list.end()) {
 					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__);
 			
 				safe_delete(spawn);
@@ -1625,8 +1632,10 @@ bool ZoneServer::SpawnProcess(){
 		if (pending_spawn_list_remove.size() > 0) {
 			MSpawnList.writelock(__FUNCTION__, __LINE__);
 			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);
+				subspawn_list[SUBSPAWN_TYPES::COLLECTOR].erase(*itr2);
+			}
 
 			pending_spawn_list_remove.clear();
 			MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
@@ -1645,6 +1654,10 @@ bool ZoneServer::SpawnProcess(){
 				Spawn* spawn = *itr2;
 				if (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();
@@ -3090,6 +3103,15 @@ GroundSpawn* ZoneServer::AddGroundSpawn(SpawnLocation* spawnlocation, SpawnEntry
 		spawn->SetSpawnEntryID(spawnentry->spawn_entry_id);
 		spawn->SetRespawnTime(spawnentry->respawn);
 		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)
 			AddSpawnExpireTimer(spawn, spawnentry->expire_time, spawnentry->expire_offset);
 
@@ -4482,8 +4504,12 @@ void ZoneServer::Despawn(Spawn* spawn, int32 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__);
 	if(!dead || this->dead_spawns.count(dead->GetID()) > 0) {
 		MDeadSpawns.releasereadlock(__FUNCTION__, __LINE__);
@@ -4524,7 +4550,7 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 		if (dead->Alive())
 			return;
 
-		RemoveSpellTimersFromSpawn(dead, true, !dead->IsPlayer());
+		RemoveSpellTimersFromSpawn(dead, true, !dead->IsPlayer(), true, !isSpell);
 
 		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
-	RemoveSpawnSupportFunctions(dead);
+	RemoveSpawnSupportFunctions(dead, !isSpell);
 
 	// Erase the expire timer if it has one
 	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.");
 		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, "Database ID | Spawn Name | X , Y , Z");
 	client->Message(CHANNEL_NARRATIVE, "========================");
 	map<int32, Spawn*>::iterator itr;
 	MSpawnList.readlock(__FUNCTION__, __LINE__);
 	int32 spawnsFound = 0;
-	std::regex re(resString, std::regex_constants::icase);
 	for (itr = spawn_list.begin(); itr != spawn_list.end(); itr++) {
 		Spawn* spawn = itr->second;
 		if (!spawn || !spawn->GetName())
@@ -8202,8 +8236,10 @@ void ZoneServer::ProcessSpawnRemovals()
 	MPendingSpawnRemoval.writelock(__FUNCTION__, __LINE__);
 	if (m_pendingSpawnRemove.size() > 0) {
 		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);
+			subspawn_list[SUBSPAWN_TYPES::COLLECTOR].erase(itr2->first);
+		}
 
 		m_pendingSpawnRemove.clear();
 	}
@@ -8325,3 +8361,14 @@ void ZoneServer::RemoveClientsFromZone(ZoneServer* zone) {
 	}
 	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;
 };
 
+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...
 class ZoneServer {
 public:
@@ -334,7 +339,7 @@ public:
 	
 	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    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);
 
 	Client*	RemoveZoneServerFromClient(ZoneServer* zone);
+	
+	void	SendSubSpawnUpdates(SUBSPAWN_TYPES subtype);
 private:
 #ifndef WIN32
 	pthread_t ZoneThread;
@@ -835,6 +842,9 @@ private:
 	/* Lists */
 	list<Spawn*>	pending_spawn_list_add;
 	
+	/* Specialized Lists to update specific scenarios */
+	std::map<int32, Spawn*>	subspawn_list[SUBSPAWN_TYPES::MAX_SUBSPAWN_TYPE];
+	
 	/* Vectors */
 	vector<RevivePoint*>*	revive_points;
 	vector<int32>			transport_spawns;

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

@@ -41,7 +41,7 @@ using namespace std;
 OpcodeManager::OpcodeManager() {
 	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
 	bool ret = true;
 	EmuOpcode emu_op;
@@ -59,7 +59,17 @@ bool OpcodeManager::LoadOpcodesMap(map<string, uint16>* eq, OpcodeSetStrategy *s
 		//find the opcode in the file
 		res = eq->find(op_name);
 		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);
 			continue;	//continue to give them a list of all missing opcodes
 		}
@@ -156,7 +166,7 @@ RegularOpcodeManager::~RegularOpcodeManager() {
 	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;
 	s.it = this;
 	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
 	memset(eq_to_emu, 0, sizeof(EmuOpcode)*MAX_EQ_OPCODE);
 	
-	bool ret = LoadOpcodesMap(eq, &s);
+	bool ret = LoadOpcodesMap(eq, &s, missingOpcodes);
 	MOpcodes.unlock();
 	return ret;
 }
@@ -263,7 +273,7 @@ NullOpcodeManager::NullOpcodeManager()
 : MutableOpcodeManager() {
 }
 
-bool NullOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
+bool NullOpcodeManager::LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes) {
 	return(true);
 }
 
@@ -292,7 +302,7 @@ bool EmptyOpcodeManager::LoadOpcodes(const char *filename) {
 	return(true);
 }
 
-bool EmptyOpcodeManager::LoadOpcodes(map<string, uint16>* eq) {
+bool EmptyOpcodeManager::LoadOpcodes(map<string, uint16>* eq, std::string* missingOpcodes) {
 	return(true);
 }
 

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

@@ -34,7 +34,7 @@ public:
 	
 	virtual bool Mutable() { return(false); }
 	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 uint16 EmuToEQ(const EmuOpcode emu_op) = 0;
@@ -57,7 +57,7 @@ protected:
 					//in a shared manager, this dosent protect others
 	
 	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 {
@@ -74,7 +74,7 @@ public:
 	virtual ~SharedOpcodeManager() {}
 	
 	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 uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -97,7 +97,7 @@ public:
 	
 	virtual bool Editable() { return(true); }
 	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 uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -126,7 +126,7 @@ public:
 	NullOpcodeManager();
 	
 	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 uint16 EmuToEQ(const EmuOpcode emu_op);
@@ -144,7 +144,7 @@ public:
 	EmptyOpcodeManager();
 	
 	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 uint16 EmuToEQ(const EmuOpcode emu_op);