Browse Source

Albireo Stage First Update

Issue #305 - partially implemented cross zone spells.  Buffs will cross with you for self, group spells can re-attach if the caster enters the zone first.  Pet spells will recreate the pet, but name and other buffs to the pet do not persist cross zone.  WORK IN PROGRESS!!

Fix #310 - no sale option supported on merchants and saved to database, see additionalfields_mar6_2021.sql

Fix #309 - mastery skills all update now and relate to fizzle (ordination, ministration, etc)

Fix #308 - fizzle support added
	RULE_INIT(R_Spells, EnableFizzleSpells, "1"); // enables/disables the 'fizzling' of spells based on can_fizzle in the spells table.  This also enables increasing specialized skills for classes based on spells/abilities.
	RULE_INIT(R_Spells, DefaultFizzleChance, "10.0"); // default percentage x / 100, eg 10% is 10.0
	RULE_INIT(R_Spells, FizzleMaxSkill, "1.2"); // 1.0 is 100%, 1.2 is 120%, so you get 120% your max skill against a spell, no fizzle
	RULE_INIT(R_Spells, FizzleDefaultSkill, ".2"); // offset against MaxSkill to average out to 100%, default of .2f so we don't go over the threshold if no skill

Fix #303 - Brokers now use /frombroker instead of /itemsearch to show correct icon (money stack like merchants)

Fix #291 - implemented selling for status, no buy back, city merchant type (set merchant_type in spawn to 64) allows selling for status
Image 3 years ago
parent
commit
963d40521e

+ 33 - 6
EQ2/source/WorldServer/Combat.cpp

@@ -376,12 +376,10 @@ bool Entity::SpellAttack(Spawn* victim, float distance, LuaSpell* luaspell, int8
 	Skill* skill = nullptr;
 	if(spell->GetSpellData()->resistibility > 0)
 		bonus -= (1 - spell->GetSpellData()->resistibility)*100;
-	skill = master_skill_list.GetSkill(spell->GetSpellData()->mastery_skill);
-	if(skill){
-		skill = GetSkillByName(skill->name.data.c_str(), true);
-		if(skill)
-			bonus += skill->current_val / 25;
-	}
+
+	skill = GetSkillByID(spell->GetSpellData()->mastery_skill, false);
+	if(skill)
+		bonus += skill->current_val / 25;
 
 	int8 hit_result = 0;
 	bool is_tick = false; // if spell is already active, this is a tick
@@ -1103,6 +1101,35 @@ void Entity::AddHate(Entity* attacker, sint32 hate) {
 	}
 }
 
+bool Entity::CheckFizzleSpell(LuaSpell* spell) {
+	if(!spell || !rule_manager.GetGlobalRule(R_Spells, EnableFizzleSpells)->GetInt8()
+	|| spell->spell->GetSpellData()->can_fizzle == false)
+		return false;
+
+	float fizzleMaxSkill = rule_manager.GetGlobalRule(R_Spells, FizzleMaxSkill)->GetFloat();
+	float baseFizzle = rule_manager.GetGlobalRule(R_Spells, DefaultFizzleChance)->GetFloat()/100.0f; // 10%
+	float skillObtained = 0.2f; // default of .2f so we don't go over the threshold if no skill
+	Skill* skill = GetSkillByID(spell->spell->GetSpellData()->mastery_skill, false);
+	if(skill && spell->spell->GetSpellData()->min_class_skill_req > 0)
+	{
+		float skillObtained = skill->current_val / spell->spell->GetSpellData()->min_class_skill_req;
+		if(skillObtained > fizzleMaxSkill) // 120% over the skill value
+		{
+			skillObtained = fizzleMaxSkill;
+		}
+			
+		baseFizzle = (fizzleMaxSkill - skillObtained) * baseFizzle;
+
+		float totalSuccessChance = 1.0f - baseFizzle;
+
+		float randResult = MakeRandomFloat(0.0f, 1.0f);
+		if(randResult > totalSuccessChance)
+			return true;
+	}
+
+	return false;
+}
+
 bool Entity::CheckInterruptSpell(Entity* attacker) {
 	if(!IsCasting())
 		return false;

+ 18 - 1
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -4508,7 +4508,8 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				client->QueuePacket(app);
 			break;
 										}
-		case COMMAND_ITEMSEARCH:{
+		case COMMAND_ITEMSEARCH:
+		case COMMAND_FROMBROKER:{
 				PacketStruct* packet = configReader.getStruct("WS_StartBroker", client->GetVersion());
 				if (packet) {
 					packet->setDataByName("spawn_id", client->GetPlayer()->GetIDWithPlayerSpawn(client->GetPlayer()));
@@ -6191,6 +6192,22 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma
 				}
 			}
 		}
+		else if(sep->arg[2][0] && strncasecmp("nosale", sep->arg[0], 6) == 0 && sep->IsNumber(1) && sep->IsNumber(2))
+		{
+			sint64 data = strtoull(sep->arg[1], NULL, 0);
+			
+			int32 character_item_id = (int32) (data >> 32);
+			int32 item_id = (int32) (data & 0xffffffffL);
+
+			int8 sale_setting = atoi(sep->arg[2]);
+			Item* item = client->GetPlayer()->item_list.GetItemFromUniqueID(character_item_id);
+			if(item)
+			{
+				item->no_sale = sale_setting;
+				item->save_needed = true;
+				client->SendSellMerchantList();
+			}
+		}
 	}
 	else
 		client->SimpleMessage(CHANNEL_COLOR_YELLOW,"Usage: /inventory {destroy|move|equip|unequip|swap_equip|pop} {item_id} [to_slot] [bag_id]");

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

@@ -894,6 +894,8 @@ private:
 
 #define COMMAND_CRAFTITEM                526
 
+#define COMMAND_FROMBROKER           	527
+
 
 #define GET_AA_XML						750
 #define ADD_AA							751

+ 23 - 9
EQ2/source/WorldServer/Entity.cpp

@@ -105,7 +105,8 @@ Entity::~Entity(){
 	for (itr4 = immunities.begin(); itr4 != immunities.end(); itr4++)
 		safe_delete(itr4->second);
 	immunities.clear();
-	DeleteSpellEffects();
+	if(!IsPlayer())
+		DeleteSpellEffects();
 }
 
 void Entity::DeleteSpellEffects()
@@ -116,11 +117,16 @@ void Entity::DeleteSpellEffects()
 		if(i<30){
 			if(GetInfoStruct()->maintained_effects[i].spell_id != 0xFFFFFFFF)
 			{
-				lua_interface->RemoveSpell(GetInfoStruct()->maintained_effects[i].spell);
-				if (IsPlayer())
-					GetInfoStruct()->maintained_effects[i].icon = 0xFFFF;
+				if(deletedPtrs.find(GetInfoStruct()->spell_effects[i].spell) == deletedPtrs.end())
+				{
+					lua_interface->RemoveSpell(GetInfoStruct()->maintained_effects[i].spell, IsPlayer() ? false:  true);
+					if (IsPlayer())
+						GetInfoStruct()->maintained_effects[i].icon = 0xFFFF;
 
-				deletedPtrs[GetInfoStruct()->maintained_effects[i].spell] = true;
+					deletedPtrs[GetInfoStruct()->maintained_effects[i].spell] = true;
+				}
+				
+				GetInfoStruct()->maintained_effects[i].spell_id = 0xFFFFFFFF;
 				GetInfoStruct()->maintained_effects[i].spell = nullptr;
 			}
 		}
@@ -128,10 +134,11 @@ void Entity::DeleteSpellEffects()
 		{
 			if(deletedPtrs.find(GetInfoStruct()->spell_effects[i].spell) == deletedPtrs.end())
 			{
-				lua_interface->RemoveSpell(GetInfoStruct()->spell_effects[i].spell);
+				lua_interface->RemoveSpell(GetInfoStruct()->spell_effects[i].spell, IsPlayer() ? false:  true);
 				deletedPtrs[GetInfoStruct()->spell_effects[i].spell] = true;
-				GetInfoStruct()->spell_effects[i].spell = nullptr;
 			}
+			GetInfoStruct()->spell_effects[i].spell_id = 0xFFFFFFFF;
+			GetInfoStruct()->spell_effects[i].spell = nullptr;
 		}
 	}
 }
@@ -999,7 +1006,7 @@ SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer
 }
 
 LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWithOtherPlayers) {
-	if(!spell->spell->GetSpellData()->linked_timer)
+	if(!spell->spell->GetSpellData()->linked_timer && !spell->spell->GetSpellData()->type_group_spell_id)
 		return nullptr;
 	LuaSpell* ret = nullptr;
 	InfoStruct* info = GetInfoStruct();
@@ -1040,6 +1047,11 @@ Skill* Entity::GetSkillByName(const char* name, bool check_update){
 	return 0;
 }
 
+Skill* Entity::GetSkillByID(int32 id, bool check_update){
+	LogWrite(MISC__TODO, 1, "TODO", "This does nothing... yet...\n\t(%s, function: %s, line #: %i)", __FILE__, __FUNCTION__, __LINE__);
+	return 0;
+}
+
 float Entity::GetMaxSpeed(){
 	return max_speed;
 }
@@ -2165,7 +2177,7 @@ vector<DetrimentalEffects>* Entity::GetDetrimentalSpellEffects() {
 	return &detrimental_spell_effects;
 }
 
-void Entity::AddDetrimentalSpell(LuaSpell* luaspell){
+void Entity::AddDetrimentalSpell(LuaSpell* luaspell, int32 override_expire_timestamp){
 	if(!luaspell || !luaspell->caster)
 		return;
 	
@@ -2183,6 +2195,8 @@ void Entity::AddDetrimentalSpell(LuaSpell* luaspell){
 	new_det.spell = luaspell;
 	if (spell->GetSpellData()->duration_until_cancel)
 		new_det.expire_timestamp = 0xFFFFFFFF;
+	else if(override_expire_timestamp)
+		new_det.expire_timestamp = override_expire_timestamp;
 	else
 		new_det.expire_timestamp = Timer::GetCurrentTime2() + (spell->GetSpellDuration()*100);
 	new_det.icon = data->icon;

+ 6 - 4
EQ2/source/WorldServer/Entity.h

@@ -1096,7 +1096,7 @@ public:
 	virtual bool HasActiveMaintainedSpell(Spell* spell, Spawn* target);
 	virtual bool HasActiveSpellEffect(Spell* spell, Spawn* target);
 	virtual void AddSkillBonus(int32 spell_id, int32 skill_id, float value);
-	void AddDetrimentalSpell(LuaSpell* spell);
+	void AddDetrimentalSpell(LuaSpell* spell, int32 override_expire_timestamp = 0);
 	DetrimentalEffects* GetDetrimentalEffect(int32 spell_id, Entity* caster);
 	virtual MaintainedEffects* GetMaintainedSpell(int32 spell_id);
 	void RemoveDetrimentalSpell(LuaSpell* spell);
@@ -1208,6 +1208,7 @@ public:
 	void	ChangeSecondaryWeapon();
 	void	ChangeRangedWeapon();
 	virtual Skill*	GetSkillByName(const char* name, bool check_update = false);
+	virtual Skill*	GetSkillByID(int32 id, bool check_update = false);
 	bool			AttackAllowed(Entity* target, float distance = 0, bool range_attack = false);
 	bool			PrimaryWeaponReady();
 	bool			SecondaryWeaponReady();
@@ -1223,6 +1224,7 @@ public:
 	bool			DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_damage, int32 high_damage, const char* spell_name, int8 crit_mod = 0, bool is_tick = false, bool no_damage_calcs = false, bool ignore_attacker = false);
 	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			HandleDeathExperienceDebt(Spawn* killer);
 	void            SetAttackDelay(bool primary = false, bool ranged = false);
@@ -1668,6 +1670,9 @@ public:
 	// when PacketStruct is fixed for C++17 this should become a shared_mutex and handle read/write lock
 	std::mutex		MEquipment;
 	std::mutex		MStats;
+
+	Mutex   MMaintainedSpells;
+	Mutex   MSpellEffects;
 protected:
 	bool	in_combat;
 	int8	m_petType;
@@ -1676,7 +1681,6 @@ protected:
 	int32	m_petSpellID;
 	int8	m_petSpellTier;
 	bool	m_petDismissing;
-
 private:
 	MutexList<BonusValues*> bonus_list;
 	map<int8, MutexList<LuaSpell*>*> control_effects;
@@ -1695,8 +1699,6 @@ private:
 	CombatData ranged_combat_data;
 	map<int8, int8> det_count_list;
 	Mutex MDetriments;
-	Mutex   MMaintainedSpells;
-	Mutex   MSpellEffects;
 	vector<DetrimentalEffects> detrimental_spell_effects;
 	// Pointers for the 4 types of pets (Summon, Charm, Deity, Cosmetic)
 	Entity*	pet;

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

@@ -818,6 +818,8 @@ Item::Item(){
 	memset(&details, 0, sizeof(ItemCore));
 	memset(&generic_info, 0, sizeof(Generic_Info));
 	generic_info.condition = 100;
+	no_buy_back = false;
+	no_sale = false;
 }
 
 Item::Item(Item* in_item){
@@ -833,6 +835,8 @@ Item::Item(Item* in_item){
 	generic_info.condition = 100;
 	spell_id = in_item->spell_id;
 	spell_tier = in_item->spell_tier;
+	no_buy_back = in_item->no_buy_back;
+	no_sale = in_item->no_sale;
 }
 
 Item::~Item(){

+ 7 - 0
EQ2/source/WorldServer/Items/Items.h

@@ -594,6 +594,11 @@ extern MasterItemList master_item_list;
 #define ITEM_STAT_UNCONTESTED_DODGE     852
 #define ITEM_STAT_UNCONTESTED_RIPOSTE     853
 
+#define	DISPLAY_FLAG_RED_TEXT			1 // old clients
+#define	DISPLAY_FLAG_NO_GUILD_STATUS	8
+#define	DISPLAY_FLAG_NO_BUYBACK			16
+#define	DISPLAY_FLAG_NOT_FOR_SALE		64
+
 #pragma pack(1)
 struct ItemStatsValues{
 	sint16			str;
@@ -884,6 +889,8 @@ public:
 	int32					spell_id;
 	int8					spell_tier;
 	string					item_script;
+	bool					no_buy_back;
+	bool					no_sale;
 	void AddEffect(string effect, int8 percentage, int8 subbulletflag);
 	void AddBookPage(int8 page, string page_text,int8 valign, int8 halign);
 	int32 GetMaxSellValue();

+ 6 - 3
EQ2/source/WorldServer/Items/ItemsDB.cpp

@@ -283,6 +283,8 @@ void WorldDatabase::LoadDataFromRow(DatabaseResult* result, Item* item)
 
 	item->generic_info.harvest					= result->GetInt8Str("harvest");
 	item->generic_info.body_drop				= result->GetInt8Str("body_drop");
+
+	item->no_buy_back							= (result->GetInt8Str("no_buy_back") == 1);
 }
 
 int32 WorldDatabase::LoadSkillItems()
@@ -1105,9 +1107,9 @@ void WorldDatabase::SaveItem(int32 account_id, int32 char_id, Item* item, const
 	LogWrite(ITEM__DEBUG, 1, "Items", "Saving ItemID: %u (Type: %s) for account: %u, player: %u", item->details.item_id, type, account_id, char_id);
 
 	Query query;
-	string update_item = string("REPLACE INTO character_items (id, type, char_id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, account_id, login_checksum) VALUES (%u, '%s', %u, %i, %u, '%s', %i, %i, %i, %i, %i, %i, %i, %u, %u, 0)");
+	string update_item = string("REPLACE INTO character_items (id, type, char_id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, no_sale, account_id, login_checksum) VALUES (%u, '%s', %u, %i, %u, '%s', %i, %i, %i, %i, %i, %i, %i, %u, %u, %u, 0)");
 	query.AddQueryAsync(char_id, this, Q_REPLACE, update_item.c_str(), item->details.unique_id, type, char_id, item->details.slot_id, item->details.item_id,
-		getSafeEscapeString(item->creator.c_str()).c_str(),item->adorn0,item->adorn1,item->adorn2, item->generic_info.condition, item->CheckFlag(ATTUNED) ? 1 : 0, item->details.inv_slot_id, item->details.count, item->GetMaxSellValue(), account_id);
+		getSafeEscapeString(item->creator.c_str()).c_str(),item->adorn0,item->adorn1,item->adorn2, item->generic_info.condition, item->CheckFlag(ATTUNED) ? 1 : 0, item->details.inv_slot_id, item->details.count, item->GetMaxSellValue(), item->no_sale, account_id);
 }
 
 void WorldDatabase::DeleteItem(int32 char_id, Item* item, const char* type) 
@@ -1137,7 +1139,7 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 
 	Query query;
 	MYSQL_ROW row;
-	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT type, id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value FROM character_items where char_id = %u or (bag_id = -4 and account_id = %u) ORDER BY slot asc", char_id, account_id);
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT type, id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, no_sale FROM character_items where char_id = %u or (bag_id = -4 and account_id = %u) ORDER BY slot asc", char_id, account_id);
 
 	if(result)
 	{
@@ -1180,6 +1182,7 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 				item->details.inv_slot_id = atol(row[10]); //bag_id
 				item->details.count = atoi(row[11]); //count
 				item->SetMaxSellValue(atoul(row[12])); //max sell value
+				item->no_sale = (atoul(row[13]) == 1);
 
 				
 				if(strncasecmp(row[0], "EQUIPPED", 8)==0)

+ 23 - 6
EQ2/source/WorldServer/LuaInterface.cpp

@@ -276,6 +276,7 @@ bool LuaInterface::LoadLuaSpell(const char* name) {
 		spell->slot_pos = 0;
 		spell->damage_remaining = 0;
 		spell->effect_bitmask = 0;
+		spell->restored = false;
 
 		MSpells.lock();
 		if (spells.count(lua_script) > 0) {
@@ -530,15 +531,28 @@ bool LuaInterface::LoadRegionScript(string name) {
 	return LoadRegionScript(name.c_str());
 }
 
-void LuaInterface::AddSpawnPointers(LuaSpell* spell, bool first_cast, bool precast, const char* function, SpellScriptTimer* timer, bool passLuaSpell) {
+std::string LuaInterface::AddSpawnPointers(LuaSpell* spell, bool first_cast, bool precast, const char* function, SpellScriptTimer* timer, bool passLuaSpell) {
+	std::string functionCalled = string(""); 
 	if (function)
+	{
+		functionCalled = string(function);
 		lua_getglobal(spell->state, function);
+	}
 	else if (precast)
+	{
+		functionCalled = "precast";
 		lua_getglobal(spell->state, "precast");
+	}
 	else if(first_cast)
+	{
+		functionCalled = "cast";
 		lua_getglobal(spell->state, "cast");
+	}
 	else
+	{
+		functionCalled = "tick";
 		lua_getglobal(spell->state, "tick");
+	}
 
 	if(passLuaSpell)
 		SetSpellValue(spell->state, spell);
@@ -571,6 +585,8 @@ void LuaInterface::AddSpawnPointers(LuaSpell* spell, bool first_cast, bool preca
 		else
 			SetSpawnValue(spell->state, 0);
 	}
+
+	return functionCalled;
 }
 
 LuaSpell* LuaInterface::GetCurrentSpell(lua_State* state) {
@@ -579,7 +595,7 @@ LuaSpell* LuaInterface::GetCurrentSpell(lua_State* state) {
 	return 0;
 }
 
-bool LuaInterface::CallSpellProcess(LuaSpell* spell, int8 num_parameters) {
+bool LuaInterface::CallSpellProcess(LuaSpell* spell, int8 num_parameters, std::string customFunction) {
 	if(shutting_down || !spell || !spell->caster)
 		return false;
 
@@ -587,9 +603,9 @@ bool LuaInterface::CallSpellProcess(LuaSpell* spell, int8 num_parameters) {
 	current_spells[spell->state] = spell;
 	MSpells.unlock();
 	if(lua_pcall(spell->state, num_parameters, 0, 0) != 0){
-		LogError("Error running %s", lua_tostring(spell->state, -1));
+		LogError("Error running function '%s' in %s: %s", customFunction.c_str(), spell->spell->GetName(), lua_tostring(spell->state, -1));
 		lua_pop(spell->state, 1);
-		RemoveSpell(spell, false);
+		RemoveSpell(spell, false); // may be in a lock
 		return false;
 	}
 	return true;
@@ -694,7 +710,7 @@ lua_State* LuaInterface::LoadLuaFile(const char* name) {
 	return 0;
 }
 
-void LuaInterface::RemoveSpell(LuaSpell* spell, bool call_remove_function, bool can_delete, string reason) {
+void LuaInterface::RemoveSpell(LuaSpell* spell, bool call_remove_function, bool can_delete, string reason, bool removing_all_spells) {
 	if(shutting_down)
 		return;
 
@@ -752,7 +768,7 @@ void LuaInterface::RemoveSpell(LuaSpell* spell, bool call_remove_function, bool
 		spell->caster->RemoveMaintainedSpell(spell);
 
 		int8 spell_type = spell->spell->GetSpellData()->spell_type;
-		if(spell->caster->IsPlayer())
+		if(spell->caster->IsPlayer() && !removing_all_spells)
 		{
 			Player* player = (Player*)spell->caster;
 			switch(spell_type)
@@ -1766,6 +1782,7 @@ LuaSpell* LuaInterface::GetSpell(const char* name)  {
 		new_spell->caster = 0;
 		new_spell->initial_target = 0;
 		new_spell->spell = 0;
+		new_spell->restored = false;
 		return new_spell;
 	}
 	else{

+ 4 - 3
EQ2/source/WorldServer/LuaInterface.h

@@ -91,6 +91,7 @@ struct LuaSpell{
 	bool            had_dmg_remaining;
 	Mutex           MSpellTargets;
 	int32           effect_bitmask;
+	bool			restored; // restored spell cross zone
 
 };
 
@@ -190,7 +191,7 @@ public:
 	bool			LoadZoneScript(const char* name);
 	bool			LoadRegionScript(string name);
 	bool			LoadRegionScript(const char* name);
-	void			RemoveSpell(LuaSpell* spell, bool call_remove_function = true, bool can_delete = true, string reason = "");
+	void			RemoveSpell(LuaSpell* spell, bool call_remove_function = true, bool can_delete = true, string reason = "", bool removing_all_spells = false);
 	Spawn*			GetSpawn(lua_State* state, int8 arg_num = 1);
 	Item*			GetItem(lua_State* state, int8 arg_num = 1);
 	Quest*			GetQuest(lua_State* state, int8 arg_num = 1);
@@ -229,9 +230,9 @@ public:
 	void			SetConversationValue(lua_State* state, vector<ConversationOption>* conversation);
 	void			SetOptionWindowValue(lua_State* state, vector<OptionWindowOption>* optionWindow);
 
-	void			AddSpawnPointers(LuaSpell* spell, bool first_cast, bool precast = false, const char* function = 0, SpellScriptTimer* timer = 0, bool passLuaSpell=false);
+	std::string		AddSpawnPointers(LuaSpell* spell, bool first_cast, bool precast = false, const char* function = 0, SpellScriptTimer* timer = 0, bool passLuaSpell=false);
 	LuaSpell*		GetCurrentSpell(lua_State* state);
-	bool			CallSpellProcess(LuaSpell* spell, int8 num_parameters);
+	bool			CallSpellProcess(LuaSpell* spell, int8 num_parameters, std::string functionCalled);
 	LuaSpell*		GetSpell(const char* name);
 	void			UseItemScript(const char* name, lua_State* state, bool val);
 	void			UseSpawnScript(const char* name, lua_State* state, bool val);

+ 15 - 1
EQ2/source/WorldServer/NPC.cpp

@@ -27,6 +27,7 @@
 #include "NPC_AI.h"
 #include "Appearances.h"
 #include "SpellProcess.h"
+#include "Skills.h"
 
 extern MasterSpellList master_spell_list;
 extern ConfigReader configReader;
@@ -34,6 +35,7 @@ extern WorldDatabase database;
 extern World world;
 extern Races races;
 extern Appearance master_appearance_list;
+extern MasterSkillList master_skill_list;
 
 NPC::NPC(){	
 	Initialize();
@@ -668,7 +670,7 @@ void NPC::Randomize(NPC* npc, int32 flags)
 		color1.green = MakeRandomInt(min_val, max_val);
 		color1.blue = MakeRandomInt(min_val, max_val);
 		LogWrite(NPC__DEBUG, 5, "NPCs", "Randomizing Hair Type Highlight - R: %i, G: %i, B: %i", color1.red, color1.green, color1.blue);
-		npc->SetHairHighlightColor(color1);
+		npc->SetHairTypeHighlightColor(color1);
 	}
 	if (flags & RANDOMIZE_SKIN_COLOR) {
 		npc->features.skin_color.red = MakeRandomInt(min_val, max_val);
@@ -704,6 +706,18 @@ Skill* NPC::GetSkillByName(const char* name, bool check_update){
 	return 0;
 }
 
+Skill* NPC::GetSkillByID(int32 id, bool check_update){
+	Skill* skill = master_skill_list.GetSkill(id);
+
+	if(skill && skills && skills->count(skill->name.data) > 0){
+		Skill* ret = (*skills)[skill->name.data];
+		if(ret && check_update && ret->current_val < ret->max_val && (rand()%100) >= 90)
+			ret->current_val++;
+		return ret;
+	}
+	return 0;
+}
+
 void NPC::SetAttackType(int8 type){
 	attack_type = type;
 }

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

@@ -89,6 +89,7 @@ public:
 	bool	CheckSameAppearance(string name, int16 id);
 	void	Randomize(NPC* npc, int32 flags);
 	Skill*	GetSkillByName(const char* name, bool check_update = false);
+	Skill*	GetSkillByID(int32 id, bool check_update = false);
 	void	SetAttackType(int8 type);
 	int8	GetAttackType();
 	void	SetAIStrategy(int8 strategy);

+ 137 - 0
EQ2/source/WorldServer/Player.cpp

@@ -119,8 +119,11 @@ Player::Player(){
 	m_playerSpawnQuestsRequired.SetName("Player::player_spawn_quests_required");
 	m_playerSpawnHistoryRequired.SetName("Player::player_spawn_history_required");
 	gm_vision = false;
+	SetSaveSpellEffects(true);
 }
 Player::~Player(){
+	SetSaveSpellEffects(true);
+	DeleteSpellEffects();
 	for(int32 i=0;i<spells.size();i++){
 		safe_delete(spells[i]);
 	}
@@ -5241,6 +5244,16 @@ Skill* Player::GetSkillByName(const char* name, bool check_update){
 	return ret;
 }
 
+Skill* Player::GetSkillByID(int32 id, bool check_update){
+	Skill* ret = skill_list.GetSkill(id);
+	if(check_update)
+		{
+			if(skill_list.CheckSkillIncrease(ret))
+				CalculateBonuses();
+		}
+	return ret;
+}
+
 void Player::SetRangeAttack(bool val){
 	range_attack = val;
 }
@@ -6338,4 +6351,128 @@ NPC* Player::InstantiateSpiritShard(float origX, float origY, float origZ, float
 			npc->SetSpawnScript(script);
 		
 		return npc;
+}
+
+void Player::SaveSpellEffects()
+{
+	if(stop_save_spell_effects)
+	{
+		LogWrite(PLAYER__WARNING, 0, "Player", "SaveSpellEffects called while player constructing / deconstructing!");
+		return;
+	}
+
+	SpellProcess* spellProcess = 0;
+	// Get the current zones spell process
+	spellProcess = GetZone()->GetSpellProcess();
+
+	Query savedEffects;
+	savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_spell_effects where charid=%u", GetCharacterID());
+	savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_spell_effect_targets where caster_char_id=%u", GetCharacterID());
+	InfoStruct* info = GetInfoStruct();
+	MSpellEffects.readlock(__FUNCTION__, __LINE__);
+	MMaintainedSpells.readlock(__FUNCTION__, __LINE__);
+	for(int i = 0; i < 45; i++) {
+		if(info->spell_effects[i].spell_id != 0xFFFFFFFF)
+		{
+			Spawn* spawn = GetZone()->GetSpawnByID(info->spell_effects[i].spell->initial_target);
+
+			int32 target_char_id = 0;
+			if(spawn && spawn->IsPlayer())
+				target_char_id = ((Player*)spawn)->GetCharacterID();
+
+			int32 timestamp = 0xFFFFFFFF;
+			if(!info->spell_effects[i].spell->spell->GetSpellData()->duration_until_cancel)
+				timestamp = info->spell_effects[i].expire_timestamp - Timer::GetCurrentTime2();
+			
+			int32 caster_char_id = (info->spell_effects[i].caster && info->spell_effects[i].caster->IsPlayer()) ? ((Player*)info->spell_effects[i].caster)->GetCharacterID() : 0;
+
+			if(caster_char_id == 0)
+				continue;
+			
+			Query effectSave;
+			effectSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
+			"insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, custom_function) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s')", 
+			database.getSafeEscapeString(info->spell_effects[i].spell->spell->GetName()).c_str(), caster_char_id,
+			target_char_id,  0 /*no target_type for spell_effects*/, DB_TYPE_SPELLEFFECTS /* db_effect_type for spell_effects */, info->spell_effects[i].spell_id, i, info->spell_effects[i].spell->slot_pos, 
+			info->spell_effects[i].icon, info->spell_effects[i].icon_backdrop, 0 /* no conc_used for spell_effects */, info->spell_effects[i].tier, 
+			info->spell_effects[i].total_time, timestamp, database.getSafeEscapeString(info->spell_effects[i].spell->file_name.c_str()).c_str(), info->spell_effects[i].spell->spell->IsCopiedSpell(), GetCharacterID(), 
+			info->spell_effects[i].spell->damage_remaining, info->spell_effects[i].spell->effect_bitmask, info->spell_effects[i].spell->num_triggers, info->spell_effects[i].spell->had_triggers, info->spell_effects[i].spell->cancel_after_all_triggers,
+			info->spell_effects[i].spell->crit, info->spell_effects[i].spell->last_spellattack_hit, info->spell_effects[i].spell->interrupted, info->spell_effects[i].spell->resisted, (info->maintained_effects[i].expire_timestamp) == 0xFFFFFFFF ? "" : database.getSafeEscapeString(spellProcess->SpellScriptTimerCustomFunction(info->spell_effects[i].spell).c_str()).c_str());
+			
+		/*	info->spell_effects[i].spell->MSpellTargets.readlock(__FUNCTION__, __LINE__);
+			std::string insertTargets = string("insert into character_spell_effect_targets (caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos) values ");
+			bool firstTarget = true;
+			for (int8 t = 0; t < info->spell_effects[i].spell->targets.size(); t++) {
+					Spawn* spawn = GetZone()->GetSpawnByID(info->spell_effects[i].spell->targets.at(t));
+					if(spawn && spawn->IsPlayer())
+					{
+						if(!firstTarget)
+							insertTargets.append(", ");
+
+						insertTargets.append("(" + caster_char_id + ", " + target_char_id + ", " + "0" + ", " std::to_string(DB_TYPE_SPELLEFFECTS) + ", " + info->spell_effects[i].spell_id + ", " + i + ", " + info->spell_effects[i].spell->slot_pos + ")");
+						firstTarget = false;
+					}
+			}
+			info->spell_effects[i].spell->MSpellTargets.releasereadlock(__FUNCTION__, __LINE__);
+			if(!firstTarget)
+			{
+				Query targetSave;
+				targetSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, insertTarget.c_str());
+			}*/
+		}
+		if (i < NUM_MAINTAINED_EFFECTS && info->maintained_effects[i].spell_id != 0xFFFFFFFF){
+			Spawn* spawn = GetZone()->GetSpawnByID(info->maintained_effects[i].spell->initial_target);
+
+			int32 target_char_id = 0;
+			if(spawn && spawn->IsPlayer())
+				target_char_id = ((Player*)spawn)->GetCharacterID();
+
+			int32 caster_char_id = (info->maintained_effects[i].spell->caster && info->maintained_effects[i].spell->caster->IsPlayer()) ? ((Player*)info->maintained_effects[i].spell->caster)->GetCharacterID() : 0;
+			
+			int32 timestamp = 0xFFFFFFFF;
+			if(!info->maintained_effects[i].spell->spell->GetSpellData()->duration_until_cancel)
+				timestamp = info->maintained_effects[i].expire_timestamp - Timer::GetCurrentTime2();
+			Query effectSave;
+			effectSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, 
+			"insert into character_spell_effects (name, caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, charid, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, custom_function) values ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %f, %u, '%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s')", 
+			database.getSafeEscapeString(info->maintained_effects[i].name).c_str(), caster_char_id, target_char_id,  info->maintained_effects[i].target_type, DB_TYPE_MAINTAINEDEFFECTS /* db_effect_type for maintained_effects */, info->maintained_effects[i].spell_id, i, info->maintained_effects[i].slot_pos, 
+			info->maintained_effects[i].icon, info->maintained_effects[i].icon_backdrop, info->maintained_effects[i].conc_used, info->maintained_effects[i].tier, 
+			info->maintained_effects[i].total_time, timestamp, database.getSafeEscapeString(info->maintained_effects[i].spell->file_name.c_str()).c_str(), info->maintained_effects[i].spell->spell->IsCopiedSpell(), GetCharacterID(), 
+			info->maintained_effects[i].spell->damage_remaining, info->maintained_effects[i].spell->effect_bitmask, info->maintained_effects[i].spell->num_triggers, info->maintained_effects[i].spell->had_triggers, info->maintained_effects[i].spell->cancel_after_all_triggers,
+			info->maintained_effects[i].spell->crit, info->maintained_effects[i].spell->last_spellattack_hit, info->maintained_effects[i].spell->interrupted, info->maintained_effects[i].spell->resisted, (info->maintained_effects[i].expire_timestamp) == 0xFFFFFFFF ? "" : database.getSafeEscapeString(spellProcess->SpellScriptTimerCustomFunction(info->maintained_effects[i].spell).c_str()).c_str());
+
+			info->maintained_effects[i].spell->MSpellTargets.readlock(__FUNCTION__, __LINE__);
+			std::string insertTargets = string("insert into character_spell_effect_targets (caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos) values ");
+			bool firstTarget = true;
+			map<int32, bool> targetsInserted;
+			for (int8 t = 0; t < info->maintained_effects[i].spell->targets.size(); t++) {
+					Spawn* spawn = GetZone()->GetSpawnByID(info->maintained_effects[i].spell->targets.at(t));
+					if(spawn && spawn->IsPlayer())
+					{
+						int32 tmpCharID = ((Player*)spawn)->GetCharacterID();
+
+						if(targetsInserted.find(tmpCharID) != targetsInserted.end())
+							continue;
+
+						if(!firstTarget)
+							insertTargets.append(", ");
+						
+						targetsInserted.insert(make_pair(tmpCharID, true));
+
+						insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(tmpCharID) + ", " + "0" + ", " + 
+						std::to_string(DB_TYPE_MAINTAINEDEFFECTS) + ", " + std::to_string(info->maintained_effects[i].spell_id) + ", " + std::to_string(i) + 
+						", " + std::to_string(info->maintained_effects[i].spell->slot_pos) + ")");
+						firstTarget = false;
+					}
+			}
+			info->maintained_effects[i].spell->MSpellTargets.releasereadlock(__FUNCTION__, __LINE__);
+			if(!firstTarget)
+			{
+				Query targetSave;
+				targetSave.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, insertTargets.c_str());
+			}
+		}
+	}
+	MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__);
+	MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
 }

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

@@ -444,6 +444,7 @@ public:
 	PlayerItemList    item_list;
 	PlayerSkillList	  skill_list;
 	Skill*	GetSkillByName(const char* name, bool check_update = false);
+	Skill*	GetSkillByID(int32 skill_id, bool check_update = false);
 	PlayerSkillList* GetSkills();
 	bool DamageEquippedItems(int8 amount = 10, Client* client = 0);
 	vector<EQ2Packet*>	EquipItem(int16 index, int16 version, int8 slot_id = 255);
@@ -963,14 +964,16 @@ public:
 
 	void DismissAllPets();
 
+	void SaveSpellEffects();
 
+	void SetSaveSpellEffects(bool val) { stop_save_spell_effects = val; }
 	AppearanceData SavedApp;
 	CharFeatures SavedFeatures;
 	bool custNPC;
 	Entity* custNPCTarget;
 	// bot index, spawn id
 	map<int32, int32> SpawnedBots;
-
+	bool StopSaveSpellEffects() { return stop_save_spell_effects; }
 private:
 	bool range_attack;
 	int16 last_movement_activity;
@@ -1091,6 +1094,7 @@ private:
 	EQ2_Color tmp_mount_saddle_color;
 
 	bool gm_vision;
+	bool stop_save_spell_effects;
 
 	map<int32, Spawn*>	player_spawn_id_map;
 	map<Spawn*, int32>	player_spawn_reverse_id_map;

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

@@ -332,6 +332,7 @@ Quest::Quest(int32 in_id){
 	tmp_reward_status = 0;
 	tmp_reward_coins = 0;
 	completed_description = string("");
+	quest_temporary_description = string("");
 }
 
 Quest::Quest(Quest* old_quest){
@@ -406,6 +407,7 @@ Quest::Quest(Quest* old_quest){
 	tmp_reward_status = 0;
 	tmp_reward_coins = 0;
 	completed_description = string("");
+	quest_temporary_description = string("");
 }
 
 Quest::~Quest(){

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

@@ -324,6 +324,10 @@ void RuleManager::Init()
 	RULE_INIT(R_Loot, AllowChestUnlockByTrapTime, "1"); // when set to 1 we will allow unlocking the chest to all players after the trap is triggered (or chest is open) and period ChestUnlockedTimeTrap elapsed
 
 	RULE_INIT(R_Spells, NoInterruptBaseChance, "50");
+	RULE_INIT(R_Spells, EnableFizzleSpells, "1"); // enables/disables the 'fizzling' of spells based on can_fizzle in the spells table.  This also enables increasing specialized skills for classes based on spells/abilities.
+	RULE_INIT(R_Spells, DefaultFizzleChance, "10.0"); // default percentage x / 100, eg 10% is 10.0
+	RULE_INIT(R_Spells, FizzleMaxSkill, "1.2"); // 1.0 is 100%, 1.2 is 120%, so you get 120% your max skill against a spell, no fizzle
+	RULE_INIT(R_Spells, FizzleDefaultSkill, ".2"); // offset against MaxSkill to average out to 100%, default of .2f so we don't go over the threshold if no skill
 
 	RULE_INIT(R_Expansion, GlobalExpansionFlag, "0");
 	RULE_INIT(R_Expansion, GlobalHolidayFlag, "0");

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

@@ -173,6 +173,10 @@ enum RuleType {
 	
 	/* SPELLS */
 	NoInterruptBaseChance,
+	EnableFizzleSpells,
+	DefaultFizzleChance,
+	FizzleMaxSkill,
+	FizzleDefaultSkill,
 
 	/* ZONE TIMERS */
 	RegenTimer,

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

@@ -148,6 +148,7 @@
 #define MERCHANT_TYPE_CRAFTING				8
 #define MERCHANT_TYPE_REPAIR				16
 #define MERCHANT_TYPE_LOTTO					32
+#define MERCHANT_TYPE_CITYMERCHANT			64
 
 #define INFO_VIS_FLAG_INVIS                 1
 #define INFO_VIS_FLAG_HIDE_HOOD             2

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

@@ -131,7 +131,8 @@ void SpellProcess::Process(){
 				// to counter this check to see if the spell has a call_frequency > 0 before we call ProcessSpell()
 				if (spell->spell->GetSpellData()->call_frequency > 0 && !ProcessSpell(spell, false))
 					active_spells.Remove(spell, true, 2000);
-				else if ((spell->timer.GetDuration() * spell->num_calls) >= spell->spell->GetSpellData()->duration1 * 100)
+				else if (((spell->timer.GetDuration() * spell->num_calls) >= spell->spell->GetSpellData()->duration1 * 100) || 
+						 (spell->restored && (spell->timer.GetSetAtTrigger() * spell->num_calls) >= spell->spell->GetSpellData()->duration1 * 100))
 					DeleteCasterSpell(spell, "expired");
 			}
 			else
@@ -361,7 +362,7 @@ bool SpellProcess::DeleteCasterSpell(Spawn* caster, Spell* spell, string reason)
 	return ret;
 }
 
-bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason){
+bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removing_all_spells){
 	bool ret = false;
 	Spawn* target = 0;
 	if(spell) {
@@ -441,7 +442,7 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason){
 			ret = true;
 		}
 		if(lua_interface)
-			lua_interface->RemoveSpell(spell, true, SpellScriptTimersHasSpell(spell), reason);
+			lua_interface->RemoveSpell(spell, true, SpellScriptTimersHasSpell(spell), reason, removing_all_spells);
 	}
 	return ret;
 }
@@ -453,7 +454,7 @@ bool SpellProcess::ProcessSpell(LuaSpell* spell, bool first_cast, const char* fu
 		LogWrite(SPELL__ERROR, 0, "Spell", "Error: State is NULL!  SpellProcess::ProcessSpell for Spell '%s'", (spell->spell != nullptr) ? spell->spell->GetName() : "Unknown");
 	}
 	else if(lua_interface && !spell->interrupted){
-		lua_interface->AddSpawnPointers(spell, first_cast, false, function, timer);
+		std::string functionCall = lua_interface->AddSpawnPointers(spell, first_cast, false, function, timer);
 		vector<LUAData*>* data = spell->spell->GetLUAData();
 		for(int32 i=0;i<data->size();i++){
 			switch(data->at(i)->type){
@@ -479,7 +480,7 @@ bool SpellProcess::ProcessSpell(LuaSpell* spell, bool first_cast, const char* fu
 				}
 			}
 		}
-		ret = lua_interface->CallSpellProcess(spell, 2 + data->size());
+		ret = lua_interface->CallSpellProcess(spell, 2 + data->size(), functionCall);
 	}
 	return ret;
 }
@@ -629,10 +630,15 @@ bool SpellProcess::CheckPower(LuaSpell* spell){
 	return false;
 }
 
-bool SpellProcess::TakePower(LuaSpell* spell){
+bool SpellProcess::TakePower(LuaSpell* spell, int32 custom_power_req){
 	int16 req = 0;
 	if(spell->caster){
-		req = spell->spell->GetPowerRequired(spell->caster);
+
+		if(custom_power_req)
+			req = custom_power_req;
+		else
+			req = spell->spell->GetPowerRequired(spell->caster);
+		
 		if(spell->caster->GetPower() >= req){
 			spell->caster->SetPower(spell->caster->GetPower() - req);
 			if(spell->caster->IsPlayer())
@@ -653,10 +659,13 @@ bool SpellProcess::CheckHP(LuaSpell* spell) {
    return false; 
 }
 
-bool SpellProcess::TakeHP(LuaSpell* spell) { 
+bool SpellProcess::TakeHP(LuaSpell* spell, int32 custom_hp_req) { 
    int16 req = 0; 
-   if(spell->caster && spell->caster->IsPlayer()){ 
-     req = spell->spell->GetHPRequired(spell->caster); 
+   if(spell->caster){ 
+		if(custom_hp_req)
+			req = custom_hp_req;
+		else
+     		req = spell->spell->GetHPRequired(spell->caster); 
      if(spell->caster->GetHP() >= req){ 
         spell->caster->SetHP(spell->caster->GetHP() - req);
 		if(spell->caster->IsPlayer())
@@ -1022,6 +1031,7 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 						if(tmpTarget && tmpTarget->IsEntity())
 						{
 							zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
+							CheckRemoveTargetFromSpell(conflictSpell);
 							((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
 							if(client)
 								UnlockSpell(client, conflictSpell->spell);
@@ -1369,14 +1379,38 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 		//Apply casting speed mod
 		spell->ModifyCastTime(caster);
 
-		LockAllSpells(client);
-
 		//cancel stealth effects on cast
 		if(caster->IsStealthed() || caster->IsInvis())
 			caster->CancelAllStealth();
+		
+		if(caster && caster->IsEntity() && caster->CheckFizzleSpell(lua_spell))
+		{
+			caster->GetZone()->SendCastSpellPacket(0, target ? target : caster, caster);
+			caster->GetZone()->SendInterruptPacket(caster, lua_spell, true);
+			caster->IsCasting(false);
+			
+			if(caster->IsPlayer())
+			{
+				((Player*)caster)->UnlockSpell(spell);
+				SendSpellBookUpdate(((Player*)caster)->GetClient());
+			}
+
+			// fizzle takes half
+			int16 power_req = spell->GetPowerRequired(caster) / 2;
+			TakePower(lua_spell, power_req);
+
+     		int16 hp_req = spell->GetHPRequired(caster) / 2; 
+			TakeHP(lua_spell, hp_req);
+
+			lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
+			DeleteSpell(lua_spell);
+			return;
+		}
 
 		SendStartCast(lua_spell, client);
-			
+
+		LockAllSpells(client);
+
 		if(spell->GetSpellData()->cast_time > 0)
 		{
 			CastTimer* cast_timer = new CastTimer;
@@ -1521,6 +1555,11 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her
 	if (!spell->caster)
 		return true;
 
+	Skill* skill = spell->caster->GetSkillByID(spell->spell->GetSpellData()->mastery_skill, false);
+	// trigger potential skill increase if we succeed in casting a mastery skill and it still has room to grow (against this spell)
+	if(skill && skill->current_val < spell->spell->GetSpellData()->min_class_skill_req)
+		spell->caster->GetSkillByID(spell->spell->GetSpellData()->mastery_skill, true);
+
 	ZoneServer* zone = spell->caster->GetZone();
 	Spawn* target = 0;
 	if(processedSpell){
@@ -1796,7 +1835,7 @@ void SpellProcess::Interrupted(Entity* caster, Spawn* interruptor, int16 error_c
 	}
 }
 
-void SpellProcess::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast){
+void SpellProcess::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast, bool call_expire_function){
 	int32 i = 0;
 	if(cast_timers.size() > 0){		
 		CastTimer* cast_timer = 0;
@@ -1818,8 +1857,8 @@ void SpellProcess::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, boo
 				continue;
 			if (spell->spell->GetSpellData()->persist_though_death)
 				continue;
-			if(spell->caster == spawn){
-				DeleteCasterSpell(spell, "expired");
+			if(spell->caster == spawn && call_expire_function){
+				DeleteCasterSpell(spell, "expired", remove_all);
 				continue;
 			}
 
@@ -2138,7 +2177,7 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 				} // end is player
 			} // end is friendly
 
-			else if (target && data->group_spell > 0 || data->icon_backdrop == 312) // is not friendly, but is a group spell, icon_backdrop 312 is green (encounter AE)
+			else if (target && (data->group_spell > 0 || data->icon_backdrop == 312)) // is not friendly, but is a group spell, icon_backdrop 312 is green (encounter AE)
 			{
 				// target is non-player
 				if (target->IsNPC())
@@ -2421,6 +2460,24 @@ bool SpellProcess::SpellScriptTimersHasSpell(LuaSpell* spell) {
 	return ret;
 }
 
+std::string SpellProcess::SpellScriptTimerCustomFunction(LuaSpell* spell) {
+	bool ret = false;
+	std::string val = string("");
+	vector<SpellScriptTimer*>::iterator itr;
+
+	MSpellScriptTimers.readlock(__FUNCTION__, __LINE__);
+	for (itr = m_spellScriptList.begin(); itr != m_spellScriptList.end(); itr++) {
+		SpellScriptTimer* timer = *itr;
+		if (timer && timer->spell == spell) {
+			val = string(timer->customFunction);
+			break;
+		}
+	}
+	MSpellScriptTimers.releasereadlock(__FUNCTION__, __LINE__);
+
+	return val;
+}
+
 void SpellProcess::ClearSpellScriptTimerList() {
 	vector<SpellScriptTimer*>::iterator itr;
 	MSpellScriptTimers.writelock(__FUNCTION__, __LINE__);
@@ -2625,4 +2682,10 @@ void SpellProcess::SpellCannotStack(ZoneServer* zone, Client* client, Entity* ca
 	zone->SendSpellFailedPacket(client, SPELL_ERROR_TAKE_EFFECT_MOREPOWERFUL);
 	lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
 	DeleteSpell(lua_spell);
+}
+
+void SpellProcess::AddActiveSpell(LuaSpell* spell)
+{
+	if(!active_spells.count(spell))
+		active_spells.Add(spell);
 }

+ 9 - 9
EQ2/source/WorldServer/SpellProcess.h

@@ -185,7 +185,7 @@ public:
 	/// <summary>Checks to see if the caster has enough power and takes it</summary>
 	/// <param name='spell'>LuaSpell to check and take power for (LuaSpell contains the caster)</param>
 	/// <returns>True if caster had enough power</returns>
-	bool TakePower(LuaSpell* spell);
+	bool TakePower(LuaSpell* spell, int32 custom_power_req = 0);
 
 	/// <summary>Check to see if the caster has enough power to cast the spell</summary>
 	/// <param name='spell'>LuaSpell to check (LuaSpell contains the caster)</param>
@@ -195,7 +195,7 @@ public:
 	/// <summary>Check to see if the caster has enough hp and take it</summary>
 	/// <param name='spell'>LuaSpell to check and take hp for (LuaSpell contains the caster)</param>
 	/// <returns>True if the caster had enough hp</returns>
-	bool TakeHP(LuaSpell* spell); 
+	bool TakeHP(LuaSpell* spell, int32 custom_hp_req = 0); 
 
 	/// <summary>Check to see if the caster has enough hp to cast the spell</summary>
 	/// <param name='spell'>LuaSpell to check (LuaSpell contains the caster)</param>
@@ -259,7 +259,7 @@ public:
 
 	/// <summary>Remove the given spell from the ZpellProcess</summary>
 	/// <param name='spell'>LuaSpell to remove</param>
-	bool DeleteCasterSpell(LuaSpell* spell, string reason="");
+	bool DeleteCasterSpell(LuaSpell* spell, string reason="", bool removing_all_spells = false);
 
 	/// <summary>Interrupt the spell</summary>
 	/// <param name='interrupt'>InterruptStruct that contains all the info</param>
@@ -268,7 +268,7 @@ public:
 	/// <summary>Removes the timers for the given spawn</summary>
 	/// <param name='spawn'>Spawn to remove the timers for</param>
 	/// <param name='remove_all'>Remove all timers (cast, recast, active, queue, interrupted)? If false only cast timers are removed</param>
-	void RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all = false, bool delete_recast = true);
+	void RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all = false, bool delete_recast = true, bool call_expire_function = true);
 
 	/// <summary>Sets the recast timer for the spell </summary>
 	/// <param name='spell'>The spell to set the recast for</param>
@@ -356,6 +356,7 @@ public:
 
 	/// <summary>Checks to see if the list has the spell</summary>
 	bool SpellScriptTimersHasSpell(LuaSpell* spell);
+	std::string SpellScriptTimerCustomFunction(LuaSpell* spell);
 
 	void ClearSpellScriptTimerList();
 
@@ -383,12 +384,11 @@ public:
 	void DeleteSpell(LuaSpell* spell);
 
 	void SpellCannotStack(ZoneServer* zone, Client* client, Entity* caster, LuaSpell* lua_spell, LuaSpell* conflictSpell);
-private:
-	/// <summary>Sends the spell data to the lua script</summary>
-	/// <param name='spell'>LuaSpell to call the lua script for</param>
-	/// <param name='first_cast'>No clue, not currently used</param>
-	/// <returns>True if the spell script was called successfully</returns>
+
 	bool ProcessSpell(LuaSpell* spell, bool first_cast = true, const char* function = 0, SpellScriptTimer* timer = 0);
+
+	void AddActiveSpell(LuaSpell* spell);
+private:
 	Mutex MSpellProcess;
 	MutexMap<Entity*,Spell*> spell_que;
 	MutexList<LuaSpell*> active_spells;

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

@@ -141,6 +141,7 @@ Spell::Spell(Spell* host_spell)
 		spell->ts_loc_index = host_spell->GetSpellData()->ts_loc_index;
 		spell->type = host_spell->GetSpellData()->type;
 		spell->type_group_spell_id = host_spell->GetSpellData()->type_group_spell_id;
+		spell->can_fizzle = host_spell->GetSpellData()->can_fizzle;
 	}
 
 	heal_spell = host_spell->IsHealSpell();
@@ -1649,6 +1650,11 @@ bool Spell::GetSpellData(lua_State* state, std::string field)
 		lua_interface->SetSInt32Value(state, GetSpellData()->type_group_spell_id);
 		valSet = true;
 	}
+	else if (field == "can_fizzle")
+	{
+		lua_interface->SetBooleanValue(state, GetSpellData()->can_fizzle);
+		valSet = true;
+	}
 
 	return valSet;
 }
@@ -2056,6 +2062,12 @@ bool Spell::SetSpellData(lua_State* state, std::string field, int8 fieldArg)
 		GetSpellData()->type_group_spell_id = type_group_spell_id;
 		valSet = true;
 	}
+	else if (field == "can_fizzle")
+	{
+		bool can_fizzle = lua_interface->GetBooleanValue(state, fieldArg);
+		GetSpellData()->can_fizzle = can_fizzle;
+		valSet = true;
+	}
 
 	return valSet;
 }

+ 1 - 0
EQ2/source/WorldServer/Spells.h

@@ -290,6 +290,7 @@ struct SpellData{
 	int8	spell_type;
 	int32	spell_name_crc;
 	sint32	type_group_spell_id;
+	bool	can_fizzle;
 };
 class Spell{
 public:

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

@@ -46,6 +46,7 @@ along with EQ2Emulator.  If not, see <http://www.gnu.org/licenses/>.
 #include "ClientPacketFunctions.h"
 #include "Zone/ChestTrap.h"
 #include "../common/version.h"
+#include "SpellProcess.h"
 
 extern Classes classes;
 extern Commands commands;
@@ -4555,7 +4556,7 @@ void WorldDatabase::LoadSpells()
 	int32 total = 0;
 	map<int32, vector<LevelArray*> >* level_data = LoadSpellClasses();
 
-	if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `min_class_skill_req`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc', type_group_spell_id "
+	if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `min_class_skill_req`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc', type_group_spell_id, can_fizzle "
 									"FROM (spells s, spell_tiers st) "
 									"LEFT JOIN spell_ts_ability_index ts "
 									"ON s.`id` = ts.spell_id "
@@ -4654,6 +4655,7 @@ void WorldDatabase::LoadSpells()
 			data->linked_timer				= result.GetInt32Str("linked_timer_id");
 			data->spell_name_crc			= result.GetInt32Str("spell_name_crc");
 			data->type_group_spell_id		= result.GetSInt32Str("type_group_spell_id");
+			data->can_fizzle				= ( result.GetInt8Str("can_fizzle") == 1);
 
 			/* Cast Messaging */
 			string message					= result.GetStringStr("success_message");
@@ -7348,4 +7350,232 @@ int32 WorldDatabase::CreateSpiritShard(const char* name, int32 level, int8 race,
 	safe_delete_array(lastname_escaped);
 
 	return query.GetLastInsertedID();
+}
+
+void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int8 db_spell_type) 
+{
+		SpellProcess* spellProcess = client->GetCurrentZone()->GetSpellProcess();
+
+		if(!spellProcess)
+			return;
+	DatabaseResult result;
+
+	Player* player = client->GetPlayer();
+	// Use -1 on type and subtype to turn the enum into an int and make it a 0 index
+	if (!database_new.Select(&result, "SELECT name, caster_char_id, target_char_id, target_type, spell_id, effect_slot, slot_pos, icon, icon_backdrop, conc_used, tier, total_time, expire_timestamp, lua_file, custom_spell, damage_remaining, effect_bitmask, num_triggers, had_triggers, cancel_after_triggers, crit, last_spellattack_hit, interrupted, resisted, custom_function FROM character_spell_effects WHERE charid = %u and db_effect_type = %u", char_id, db_spell_type)) {
+		LogWrite(DATABASE__ERROR, 0, "DBNew", "MySQL Error %u: %s", database_new.GetError(), database_new.GetErrorMsg());
+		return;
+	}
+	InfoStruct* info = player->GetInfoStruct();
+	while (result.Next()) {
+//result.GetInt8Str
+		char spell_name[60];
+		strncpy(spell_name, result.GetStringStr("name"), 60);
+
+		int32 caster_char_id = result.GetInt32Str("caster_char_id");
+		int32 target_char_id = result.GetInt32Str("target_char_id");
+		int8 target_type = result.GetInt8Str("target_type");
+		int32 spell_id = result.GetInt32Str("spell_id");
+		int32 effect_slot = result.GetInt32Str("effect_slot");
+		int32 slot_pos = result.GetInt32Str("slot_pos");
+		int16 icon = result.GetInt32Str("icon");
+		int16 icon_backdrop = result.GetInt32Str("icon_backdrop");
+		int8 conc_used = result.GetInt32Str("conc_used");
+		int8 tier = result.GetInt32Str("tier");
+		float total_time = result.GetFloatStr("total_time");
+		int32 expire_timestamp = result.GetInt32Str("expire_timestamp");
+
+		string lua_file (result.GetStringStr("lua_file"));
+
+		int8 custom_spell = result.GetInt32Str("custom_spell");
+
+		int32 damage_remaining = result.GetInt32Str("damage_remaining");
+		int32 effect_bitmask = result.GetInt32Str("effect_bitmask");
+		int16 num_triggers = result.GetInt32Str("num_triggers");
+		int8 had_triggers = result.GetInt32Str("had_triggers");
+		int8 cancel_after_triggers = result.GetInt32Str("cancel_after_triggers");
+		int8 crit = result.GetInt32Str("crit");
+		int8 last_spellattack_hit = result.GetInt32Str("last_spellattack_hit");
+		int8 interrupted = result.GetInt32Str("interrupted");
+		int8 resisted = result.GetInt32Str("resisted");
+		std::string custom_function = std::string(result.GetStringStr("custom_function"));
+		LuaSpell* lua_spell = 0;
+		if(custom_spell)
+		{
+			if((lua_spell = lua_interface->GetSpell(lua_file.c_str())) == nullptr)
+			{		
+				LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: GetSpell(%u, %u, '%s'), custom lua script not loaded, when attempting to load.", spell_id, tier, lua_file.c_str());
+				lua_interface->LoadLuaSpell(lua_file);
+			}
+		}
+
+		Spell* spell = master_spell_list.GetSpell(spell_id, tier);
+		
+		bool isMaintained = false;
+		bool isExistingLuaSpell = false;
+		MaintainedEffects* effect;
+		Client* tmpCaster = nullptr;
+		if(caster_char_id == player->GetCharacterID() && target_char_id == player->GetCharacterID() && (effect = player->GetMaintainedSpell(spell_id)) != nullptr)
+		{
+			safe_delete(lua_spell);
+			lua_spell = effect->spell;
+			if(lua_spell)
+				spell = lua_spell->spell;
+			isMaintained = true;
+			isExistingLuaSpell = true;
+		}
+		else if ( caster_char_id != player->GetCharacterID() && (tmpCaster = zone_list.GetClientByCharID(caster_char_id)) != nullptr 
+					 && tmpCaster->GetPlayer() && (effect = tmpCaster->GetPlayer()->GetMaintainedSpell(spell_id)) != nullptr)
+		{
+			if(tmpCaster->GetCurrentZone() != client->GetCurrentZone())
+			{
+				LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: GetSpell(%u, %u, '%s'), characters in different zones, cannot assign maintained spell.", spell_id, tier, lua_file.c_str());
+				safe_delete(lua_spell);
+				continue;
+			}
+			else if(effect->spell)
+			{
+				safe_delete(lua_spell);
+				effect->spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
+				effect->spell->targets.push_back(client->GetPlayer()->GetID());
+				effect->spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
+				lua_spell = effect->spell;
+				spell = effect->spell->spell;
+				isExistingLuaSpell = true;
+			}
+			else
+			{
+				LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: GetSpell(%u, %u, '%s'), something went wrong loading another characters maintained spell.", spell_id, tier, lua_file.c_str());
+				safe_delete(lua_spell);
+				continue;
+			}
+		}
+		else if(custom_spell && lua_spell)
+		{
+			lua_spell->spell = new Spell(spell);
+
+			lua_interface->AddCustomSpell(lua_spell);
+		}
+		else
+		{
+			safe_delete(lua_spell);
+			lua_spell = lua_interface->GetSpell(spell->GetSpellData()->lua_script.c_str());
+			if(lua_spell)
+				lua_spell->spell = spell;
+		}
+		
+		if(!lua_spell)
+		{
+			LogWrite(LUA__ERROR, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: GetSpell(%u, %u, '%s'), lua_spell FAILED, when attempting to load.", spell_id, tier, lua_file.c_str());
+			continue;
+		}
+	
+		SpellScriptTimer* timer = nullptr;
+		if(!isExistingLuaSpell && expire_timestamp != 0xFFFFFFFF && custom_function.size() > 0)
+		{
+			timer = new SpellScriptTimer;
+
+			timer->caster = 0;
+			timer->deleteWhenDone = false;
+			timer->target = 0;
+
+			timer->time = expire_timestamp;
+			timer->customFunction = string(custom_function); // TODO
+			timer->spell = lua_spell;
+			timer->caster = (caster_char_id == player->GetCharacterID()) ? player->GetID() : 0;
+			timer->target = (target_char_id == player->GetCharacterID()) ? player->GetID() : 0;
+		}
+		
+		lua_spell->crit = crit;
+		lua_spell->damage_remaining = damage_remaining;
+		lua_spell->effect_bitmask = effect_bitmask;
+		lua_spell->had_dmg_remaining = (damage_remaining>0) ? true : false;
+		lua_spell->had_triggers = had_triggers;
+		lua_spell->initial_target = (target_char_id == player->GetCharacterID()) ? player->GetID() : 0;
+		lua_spell->interrupted = interrupted;
+		lua_spell->last_spellattack_hit = last_spellattack_hit;
+		lua_spell->num_triggers = num_triggers;
+		//lua_spell->num_calls  ??
+		//if(target_char_id == player->GetCharacterID())
+		//	lua_spell->targets.push_back(player->GetID());
+		
+		if(db_spell_type == DB_TYPE_SPELLEFFECTS)
+		{
+			player->MSpellEffects.writelock();
+			info->spell_effects[effect_slot].caster = (caster_char_id == player->GetCharacterID()) ? player : 0;
+			info->spell_effects[effect_slot].expire_timestamp = Timer::GetCurrentTime2() + expire_timestamp;
+			info->spell_effects[effect_slot].icon = icon;
+			info->spell_effects[effect_slot].icon_backdrop = icon_backdrop;
+			info->spell_effects[effect_slot].spell_id = spell_id;
+			info->spell_effects[effect_slot].tier = tier;
+			info->spell_effects[effect_slot].total_time = total_time;
+			info->spell_effects[effect_slot].spell = lua_spell;
+			lua_spell->caster = player; // TODO: get actual player
+			player->MSpellEffects.releasewritelock();
+
+			if(!isMaintained)
+				spellProcess->ProcessSpell(lua_spell, true, "cast", timer);
+		}
+		else if ( db_spell_type == DB_TYPE_MAINTAINEDEFFECTS )
+		{
+			player->MMaintainedSpells.writelock();
+
+			DatabaseResult targets;
+			// Use -1 on type and subtype to turn the enum into an int and make it a 0 index
+			if (database_new.Select(&targets, "SELECT target_char_id, target_type, db_effect_type, spell_id from character_spell_effect_targets where caster_char_id = %u and effect_slot = %u and slot_pos = %u", char_id, effect_slot, slot_pos)) {
+				while (targets.Next()) {
+					int32 target_char = targets.GetInt32Str("target_char_id");
+					int16 target_type = targets.GetInt32Str("target_type");
+					int32 in_spell_id = targets.GetInt32Str("spell_id");
+					if(spell_id != in_spell_id)
+						continue;
+					
+					Client* client2 = zone_list.GetClientByCharID(target_char);
+					lua_spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
+					if(client2 && client2->GetPlayer() && client2->GetCurrentZone() == client->GetCurrentZone())
+						lua_spell->targets.push_back(client2->GetPlayer()->GetID());
+					lua_spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
+				}
+			}
+
+			info->maintained_effects[effect_slot].conc_used = conc_used;
+			strncpy(info->maintained_effects[effect_slot].name, spell_name, 60);
+			info->maintained_effects[effect_slot].slot_pos = slot_pos;
+			info->maintained_effects[effect_slot].target = (target_char_id == player->GetCharacterID()) ? player->GetID() : 0;
+			info->maintained_effects[effect_slot].target_type = target_type;
+			info->maintained_effects[effect_slot].expire_timestamp = Timer::GetCurrentTime2() + expire_timestamp;
+			info->maintained_effects[effect_slot].icon = icon;
+			info->maintained_effects[effect_slot].icon_backdrop = icon_backdrop;
+			info->maintained_effects[effect_slot].spell_id = spell_id;
+			info->maintained_effects[effect_slot].tier = tier;
+			info->maintained_effects[effect_slot].total_time = total_time;
+			info->maintained_effects[effect_slot].spell = lua_spell;
+			lua_spell->caster = player;
+			player->MMaintainedSpells.releasewritelock();
+
+			spellProcess->ProcessSpell(lua_spell, true, "cast", timer);
+		}
+		if(!isExistingLuaSpell && expire_timestamp != 0xFFFFFFFF && !isMaintained)
+		{
+			lua_spell->timer.SetTimer(expire_timestamp);
+			lua_spell->timer.SetAtTrigger(lua_spell->spell->GetSpellData()->duration1 * 100);
+			lua_spell->timer.Start();
+		}
+
+		if(lua_spell->spell->GetSpellData()->det_type)
+			player->AddDetrimentalSpell(lua_spell, expire_timestamp);
+
+		if(timer)
+			spellProcess->AddSpellScriptTimer(timer);
+		
+		lua_spell->num_calls = 1;
+		lua_spell->restored = true;
+		if(!lua_spell->resisted && (lua_spell->spell->GetSpellDuration() > 0 || lua_spell->spell->GetSpellData()->duration_until_cancel))
+				spellProcess->AddActiveSpell(lua_spell);
+
+		if (num_triggers > 0)
+			ClientPacketFunctions::SendMaintainedExamineUpdate(client, slot_pos, num_triggers, 0);
+		if (damage_remaining > 0)
+			ClientPacketFunctions::SendMaintainedExamineUpdate(client, slot_pos, damage_remaining, 1);
+	}
 }

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

@@ -111,6 +111,8 @@ using namespace std;
 #define CHAR_PROPERTY_GMVISION		"modify_gmvision"
 #define CHAR_PROPERTY_LUADEBUG		"modify_luadebug"
 
+#define DB_TYPE_SPELLEFFECTS		1
+#define DB_TYPE_MAINTAINEDEFFECTS	2
 
 struct StartingItem{
 	string	type;
@@ -605,6 +607,8 @@ public:
 									  int16 pos_state, int16 activity_status, char* sub_title, char* prefix_title, char* suffix_title, char* lastname, 
 									  float x, float y, float z, float heading, int32 gridid, int32 charid, int32 zoneid, int32 instanceid);
 	bool				DeleteSpiritShard(int32 id);
+
+	void				LoadCharacterSpellEffects(int32 char_id, Client *client, int8 db_spell_type);
 private:
 	DatabaseNew			database_new;
 	map<int32, string>	zone_names;

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

@@ -805,6 +805,11 @@ void Client::SendCharInfo() {
 
 	if (version > 546)
 		ClientPacketFunctions::SendHousingList(this);
+	
+	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_MAINTAINEDEFFECTS);
+	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_SPELLEFFECTS);
+	GetPlayer()->SetSaveSpellEffects(false);
+	GetPlayer()->SetCharSheetChanged(true);
 }
 
 void Client::SendZoneSpawns() {
@@ -3509,15 +3514,14 @@ void Client::Message(int8 type, const char* message, ...) {
 void Client::Disconnect(bool send_disconnect)
 {
 	LogWrite(CCLIENT__DEBUG, 0, "CClient", "Client Disconnect...");
+	this->Save();
+	this->GetPlayer()->WritePlayerStatistics();
 
 	SetConnected(false);
 
 	if (send_disconnect && getConnection())
 		getConnection()->SendDisconnect(true);
 
-	this->Save();
-	this->GetPlayer()->WritePlayerStatistics();
-
 	eqs = 0;
 }
 
@@ -3888,7 +3892,8 @@ void Client::Zone(ZoneServer* new_zone, bool set_coords) {
 
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "%s: Removing player from fighting...", __FUNCTION__);
 	//GetCurrentZone()->GetCombat()->RemoveHate(player);
-
+	player->SaveSpellEffects();
+	player->SetSaveSpellEffects(true);
 	// Remove players pet from zone if there is one
 	((Entity*)player)->DismissAllPets();
 
@@ -4060,6 +4065,7 @@ void Client::Save() {
 
 		GetPlayer()->SaveHistory();
 		GetPlayer()->SaveLUAHistory();
+		GetPlayer()->SaveSpellEffects();
 	}
 
 }
@@ -5641,9 +5647,6 @@ void Client::AcceptQuestReward(Quest* quest, int32 item_id) {
 			else
 				player->GetFactions()->DecreaseFaction(faction_id, (amount * -1));
 		}
-
-		player->GetInfoStruct()->add_status_points(quest->GetStatusPoints());
-		player->SetCharSheetChanged(true);
 		
 		if(quest->GetQuestTemporaryState())
 		{
@@ -6408,7 +6411,8 @@ float Client::CalculateSellMultiplier(int32 merchant_id) {
 void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) {
 	Spawn* spawn = GetMerchantTransaction();
 	Guild* guild = GetPlayer()->GetGuild();
-	if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) {
+	if (spawn && spawn->GetMerchantID() > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY)) && 
+		spawn->IsClientInMerchantLevelRange(this)) {
 		int32 total_sell_price = 0;
 		int32 total_status_sell_price = 0; //for status
 		float multiplier = CalculateBuyMultiplier(spawn->GetMerchantID());
@@ -6422,7 +6426,7 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) {
 			item = player->item_list.GetItemFromUniqueID(unique_id);
 
 		if (!item)
-			player->item_list.GetItemFromID(item_id);
+			item = player->item_list.GetItemFromID(item_id);
 		if (item && master_item) {
 			int32 sell_price = (int32)(master_item->sell_price * multiplier);
 			if (sell_price > item->sell_price)
@@ -6437,19 +6441,40 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) {
 				status_sell_price = item->sell_status;
 			if (quantity > item->details.count)
 				quantity = item->details.count;
-			if (player->GetGuild()) {
-				total_status_sell_price = status_sell_price * quantity;
-				player->GetInfoStruct()->add_status_points(total_status_sell_price);
+
+			total_status_sell_price = status_sell_price * quantity;
+
+			if(total_status_sell_price > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_CITYMERCHANT)))
+				total_status_sell_price = 0;
+
+			player->GetInfoStruct()->add_status_points(total_status_sell_price);
+
+			int32 guildMaxLevel = 5 + item->details.recommended_level; // client hard codes +5 to the level
+
+			if (player->GetGuild() && guild->GetLevel() < guildMaxLevel) {
 				guild->UpdateGuildStatus(GetPlayer(), total_status_sell_price / 10);
 				guild->SendGuildMemberList();
 				guild->AddEXPCurrent((total_status_sell_price / 10), true);
 			}
 			if (quantity > 1)
-				Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %i %s to %s for %s.", quantity, master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str());
+			{
+				if(total_status_sell_price)
+					Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %i %s to %s for %s and %u Status Points.", quantity, master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str(), status_sell_price);
+				else
+					Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %i %s to %s for %s%s.", quantity, master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str());
+			}
 			else
-				Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %s to %s for %s.", master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str());
+			{
+				if(total_status_sell_price)
+					Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %s to %s for %s and %u Status Points.", master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str(), status_sell_price);
+				else
+					Message(CHANNEL_MERCHANT_BUY_SELL, "You sell %s to %s for %s.", master_item->CreateItemLink(GetVersion()).c_str(), spawn->GetName(), GetCoinMessage(total_sell_price).c_str());
+			}
 			player->AddCoins(total_sell_price);
-			AddBuyBack(unique_id, item_id, quantity, sell_price);
+
+			if(!item->no_buy_back && (total_status_sell_price == 0 || (total_status_sell_price > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_CITYMERCHANT)))))
+				AddBuyBack(unique_id, item_id, quantity, sell_price);
+
 			if (quantity >= item->details.count) {
 				database.DeleteItem(GetCharacterID(), item, 0);
 				player->item_list.DestroyItem(item->details.index);
@@ -6461,8 +6486,8 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) {
 			EQ2Packet* outapp = player->SendInventoryUpdate(GetVersion());
 			if (outapp)
 				QueuePacket(outapp);
-			if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY))
-				SendSellMerchantList();
+			
+			SendSellMerchantList();
 			if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK))
 				SendBuyBackList();
 		}
@@ -6472,7 +6497,8 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) {
 
 void Client::BuyBack(int32 item_id, int16 quantity) {
 	Spawn* spawn = GetMerchantTransaction();
-	if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) {
+	if (spawn && spawn->GetMerchantID() > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK)) && 
+		spawn->IsClientInMerchantLevelRange(this)) {
 		deque<BuyBackItem*>::iterator itr;
 		BuyBackItem* buyback = 0;
 		BuyBackItem* closest = 0;
@@ -6523,8 +6549,8 @@ void Client::BuyBack(int32 item_id, int16 quantity) {
 					database.DeleteBuyBack(GetCharacterID(), closest->item_id, closest->quantity, closest->price);
 					safe_delete(closest);
 				}
-				if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY))
-					SendSellMerchantList();
+				
+				SendSellMerchantList();
 				if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK))
 					SendBuyBackList();
 			}
@@ -6571,6 +6597,12 @@ void Client::BuyItem(int32 item_id, int16 quantity) {
 				if (quantity > total_available)
 					quantity = total_available;
 			}
+			if(quantity < 1)
+			{
+				SimpleMessage(CHANNEL_COLOR_RED, "Merchant does not have item for purchase (quantity < 1).");
+				return;
+			}
+
 			total_buy_price = sell_price * quantity;
 			item = new Item(master_item);
 			item->details.count = quantity;
@@ -6596,8 +6628,8 @@ void Client::BuyItem(int32 item_id, int16 quantity) {
 						//EQ2Packet* outapp = player->SendInventoryUpdate(GetVersion());
 						//if(outapp)
 						//	QueuePacket(outapp);
-						if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY) && !(spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO))
-							SendSellMerchantList();
+						
+						SendSellMerchantList();
 						if (spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)
 							PlayLotto(total_buy_price, item->details.item_id);
 					}
@@ -6612,7 +6644,7 @@ void Client::BuyItem(int32 item_id, int16 quantity) {
 
 					// Check if the player has enough status, coins and staion cash to buy the item before checking the items
 					// TODO: need to add support for station cash
-					if (player->GetInfoStruct()->get_status_points() >= ItemInfo->price_status && player->HasCoins(ItemInfo->price_coins * quantity)) {
+					if (player->GetInfoStruct()->get_status_points() >= (ItemInfo->price_status * quantity) && player->HasCoins(ItemInfo->price_coins * quantity)) {
 						// Check items
 						int16 item_quantity = 0;
 						// Default these to true in case price_item_id or price_item2_id was never set
@@ -6651,7 +6683,7 @@ void Client::BuyItem(int32 item_id, int16 quantity) {
 						}
 						// if we have every thing then remove the price and give the item
 						if (hasItem1 && hasItem2) {
-							player->GetInfoStruct()->set_status_points(player->GetInfoStruct()->get_status_points() - ItemInfo->price_status);
+							player->GetInfoStruct()->set_status_points(player->GetInfoStruct()->get_status_points() - (ItemInfo->price_status * quantity));
 							// TODO: station cash
 
 							// The update that would normally be sent after modifing the players inventory is automatically sent in AddItem wich is called later
@@ -6691,8 +6723,8 @@ void Client::BuyItem(int32 item_id, int16 quantity) {
 								world.DecreaseMerchantQuantity(spawn->GetMerchantID(), item_id, quantity);
 								SendBuyMerchantList();
 							}
-							if (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY) && !(spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO))
-								SendSellMerchantList();
+							
+							SendSellMerchantList();
 							if (spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)
 								PlayLotto(total_buy_price, item->details.item_id);
 
@@ -7022,6 +7054,9 @@ void Client::SendBuyMerchantList(bool sell) {
 
 void Client::SendSellMerchantList(bool sell) {
 	Spawn* spawn = GetMerchantTransaction();
+	if (!spawn || (spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY) || (spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO))
+		return;
+	
 	if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) {
 		map<int32, Item*>* items = player->GetItemList();
 		if (items) {
@@ -7056,10 +7091,32 @@ void Client::SendSellMerchantList(bool sell) {
 					string thename = item->name;
 
 					packet->setArrayDataByName("price", sell_price, i);
-					if (player->GetGuild()) {
-						packet->setArrayDataByName("status", 0, i);//additive to status 2 maybe for server bonus etc
+					packet->setArrayDataByName("status", 0, i);//additive to status 2 maybe for server bonus etc
+
+					int8 dispFlags = 0;
+
+					// only city merchants allow selling for status
+					if(item->sell_status > 0 && (spawn->GetMerchantType() & MERCHANT_TYPE_CITYMERCHANT))
+					{
 						packet->setArrayDataByName("status2", item->sell_status, i); //this one is the main status
+						int32 guildMaxLevel = 5 + item->details.recommended_level; // client hard codes +5 to the level
+						if (GetPlayer()->GetGuild() && GetPlayer()->GetGuild()->GetLevel() >= guildMaxLevel) {
+							dispFlags += DISPLAY_FLAG_NO_GUILD_STATUS;
+						}
+
+					}
+					if(item->no_buy_back || (item->sell_status > 0 && (spawn->GetMerchantType() & MERCHANT_TYPE_CITYMERCHANT)))
+					{
+						if(GetVersion() < 1188)
+							dispFlags += DISPLAY_FLAG_RED_TEXT; // for older clients it isn't "no buy back", you can either have 1 for red text or 255 for 'not for sale' to be checked
+						else
+							dispFlags += DISPLAY_FLAG_NO_BUYBACK;
 					}
+
+					if(item->no_sale)
+						dispFlags += DISPLAY_FLAG_NOT_FOR_SALE;
+					
+					packet->setArrayDataByName("display_flags", dispFlags, i);
 					packet->setArrayDataByName("item_id", item->details.item_id, i);
 					packet->setArrayDataByName("unique_item_id", item->details.unique_id, i);
 					packet->setArrayDataByName("stack_size", item->details.count, i);
@@ -10158,6 +10215,9 @@ void Client::PurgeItem(Item* item)
 
 void Client::ConsumeFoodDrink(Item* item, int32 slot)
 {
+	if(GetPlayer()->StopSaveSpellEffects())
+		return;
+
 		if(item) {
 			LogWrite(MISC__INFO, 1, "Command", "ItemID: %u, ItemName: %s ItemCount: %i ", item->details.item_id, item->name.c_str(), item->details.count);
 			if(item->GetItemScript() && lua_interface){

+ 20 - 6
EQ2/source/WorldServer/zoneserver.cpp

@@ -3326,6 +3326,21 @@ Client*	ZoneServer::GetClientByName(char* name) {
 	return ret;
 }
 
+Client*	ZoneServer::GetClientByCharID(int32 charid) {
+	Client* ret = 0;
+	vector<Client*>::iterator itr;
+
+	MClientList.readlock(__FUNCTION__, __LINE__);
+	for (itr = clients.begin(); itr != clients.end(); itr++) {
+		if ((*itr)->GetCharacterID() == charid) {
+			ret = *itr;
+			break;
+		}
+	}
+	MClientList.releasereadlock(__FUNCTION__, __LINE__);
+	return ret;
+}
+
 void ZoneServer::AddMovementNPC(Spawn* spawn){
 	if (spawn)
 		movement_spawns.Put(spawn->GetID(), 1);
@@ -4654,7 +4669,7 @@ void ZoneServer::SendSpellFailedPacket(Client* client, int16 error){
 	}
 }
 
-void ZoneServer::SendInterruptPacket(Spawn* interrupted, LuaSpell* spell){
+void ZoneServer::SendInterruptPacket(Spawn* interrupted, LuaSpell* spell, bool fizzle){
 	if(!interrupted || !spell)
 		return;
 	
@@ -4668,7 +4683,7 @@ void ZoneServer::SendInterruptPacket(Spawn* interrupted, LuaSpell* spell){
 		client = *client_itr;
 		if(!client || !client->GetPlayer()->WasSentSpawn(interrupted->GetID()) || client->GetPlayer()->WasSpawnRemoved(interrupted))
 			continue;
-		packet = configReader.getStruct("WS_Interrupt", client->GetVersion());
+		packet = configReader.getStruct(fizzle ? "WS_SpellFizzle" : "WS_Interrupt", client->GetVersion());
 		if(packet){
 			packet->setDataByName("spawn_id", client->GetPlayer()->GetIDWithPlayerSpawn(interrupted));
 			packet->setArrayLengthByName("num_targets", spell->targets.size());
@@ -5691,9 +5706,9 @@ void ZoneServer::RemoveLocationGrids() {
 	location_grids.clear(true);
 }
 
-void ZoneServer::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast){
+void ZoneServer::RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast, bool call_expire_function){
 	if(spellProcess)
-		spellProcess->RemoveSpellTimersFromSpawn(spawn, remove_all, delete_recast);
+		spellProcess->RemoveSpellTimersFromSpawn(spawn, remove_all, delete_recast, call_expire_function);
 }
 
 void ZoneServer::Interrupted(Entity* caster, Spawn* interruptor, int16 error_code, bool cancel, bool from_movement){
@@ -5730,8 +5745,7 @@ void ZoneServer::RemoveSpawnSupportFunctions(Spawn* spawn) {
 
 	LogWrite(ZONE__DEBUG, 7, "Zone", "Processing RemoveSpawnSupportFunctions...");
 
-	if(spawn->IsEntity())
-		RemoveSpellTimersFromSpawn((Entity*)spawn, true);
+	RemoveSpellTimersFromSpawn((Entity*)spawn, true);
 
 	RemoveDamagedSpawn(spawn);
 	spawn->SendSpawnChanges(false);

+ 3 - 2
EQ2/source/WorldServer/zoneserver.h

@@ -313,7 +313,7 @@ public:
 	bool	CallSpawnScript(Spawn* npc, int8 type, Spawn* spawn = 0, const char* message = 0, bool is_door_open = false);
 	void	SendSpawnVisualState(Spawn* spawn, int16 type);
 	void	SendSpellFailedPacket(Client* client, int16 error);
-	void	SendInterruptPacket(Spawn* interrupted, LuaSpell* spell);
+	void	SendInterruptPacket(Spawn* interrupted, LuaSpell* spell, bool fizzle=false);
 	void	HandleEmote(Client* originator, string name);
 	Client*	GetClientBySpawn(Spawn* spawn);
 	Spawn*	GetSpawnByDatabaseID(int32 id);
@@ -392,7 +392,7 @@ public:
 	void	LoadSpellProcess();
 	void	LockAllSpells(Player* player);
 	void	UnlockAllSpells(Player* player);
-	void	RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast = true);
+	void	RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all, bool delete_recast = true, bool call_expire_function = true);
 	void	Interrupted(Entity* caster, Spawn* interruptor, int16 error_code, bool cancel = false, bool from_movement = false);
 	Spell*	GetSpell(Entity* caster);
 	void	ProcessSpell(Spell* spell, Entity* caster, Spawn* target = 0, bool lock = true, bool harvest_spell = false, LuaSpell* customSpell = 0, int16 custom_cast_time = 0, bool in_heroic_opp = false);
@@ -422,6 +422,7 @@ public:
 
 	void	HidePrivateSpawn(Spawn* spawn);
 	Client*	GetClientByName(char* name);
+	Client*	GetClientByCharID(int32 charid);
 
 	/// <summary>Gets spawns for a true AoE spell</summary>
 	vector<Spawn*> GetAttackableSpawnsByDistance(Spawn* spawn, float distance);