Browse Source

Fix #451 - basic pvp mitigation / mitigation integration
Fix #458 - fixed memory leaks in lua quest step location/zone loc functions and also languages memory cleanup

New Rules:
RULE_INIT(R_PVP, PVPMitigationModByLevel, "25"); // gives a bonus to mitigation for PVP combat to offset the percentage level * mod (default 25)
RULE_INIT(R_Combat, EffectiveMitigationCapLevel, "80"); // level multiplier for max effective cap, level * 80 (default)
RULE_INIT(R_Combat, CalculatedMitigationCapLevel, "100"); // The cap to calculate your mitigation from is [level*100].
RULE_INIT(R_Combat, MitigationLevelEffectivenessMax, "1.5"); // ratio victim level / attacker level for max effectiveness, when victim is higher level cap can reach 1.5
RULE_INIT(R_Combat, MitigationLevelEffectivenessMin, ".5"); // ratio victim level / attacker level for min effectiveness
RULE_INIT(R_Combat, MaxMitigationAllowed, ".75"); // percentage max mitigation allowed, eg. 75% of damage can be mitigated max in PVE
RULE_INIT(R_Combat, MaxMitigationAllowedPVP, ".75"); // percentage max mitigation allowed, eg. 75% of damage can be mitigated max in PVP

InfoStruct now has two unsigned int16 values that offer mitigation percentage values in integer formats, eg 155 = 15.5% mitigation
mitigation_pve
mitigation_pvp

Updated formulas to use effective_level (mentor/actual level) vs just player level
* Dodge
* Block

Bug fix for crash in non existent quest being called for /modify quest advance
Bug fix for improperly trying to stack items that are not stackable (count of 0 items, stack count of 1)
Bug fix for inventory updates, typically with overflow slots and after deleting items from inventory (packet count needs to be updated with current size in PlayerItemList::serialize)

Collections/Rewards:
- Display is now one reward at a time
- Display of award now only allows the reward cash, status to be provided once
- Database persistence of unaccepted rewards cross-zone with two new tables
- Selectable collections now checks if either field provided in /accept_reward is a potential item id (this is likely due to a client versioning)

Emagi 1 year ago
parent
commit
1068849ef8

+ 17 - 0
DB/updates/character_quest_rewards_aug6th_2022.sql

@@ -0,0 +1,17 @@
+CREATE TABLE `character_quest_rewards` (
+  `char_id` int(10) unsigned NOT NULL default 0,
+  `quest_id` int(10) unsigned NOT NULL default 0,
+  `indexed` int(10) unsigned NOT NULL default 0,
+  `is_temporary` tinyint(3) unsigned NOT NULL default 0,
+  `is_collection` tinyint(3) unsigned NOT NULL default 0,
+  `has_displayed` tinyint(3) unsigned NOT NULL default 0,
+  `tmp_coin` bigint unsigned NOT NULL DEFAULT 0,
+  `tmp_status` int(10) unsigned NOT NULL default 0,
+  `description` text not null default ''
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+CREATE TABLE `character_quest_temporary_rewards` (
+  `char_id` int(10) unsigned NOT NULL default 0,
+  `quest_id` int(10) unsigned NOT NULL default 0,
+  `item_id` int(10) unsigned NOT NULL default 0
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;

+ 84 - 2
EQ2/source/WorldServer/Combat.cpp

@@ -912,6 +912,7 @@ bool Entity::DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_
 	int8 hit_result = 0;
 	int16 blow_type = 0;
 	sint32 damage = 0;
+	sint32 damage_before_crit = 0;
 	bool crit = false;
 
 	if(low_damage > high_damage)
@@ -949,6 +950,7 @@ bool Entity::DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_
 					crit = true;
 			}
 			if(crit){
+				damage_before_crit = damage;
 				//Apply total crit multiplier with crit bonus
 				if(info_struct.get_crit_bonus() > 0)
 					damage *= (1.3 + (info_struct.get_crit_bonus() / 100));
@@ -963,8 +965,21 @@ bool Entity::DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_
 			}
 		}
 
-		// TODO: Mitigation equation from http://www.guildportal.com/Guild.aspx?GuildID=20881&TabID=189653&ForumID=95908&TopicID=9024250
-		
+		if(type == DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE || type == DAMAGE_PACKET_TYPE_SIMPLE_CRIT_DMG || type == DAMAGE_PACKET_TYPE_RANGE_DAMAGE) {			
+			int16 effective_level_attacker = GetInfoStruct()->get_effective_level() != 0 ? GetInfoStruct()->get_effective_level() : GetLevel();
+			float mit_percentage = CalculateMitigation(type, damage_type, effective_level_attacker, (IsPlayer() && victim->IsPlayer()));
+			sint32 damage_to_reduce = (damage * mit_percentage);
+			if(damage_to_reduce > damage)
+				damage = 0;
+			else
+				damage -= damage_to_reduce;
+			
+			// if we reduce damage back below crit level then its no longer a crit, but we don't go below base damage
+			if(type == DAMAGE_PACKET_TYPE_SIMPLE_CRIT_DMG && damage <= damage_before_crit) {
+				damage = damage_before_crit;
+				type = DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE;
+			}
+		}
 	}
 
 	LogWrite(MISC__TODO, 3, "TODO", "Take players armor into account\nfile: %s, func: %s, line: %i)", __FILE__, __FUNCTION__, __LINE__);
@@ -1057,6 +1072,73 @@ bool Entity::DamageSpawn(Entity* victim, int8 type, int8 damage_type, int32 low_
 	return crit;
 }
 
+float Entity::CalculateMitigation(int8 type, int8 damage_type, int16 effective_level_attacker, bool for_pvp) {
+	int16 effective_level_victim = GetInfoStruct()->get_effective_level() != 0 ? GetInfoStruct()->get_effective_level() : GetLevel();
+	if(effective_level_attacker < 1 && effective_level_victim)
+		effective_level_attacker = effective_level_victim;
+	else
+		effective_level_attacker = 1;
+
+	int32 effective_mit_cap = effective_level_victim * rule_manager.GetGlobalRule(R_Combat, EffectiveMitigationCapLevel)->GetInt32();
+	float max_mit = (float)GetInfoStruct()->get_max_mitigation();
+	if(max_mit == 0.0f)
+		max_mit = effective_level_victim * 100.0f;
+	
+	int32 mit_to_use = 0;
+	switch(type) {
+	
+		case DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE:
+		case DAMAGE_PACKET_TYPE_RANGE_DAMAGE:
+			mit_to_use = GetInfoStruct()->get_cur_mitigation();
+		break;
+		case DAMAGE_PACKET_TYPE_SIMPLE_CRIT_DMG:
+			// since critical mitigation is a percentage we will reverse the mit value so we can add skill from specific types of weapons
+			mit_to_use = (int32)((float)GetInfoStruct()->get_max_mitigation() * (float)(GetInfoStruct()->get_critical_mitigation()/100.0f));
+		break;
+		
+	}
+	
+	switch(damage_type) {
+			case DAMAGE_PACKET_DAMAGE_TYPE_SLASH:
+				mit_to_use += GetInfoStruct()->get_mitigation_skill1(); // slash
+			break;
+			case DAMAGE_PACKET_DAMAGE_TYPE_PIERCE:
+				mit_to_use += GetInfoStruct()->get_mitigation_skill2(); // pierce
+			break;
+			case DAMAGE_PACKET_DAMAGE_TYPE_CRUSH:
+				mit_to_use += GetInfoStruct()->get_mitigation_skill3(); // crush
+			break;
+			default:
+			// do nothing
+			break;
+	}
+	
+	if(for_pvp) {
+		mit_to_use += effective_level_victim * rule_manager.GetGlobalRule(R_Combat, MitigationLevelEffectivenessMax)->GetInt32();
+	}
+	
+	if(mit_to_use > effective_mit_cap) {
+		mit_to_use = effective_mit_cap;
+	}
+	float level_diff = ((float)effective_level_victim / (float)effective_level_attacker);
+	if(level_diff > rule_manager.GetGlobalRule(R_Combat, MitigationLevelEffectivenessMax)->GetFloat()) {
+		level_diff = rule_manager.GetGlobalRule(R_Combat, MitigationLevelEffectivenessMax)->GetFloat();
+	}
+	else if(level_diff < rule_manager.GetGlobalRule(R_Combat, MitigationLevelEffectivenessMax)->GetFloat()) {
+		level_diff = rule_manager.GetGlobalRule(R_Combat, MitigationLevelEffectivenessMin)->GetFloat();
+	}
+	float mit_percentage = ((float)mit_to_use / max_mit) * level_diff;
+	
+	if(!for_pvp && mit_percentage > rule_manager.GetGlobalRule(R_Combat, MaxMitigationAllowed)->GetFloat()) {
+		mit_percentage = rule_manager.GetGlobalRule(R_Combat, MaxMitigationAllowed)->GetFloat();
+	}
+	else if(for_pvp && mit_percentage > rule_manager.GetGlobalRule(R_Combat, MaxMitigationAllowedPVP)->GetFloat()) {
+		mit_percentage = rule_manager.GetGlobalRule(R_Combat, MaxMitigationAllowedPVP)->GetFloat();
+	} 
+	
+	return mit_percentage;
+}
+
 void Entity::AddHate(Entity* attacker, sint32 hate) {
 	if(!attacker || GetHP() <= 0 || attacker->GetHP() <= 0)
 		return;

+ 17 - 11
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -4133,7 +4133,6 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 			int32 selectable_item_id = 0;
 			//Quest *quest = 0;
 			Collection *collection = 0;
-			
 			/* no idea what the first argument is for (faction maybe?)
 			   if the reward has a selectable item reward, it's sent as the second argument
 			   if neither of these are included in the reward, there is no sep
@@ -4144,10 +4143,10 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				selectable_item_id = atoul(sep->arg[1]);
 			}
 
-			//if ((quest = player->GetPendingQuestReward()))
-			//	client->AcceptQuestRewards(quest, selectable_item_id);
-
-			/* the below needs to go away eventually and be redone */
+			/* this logic here may seem unexpected, but the quest queue response for GetPendingQuestAcceptance is only populated if it is the current reward displayed to the client based on a quest
+			** Otherwise it will likely be a DoF client scenario (pending item rewards, selectable item rewards) which is specifying an item id
+			** lastly it will be a collection which also supplies an item id and you can only have one pending collection turn in at a time (they queue against Client::HandInCollections
+			*/
 			int32 item_id = 0;
 			if(sep && sep->arg[0][0] && sep->IsNumber(0))
 				item_id = atoul(sep->arg[0]);
@@ -4157,12 +4156,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				break;
 			}
 			bool collectedItems = false;
-			if (collection = player->GetPendingCollectionReward())
-			{
-				client->AcceptCollectionRewards(collection, selectable_item_id);
-				collectedItems = true;
-			}
-			else if (client->GetPlayer()->HasPendingItemRewards()) {
+			if (client->GetPlayer()->HasPendingItemRewards()) {
 				vector<Item*> items = client->GetPlayer()->GetPendingItemRewards();
 				if (items.size() > 0) {
 					collectedItems = true;
@@ -4170,6 +4164,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						client->GetPlayer()->AddItem(new Item(items[i]));
 					}
 					client->GetPlayer()->ClearPendingItemRewards();
+					client->GetPlayer()->SetActiveReward(false);
 				}
 				map<int32, Item*> selectable_item = client->GetPlayer()->GetPendingSelectableItemReward(item_id);
 				if (selectable_item.size() > 0) {
@@ -4179,8 +4174,15 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						client->GetPlayer()->AddItem(new Item(itr->second));
 						client->GetPlayer()->ClearPendingSelectableItemRewards(itr->first);
 					}
+					client->GetPlayer()->SetActiveReward(false);
 				}
 			}
+			else if (collection = player->GetPendingCollectionReward())
+			{
+				client->AcceptCollectionRewards(collection, (selectable_item_id > 0) ? selectable_item_id : item_id);
+				collectedItems = true;
+			}
+			
 			if (collectedItems) {
 				EQ2Packet* outapp = client->GetPlayer()->SendInventoryUpdate(client->GetVersion());
 				if (outapp)
@@ -7729,6 +7731,10 @@ void Commands::Command_ModifyQuest(Client* client, Seperator* sep)
 				quest_id = atoul(sep->arg[1]);
 				Quest* quest = client->GetPlayer()->player_quests[quest_id];
 
+				if(!quest) {
+					client->Message(CHANNEL_COLOR_RED, "Quest not found!");
+					return;
+				}
 				if (sep && sep->arg[2] && sep->IsNumber(1))
 				{
 					step = atoul(sep->arg[2]);

+ 17 - 4
EQ2/source/WorldServer/Entity.cpp

@@ -249,6 +249,8 @@ void Entity::MapInfoStruct()
 	get_int16_funcs["mitigation_skill1"] = l::bind(&InfoStruct::get_mitigation_skill1, &info_struct);
 	get_int16_funcs["mitigation_skill2"] = l::bind(&InfoStruct::get_mitigation_skill2, &info_struct);
 	get_int16_funcs["mitigation_skill3"] = l::bind(&InfoStruct::get_mitigation_skill3, &info_struct);
+	get_int16_funcs["mitigation_pve"] = l::bind(&InfoStruct::get_mitigation_pve, &info_struct);
+	get_int16_funcs["mitigation_pvp"] = l::bind(&InfoStruct::get_mitigation_pvp, &info_struct);
 	get_float_funcs["ability_modifier"] = l::bind(&InfoStruct::get_ability_modifier, &info_struct);
 	get_float_funcs["critical_mitigation"] = l::bind(&InfoStruct::get_critical_mitigation, &info_struct);
 	get_float_funcs["block_chance"] = l::bind(&InfoStruct::get_block_chance, &info_struct);
@@ -427,6 +429,8 @@ void Entity::MapInfoStruct()
 	set_int16_funcs["mitigation_skill1"] = l::bind(&InfoStruct::set_mitigation_skill1, &info_struct, l::_1);
 	set_int16_funcs["mitigation_skill2"] = l::bind(&InfoStruct::set_mitigation_skill2, &info_struct, l::_1);
 	set_int16_funcs["mitigation_skill3"] = l::bind(&InfoStruct::set_mitigation_skill3, &info_struct, l::_1);
+	set_int16_funcs["mitigation_pve"] = l::bind(&InfoStruct::set_mitigation_pve, &info_struct, l::_1);
+	set_int16_funcs["mitigation_pvp"] = l::bind(&InfoStruct::set_mitigation_pvp, &info_struct, l::_1);
 	set_float_funcs["ability_modifier"] = l::bind(&InfoStruct::set_ability_modifier, &info_struct, l::_1);
 	set_float_funcs["critical_mitigation"] = l::bind(&InfoStruct::set_critical_mitigation, &info_struct, l::_1);
 	set_float_funcs["block_chance"] = l::bind(&InfoStruct::set_block_chance, &info_struct, l::_1);
@@ -1289,6 +1293,15 @@ void Entity::CalculateBonuses(){
 	CalculateSpellBonuses(values);
 	
 	info->set_cur_mitigation(info->get_mitigation_base());
+	
+	int32 calc_mit_cap = effective_level * rule_manager.GetGlobalRule(R_Combat, CalculatedMitigationCapLevel)->GetInt32();
+	info->set_max_mitigation(calc_mit_cap);
+	
+	int16 mit_percent = (int16)(CalculateMitigation() * 1000.0f);
+	info->set_mitigation_pve(mit_percent);
+	mit_percent = (int16)(CalculateMitigation(DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE,0,0,true) * 1000.0f);
+	info->set_mitigation_pvp(mit_percent);
+	
 	info->add_sta((float)values->sta);
 	info->add_str((float)values->str);
 	info->add_agi((float)values->agi);
@@ -1397,10 +1410,10 @@ void Entity::CalculateBonuses(){
 					else if (skill->short_name.data == "buckler") 
 						baseBlock = 3.0f;
 				}
-				if(GetLevel() > mitigation)
-					block_pct = log10f((float)mitigation/((float)GetLevel()*10.0f));
+				if(effective_level > mitigation)
+					block_pct = log10f((float)mitigation/((float)effective_level*10.0f));
 				else
-					block_pct = log10f(((float)GetLevel()/(float)mitigation)) * log10f(GetLevel()) * 2.0f;
+					block_pct = log10f(((float)effective_level/(float)mitigation)) * log10f(effective_level) * 2.0f;
 				
 				if(block_pct < 0.0f)
 					block_pct *= -1.0f;
@@ -1441,7 +1454,7 @@ void Entity::CalculateBonuses(){
 
 	float dodge_actual = 0.0f;
 	if(full_pct_hit > 0.0f)
-		dodge_actual = dodge_pct * (full_pct_hit / 100.0f) + (log10f(GetLevel() * GetAgi()) / 100.0f);
+		dodge_actual = dodge_pct * (full_pct_hit / 100.0f) + (log10f(effective_level * GetAgi()) / 100.0f);
 
 	info->set_avoidance_base(dodge_actual);
 

+ 13 - 0
EQ2/source/WorldServer/Entity.h

@@ -190,6 +190,8 @@ struct InfoStruct{
 		mitigation_skill1_ = 0;
 		mitigation_skill2_ = 0;
 		mitigation_skill3_ = 0;
+		mitigation_pve_ = 0;
+		mitigation_pvp_ = 0;
 		ability_modifier_ = 0;
 		critical_mitigation_ = 0;
 		block_chance_ = 0;
@@ -370,6 +372,8 @@ struct InfoStruct{
 		mitigation_skill1_ = oldStruct->get_mitigation_skill1();
 		mitigation_skill2_ = oldStruct->get_mitigation_skill2();
 		mitigation_skill3_ = oldStruct->get_mitigation_skill3();
+		mitigation_pve_ = oldStruct->get_mitigation_pve();
+		mitigation_pvp_ = oldStruct->get_mitigation_pvp();
 		ability_modifier_ = oldStruct->get_ability_modifier();
 		critical_mitigation_ = oldStruct->get_critical_mitigation();
 		block_chance_ = oldStruct->get_block_chance();
@@ -557,6 +561,9 @@ struct InfoStruct{
 	int16	 get_mitigation_skill1() { std::lock_guard<std::mutex> lk(classMutex); return mitigation_skill1_; }
 	int16	 get_mitigation_skill2() { std::lock_guard<std::mutex> lk(classMutex); return mitigation_skill2_; }
 	int16	 get_mitigation_skill3() { std::lock_guard<std::mutex> lk(classMutex); return mitigation_skill3_; }
+	
+	int16	 get_mitigation_pve() { std::lock_guard<std::mutex> lk(classMutex); return mitigation_pve_; }
+	int16	 get_mitigation_pvp() { std::lock_guard<std::mutex> lk(classMutex); return mitigation_pvp_; }
 
 	float	 get_ability_modifier() { std::lock_guard<std::mutex> lk(classMutex); return ability_modifier_; }
 	float	 get_critical_mitigation() { std::lock_guard<std::mutex> lk(classMutex); return critical_mitigation_; }
@@ -792,6 +799,9 @@ struct InfoStruct{
 	void	set_mitigation_skill1(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_skill1_ = value; }
 	void	set_mitigation_skill2(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_skill2_ = value; }
 	void	set_mitigation_skill3(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_skill3_ = value; }
+	
+	void	set_mitigation_pve(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_pve_ = value; }
+	void	set_mitigation_pvp(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_pvp_ = value; }
 
 	void	add_mitigation_skill1(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_skill1_ += value; }
 	void	add_mitigation_skill2(int16 value) { std::lock_guard<std::mutex> lk(classMutex); mitigation_skill2_ += value; }
@@ -1037,6 +1047,8 @@ private:
 	int16			mitigation_skill1_;
 	int16			mitigation_skill2_;
 	int16			mitigation_skill3_;
+	int16			mitigation_pve_;
+	int16			mitigation_pvp_;
 	float			ability_modifier_;
 	float			critical_mitigation_;
 	float			block_chance_;
@@ -1363,6 +1375,7 @@ public:
 	float			GetDamageTypeResistPercentage(int8 damage_type);
 	Skill*			GetSkillByWeaponType(int8 type, bool update);
 	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, LuaSpell* spell = 0);
+	float			CalculateMitigation(int8 type = DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE, int8 damage_type = 0, int16 attacker_level = 0, bool for_pvp = false);
 	void			AddHate(Entity* attacker, sint32 hate);
 	bool			CheckInterruptSpell(Entity* attacker);
 	bool			CheckFizzleSpell(LuaSpell* spell);

+ 9 - 6
EQ2/source/WorldServer/Items/Items.cpp

@@ -3126,8 +3126,9 @@ bool PlayerItemList::GetFirstFreeSlot(sint32* bag_id, sint16* slot) {
 }
 
 Item* PlayerItemList::CanStack(Item* item, bool include_bank){
-	if(!item)
+	if(!item || item->stack_count < 2)
 		return 0;
+	
 	Item* ret = 0;
 	map<sint32, map<int8, map<int16, Item*>> >::iterator itr;
 	map<int16, Item*>::iterator slot_itr;
@@ -3135,13 +3136,13 @@ Item* PlayerItemList::CanStack(Item* item, bool include_bank){
 	for(itr = items.begin(); itr != items.end(); itr++){
 		if(include_bank || (!include_bank && itr->first >= 0)){
 			for(slot_itr=itr->second[0].begin();slot_itr!=itr->second[0].end(); slot_itr++){
-				if(slot_itr->second && slot_itr->second->details.item_id == item->details.item_id && ((slot_itr->second->details.count + item->details.count) <= slot_itr->second->stack_count)){
+				if(slot_itr->second && slot_itr->second->details.item_id == item->details.item_id && (((slot_itr->second->details.count ? slot_itr->second->details.count : 1) + (item->details.count > 0 ? item->details.count : 1)) <= slot_itr->second->stack_count)){
 					ret = slot_itr->second;
 					break;
 				}
 			}
 			for(slot_itr=itr->second[1].begin();slot_itr!=itr->second[1].end(); slot_itr++){
-				if(slot_itr->second && slot_itr->second->details.item_id == item->details.item_id && ((slot_itr->second->details.count + item->details.count) <= slot_itr->second->stack_count)){
+				if(slot_itr->second && slot_itr->second->details.item_id == item->details.item_id && (((slot_itr->second->details.count ? slot_itr->second->details.count : 1) + (item->details.count > 0 ? item->details.count : 1)) <= slot_itr->second->stack_count)){
 					ret = slot_itr->second;
 					break;
 				}
@@ -3416,9 +3417,10 @@ EQ2Packet* PlayerItemList::serialize(Player* player, int16 version){
 				safe_delete_array(xor_packet);
 				xor_packet = new uchar[packet_size * size];
 			}
-			packet_count = size;
 		}
 		
+		packet_count = size;
+		
 		for(int16 i = 0; i < indexed_items.size(); i++){
 			item = indexed_items[i];
 			if (item && item->details.item_id > 0)
@@ -3574,9 +3576,10 @@ void PlayerItemList::AddItemToPacket(PacketStruct* packet, Player* player, Item*
 	packet->setSubstructArrayDataByName("items", "menu_type", menu_data, 0, i);
 	if (overflow)
 		packet->setSubstructArrayDataByName("items", "index", 0xFFFF, 0, i);
-	else
+	else {
 		packet->setSubstructArrayDataByName("items", "index", i, 0, i);
-	item->details.index = i;
+		item->details.index = i;
+	}
 	packet->setSubstructArrayDataByName("items", "icon", item->details.icon, 0, i);
 	packet->setSubstructArrayDataByName("items", "slot_id", item->details.slot_id, 0, i);
 	if (client->GetVersion() <= 1208) {

+ 8 - 0
EQ2/source/WorldServer/Languages.cpp

@@ -41,7 +41,15 @@ MasterLanguagesList::~MasterLanguagesList(){
 	Clear();
 }
 
+
+// don't bother calling this beyond its deconstructor its not thread-safe
 void MasterLanguagesList::Clear(){
+	list<Language*>::iterator itr;
+	Language* language = 0;
+	for(itr = languages_list.begin(); itr != languages_list.end(); itr++){
+		language = *itr;
+		safe_delete(language);
+	}
 	languages_list.clear();
 }
 

+ 4 - 5
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -4092,6 +4092,7 @@ int EQ2Emu_lua_AddQuestStepZoneLoc(lua_State* state) {
 			i += 4;
 		}
 		QuestStep* quest_step = quest->AddQuestStep(step, QUEST_STEP_TYPE_LOCATION, description, 0, 1, taskgroup, locations, max_variation);
+		safe_delete(locations); // gets duplicated into new table in QuestStep constructor
 		if (quest_step && icon > 0)
 			quest_step->SetIcon(icon);
 		if (quest->GetPlayer()) {
@@ -4134,6 +4135,7 @@ int EQ2Emu_lua_AddQuestStepLocation(lua_State* state) {
 			i += 3;
 		}
 		QuestStep* quest_step = quest->AddQuestStep(step, QUEST_STEP_TYPE_LOCATION, description, 0, 1, taskgroup, locations, max_variation);
+		safe_delete(locations); // gets duplicated into new table in QuestStep constructor
 		if (quest_step && icon > 0)
 			quest_step->SetIcon(icon);
 		if (quest->GetPlayer()) {
@@ -4407,9 +4409,7 @@ int EQ2Emu_lua_GiveQuestReward(lua_State* state) {
 	if (quest && spawn) {
 		if (spawn->IsPlayer()) {
 			Client* client = spawn->GetZone()->GetClientBySpawn(spawn);
-			if (client)
-			{
-				client->AddPendingQuestAcceptReward(quest);
+			if (client) {
 				client->AddPendingQuestReward(quest);
 			}
 		}
@@ -6034,8 +6034,7 @@ int EQ2Emu_lua_GiveQuestItem(lua_State* state)
 				itemsAddedSuccessfully = false;
 		}
 	}
-	client->AddPendingQuestAcceptReward(quest);
-	client->DisplayQuestComplete(quest, true, description);
+	client->AddPendingQuestReward(quest, true, true, description); // queue for display
 	
 	lua_interface->SetBooleanValue(state, itemsAddedSuccessfully);
 	return 1;

+ 7 - 8
EQ2/source/WorldServer/LuaInterface.cpp

@@ -403,9 +403,9 @@ Mutex*  LuaInterface::GetQuestMutex(Quest* quest) {
 	return ret;
 }
 
-void LuaInterface::CallQuestFunction(Quest* quest, const char* function, Spawn* player, int32 step_id) {
+bool LuaInterface::CallQuestFunction(Quest* quest, const char* function, Spawn* player, int32 step_id, int32* returnValue) {
 	if(shutting_down)
-		return;
+		return false;
 	lua_State* state = 0;
 	if(quest){
 		LogWrite(LUA__DEBUG, 0, "LUA", "Quest: %s, function: %s", quest->GetName(), function);
@@ -413,6 +413,7 @@ void LuaInterface::CallQuestFunction(Quest* quest, const char* function, Spawn*
 		mutex->lock();
 		if(quest_states.count(quest->GetQuestID()) > 0)
 			state = quest_states[quest->GetQuestID()];
+		bool success = false; // if no state then we return false
 		if(state){
 			int8 arg_count = 3;
 			lua_getglobal(state, function);
@@ -424,16 +425,14 @@ void LuaInterface::CallQuestFunction(Quest* quest, const char* function, Spawn*
 				SetInt32Value(state, step_id);
 				arg_count++;
 			}
-			if(lua_pcall(state, arg_count, 0, 0) != 0){
-				LogError("%s: Error processing quest function '%s': %s ", GetScriptName(state), function, lua_tostring(state, -1));
-				lua_pop(state, 1);
-				mutex->unlock();
-				return;
-			}
+			
+			success = CallScriptInt32(state, arg_count, returnValue);
 		}
 		mutex->unlock();
 		LogWrite(LUA__DEBUG, 0, "LUA", "Done!");
+		return success;
 	}
+	return false;
 }
 
 Quest* LuaInterface::LoadQuest(int32 id, const char* name, const char* type, const char* zone, int8 level, const char* description, char* script_name) {

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

@@ -276,7 +276,7 @@ public:
 	void			LogError(const char* error, ...);
 
 
-	void			CallQuestFunction(Quest* quest, const char* function, Spawn* player, int32 step_id = 0xFFFFFFFF);
+	bool			CallQuestFunction(Quest* quest, const char* function, Spawn* player, int32 step_id = 0xFFFFFFFF, int32* returnValue = 0);
 	void			RemoveDebugClients(Client* client);
 	void			UpdateDebugClients(Client* client);
 	void			ProcessErrorMessage(const char* message);

+ 25 - 6
EQ2/source/WorldServer/Player.cpp

@@ -124,6 +124,7 @@ Player::Player(){
 	reset_mentorship = false;
 	all_spells_locked = false;
 	current_language_id = 0;
+	active_reward = false;
 }
 Player::~Player(){
 	SetSaveSpellEffects(true);
@@ -703,8 +704,9 @@ EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyVal
 		packet->setDataByName("stat_bonus_damage", 95); //stat_bonus_damage
 		packet->setDataByName("mitigation_cur", info_struct->get_cur_mitigation());// confirmed DoV
 		packet->setDataByName("mitigation_base", info_struct->get_mitigation_base());// confirmed DoV
-		packet->setDataByName("mitigation_pct_pve", 392); // % calculation Mitigation % vs PvE 392 = 39.2%// confirmed DoV
-		packet->setDataByName("mitigation_pct_pvp", 559); // % calculation Mitigation % vs PvP 559 = 55.9%// confirmed DoV
+		
+		packet->setDataByName("mitigation_pct_pve", info_struct->get_mitigation_pve()); // % calculation Mitigation % vs PvE 392 = 39.2%// confirmed DoV
+		packet->setDataByName("mitigation_pct_pvp", info_struct->get_mitigation_pvp()); // % calculation Mitigation % vs PvP 559 = 55.9%// confirmed DoV
 		packet->setDataByName("toughness", 0);//toughness// confirmed DoV
 		packet->setDataByName("toughness_resist_dmg_pvp", 0);//toughness_resist_dmg_pvp 73 = 7300% // confirmed DoV 
 		packet->setDataByName("avoidance_pct", (int16)info_struct->get_avoidance_display()*10.0f);//avoidance_pct 192 = 19.2% // confirmed DoV
@@ -5058,10 +5060,10 @@ map<int32, Quest*>*	Player::GetCompletedPlayerQuests(){
 }
 
 Quest* Player::GetAnyQuest(int32 quest_id) {
-	if(completed_quests.count(quest_id) > 0)
-		return completed_quests[quest_id];
 	if(player_quests.count(quest_id) > 0)
 		return player_quests[quest_id];
+	if(completed_quests.count(quest_id) > 0)
+		return completed_quests[quest_id];
 	
 	return 0;
 }
@@ -5120,7 +5122,12 @@ int8 Player::CheckQuestFlag(Spawn* spawn){
 				}
 			}
 			MPlayerQuests.releasereadlock(__FUNCTION__, __LINE__);
-			if (CanReceiveQuest(quests->at(i))){
+			int8 flag = 0;
+			if (CanReceiveQuest(quests->at(i), &flag)){
+				if(flag) {
+					ret = flag;
+					break;
+				}
 				master_quest_list.LockQuests();
 				quest = master_quest_list.GetQuest(quests->at(i), false);
 				master_quest_list.UnlockQuests();
@@ -5155,7 +5162,7 @@ int8 Player::CheckQuestFlag(Spawn* spawn){
 	return ret;
 }
 
-bool Player::CanReceiveQuest(int32 quest_id){
+bool Player::CanReceiveQuest(int32 quest_id, int8* ret){
 	bool passed = true;
 	int32 x;
 	master_quest_list.LockQuests();
@@ -5266,6 +5273,18 @@ bool Player::CanReceiveQuest(int32 quest_id){
 				passed = false;
 		}
 	}
+	
+	int32 flag = 0;
+	if(lua_interface->CallQuestFunction(quest, "ReceiveQuestCriteria", this, 0xFFFFFFFF, &flag)) {
+		if(ret)
+			*ret = flag;
+		if(!flag) {
+			passed = false;
+		}
+		else {
+			passed = true;
+		}
+	}
 
 	return passed;
 }

+ 7 - 2
EQ2/source/WorldServer/Player.h

@@ -265,7 +265,7 @@ public:
 
 	void RemoveEquipmentUpdates()
 	{
-		appearanceList->clear();
+	 	appearanceList->clear();
 		safe_delete(appearanceList);
 	}
 
@@ -865,7 +865,7 @@ public:
 	PlayerLanguagesList* GetPlayerLanguages() { return &player_languages_list; }
 	bool				HasLanguage(int32 id);
 	bool				HasLanguage(const char* name);
-	bool                CanReceiveQuest(int32 quest_id);
+	bool                CanReceiveQuest(int32 quest_id, int8* ret = 0);
 	float               GetBoatX() { if (info) return info->GetBoatX(); return 0; }
 	float               GetBoatY() { if (info) return info->GetBoatY(); return 0; }
 	float               GetBoatZ() { if (info) return info->GetBoatZ(); return 0; }
@@ -1051,6 +1051,9 @@ public:
 	int32	GetCurrentLanguage() { return current_language_id; }
 	void	SetCurrentLanguage(int32 language_id) { current_language_id = language_id; }
 	
+	void	SetActiveReward(bool val) { active_reward = val; }
+	bool	IsActiveReward() { return active_reward; }
+	
 	Mutex MPlayerQuests;
 	float   pos_packet_speed;
 private:
@@ -1185,6 +1188,8 @@ private:
 	vector<GMTagFilter> gm_visual_filters;
 	
 	int32 current_language_id;
+	
+	bool active_reward;
 };
 #pragma pack()
 #endif

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

@@ -1475,6 +1475,13 @@ void Quest::AddTmpRewardItem(Item* item){
 	tmp_reward_items.push_back(item);
 }
 
+void Quest::GetTmpRewardItemsByID(std::vector<int32>* items) {
+	if(!items)
+		return;
+	for(int32 i=0;i<tmp_reward_items.size();i++)
+		items->push_back(tmp_reward_items[i]->details.item_id);
+}
+
 void Quest::AddRewardCoins(int32 copper, int32 silver, int32 gold, int32 plat){
 	reward_coins = copper + (silver*100) + (gold*10000) + ((int64)plat*1000000);
 }

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

@@ -140,6 +140,7 @@ public:
 
 	void				AddRewardItem(Item* item);
 	void				AddTmpRewardItem(Item* item);
+	void				GetTmpRewardItemsByID(std::vector<int32>* items);
 	void				AddSelectableRewardItem(Item* item);
 	void				AddRewardCoins(int32 copper, int32 silver, int32 gold, int32 plat);
 	void                AddRewardCoinsMax(int64 coins);

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

@@ -224,6 +224,7 @@ void RuleManager::Init()
 	RULE_INIT(R_PVP, AllowPVP, "0");
 	RULE_INIT(R_PVP, LevelRange, "4");
 	RULE_INIT(R_PVP, InvisPlayerDiscoveryRange, "20"); // value > 0 sets radius inner to see, = 0 means always seen, -1 = never seen
+	RULE_INIT(R_PVP, PVPMitigationModByLevel, "25"); // gives a bonus to mitigation for PVP combat to offset the percentage level * mod (default 25)
 
 	/* COMBAT */
 	RULE_INIT(R_Combat, MaxCombatRange, "4.0");
@@ -237,6 +238,12 @@ void RuleManager::Init()
 	RULE_INIT(R_Combat, SpiritShardSpawnScript, "SpawnScripts/Generic/SpiritShard.lua");
 	RULE_INIT(R_Combat, ShardDebtRecoveryPercent, "25.00"); // recovered percentage of debt upon obtainig shard, 25/100 means 25%.  If there is .5 DeathExperienceDebt, .5*25% = .125,  .5 - .125 = .375
 	RULE_INIT(R_Combat, ShardRecoveryByRadius, "1"); // allow shards to auto pick up by radius, not requiring to click/right click the shard
+	RULE_INIT(R_Combat, EffectiveMitigationCapLevel, "80"); // level multiplier for max effective cap, level * 80 (default)
+	RULE_INIT(R_Combat, CalculatedMitigationCapLevel, "100"); // The cap to calculate your mitigation from is [level*100].
+	RULE_INIT(R_Combat, MitigationLevelEffectivenessMax, "1.5"); // ratio victim level / attacker level for max effectiveness, when victim is higher level cap can reach 1.5
+	RULE_INIT(R_Combat, MitigationLevelEffectivenessMin, ".5"); // ratio victim level / attacker level for min effectiveness
+	RULE_INIT(R_Combat, MaxMitigationAllowed, ".75"); // percentage max mitigation allowed, eg. 75% of damage can be mitigated max in PVE
+	RULE_INIT(R_Combat, MaxMitigationAllowedPVP, ".75"); // percentage max mitigation allowed, eg. 75% of damage can be mitigated max in PVP
 
 	/* SPAWN */
 	RULE_INIT(R_Spawn, SpeedMultiplier, "300"); // note: this value was 1280 until 6/1/2009, then was 600 til Sep 2009, when it became 300...?

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

@@ -83,6 +83,7 @@ enum RuleType {
 	AllowPVP,
 	LevelRange,
 	InvisPlayerDiscoveryRange,
+	PVPMitigationModByLevel,
 
 	/* COMBAT */
 	MaxCombatRange,
@@ -96,6 +97,12 @@ enum RuleType {
 	SpiritShardSpawnScript,
 	ShardDebtRecoveryPercent,
 	ShardRecoveryByRadius,
+	EffectiveMitigationCapLevel,
+	CalculatedMitigationCapLevel,
+	MitigationLevelEffectivenessMax,
+	MitigationLevelEffectivenessMin,
+	MaxMitigationAllowed,
+	MaxMitigationAllowedPVP,
 
 	/* SPAWN */
 	SpeedMultiplier,

+ 86 - 0
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -1833,6 +1833,88 @@ SOGA chars looked ok in LoginServer screen tho... odd.
 	return false;
 }
 
+void WorldDatabase::LoadCharacterQuestRewards(Client* client) {
+		Query query;
+	MYSQL_ROW row;
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT indexed, quest_id, is_temporary, is_collection, has_displayed, tmp_coin, tmp_status, description FROM character_quest_rewards where char_id = %u ORDER BY indexed asc", client->GetCharacterID());
+	int8 count = 0;
+	if(result)
+	{
+		while(result && (row = mysql_fetch_row(result)))
+		{
+			int32 index = atoul(row[0]);
+			int32 quest_id = atoul(row[1]);
+
+			bool is_temporary = atoul(row[2]);
+
+			bool is_collection = atoul(row[3]);
+
+			bool has_displayed = atoul(row[4]);
+
+			
+		int64 tmp_coin = 0;
+#ifdef WIN32
+			tmp_coin = _strtoui64(row[5], NULL, 10);
+#else
+			tmp_coin = strtoull(row[5], 0, 10);
+#endif
+
+
+			int32 tmp_status = atoul(row[6]);
+			
+			std::string description = std::string("");
+			
+			if(row[7]) {
+				std::string description = std::string(row[7]);
+			}
+			
+			if(is_collection) { 			
+				map<int32, Collection*>* collections = client->GetPlayer()->GetCollectionList()->GetCollections();
+				map<int32, Collection*>::iterator itr;
+				Collection* collection = 0;
+				for (itr = collections->begin(); itr != collections->end(); itr++) {
+					collection = itr->second;
+				if (collection->GetIsReadyToTurnIn()) {
+						client->GetPlayer()->SetPendingCollectionReward(collection);
+						break;
+					}
+				}
+			}
+			if(is_temporary) {
+				LoadCharacterQuestTemporaryRewards(client, quest_id);
+			}
+			client->QueueQuestReward(quest_id, is_temporary, is_collection, has_displayed, tmp_coin, tmp_status, description, true, index);
+			count++;
+		}
+	}
+	
+	if(count) {
+		client->SetQuestUpdateState(true);
+	}
+}
+
+
+void WorldDatabase::LoadCharacterQuestTemporaryRewards(Client* client, int32 quest_id) {
+		Query query;
+	MYSQL_ROW row;
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT item_id FROM character_quest_temporary_rewards where char_id = %u and quest_id = %u", client->GetCharacterID(), quest_id);
+	int8 count = 0;
+	if(result)
+	{
+		while(result && (row = mysql_fetch_row(result)))
+		{
+			int32 item_id = atoul(row[0]);
+			Quest* quest = client->GetPlayer()->GetAnyQuest(quest_id);
+			if(quest) {
+				Item* item = master_item_list.GetItem(item_id);
+				if(item) {
+					quest->AddTmpRewardItem(new Item(item));
+				}
+			}
+		}
+	}
+}
+
 bool WorldDatabase::InsertCharacterStats(int32 character_id, int8 class_id, int8 race_id){
 	Query query1;
 	Query query2;
@@ -4085,6 +4167,7 @@ void WorldDatabase::Save(Client* client){
 	SavePlayerSpells(client);
 	SavePlayerMail(client);
 	SavePlayerCollections(client);
+	client->SaveQuestRewardData();
 
 	LogWrite(PLAYER__INFO, 3, "Player", "Player '%s' (%u) data saved.", player->GetName(), player->GetCharacterID());
 }
@@ -4924,6 +5007,9 @@ bool WorldDatabase::DeleteCharacter(int32 account_id, int32 character_id){
 	query.RunQuery2(Q_DELETE, "DELETE FROM characters WHERE id=%u AND account_id=%u", character_id, account_id);
 	//delete languages
 	query2.RunQuery2(Q_DELETE, "DELETE FROM character_languages WHERE char_id=%u", character_id);
+	//delete quest rewards
+	query2.RunQuery2(Q_DELETE, "DELETE FROM character_quest_rewards WHERE char_id=%u", character_id);
+	query2.RunQuery2(Q_DELETE, "DELETE FROM character_quest_temporary_rewards WHERE char_id=%u", character_id);
 
 	if(!query.GetAffectedRows())
 	{

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

@@ -296,6 +296,8 @@ public:
 	void	LoadCharacterItemList(int32 account_id, int32 char_id, Player* player, int16);
 	bool	loadCharacter(const char* name, int32 account_id, Client* client);
 	bool	LoadCharacterStats(int32 id, int32 account_id, Client* client);
+	void	LoadCharacterQuestRewards(Client* client);
+	void	LoadCharacterQuestTemporaryRewards(Client* client, int32 quest_id);
 	bool	InsertCharacterStats(int32 character_id, int8 class_id, int8 race_id);
 	bool	UpdateCharacterTimeStamp(int32 account_id, int32 character_id, int32 timestamp);
 	bool	insertCharacterProperty(Client* client, char* propName, char* propValue);

+ 245 - 66
EQ2/source/WorldServer/client.cpp

@@ -402,6 +402,7 @@ void Client::SendLoginInfo() {
 		}
 		database.LoadPlayerFactions(this);
 		database.LoadCharacterQuests(this);
+		database.LoadCharacterQuestRewards(this);
 		database.LoadPlayerMail(this);
 	}
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "Send Quest Journal...");
@@ -5444,12 +5445,73 @@ void Client::AddPendingQuestAcceptReward(Quest* quest)
 	MPendingQuestAccept.unlock();
 }
 
-void Client::AddPendingQuestReward(Quest* quest, bool update) {
-	MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
-	quest_pending_reward.push_back(quest->GetQuestID());
+void Client::AddPendingQuestReward(Quest* quest, bool update, bool is_temporary, std::string description) {
+	QueueQuestReward(quest->GetQuestID(), is_temporary, false, false, (is_temporary ? quest->GetCoinTmpReward() : 0), 
+	(is_temporary ? quest->GetStatusTmpReward() : 0), description, false, 0);
 	quest_updates = update;
+	if(quest_updates) {
+		SaveQuestRewardData(true);
+	}
+
+}
+
+void Client::QueueQuestReward(int32 quest_id, bool is_temporary, bool is_collection, bool has_displayed, int64 tmp_coin, int32 tmp_status, std::string description, bool db_saved, int32 index) {
+	if(HasQuestRewardQueued(quest_id, is_temporary, is_collection))
+		return;
+	
+	QuestRewardData data;
+	data.quest_id = quest_id;
+	data.is_temporary = is_temporary;
+	data.is_collection = is_collection;
+	data.has_displayed = has_displayed;
+	data.tmp_coin = tmp_coin;
+	data.tmp_status = tmp_status;
+	data.description = std::string(description);
+	data.db_saved = db_saved;
+	data.db_index = index;
+	MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
+	quest_pending_reward.push_back(data);
 	MQuestPendingUpdates.releasewritelock(__FUNCTION__, __LINE__);
+}
 
+bool Client::HasQuestRewardQueued(int32 quest_id, bool is_temporary, bool is_collection) {
+	
+	bool success = false;
+	MQuestPendingUpdates.readlock(__FUNCTION__, __LINE__);
+	if (quest_pending_reward.size() > 0) {
+		vector<QuestRewardData>::iterator itr;
+		
+		for (itr = quest_pending_reward.begin(); itr != quest_pending_reward.end(); itr++) {
+			int32 questID = (*itr).quest_id;
+			bool temporary = (*itr).is_temporary;
+			bool collection = (*itr).is_collection;
+			if( questID == quest_id && is_temporary == temporary && is_collection == collection ) {
+				success = true;
+				break;
+			}
+		}
+	}
+	MQuestPendingUpdates.releasereadlock(__FUNCTION__, __LINE__);
+	
+	return success;
+}
+
+void Client::RemoveQueuedQuestReward() {
+	MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
+	if(quest_pending_reward.size() > 0) {
+		QuestRewardData data = quest_pending_reward.at(0);
+		if(data.db_saved) {
+			Query query;
+			query.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete FROM character_quest_rewards where char_id = %u and indexed = %u", GetCharacterID(), data.db_index);
+			if(data.is_temporary && data.quest_id) {
+				query.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete FROM character_quest_temporary_rewards where char_id = %u and quest_id = %u", GetCharacterID(), data.quest_id);
+			}
+		}
+		quest_pending_reward.erase(quest_pending_reward.begin());
+	}
+	MQuestPendingUpdates.releasewritelock(__FUNCTION__, __LINE__);
+	
+	SaveQuestRewardData(true);
 }
 
 void Client::AddPendingQuestUpdate(int32 quest_id, int32 step_id, int32 progress) {
@@ -5461,6 +5523,9 @@ void Client::AddPendingQuestUpdate(int32 quest_id, int32 step_id, int32 progress
 }
 
 void Client::ProcessQuestUpdates() {
+	if(!GetPlayer()->IsFullyLoggedIn())
+		return;
+
 	if (quest_pending_updates.size() > 0) {
 		map<int32, map<int32, int32> > tmp_quest_updates;
 		MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
@@ -5483,17 +5548,41 @@ void Client::ProcessQuestUpdates() {
 	MQuestPendingUpdates.readlock(__FUNCTION__, __LINE__);
 	if (quest_pending_reward.size() > 0) {
 		MQuestPendingUpdates.releasereadlock(__FUNCTION__, __LINE__);
-		vector<int32>::iterator itr;
-		vector<int32> tmp_quest_rewards;
+		
+		// only able to display one reward at a time
+		if(GetPlayer()->IsActiveReward())
+			return;
+		
+		Query query;
+		vector<QuestRewardData>::iterator itr;
+		vector<QuestRewardData> tmp_quest_rewards;
 		MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
-		tmp_quest_rewards.insert(tmp_quest_rewards.begin(), quest_pending_reward.begin(), quest_pending_reward.end());
-		quest_pending_reward.clear();
+		tmp_quest_rewards.insert(tmp_quest_rewards.begin(), quest_pending_reward.begin(), quest_pending_reward.begin()+1);
 		MQuestPendingUpdates.releasewritelock(__FUNCTION__, __LINE__);
+		
 		for (itr = tmp_quest_rewards.begin(); itr != tmp_quest_rewards.end(); itr++) {
-			int32 questID = *itr;
-			Quest* quest = GetPlayer()->GetAnyQuest(questID);
-			if(quest) {
-				GiveQuestReward(quest);
+			int32 questID = (*itr).quest_id;
+			Quest* quest = 0;
+			if((*itr).is_collection && GetPlayer()->GetPendingCollectionReward()) {
+				DisplayCollectionComplete(GetPlayer()->GetPendingCollectionReward());
+				GetPlayer()->SetActiveReward(true);
+				(*itr).has_displayed = true;
+				
+				UpdateCharacterRewardData(&(*itr));
+			}
+			else if(questID > 0 && (quest = GetPlayer()->GetAnyQuest(questID))) {
+				quest->SetQuestTemporaryState((*itr).is_temporary, (*itr).description);
+				if((*itr).is_temporary) {
+					quest->SetStatusTmpReward((*itr).tmp_status);
+					quest->SetCoinTmpReward((*itr).tmp_coin);
+				}
+				GiveQuestReward(quest, (*itr).has_displayed);
+				GetPlayer()->SetActiveReward(true);
+				(*itr).has_displayed = true;
+				
+				UpdateCharacterRewardData(&(*itr));
+				// only able to display one reward at a time
+				break;
 			} else {
 				LogWrite(CCLIENT__ERROR, 0, "Client", "Quest ID %u missing for Player %s, skipping quest id from tmp_quest_rewards.", questID, GetPlayer()->GetName());
 			}
@@ -5501,7 +5590,15 @@ void Client::ProcessQuestUpdates() {
 	} else {
 		MQuestPendingUpdates.releasereadlock(__FUNCTION__, __LINE__);
 	}
-	quest_updates = false;
+	
+	MQuestPendingUpdates.readlock(__FUNCTION__, __LINE__);
+	if (quest_pending_reward.size() > 0) {
+		quest_updates = true;
+	}
+	else {
+		quest_updates = false;
+	}
+	MQuestPendingUpdates.releasereadlock(__FUNCTION__, __LINE__);
 
 }
 
@@ -5961,7 +6058,11 @@ void Client::AcceptQuestReward(Quest* quest, int32 item_id) {
 			totalItems += items->size();
 		}
 	}
-
+	
+	RemoveQueuedQuestReward();
+	
+	GetPlayer()->SetActiveReward(false);
+		
 	if (free_slots >= num_slots_needed || (player->item_list.HasFreeBagSlot() && master_item && master_item->IsBag() && master_item->bag_info->num_slots >= totalItems)) {
 		if (master_item)
 			AddItem(item_id);
@@ -5994,31 +6095,34 @@ void Client::AcceptQuestReward(Quest* quest, int32 item_id) {
 				AwardCoins(total_coins, std::string("for completing ").append(quest->GetName()));
 
 			player->GetInfoStruct()->add_status_points(quest->GetStatusTmpReward());
-			
-			quest->SetQuestTemporaryState(false);
 		}
-		else
-			player->GetInfoStruct()->add_status_points(quest->GetStatusPoints());
+		else {
+			player->GetInfoStruct()->add_status_points(quest->GetStatusPoints());		
+		}
 		
+		quest->SetQuestTemporaryState(false);
 		player->SetCharSheetChanged(true);
 	}
 	else {
-		MPendingQuestAccept.lock();
-		pending_quest_accept.push_back(quest->GetQuestID());
-		MPendingQuestAccept.unlock();
+		GetPlayer()->SetActiveReward(true);
+		AddPendingQuestAcceptReward(quest);
 		SimpleMessage(CHANNEL_COLOR_RED, "You do not have enough free slots!  Free some slots and try again.");
 		DisplayQuestComplete(quest, quest->GetQuestTemporaryState(), quest->GetQuestTemporaryDescription());
 	}
 
 }
 
-void Client::DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* rewards, vector<Item*>* selectable_rewards, map<int32, sint32>* factions, const char* header, int32 status_points, const char* text) {
+void Client::DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* rewards, vector<Item*>* selectable_rewards, map<int32, sint32>* factions, const char* header, int32 status_points, const char* text, bool was_displayed) {
 	if (coin == 0 && (!rewards || rewards->size() == 0) && (!selectable_rewards || selectable_rewards->size() == 0) && (!factions || factions->size() == 0) && status_points == 0 && text == 0 && (!quest || (quest->GetCoinsReward() == 0 && quest->GetCoinsRewardMax() == 0))) {
 		/*if (quest)
 			text = quest->GetName();
 		else*/
 		return;//nothing to give
 	}
+	
+	GetPlayer()->ClearPendingSelectableItemRewards(0, true);
+	GetPlayer()->ClearPendingItemRewards();
+	
 	PacketStruct* packet2 = configReader.getStruct("WS_QuestRewardPackMsg", GetVersion());
 	if (packet2) {
 		int32 source_id = 0;
@@ -6036,7 +6140,7 @@ void Client::DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* reward
 		}
 		if (rewarded_coin > coin)
 			coin = rewarded_coin;
-		if (!quest) { //this entire function is either for version <=546 or for quest rewards in middle of quest, so quest should be 0, otherwise quest will handle the rewards
+		if (!quest && !was_displayed) { //this entire function is either for version <=546 or for quest rewards in middle of quest, so quest should be 0, otherwise quest will handle the rewards
 			if (coin > 0) {
 				player->AddCoins(coin);
 				PlaySound("coin_cha_ching");
@@ -6045,7 +6149,7 @@ void Client::DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* reward
 		packet2->setSubstructDataByName("reward_data", "unknown1", 255);
 		packet2->setSubstructDataByName("reward_data", "reward", header);
 		packet2->setSubstructDataByName("reward_data", "max_coin", coin);
-		if (player->GetGuild()) {
+		if (player->GetGuild() && !was_displayed) {
 			if (!quest) { //this entire function is either for version <=546 or for quest rewards in middle of quest, so quest should be 0, otherwise quest will handle the rewards
 				player->GetInfoStruct()->add_status_points(status_points);
 				player->SetCharSheetChanged(true);
@@ -6107,16 +6211,12 @@ void Client::DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* reward
 	}
 }
 
-void Client::DisplayQuestComplete(Quest* quest, bool tempReward, std::string customDescription) {	
+void Client::DisplayQuestComplete(Quest* quest, bool tempReward, std::string customDescription, bool was_displayed) {	
 	if (!quest)
 		return;
 	
-	quest->SetQuestTemporaryState(tempReward, customDescription);
-	
-	AddPendingQuestAcceptReward(quest);
-	
 	if (GetVersion() <= 546) {
-		DisplayQuestRewards(quest, 0, quest->GetRewardItems(), quest->GetSelectableRewardItems(), quest->GetRewardFactions(), "Quest Complete!", quest->GetStatusPoints());
+		DisplayQuestRewards(quest, 0, quest->GetRewardItems(), quest->GetSelectableRewardItems(), quest->GetRewardFactions(), "Quest Complete!", quest->GetStatusPoints(), customDescription.c_str(), was_displayed);
 		return;
 	}
 	PacketStruct* packet = configReader.getStruct("WS_QuestComplete", GetVersion());
@@ -6289,51 +6389,59 @@ void Client::DisplayRandomizeFeatures(int32 flags) {
 
 }
 
-void Client::GiveQuestReward(Quest* quest) {
+void Client::GiveQuestReward(Quest* quest, bool has_displayed) {
 	current_quest_id = 0;
 
-	if(!quest->GetQuestTemporaryState())
+	if(!quest->GetQuestTemporaryState() && !has_displayed)
 	{
 		quest->IncrementCompleteCount();
 		player->AddCompletedQuest(quest);
 	}
 	
+	AddPendingQuestAcceptReward(quest);
+		
 	DisplayQuestComplete(quest, quest->GetQuestTemporaryState(), quest->GetQuestTemporaryDescription());
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "Send Quest Journal...");
 	SendQuestJournal();
 	
-	if(quest->GetQuestTemporaryState())
+	if(quest->GetQuestTemporaryState()) {
 		return;
-	
-	player->RemoveQuest(quest->GetQuestID(), false);
-	if (quest->GetExpReward() > 0) {
-		int16 level = player->GetLevel();
-		int32 xp = quest->GetExpReward();
-		if (player->AddXP(xp)) {
-			Message(CHANNEL_REWARD, "You gain %u experience!", (int32)xp);
-			if (player->GetLevel() != level)
-				ChangeLevel(level, player->GetLevel());
-			player->SetCharSheetChanged(true);
-		}
-	}
-	if (quest->GetTSExpReward() > 0) {
-		int8 ts_level = player->GetTSLevel();
-		int32 xp = quest->GetTSExpReward();
-		if (player->AddTSXP(xp)) {
-			Message(CHANNEL_REWARD, "You gain %u tradeskill experience!", (int32)xp);
-			if (player->GetTSLevel() != ts_level)
-				ChangeTSLevel(ts_level, player->GetTSLevel());
-			player->SetCharSheetChanged(true);
-		}
-	}
-	int64 total_coins = quest->GetGeneratedCoin();
-	if (total_coins > 0)
-		AwardCoins(total_coins, std::string("for completing ").append(quest->GetName()));
+	}
+
+	if(!has_displayed) {
+		if (quest->GetExpReward() > 0) {
+			int16 level = player->GetLevel();
+			int32 xp = quest->GetExpReward();
+			if (player->AddXP(xp)) {
+				Message(CHANNEL_REWARD, "You gain %u experience!", (int32)xp);
+				if (player->GetLevel() != level)
+					ChangeLevel(level, player->GetLevel());
+				player->SetCharSheetChanged(true);
+			}
+		}
+		if (quest->GetTSExpReward() > 0) {
+			int8 ts_level = player->GetTSLevel();
+			int32 xp = quest->GetTSExpReward();
+			if (player->AddTSXP(xp)) {
+				Message(CHANNEL_REWARD, "You gain %u tradeskill experience!", (int32)xp);
+				if (player->GetTSLevel() != ts_level)
+					ChangeTSLevel(ts_level, player->GetTSLevel());
+				player->SetCharSheetChanged(true);
+			}
+		}
+		int64 total_coins = quest->GetGeneratedCoin();
+		if (total_coins > 0)
+			AwardCoins(total_coins, std::string("for completing ").append(quest->GetName()));
+		
+		player->RemoveQuest(quest->GetQuestID(), false);
+	}
 	
 	if (quest->GetQuestGiver() > 0)
 		GetCurrentZone()->SendSpawnChangesByDBID(quest->GetQuestGiver(), this, false, true);
 	
-	RemovePlayerQuest(quest->GetQuestID(), true, false);	
+	if(!has_displayed) {
+		RemovePlayerQuest(quest->GetQuestID(), true, false);	
+	}
 }
 
 void Client::DisplayConversation(int32 conversation_id, int32 spawn_id, vector<ConversationOption>* conversations, const char* text, const char* mp3, int32 key1, int32 key2, int8 language, int8 can_close) {
@@ -8805,6 +8913,7 @@ void Client::SetReadyForSpawns(bool val) {
 			world.GetGroupManager()->GroupMessage(GetPlayer()->GetGroupMemberInfo()->group_id, "%s has returned from Linkdead.", GetPlayer()->GetName());
 		}
 	}
+	GetPlayer()->SetActiveReward(false);
 	zone_list.CheckFriendZoned(this);
 
 }
@@ -9043,9 +9152,8 @@ void Client::InspectPlayer(Player* player_to_inspect) {
 			packet->setDataByName("gender", player_to_inspect->GetGender());
 			packet->setDataByName("adventure_level", player_to_inspect->GetLevel());
 
-			LogWrite(MISC__TODO, 1, "TODO", "Put mentored level here (adventure_level_effective)\nfile: %s, func: %s, line: %i", __FILE__, __FUNCTION__, __LINE__);
-
-			packet->setDataByName("adventure_level_effective", player_to_inspect->GetLevel());
+			int16 effective_level = player_to_inspect->GetInfoStruct()->get_effective_level() != 0 ? player_to_inspect->GetInfoStruct()->get_effective_level() : player_to_inspect->GetLevel();
+			packet->setDataByName("adventure_level_effective", effective_level);
 			packet->setDataByName("adventure_class", player_to_inspect->GetAdventureClass());
 			packet->setDataByName("tradeskill_level", player_to_inspect->GetTSLevel());
 			packet->setDataByName("tradeskill_class", player_to_inspect->GetTradeskillClass());
@@ -9543,11 +9651,26 @@ void Client::HandInCollections() {
 		collection = itr->second;
 		if (collection->GetIsReadyToTurnIn()) {
 			player->SetPendingCollectionReward(collection);
-			DisplayCollectionComplete(collection);
-
-			return;
+			MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
+			QuestRewardData data;
+			data.quest_id = 0;
+			data.is_temporary = false;
+			data.description = std::string("");
+			data.is_collection = true;
+			data.has_displayed = false;
+			data.tmp_coin = 0;
+			data.tmp_status = 0;
+			data.db_saved = false;
+			data.db_index = 0;
+			quest_pending_reward.push_back(data);
+			MQuestPendingUpdates.releasewritelock(__FUNCTION__, __LINE__);
+			quest_updates = true;
+			break;
 		}
 	}
+	if(quest_updates) {
+		SaveQuestRewardData(true);
+	}
 }
 
 void Client::AcceptCollectionRewards(Collection* collection, int32 selectable_item_id) {
@@ -9567,8 +9690,7 @@ void Client::AcceptCollectionRewards(Collection* collection, int32 selectable_it
 	num_slots = player->GetPlayerItemList()->GetNumberOfFreeSlots();
 	if (num_slots < num_slots_needed) {
 		SimpleMessage(CHANNEL_COLOR_RED, "You do not have enough free slots. Free up some slots and try again");
-		HandInCollections();
-
+		DisplayCollectionComplete(collection);
 		return;
 	}
 
@@ -9606,6 +9728,11 @@ void Client::AcceptCollectionRewards(Collection* collection, int32 selectable_it
 
 	/* reset the pending collection reward and check for my collections that the player needs to hand in */
 	player->SetPendingCollectionReward(0);
+	
+	RemoveQueuedQuestReward();
+	
+	GetPlayer()->SetActiveReward(false);
+	
 	HandInCollections();
 
 }
@@ -11041,4 +11168,56 @@ void Client::SendPlayFlavor(Spawn* spawn, int8 language, VoiceOverStruct* non_ga
 		if(resStruct) {
 			GetPlayer()->GetZone()->PlayFlavor(this, spawn, resStruct->mp3_string.c_str(), resStruct->text_string.c_str(), resStruct->emote_string.c_str(), resStruct->key1, resStruct->key2, language);
 		}
+}
+
+void Client::SaveQuestRewardData(bool force_refresh) {
+		Query query;
+		if(force_refresh) {
+			query.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_quest_rewards where char_id = %u", 
+							GetCharacterID());
+							
+			query.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_quest_temporary_rewards where char_id = %u", 
+							GetCharacterID());
+		}
+		vector<QuestRewardData>::iterator itr;
+		vector<QuestRewardData> tmp_quest_rewards;
+		MQuestPendingUpdates.writelock(__FUNCTION__, __LINE__);
+		int index = 0;
+		for (itr = quest_pending_reward.begin(); itr != quest_pending_reward.end(); itr++) {
+			int32 questID = (*itr).quest_id;
+			if(!(*itr).db_saved || force_refresh) {
+				query.AddQueryAsync(GetCharacterID(), &database, Q_REPLACE, "replace into character_quest_rewards (char_id, indexed, quest_id, is_temporary, is_collection, has_displayed, tmp_coin, tmp_status, description) values(%u, %u, %u, %u, %u, %u, %I64u, %u, '%s')", 
+					GetCharacterID(), index, questID, (*itr).is_temporary, (*itr).is_collection, (*itr).has_displayed, (*itr).tmp_coin, (*itr).tmp_status, database.getSafeEscapeString((*itr).description.c_str()).c_str());
+				(*itr).db_saved = true;
+				(*itr).db_index = index;
+				if((*itr).is_temporary) {
+					std::vector<int32> items;
+					Quest* quest = GetPlayer()->GetAnyQuest(questID);
+					if(quest) {
+						quest->GetTmpRewardItemsByID(&items);
+						if(!force_refresh && items.size() > 0) {
+							query.AddQueryAsync(GetCharacterID(), &database, Q_REPLACE, "delete from character_quest_temporary_rewards where char_id = %u and quest_id = %u", 
+								GetCharacterID(), questID);
+						}
+						for(int i=0;i<items.size();i++) {
+							query.AddQueryAsync(GetCharacterID(), &database, Q_REPLACE, "replace into character_quest_temporary_rewards (char_id, quest_id, item_id) values(%u, %u, %u)", 
+								GetCharacterID(), questID, items[i]);
+						}
+					}
+				}
+			}
+			index++;
+		}
+		MQuestPendingUpdates.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+void Client::UpdateCharacterRewardData(QuestRewardData* data) {
+	
+	if(!data)
+		return;
+	if(data->db_saved) {
+		Query query;
+		query.AddQueryAsync(GetCharacterID(), &database, Q_INSERT, "update character_quest_rewards set is_temporary = %u, is_collection = %u, has_displayed = %u, tmp_coin = %I64u, tmp_status = %u, description = '%s' where char_id=%u and indexed=%u and quest_id=%u", 
+			data->is_temporary, data->is_collection, data->has_displayed, data->tmp_coin, data->tmp_status, database.getSafeEscapeString(data->description.c_str()).c_str(), GetCharacterID(), data->db_index, data->quest_id);
+	}
 }

+ 23 - 5
EQ2/source/WorldServer/client.h

@@ -142,6 +142,18 @@ struct WaypointInfo {
 	int8 type;
 };
 
+struct QuestRewardData {
+	int32 quest_id;
+	bool is_temporary;
+	std::string description;
+	bool is_collection;
+	bool has_displayed;
+	int64 tmp_coin;
+	int32 tmp_status;
+	bool db_saved;
+	int32 db_index;
+};
+
 class Client {
 public:
 	Client(EQStream* ieqs);
@@ -280,8 +292,8 @@ public:
 	void	SendQuestFailure(Quest* quest);
 	void	SendQuestUpdateStep(Quest* quest, int32 step, bool display_quest_helper = true);
 	void	SendQuestUpdateStepImmediately(Quest* quest, int32 step, bool display_quest_helper = true);
-	void	DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* rewards=0, vector<Item*>* selectable_rewards=0, map<int32, sint32>* factions=0, const char* header="Quest Reward!", int32 status_points=0, const char* text=0);
-	void	DisplayQuestComplete(Quest* quest, bool tempReward = false, std::string customDescription = string(""));
+	void	DisplayQuestRewards(Quest* quest, int64 coin, vector<Item*>* rewards=0, vector<Item*>* selectable_rewards=0, map<int32, sint32>* factions=0, const char* header="Quest Reward!", int32 status_points=0, const char* text=0, bool was_displayed = false);
+	void	DisplayQuestComplete(Quest* quest, bool tempReward = false, std::string customDescription = string(""), bool was_displayed = false);
 	void	DisplayRandomizeFeatures(int32 features);
 	void	AcceptQuestReward(Quest* quest, int32 item_id);
 	Quest*	GetPendingQuestAcceptance(int32 item_id);
@@ -348,7 +360,10 @@ public:
 	void	SetPlayer(Player* new_player);
 
 	void	AddPendingQuestAcceptReward(Quest* quest);
-	void	AddPendingQuestReward(Quest* quest, bool update=true);
+	void	AddPendingQuestReward(Quest* quest, bool update=true, bool is_temporary = false, std::string description = std::string(""));
+	bool	HasQuestRewardQueued(int32 quest_id, bool is_temporary, bool is_collection);
+	void	QueueQuestReward(int32 quest_id, bool is_temporary, bool is_collection, bool has_displayed, int64 tmp_coin, int32 tmp_status, std::string description, bool db_saved=false, int32 index=0);
+	void	RemoveQueuedQuestReward();
 	void	AddPendingQuestUpdate(int32 quest_id, int32 step_id, int32 progress = 0xFFFFFFFF);
 	void	ProcessQuestUpdates();	
 	void	AddWaypoint(const char* waypoint_name, int8 waypoint_category, int32 spawn_id);
@@ -543,15 +558,18 @@ public:
 	bool	UseItem(Item* item, Spawn* target = nullptr);
 	
 	void	SendPlayFlavor(Spawn* spawn, int8 language, VoiceOverStruct* non_garble, VoiceOverStruct* garble, bool success = false, bool garble_success = false);
+	void	SaveQuestRewardData(bool force_refresh = false);
+	void	UpdateCharacterRewardData(QuestRewardData* data);
+	void	SetQuestUpdateState(bool val) { quest_updates = val; }
 private:
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
-	void	GiveQuestReward(Quest* quest);
+	void	GiveQuestReward(Quest* quest, bool has_displayed = false);
 	void	SetStepComplete(int32 quest_id, int32 step);
 	void	AddStepProgress(int32 quest_id, int32 step, int32 progress);
 	map<int32, map<int32, int32> > quest_pending_updates;
 	vector<QueuedQuest*> quest_queue;
-	vector<int32> quest_pending_reward;
+	vector<QuestRewardData> quest_pending_reward;
 	volatile bool	quest_updates;
 	Mutex	MQuestPendingUpdates;
 	Mutex	MQuestQueue;