Browse Source

- Manipulate the game world! Remove widgets!
LUA Functions and Examples:
* RemoveWidgetFromZoneMap(Zone, WidgetID) - must be used in preinit_zone_script

ZoneScripts/Commonlands.lua example:
function preinit_zone_script(zone)
-- removes KoS Commonlands spires from all spawns and playesr
-- visibly and in terms of player map (line of sight, and Y coordinate)
RemoveWidgetFromZoneMap(zone, 1101229151)
end

* ReplaceWidgetFromClient(Player, WidgetID, DeleteWidget, x, y, z, NewGridID) - must be called after client has sent sys_client_avatar_ready

ZoneScripts/Commonlands.lua example:
function signal_changed(Zone, Player, Reason)
if Reason == "sys_client_avatar_ready" then
-- Slightly moves the spires further down for the client visual
-- we do yet not support "moving" within the map space so this would not address line of sight or heightmap
ReplaceWidgetFromClient(Player, 1101229151, 0, 171.31, -48.04, -412.99, 783227007)
-- Alternatively ReplaceWidgetFromClient(Player, 1101229151, 1) could be called to remove the widget from the client visually
end
end

* RemoveWidgetFromSpawnMap(Spawn, WidgetID) -- removes the widget from the map (line of sight and heightmap checks). Can be called at any time (Spawn/Player).

- NPC Spells now supports self-HP ratios
alter table spawn_npc_spells add column required_hp_ratio tinyint signed not null default 0;
If greater than 0, hp ratio must be HIGHER or EQUAL to the value
If less than 0, hp ratio must be LOWER or EQUAL to the value
values -100 to 100 are accepted. Default value of 0 skips checks.

- Reduced some messages by timer that could spam on zone startup

Emagi 1 year ago
parent
commit
a74469795d

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

@@ -4701,7 +4701,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				details += "Class:		" + to_string(spawn->GetAdventureClass()) + "\n";
 				details += "Gender:		" + to_string(spawn->GetGender()) + "\n";
 				details += "Level:		" + to_string(spawn->GetLevel()) + "\n";
-				details += "HP:		" + to_string(spawn->GetHP()) + " / " + to_string(spawn->GetTotalHP()) + "\n";
+				details += "HP:		" + to_string(spawn->GetHP()) + " / " + to_string(spawn->GetTotalHP()) + "(" + to_string(spawn->GetIntHPRatio()) + "%)\n";
 				details += "Power:		" + to_string(spawn->GetPower()) + + " / " + to_string(spawn->GetTotalPower()) + "\n";
 				details += "Difficulty:		" + to_string(spawn->GetEncounterLevel()) + "\n";
 				details += "Heroic:		" + to_string(spawn->GetHeroic()) + "\n";
@@ -6098,7 +6098,7 @@ void Commands::Command_Grid(Client* client, Seperator* sep)
 			auto loc = glm::vec3(client->GetPlayer()->GetX(), client->GetPlayer()->GetZ(), client->GetPlayer()->GetY());
 			uint32 GridID = 0;
 			uint32 WidgetID = 0;
-			float new_z = client->GetPlayer()->GetMap()->FindBestZ(loc, nullptr, &GridID, &WidgetID);
+			float new_z = client->GetPlayer()->FindBestZ(loc, nullptr, &GridID, &WidgetID);
 			float minY = client->GetPlayer()->GetMap()->GetMinY();
 			float maxY = client->GetPlayer()->GetMap()->GetMaxY();
 			float minZ = client->GetPlayer()->GetMap()->GetMinZ();

+ 8 - 3
EQ2/source/WorldServer/Items/Loot.cpp

@@ -113,9 +113,14 @@ NPC* Entity::DropChest() {
 		chest->SetZ(GetZ());
 		((Entity*)chest)->GetInfoStruct()->set_flying_type(false);
 		chest->is_flying_creature = false;
-		auto loc = glm::vec3(GetX(), GetZ(), GetY());
-		float new_z = GetMap()->FindBestZ(loc, nullptr);
-		chest->appearance.pos.Y = new_z; // don't use SetY here can cause a loop
+		if(GetMap()) {
+			auto loc = glm::vec3(GetX(), GetZ(), GetY());
+			float new_z = FindBestZ(loc, nullptr);
+			chest->appearance.pos.Y = new_z; // don't use SetY here can cause a loop
+		}
+		else {
+			chest->appearance.pos.Y = GetY();
+		}
 	}
 
 	return chest;

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

@@ -13638,3 +13638,98 @@ int EQ2Emu_lua_RemoveRecipeFromPlayer(lua_State* state) {
 	return 1;
 }
 
+int EQ2Emu_lua_ReplaceWidgetFromClient(lua_State* state) {
+	Client* client = nullptr;
+	Spawn* player = lua_interface->GetSpawn(state);
+	int32 widget_id = lua_interface->GetInt32Value(state, 2);
+	bool delete_widget = (lua_interface->GetInt8Value(state, 3) == 1);
+	
+	// rest are all optional fields
+	float x = lua_interface->GetFloatValue(state, 4);
+	float y = lua_interface->GetFloatValue(state, 5);
+	float z = lua_interface->GetFloatValue(state, 6);
+	int32 grid_id = lua_interface->GetInt32Value(state, 7);
+	
+	lua_interface->ResetFunctionStack(state);
+	if (!player) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command error: spawn is not valid");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	if (!player->IsPlayer()) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command error: spawn is not a player");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	if(!player->GetZone()) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command error: player is not in a zone");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	client = player->GetClient();
+
+	if (!client) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command error: could not find client");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	if(!client->IsReadyForUpdates()) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command failed: client has not signaled sys_client_avatar_ready");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	client->SendReplaceWidget(widget_id, delete_widget, x, y, z, grid_id);
+	
+	lua_interface->SetBooleanValue(state, true);
+	return 1;
+}
+
+int EQ2Emu_lua_RemoveWidgetFromSpawnMap(lua_State* state) {
+	Client* client = nullptr;
+	Spawn* spawn = lua_interface->GetSpawn(state);
+	int32 widget_id = lua_interface->GetInt32Value(state, 2);
+	
+	lua_interface->ResetFunctionStack(state);
+	if (!spawn) {
+		lua_interface->LogError("LUA RemoveWidgetFromSpawnMap command error: spawn is not valid");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+
+	if(!spawn->GetZone()) {
+		lua_interface->LogError("LUA ReplaceWidgetFromClient command error: player is not in a zone");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	spawn->AddIgnoredWidget(widget_id);
+	
+	lua_interface->SetBooleanValue(state, true);
+	return 1;
+}
+
+int EQ2Emu_lua_RemoveWidgetFromZoneMap(lua_State* state) {
+	ZoneServer* zone = lua_interface->GetZone(state);
+	int32 widget_id = lua_interface->GetInt32Value(state, 2);
+	lua_interface->ResetFunctionStack(state);
+	
+	if(!zone) {
+		lua_interface->LogError("LUA RemoveWidgetFromZoneMap command error: zone is not valid");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	if(!zone->IsLoading()) {
+		lua_interface->LogError("LUA RemoveWidgetFromZoneMap command error: can only be called during zone loading, in preinit_zone_script ZoneScript function");
+		lua_interface->SetBooleanValue(state, false);
+		return 1;
+	}
+	
+	zone->AddIgnoredWidget(widget_id);
+	lua_interface->SetBooleanValue(state, true);
+	return 1;
+}

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

@@ -637,4 +637,8 @@ int EQ2Emu_lua_IsCastOnAggroComplete(lua_State* state);
 
 int EQ2Emu_lua_AddRecipeBookToPlayer(lua_State* state);
 int EQ2Emu_lua_RemoveRecipeFromPlayer(lua_State* state);
+
+int EQ2Emu_lua_ReplaceWidgetFromClient(lua_State* state);
+int EQ2Emu_lua_RemoveWidgetFromSpawnMap(lua_State* state);
+int EQ2Emu_lua_RemoveWidgetFromZoneMap(lua_State* state);
 #endif

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

@@ -1507,6 +1507,10 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	
 	lua_register(state, "AddRecipeBookToPlayer", EQ2Emu_lua_AddRecipeBookToPlayer);
 	lua_register(state, "RemoveRecipeFromPlayer", EQ2Emu_lua_RemoveRecipeFromPlayer);
+	
+	lua_register(state, "ReplaceWidgetFromClient", EQ2Emu_lua_ReplaceWidgetFromClient);
+	lua_register(state, "RemoveWidgetFromSpawnMap", EQ2Emu_lua_RemoveWidgetFromSpawnMap);
+	lua_register(state, "RemoveWidgetFromZoneMap", EQ2Emu_lua_RemoveWidgetFromZoneMap);
 }
 
 void LuaInterface::LogError(const char* error, ...)  {

+ 12 - 0
EQ2/source/WorldServer/NPC.cpp

@@ -883,6 +883,12 @@ Spell* NPC::GetNextSpell(float distance, int8 type){
 		Spell* tmpSpell = 0;
 		vector<NPCSpell*>::iterator itr;
 		for(itr = spells->begin(); itr != spells->end(); itr++){
+			// if positive, then say the hp ratio must be GREATER than OR EQUAL TO
+			if((*itr)->required_hp_ratio > 0 && (*itr)->required_hp_ratio < 101 && GetIntHPRatio() >= (*itr)->required_hp_ratio)
+				continue;
+			// if negative, then say the hp ratio must be LESS than OR EQUAL TO
+			if((*itr)->required_hp_ratio < 0 && (*itr)->required_hp_ratio > -101 && (-(*itr)->required_hp_ratio) >= GetIntHPRatio())
+				continue;
 			tmpSpell = master_spell_list.GetSpell((*itr)->spell_id, (*itr)->tier);
 			if(!tmpSpell || (type == AI_STRATEGY_OFFENSIVE && tmpSpell->GetSpellData()->friendly_spell > 0))
 				continue;
@@ -934,6 +940,12 @@ Spell* NPC::GetNextBuffSpell(Spawn* target) {
 		}
 		
 		for (itr = spells->begin(); itr != spells->end(); itr++) {
+			// if positive, then say the hp ratio must be GREATER than OR EQUAL TO
+			if((*itr)->required_hp_ratio > 0 && (*itr)->required_hp_ratio < 101 && GetIntHPRatio() >= (*itr)->required_hp_ratio)
+				continue;
+			// if negative, then say the hp ratio must be LESS than OR EQUAL TO
+			if((*itr)->required_hp_ratio < 0 && (*itr)->required_hp_ratio > -101 && (-(*itr)->required_hp_ratio) >= GetIntHPRatio())
+				continue;
 			tmpSpell = master_spell_list.GetSpell((*itr)->spell_id, (*itr)->tier);
 			if (tmpSpell && tmpSpell->GetSpellData() && tmpSpell->GetSpellData()->cast_type == SPELL_CAST_TYPE_TOGGLE) {
 				SpellEffects* effect = ((Entity*)target)->GetSpellEffect(tmpSpell->GetSpellID());

+ 2 - 0
EQ2/source/WorldServer/NPC.h

@@ -88,6 +88,7 @@ public:
 			tier = inherit->tier;
 			cast_on_spawn = inherit->cast_on_spawn;
 			cast_on_initial_aggro = inherit->cast_on_initial_aggro;
+			required_hp_ratio = inherit->required_hp_ratio;
 	}
 	
 	int32 	list_id;
@@ -95,6 +96,7 @@ public:
 	int8 	tier;
 	bool	cast_on_spawn;
 	bool	cast_on_initial_aggro;
+	sint8	required_hp_ratio;
 };
 
 class NPC : public Entity {

+ 28 - 7
EQ2/source/WorldServer/Spawn.cpp

@@ -2080,7 +2080,7 @@ void Spawn::InitializePosPacketData(Player* player, PacketStruct* packet, bool b
 			}
 			else {
 				auto loc = glm::vec3(GetX(), GetZ(), GetY());
-				float new_z = player->GetMap()->FindBestZ(loc, nullptr, &new_grid_id, &new_widget_id);
+				float new_z = player->FindBestZ(loc, nullptr, &new_grid_id, &new_widget_id);
 				TimedGridData data;
 				data.grid_id = new_grid_id;
 				data.widget_id = new_widget_id;
@@ -3387,7 +3387,7 @@ bool Spawn::CalculateChange(){
 				}
 				else {
 					auto loc = glm::vec3(GetX(), GetZ(), GetY());
-					float new_z = GetMap()->FindBestZ(loc, nullptr, &newGrid);
+					float new_z = FindBestZ(loc, nullptr, &newGrid);
 					TimedGridData data;
 					data.grid_id = newGrid;
 					data.x = GetX();
@@ -3976,11 +3976,21 @@ float Spawn::FindDestGroundZ(glm::vec3 dest, float z_offset)
 	if (GetZone() != nullptr && GetMap() != nullptr)
 	{
 		dest.z += z_offset;
-		best_z = GetMap()->FindBestZ(dest, nullptr);
+		best_z = FindBestZ(dest, nullptr);
 	}
 	return best_z;
 }
 
+float Spawn::FindBestZ(glm::vec3 loc, glm::vec3* result, int32* new_grid_id, int32* new_widget_id) {
+	std::shared_lock lock(MIgnoredWidgets);
+	
+	if(!GetMap())
+		return BEST_Z_INVALID;
+	
+	float new_z = GetMap()->FindBestZ(loc, nullptr, &ignored_widgets, new_grid_id, new_widget_id);
+	return new_z;
+}
+
 float Spawn::GetFixedZ(const glm::vec3& destination, int32 z_find_offset) {
 	BenchTimer timer;
 	timer.reset();
@@ -4039,7 +4049,7 @@ void Spawn::FixZ(bool forceUpdate) {
 	uint32 WidgetID = 0;
 	float new_z = GetY();
 	if(GetMap() != nullptr) {
-		float new_z = GetMap()->FindBestZ(current_loc, nullptr, &GridID, &WidgetID);
+		float new_z = FindBestZ(current_loc, nullptr, &GridID, &WidgetID);
 
 		if ((IsTransportSpawn() || !IsFlyingCreature()) && GridID != 0 && GridID != GetLocation()) {
 			LogWrite(PLAYER__DEBUG, 0, "Player", "%s left grid %u and entered grid %u", appearance.name, GetLocation(), GridID);
@@ -4095,13 +4105,17 @@ bool Spawn::CheckLoS(Spawn* target)
 
 bool Spawn::CheckLoS(glm::vec3 myloc, glm::vec3 oloc)
 {
+	bool res = false;
 	ZoneServer* zone = GetZone();
 	if (zone == NULL || GetMap() == NULL || !GetMap()->IsMapLoaded())
 		return true;
-	else
-		return GetMap()->CheckLoS(myloc, oloc);
+	else {
+		MIgnoredWidgets.lock_shared();
+		res = GetMap()->CheckLoS(myloc, oloc, &ignored_widgets);
+		MIgnoredWidgets.unlock_shared();
+	}
 
-	return false;
+	return res;
 }
 
 void Spawn::CalculateNewFearpoint()
@@ -4631,3 +4645,10 @@ int8 Spawn::GetArrowColor(int8 spawn_level){
 		color = ARROW_COLOR_BLUE;
 	return color;
 }
+
+void Spawn::AddIgnoredWidget(int32 id) {
+	std::unique_lock lock(MIgnoredWidgets);
+	if(ignored_widgets.find(id) == ignored_widgets.end()) {
+		ignored_widgets.insert(make_pair(id,true));
+	}
+}

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

@@ -42,6 +42,7 @@
 #include <deque>
 #include <memory> // needed for LS to compile properly on linux
 #include <mutex>
+#include <algorithm>
 
 #define DAMAGE_PACKET_TYPE_SIPHON_SPELL		0x41
 #define DAMAGE_PACKET_TYPE_SIPHON_SPELL2	0x49
@@ -676,7 +677,9 @@ public:
 	sint32 GetHP();
 	sint32 GetTotalHPBase();
 	sint32 GetTotalPowerBase();
-
+	float GetHPRatio() { return GetHP() == 0 || GetTotalHP() == 0 ? 0 : ((float) GetHP() / GetTotalHP() * 100); }
+	int GetIntHPRatio() { return GetTotalHP() == 0 ? 0 : static_cast<int>(GetHPRatio()); }
+	
 	sint32 GetTotalSavagery();
 	sint32 GetSavagery();
 	sint32 GetTotalDissonance();
@@ -1170,6 +1173,7 @@ public:
 	void SetSpawnAnimLeeway(int16 value) { m_spawnAnimLeeway = value; }
 
 	float FindDestGroundZ(glm::vec3 dest, float z_offset);
+	float FindBestZ(glm::vec3 loc, glm::vec3* result=nullptr, int32* new_grid_id=nullptr, int32* new_widget_id=nullptr);
 	float GetFixedZ(const glm::vec3& destination, int32 z_find_offset = 1);
 	void FixZ(bool forceUpdate=false);
 	bool CheckLoS(Spawn* target);
@@ -1301,8 +1305,14 @@ public:
 	
 	int8 GetArrowColor(int8 spawn_level);
 	
+	void AddIgnoredWidget(int32 id);
+	
+	mutable std::shared_mutex MIgnoredWidgets;
+	std::map<int32, bool> ignored_widgets;
+	
 	EquipmentItemList equipment_list;
 	EquipmentItemList appearance_equipment_list;
+	
 protected:
 
 	bool	has_quests_required;

+ 2 - 1
EQ2/source/WorldServer/World.cpp

@@ -2794,7 +2794,7 @@ void World::CopyVoiceOver(VoiceOverStruct* struct1, VoiceOverStruct* struct2) {
 	struct1->garble_link_id = struct2->garble_link_id;
 }
 
-void World::AddNPCSpell(int32 list_id, int32 spell_id, int8 tier, bool spawn_cast, bool aggro_cast){
+void World::AddNPCSpell(int32 list_id, int32 spell_id, int8 tier, bool spawn_cast, bool aggro_cast, sint8 req_hp_ratio){
     std::unique_lock lock(MNPCSpells);
 	NPCSpell* npc_spell_struct = new NPCSpell;
 	npc_spell_struct->list_id = list_id;
@@ -2802,6 +2802,7 @@ void World::AddNPCSpell(int32 list_id, int32 spell_id, int8 tier, bool spawn_cas
 	npc_spell_struct->tier = tier;
 	npc_spell_struct->cast_on_spawn = spawn_cast;
 	npc_spell_struct->cast_on_initial_aggro = aggro_cast;
+	npc_spell_struct->required_hp_ratio = req_hp_ratio;
 	if(npc_spell_list.count(list_id) && npc_spell_list[list_id].count(spell_id)) {
 		map<int32, NPCSpell*>::iterator spell_itr = npc_spell_list[list_id].find(spell_id);
 		if(spell_itr != npc_spell_list[list_id].end()) {

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

@@ -632,7 +632,7 @@ public:
 	void CopyVoiceOver(VoiceOverStruct* struct1, VoiceOverStruct* struct2);
 	
 	/* NPC Spells */
-	void AddNPCSpell(int32 list_id, int32 spell_id, int8 tier, bool spawn_cast, bool aggro_cast);
+	void AddNPCSpell(int32 list_id, int32 spell_id, int8 tier, bool spawn_cast, bool aggro_cast, sint8 req_hp_ratio);
 	vector<NPCSpell*>* GetNPCSpells(int32 primary_list, int32 secondary_list);
 	
 	void PurgeNPCSpells();

+ 10 - 5
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -509,12 +509,17 @@ int32 WorldDatabase::LoadNPCSpells(){
 	Query query;
 	MYSQL_ROW row;
 	int32 count = 0;
-	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT spell_list_id, spell_id, spell_tier, on_spawn_cast, on_aggro_cast FROM spawn_npc_spells where spell_list_id > 0");
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT spell_list_id, spell_id, spell_tier, on_spawn_cast, on_aggro_cast, required_hp_ratio FROM spawn_npc_spells where spell_list_id > 0");
 	while(result && (row = mysql_fetch_row(result))){
-		world.AddNPCSpell(atoul(row[0]), atoul(row[1]), atoi(row[2]), atoul(row[3]), atoul(row[4]));
-		count++;
+		if(!row[0] || !row[1] || !row[2] || !row[3] || !row[4] || !row[5]) {
+			LogWrite(NPC__ERROR, 0, "NPC", "---Loading NPC Spell List: %u, found NULL values in entry, SKIP!", row[0] ? atoul(row[0]) : 0);
+		}
+		else {
+			world.AddNPCSpell(atoul(row[0]), atoul(row[1]), atoi(row[2]), atoul(row[3]), atoul(row[4]), atoi(row[5]));
+			count++;
 
-		LogWrite(NPC__DEBUG, 5, "NPC", "---Loading NPC Spell List: %u, spell id: %u, tier: %i", atoul(row[0]), atoul(row[1]), atoi(row[2]));
+			LogWrite(NPC__DEBUG, 5, "NPC", "---Loading NPC Spell List: %u, spell id: %u, tier: %i", atoul(row[0]), atoul(row[1]), atoi(row[2]));
+		}
 
 	}
 	return count;
@@ -7387,7 +7392,7 @@ void WorldDatabase::GetHouseSpawnInstanceData(ZoneServer* zone, Spawn* spawn)
 		{
 			auto loc = glm::vec3(spawn->GetX(),spawn->GetZ(),spawn->GetY());
 			uint32 GridID = 0;
-			float new_z = spawn->GetMap()->FindBestZ(loc, nullptr, &GridID);
+			float new_z = spawn->FindBestZ(loc, nullptr, &GridID);
 			spawn->SetPos(&(spawn->appearance.pos.grid_id), GridID);
 		}
 	}

+ 15 - 15
EQ2/source/WorldServer/Zone/map.cpp

@@ -78,7 +78,7 @@ Map::~Map() {
 	}
 }
 
-float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result, uint32* GridID, uint32* WidgetID)
+float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result, std::map<int32, bool>* ignored_widgets, uint32* GridID, uint32* WidgetID)
 {
 	if (!IsMapLoaded())
 		return BEST_Z_INVALID;
@@ -95,7 +95,7 @@ float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result, uint32* GridID, uint32
 	float hit_distance;
 	bool hit = false;
 
-	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID);
+	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID, (RmMap*)ignored_widgets);
 	if(hit) {
 		return result->z;
 	}
@@ -103,7 +103,7 @@ float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result, uint32* GridID, uint32
 	// Find nearest Z above us
 	
 	to.z = -BEST_Z_INVALID;
-	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID);
+	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID, (RmMap*)ignored_widgets);
 	if (hit)
 	{
 		return result->z;
@@ -112,7 +112,7 @@ float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result, uint32* GridID, uint32
 	return BEST_Z_INVALID;
 }
 
-float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID, uint32* WidgetID) {
+float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result, std::map<int32, bool>* ignored_widgets, uint32 *GridID, uint32* WidgetID) {
 	if (!IsMapLoaded())
 		return false;
 	// Unlike FindBestZ, this method finds the closest Z value above or below the specified point.
@@ -131,7 +131,7 @@ float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID, uin
 	float hit_distance;
 	bool hit = false;
 	// first check is below us
-	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID);
+	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID, (RmMap*)ignored_widgets);
 	if (hit) {
 		ClosestZ = result->z;
 		
@@ -139,7 +139,7 @@ float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID, uin
 	
 	// Find nearest Z above us
 	to.z = -BEST_Z_INVALID;
-	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID);
+	hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance, (RmUint32*)GridID, (RmUint32*)WidgetID, (RmMap*)ignored_widgets);
 	if (hit) {
 		if (std::abs(from.z - result->z) < std::abs(ClosestZ - from.z))
 			return result->z;
@@ -148,15 +148,15 @@ float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID, uin
 	return ClosestZ;
 }
 
-bool Map::LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, glm::vec3 *result) {
+bool Map::LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, std::map<int32, bool>* ignored_widgets, glm::vec3 *result) {
 	if (!IsMapLoaded())
 		return false;
 	if(!imp)
 		return false;
-	return imp->rm->raycast((const RmReal*)&start, (const RmReal*)&end, (RmReal*)result, nullptr, nullptr, nullptr, nullptr);
+	return imp->rm->raycast((const RmReal*)&start, (const RmReal*)&end, (RmReal*)result, nullptr, nullptr, nullptr, nullptr, (RmMap*)ignored_widgets);
 }
 
-bool Map::LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_mag, glm::vec3 *result) {
+bool Map::LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_mag, std::map<int32, bool>* ignored_widgets, glm::vec3 *result) {
 	if (!IsMapLoaded())
 		return false;
 	if (!imp)
@@ -204,7 +204,7 @@ bool Map::LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_
 		me.z = cur.z;
 		glm::vec3 hit;
 
-		float best_z = FindBestZ(me, &hit);
+		float best_z = FindBestZ(me, &hit, ignored_widgets);
 		float diff = best_z - z;
 		diff = diff < 0 ? -diff : diff;
 
@@ -214,7 +214,7 @@ bool Map::LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_
 			return true;
 
 		//look at current location
-		if(LineIntersectsZone(start, end, step_mag, result))
+		if(LineIntersectsZone(start, end, step_mag, ignored_widgets, result))
 		{
 			return true;
 		}
@@ -243,24 +243,24 @@ bool Map::LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_
 	return false;
 }
 
-bool Map::CheckLoS(glm::vec3 myloc, glm::vec3 oloc)
+bool Map::CheckLoS(glm::vec3 myloc, glm::vec3 oloc, std::map<int32, bool>* ignored_widgets)
 {
 	if (!IsMapLoaded())
 		return false;
 	if(!imp)
 		return false;
 
-	return !imp->rm->raycast((const RmReal*)&myloc, (const RmReal*)&oloc, nullptr, nullptr, nullptr, nullptr, nullptr);
+	return !imp->rm->raycast((const RmReal*)&myloc, (const RmReal*)&oloc, nullptr, nullptr, nullptr, nullptr, nullptr, (RmMap*)ignored_widgets);
 }
 
 // returns true if a collision happens
-bool Map::DoCollisionCheck(glm::vec3 myloc, glm::vec3 oloc, glm::vec3 &outnorm, float &distance) {
+bool Map::DoCollisionCheck(glm::vec3 myloc, glm::vec3 oloc, std::map<int32, bool>* ignored_widgets, glm::vec3 &outnorm, float &distance) {
 	if (!IsMapLoaded())
 		return false;
 	if(!imp)
 		return false;
 
-	return imp->rm->raycast((const RmReal*)&myloc, (const RmReal*)&oloc, nullptr, (RmReal *)&outnorm, (RmReal *)&distance, nullptr, nullptr);
+	return imp->rm->raycast((const RmReal*)&myloc, (const RmReal*)&oloc, nullptr, (RmReal *)&outnorm, (RmReal *)&distance, nullptr, nullptr, (RmMap*)ignored_widgets);
 }
 
 Map *Map::LoadMapFile(std::string zonename, std::string file) {

+ 6 - 6
EQ2/source/WorldServer/Zone/map.h

@@ -36,12 +36,12 @@ public:
 	Map(string zonename, string filename);
 	~Map();
 
-	float FindBestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID = 0, uint32* WidgetID = 0);
-	float FindClosestZ(glm::vec3 &start, glm::vec3 *result, uint32 *GridID = 0, uint32* WidgetID = 0);
-	bool LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, glm::vec3 *result);
-	bool LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_mag, glm::vec3 *result);
-	bool CheckLoS(glm::vec3 myloc, glm::vec3 oloc);
-	bool DoCollisionCheck(glm::vec3 myloc, glm::vec3 oloc, glm::vec3 &outnorm, float &distance);
+	float FindBestZ(glm::vec3 &start, glm::vec3 *result, std::map<int32, bool>* ignored_widgets, uint32 *GridID = 0, uint32* WidgetID = 0);
+	float FindClosestZ(glm::vec3 &start, glm::vec3 *result, std::map<int32, bool>* ignored_widgets, uint32 *GridID = 0, uint32* WidgetID = 0);
+	bool LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, std::map<int32, bool>* ignored_widgets, glm::vec3 *result);
+	bool LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_mag, std::map<int32, bool>* ignored_widgets, glm::vec3 *result);
+	bool CheckLoS(glm::vec3 myloc, glm::vec3 oloc, std::map<int32, bool>* ignored_widgets);
+	bool DoCollisionCheck(glm::vec3 myloc, glm::vec3 oloc, std::map<int32, bool>* ignored_widgets, glm::vec3 &outnorm, float &distance);
 
 	bool Load(const std::string& filename);
 

+ 2 - 2
EQ2/source/WorldServer/Zone/mob_movement_manager.cpp

@@ -484,7 +484,7 @@ void AdjustRoute(std::list<IPathfinder::IPathNode> &nodes, Entity *who)
 
 	for (auto &node : nodes) {
 		//if (!zone->watermap->InLiquid(node.pos)) {
-			auto best_z = who->GetMap()->FindBestZ(node.pos, nullptr);
+			auto best_z = who->FindBestZ(node.pos, nullptr);
 			if (best_z != BEST_Z_INVALID) {
 				node.pos.z = best_z + offset;
 			}
@@ -1020,7 +1020,7 @@ void MobMovementManager::UpdatePathUnderwater(Entity *who, float x, float y, flo
 
 	auto &ent  = (*eiter);
 	if (/*zone->watermap->InLiquid(who->GetPosition()) && zone->watermap->InLiquid(glm::vec3(x, y, z)) &&*/
-		who->GetMap()->CheckLoS(glm::vec3(who->GetX(),who->GetZ(),who->GetY()), glm::vec3(x, y, z))) {
+		who->CheckLoS(glm::vec3(who->GetX(),who->GetZ(),who->GetY()), glm::vec3(x, z, y))) {
 		PushSwimTo(ent.second, x, y, z, movement_mode);
 		PushStopMoving(ent.second);
 		MobListMutex.releasereadlock();

+ 17 - 8
EQ2/source/WorldServer/Zone/raycast_mesh.cpp

@@ -40,8 +40,6 @@
 namespace RAYCAST_MESH
 {
 
-typedef std::vector< RmUint32 > TriVector;
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 /**
 *	A method to compute a ray-AABB intersection.
@@ -661,7 +659,8 @@ public:
 							RmUint32 *raycastTriangles,
 							RmUint32 raycastFrame,
 							const TriVector &leafTriangles,
-							RmUint32 &nearestTriIndex)
+							RmUint32 &nearestTriIndex,
+							RmMap* ignored_widgets)
 		{
 			RmReal sect[3];
 			RmReal nd = nearestDistance;
@@ -697,6 +696,10 @@ public:
 							}
 							if ( t < nearestDistance || accept )
 							{
+								if(ignored_widgets && ignored_widgets->find(widgets[tri]) != ignored_widgets->end()) {
+									continue;
+								}
+								
 								nearestDistance = t;
 								if ( hitLocation )
 								{
@@ -718,6 +721,7 @@ public:
 								if(WidgetID) {
 									*WidgetID = widgets[tri];
 								}
+								
 								nearestTriIndex = tri;
 								hit = true;
 							}
@@ -729,11 +733,11 @@ public:
 			{
 				if ( mLeft )
 				{
-					mLeft->raycast(hit,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,vertices,indices,grids,widgets,nearestDistance,callback,raycastTriangles,raycastFrame,leafTriangles,nearestTriIndex);
+					mLeft->raycast(hit,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,vertices,indices,grids,widgets,nearestDistance,callback,raycastTriangles,raycastFrame,leafTriangles,nearestTriIndex,ignored_widgets);
 				}
 				if ( mRight )
 				{
-					mRight->raycast(hit,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,vertices,indices,grids,widgets,nearestDistance,callback,raycastTriangles,raycastFrame,leafTriangles,nearestTriIndex);
+					mRight->raycast(hit,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,vertices,indices,grids,widgets,nearestDistance,callback,raycastTriangles,raycastFrame,leafTriangles,nearestTriIndex,ignored_widgets);
 				}
 			}
 		}
@@ -795,7 +799,7 @@ public:
 		::free(mWidgets);
 	}
 
-	virtual bool raycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance,RmUint32 *GridID,RmUint32 *WidgetID)
+	virtual bool raycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance,RmUint32 *GridID,RmUint32 *WidgetID, RmMap* ignored_widgets)
 	{
 		bool ret = false;
 
@@ -811,7 +815,7 @@ public:
 		dir[2]*=recipDistance;
 		mRaycastFrame++;
 		RmUint32 nearestTriIndex=TRI_EOF;
-		mRoot->raycast(ret,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,mVertices,mIndices,mGrids,mWidgets,distance,this,mRaycastTriangles,mRaycastFrame,mLeafTriangles,nearestTriIndex);
+		mRoot->raycast(ret,from,to,dir,hitLocation,hitNormal,hitDistance,GridID,WidgetID,mVertices,mIndices,mGrids,mWidgets,distance,this,mRaycastTriangles,mRaycastFrame,mLeafTriangles,nearestTriIndex,ignored_widgets);
 		return ret;
 	}
 
@@ -860,7 +864,7 @@ public:
 		faceNormal[2] = src[2];
 	}
 
-	virtual bool bruteForceRaycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance,RmUint32 *GridID,RmUint32 *WidgetID)
+	virtual bool bruteForceRaycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance,RmUint32 *GridID,RmUint32 *WidgetID, RmMap* ignored_widgets)
 	{
 		bool ret = false;
 
@@ -895,6 +899,10 @@ public:
 			{
 				if ( t < nearestDistance )
 				{
+					if(ignored_widgets && ignored_widgets->find(mWidgets[tri]) != ignored_widgets->end()) {
+						continue;
+					}
+					
 					nearestDistance = t;
 					if ( hitLocation )
 					{
@@ -919,6 +927,7 @@ public:
 					if(WidgetID) {
 						*WidgetID = mWidgets[tri];
 					}
+								
 					ret = true;
 				}
 			}

+ 7 - 2
EQ2/source/WorldServer/Zone/raycast_mesh.h

@@ -32,12 +32,17 @@
 
 typedef float RmReal;
 typedef unsigned int RmUint32;
+#include <map>
+#include <vector>
+using namespace std;
+typedef std::vector< RmUint32 > TriVector;
+typedef std::map< unsigned int , bool> RmMap;
 
 class RaycastMesh
 {
 public:
-	virtual bool raycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance, RmUint32 *GridID, RmUint32 *WidgetID) = 0;
-	virtual bool bruteForceRaycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance, RmUint32 *GridID, RmUint32 *WidgetID) = 0;
+	virtual bool raycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance, RmUint32 *GridID, RmUint32 *WidgetID, RmMap* ignored_widgets) = 0;
+	virtual bool bruteForceRaycast(const RmReal *from,const RmReal *to,RmReal *hitLocation,RmReal *hitNormal,RmReal *hitDistance, RmUint32 *GridID, RmUint32 *WidgetID, RmMap* ignored_widgets) = 0;
 
 	virtual const RmReal * getBoundMin(void) const = 0; // return the minimum bounding box
 	virtual const RmReal * getBoundMax(void) const = 0; // return the maximum bounding box.

+ 2 - 1
EQ2/source/WorldServer/Zone/region_map_v1.cpp

@@ -204,7 +204,8 @@ void RegionMapV1::IdentifyRegionsInGrid(Client *client, const glm::vec3 &locatio
 	if (client->GetPlayer()->GetMap() != nullptr && client->GetPlayer()->GetMap()->IsMapLoaded())
 	{
 		auto loc = glm::vec3(location.x, location.z, location.y);
-		float new_z = client->GetPlayer()->GetMap()->FindBestZ(loc, nullptr, &grid, &widget_id);
+		
+		float new_z = client->GetPlayer()->FindBestZ(loc, nullptr, &grid, &widget_id);
 		
 		std::map<int32, glm::vec3>::iterator itr = client->GetPlayer()->GetMap()->widget_map.find(widget_id);
 		if(itr != client->GetPlayer()->GetMap()->widget_map.end()) {

+ 38 - 2
EQ2/source/WorldServer/client.cpp

@@ -1766,6 +1766,7 @@ bool Client::HandlePacket(EQApplicationPacket* app) {
 					GetPlayer()->vis_changed = true;
 					player_pos_changed = true;
 					GetPlayer()->AddChangedZoneSpawn();
+					ProcessZoneIgnoreWidgets();
 				}
 				else {
 					LogWrite(CCLIENT__WARNING, 0, "Client", "Player %s reported SysClient/SignalMsg state %s.", GetPlayer()->GetName(), str.data.c_str());
@@ -3199,7 +3200,10 @@ bool Client::Process(bool zone_process) {
 			break;
 		}
 		case NewLoginState::LOGIN_DELAYED: {
-			LogWrite(CCLIENT__INFO, 0, "Client", "Wait for zone %s to load for new client %s...", GetCurrentZone()->GetZoneName(), GetPlayer()->GetName());
+			if(!delay_msg_timer.Enabled() || delay_msg_timer.Check()) {
+				LogWrite(CCLIENT__INFO, 0, "Client", "Wait for zone %s to load for new client %s...", GetCurrentZone()->GetZoneName(), GetPlayer()->GetName());
+				delay_msg_timer.Start(1000, true);
+			}
 			if(!GetCurrentZone()->IsLoading()) {
 				new_client_login = NewLoginState::LOGIN_ALLOWED;
 			}
@@ -3208,6 +3212,9 @@ bool Client::Process(bool zone_process) {
 			break;	
 		}
 	}
+	
+	delay_msg_timer.Disable();
+	
 	sockaddr_in to;
 
 	memset((char*)&to, 0, sizeof(to));
@@ -9340,7 +9347,7 @@ bool Client::ShowPathToTarget(float x, float y, float z, float y_offset) {
 			if (z < GetPlayer()->GetMap()->GetMinZ() || z > GetPlayer()->GetMap()->GetMaxZ())
 				return false;
 			auto loc = glm::vec3(x, z, y);
-			float new_z = GetPlayer()->GetMap()->FindBestZ(loc, nullptr);
+			float new_z = GetPlayer()->FindBestZ(loc, nullptr);
 			if (new_z != BEST_Z_INVALID) //this is actually y
 				y = new_z;
 		}
@@ -11959,4 +11966,33 @@ void Client::SaveSpells() {
 	player->SaveSpellEffects();
 	player->SetSaveSpellEffects(true);
 	MSaveSpellStateMutex.unlock();	
+}
+
+void Client::SendReplaceWidget(int32 widget_id, bool delete_widget, float x, float y, float z, int32 grid_id) {
+	Widget* new_spawn = new Widget();
+	new_spawn->SetWidgetID(widget_id);
+	new_spawn->SetLocation(grid_id);
+	new_spawn->SetWidgetX(x);
+	new_spawn->SetWidgetY(y);
+	new_spawn->SetWidgetZ(z);
+	new_spawn->SetX(x);
+	new_spawn->SetY(y);
+	new_spawn->SetZ(z);
+
+	EQ2Packet* ret = new_spawn->serialize(GetPlayer(), GetVersion());
+	QueuePacket(ret);
+	
+	// we have to delete spawn* references anyway, we don't keep this widget live in the spawn list
+	GetPlayer()->RemoveSpawn(new_spawn, delete_widget);
+	
+	safe_delete(new_spawn);
+}
+
+void Client::ProcessZoneIgnoreWidgets() {
+	GetPlayer()->MIgnoredWidgets.lock_shared();
+	std::map<int32, bool>::iterator itr;
+	for(itr = GetPlayer()->ignored_widgets.begin(); itr != GetPlayer()->ignored_widgets.end(); itr++) {
+		SendReplaceWidget(itr->first, true);
+	}
+	GetPlayer()->MIgnoredWidgets.unlock_shared();
 }

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

@@ -577,6 +577,9 @@ public:
 	void	SaveSpells();
 	
 	void	GiveQuestReward(Quest* quest, bool has_displayed = false);
+	
+	void	SendReplaceWidget(int32 widget_id, bool delete_widget, float x=0.0f, float y=0.0f, float z=0.0f, int32 grid_id=0);
+	void	ProcessZoneIgnoreWidgets();
 private:
 	void	AddRecipeToPlayerPack(Recipe* recipe, PacketStruct* packet, int16* i);
 	void    SavePlayerImages();
@@ -722,7 +725,7 @@ private:
 	vector< string > devices;
 	
 	std::atomic<int32> pov_ghost_spawn_id;
-	
+	Timer delay_msg_timer;
 };
 
 class ClientList {

+ 28 - 3
EQ2/source/WorldServer/zoneserver.cpp

@@ -180,10 +180,22 @@ 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);
+	int32 disp_count = 0;
+	int32 next_disp_count = 100;
 	while (spawnthread_active || initial_spawn_threads_active > 0){
-		if (spawnthread_active)
+		bool disp = false;
+		if ( disp_count == 0 ) {
+			disp = true;
+		}
+		else if(disp_count >= next_disp_count) {
+			disp_count = 0;
+			disp = true;
+		}
+		
+		disp_count++;
+		if (spawnthread_active && disp)
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on spawn thread");
-		if (initial_spawn_threads_active > 0)
+		if (initial_spawn_threads_active > 0 && disp)
 			LogWrite(ZONE__DEBUG, 7, "Zone", "Zone shutdown waiting on initial spawn thread");
 		Sleep(10);
 	}
@@ -3204,6 +3216,12 @@ void ZoneServer::AddSpawn(Spawn* spawn) {
 	if(!spawn->IsPlayer()) // we already set it on loadCharacter
 		spawn->SetZone(this);
 
+	MIgnoredWidgets.lock_shared();
+	std::map<int32, bool>::iterator itr;
+	for(itr = ignored_widgets.begin(); itr != ignored_widgets.end(); itr++) {
+		spawn->AddIgnoredWidget(itr->first);
+	}
+	MIgnoredWidgets.unlock_shared();
 	spawn->position_changed = false;
 	spawn->info_changed = false;
 	spawn->vis_changed = false;
@@ -8677,4 +8695,11 @@ void ZoneServer::SendClientSpawnListInGrid(Client* client, int32 grid_id){
 		}
 		grids->second->MSpawns.unlock_shared();
 	}
-}
+}
+
+void ZoneServer::AddIgnoredWidget(int32 id) {
+	std::unique_lock lock(MIgnoredWidgets);
+	if(ignored_widgets.find(id) == ignored_widgets.end()) {
+		ignored_widgets.insert(make_pair(id,true));
+	}
+}

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

@@ -715,6 +715,9 @@ public:
 	void	RemoveSpawnFromGrid(Spawn* spawn, int32 grid_id);
 	int32	GetSpawnCountInGrid(int32 grid_id);
 	void	SendClientSpawnListInGrid(Client* client, int32 grid_id);
+	
+	void AddIgnoredWidget(int32 id);	
+	
 private:
 #ifndef WIN32
 	pthread_t ZoneThread;
@@ -1011,6 +1014,9 @@ private:
 	std::map<int32, int32> lua_queued_state_commands;
 	std::map<int32, std::map<std::string, float>> lua_spawn_update_command;
 	std::mutex MLuaQueueStateCmd;
+	
+	mutable std::shared_mutex MIgnoredWidgets;
+	std::map<int32, bool> ignored_widgets;
 public:
 	Spawn*				GetSpawn(int32 id);