Browse Source

Spirit Shards, Group and Solo EXP Debt (PVE/PVP), new lua functions, rules included

xp_debt in InfoStruct changed to float use SetInfoStructFloat and GetInfoStructFloat for it

SpawnScript file is now listed in /spawn details on the last page

LUA functions added:
bool GetRuleFlagBool(category, name)
float GetRuleFlagFloat(category, name)
int32 GetShardID(Spawn)
int32 GetShardCharID(Spawn)
int64 GetShardCreatedTimestamp(Spawn)
bool DeleteDBShardID(shardid) -- using GetShardID
int32 GetCharacterID(Spawn)
bool SetAccessToEntityCommandByCharID(Spawn, CharID, command_string, val) -- same as SetAccessToEntityCommand, but using a CharID instead of player

Rules added:

RULE_INIT(R_Combat, DeathExperienceDebt, "50.00"); // divide by 100, 50/100 = .5% debt per pve death
RULE_INIT(R_Combat, PVPDeathExperienceDebt, "25.00"); // divide by 100, 25/100 = .25% debt per pvp death
RULE_INIT(R_Combat, GroupExperienceDebt, "0"); // set to 1 means we will share debt between the group
RULE_INIT(R_Combat, ExperienceToDebt, "50.00"); // percentage of xp earned to debt vs obtained xp 50/100 = 50% to debt
RULE_INIT(R_Combat, ExperienceDebtRecoveryPercent, "5.00"); // recovery percentage per period of time, 5/100 = 5% recovered (so if .5% debt, .5*.05 = .025, .5-.025=.475% debt left)
RULE_INIT(R_Combat, ExperienceDebtRecoveryPeriod, "600"); // every 10 minutes (x*60 seconds) recover ExperienceDebtRecoveryPercent
RULE_INIT(R_Combat, EnableSpiritShards, "1");
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
Image 3 years ago
parent
commit
65e7222de5

+ 41 - 0
DB/updates/spiritshards_xpdebt_feb7_2021.sql

@@ -0,0 +1,41 @@
+alter table character_details modify column xp_debt float not null default 0.0;
+CREATE TABLE `character_spirit_shards` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `timestamp` BIGINT signed NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `name` varchar(64) not null default '',
+  `level` int(10) unsigned NOT NULL DEFAULT 0,
+  `race` tinyint(3) unsigned NOT NULL DEFAULT 0,
+  `gender` tinyint(3) unsigned NOT NULL DEFAULT 0,
+  `adventure_class` tinyint(3) unsigned NOT NULL DEFAULT 0,
+  `model_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `soga_model_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `hair_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `hair_face_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `wing_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `chest_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `legs_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `soga_hair_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `soga_hair_face_type` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `hide_hood` tinyint(3) unsigned NOT NULL DEFAULT 0,
+  `size` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `collision_radius` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `action_state` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `visual_state` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `mood_state` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `emote_state` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `pos_state` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `activity_status` mediumint(8) unsigned NOT NULL DEFAULT 0,
+  `sub_title` varchar(255) not null default '',
+  `prefix_title` varchar(128) not null default '',
+  `suffix_title` varchar(128) not null default '',
+  `lastname` varchar(64) not null default '',
+  `x` float not null default 0.0,
+  `y` float not null default 0.0,
+  `z` float not null default 0.0,
+  `heading` float not null default 0.0,
+  `gridid` int(10) unsigned NOT NULL DEFAULT 0,
+  `zoneid` int(10) unsigned NOT NULL DEFAULT 0,
+  `instanceid` int(10) unsigned NOT NULL DEFAULT 0,
+  `charid` int(10) unsigned NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;

+ 47 - 0
EQ2/source/WorldServer/Combat.cpp

@@ -30,6 +30,7 @@
 #include "LuaInterface.h"
 #include "Rules/Rules.h"
 #include "SpellProcess.h"
+#include "World.h"
 #include <math.h>
 
 extern Classes classes;
@@ -37,6 +38,7 @@ extern ConfigReader configReader;
 extern MasterSkillList master_skill_list;
 extern RuleManager rule_manager;
 extern LuaInterface* lua_interface;
+extern World world;
 
 /* ******************************************************************************
 
@@ -1183,6 +1185,51 @@ void Entity::KillSpawn(Spawn* dead, int8 damage_type, int16 kill_blow_type) {
 	GetZone()->KillSpawn(true, dead, this, true, damage_type, kill_blow_type);
 }
 
+void Entity::HandleDeathExperienceDebt(Spawn* killer)
+{
+	if(!IsPlayer())
+		return;
+
+	float ruleDebt = 0.0f;
+	
+	if(killer && killer->IsPlayer())
+		ruleDebt = rule_manager.GetGlobalRule(R_Combat, PVPDeathExperienceDebt)->GetFloat()/100.0f;
+	else
+		ruleDebt = rule_manager.GetGlobalRule(R_Combat, DeathExperienceDebt)->GetFloat()/100.0f;
+
+	if(ruleDebt > 0.0f)
+	{
+		bool groupDebt = rule_manager.GetGlobalRule(R_Combat, GroupExperienceDebt)->GetBool();
+		if(groupDebt && ((Player*)this)->GetGroupMemberInfo())
+		{
+			world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__);
+			PlayerGroup* group = world.GetGroupManager()->GetGroup(((Player*)this)->GetGroupMemberInfo()->group_id);
+			if (group)
+			{
+				group->MGroupMembers.readlock(__FUNCTION__, __LINE__);
+				deque<GroupMemberInfo*>* members = group->GetMembers();
+				int32 size = members->size();
+				float xpDebtPerMember = ruleDebt/(float)size;
+				deque<GroupMemberInfo*>::iterator itr;
+				for (itr = members->begin(); itr != members->end(); itr++) {
+					GroupMemberInfo* gmi = *itr;
+					if (gmi->client && gmi->client->GetPlayer()) {
+						gmi->client->GetPlayer()->GetInfoStruct()->set_xp_debt(gmi->client->GetPlayer()->GetInfoStruct()->get_xp_debt()+xpDebtPerMember);
+						gmi->client->GetPlayer()->SetCharSheetChanged(true);
+					}
+				}
+				group->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__);
+			}
+			world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
+		}
+		else
+		{
+			((Player*)this)->GetInfoStruct()->set_xp_debt(((Player*)this)->GetInfoStruct()->get_xp_debt()+ruleDebt);
+			((Player*)this)->SetCharSheetChanged(true);
+		}
+	}
+}
+
 void Entity::ProcessCombat() {
 	// This is a virtual function so when a NPC calls this it will use the NPC::ProcessCombat() version
 	// and a player will use the Player::ProcessCombat() version, leave this function blank.

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

@@ -3830,6 +3830,10 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 					if (spawn->IsNPC()) {
 						details4 += "\nRandomize:		" + to_string(((NPC*)spawn)->GetRandomize()) + "\n";
 					}
+
+					const char* spawnScriptMsg = (spawn->GetSpawnScript() && strlen(spawn->GetSpawnScript())>0) ? spawn->GetSpawnScript() : "Not Set";
+
+						details4 += "\nSpawnScript:		" + std::string(spawnScriptMsg) + "\n";
 				}
 
 				string title = string(spawn->GetName()) + "(" + to_string(spawn->GetDatabaseID()) + ")";

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

@@ -178,7 +178,7 @@ void Entity::MapInfoStruct()
 	get_int16_funcs["absorb"] = l::bind(&InfoStruct::get_absorb, &info_struct);
 	get_int32_funcs["xp"] = l::bind(&InfoStruct::get_xp, &info_struct);
 	get_int32_funcs["xp_needed"] = l::bind(&InfoStruct::get_xp_needed, &info_struct);
-	get_int32_funcs["xp_debt"] = l::bind(&InfoStruct::get_xp_debt, &info_struct);
+	get_float_funcs["xp_debt"] = l::bind(&InfoStruct::get_xp_debt, &info_struct);
 	get_int16_funcs["xp_yellow"] = l::bind(&InfoStruct::get_xp_yellow, &info_struct);
 	get_int16_funcs["xp_yellow_vitality_bar"] = l::bind(&InfoStruct::get_xp_yellow_vitality_bar, &info_struct);
 	get_int16_funcs["xp_blue_vitality_bar"] = l::bind(&InfoStruct::get_xp_blue_vitality_bar, &info_struct);
@@ -318,7 +318,7 @@ void Entity::MapInfoStruct()
 	set_int16_funcs["absorb"] = l::bind(&InfoStruct::set_absorb, &info_struct, l::_1);
 	set_int32_funcs["xp"] = l::bind(&InfoStruct::set_xp, &info_struct, l::_1);
 	set_int32_funcs["xp_needed"] = l::bind(&InfoStruct::set_xp_needed, &info_struct, l::_1);
-	set_int32_funcs["xp_debt"] = l::bind(&InfoStruct::set_xp_debt, &info_struct, l::_1);
+	set_float_funcs["xp_debt"] = l::bind(&InfoStruct::set_xp_debt, &info_struct, l::_1);
 	set_int16_funcs["xp_yellow"] = l::bind(&InfoStruct::set_xp_yellow, &info_struct, l::_1);
 	set_int16_funcs["xp_yellow_vitality_bar"] = l::bind(&InfoStruct::set_xp_yellow_vitality_bar, &info_struct, l::_1);
 	set_int16_funcs["xp_blue_vitality_bar"] = l::bind(&InfoStruct::set_xp_blue_vitality_bar, &info_struct, l::_1);

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

@@ -191,7 +191,7 @@ struct InfoStruct{
 		absorb_ = 0;
 		xp_ = 0;
 		xp_needed_ = 0;
-		xp_debt_ = 0;
+		xp_debt_ = 0.0f;
 		xp_yellow_ = 0;
 		xp_yellow_vitality_bar_ = 0;
 		xp_blue_vitality_bar_ = 0;
@@ -489,7 +489,7 @@ struct InfoStruct{
 	int16	 get_absorb() { std::lock_guard<std::mutex> lk(classMutex); return absorb_; }
 	int32	 get_xp() { std::lock_guard<std::mutex> lk(classMutex); return xp_; }
 	int32	 get_xp_needed() { std::lock_guard<std::mutex> lk(classMutex); return xp_needed_; }
-	int32	 get_xp_debt() { std::lock_guard<std::mutex> lk(classMutex); return xp_debt_; }
+	float	 get_xp_debt() { std::lock_guard<std::mutex> lk(classMutex); return xp_debt_; }
 	int16	 get_xp_yellow() { std::lock_guard<std::mutex> lk(classMutex); return xp_yellow_; }
 	int16	 get_xp_yellow_vitality_bar() { std::lock_guard<std::mutex> lk(classMutex); return xp_yellow_vitality_bar_; }
 	int16	 get_xp_blue_vitality_bar() { std::lock_guard<std::mutex> lk(classMutex); return xp_blue_vitality_bar_; }
@@ -658,7 +658,7 @@ struct InfoStruct{
 	void	set_xp(int32 value) { std::lock_guard<std::mutex> lk(classMutex); xp_ = value; }
 	void	set_xp_needed(int32 value) { std::lock_guard<std::mutex> lk(classMutex); xp_needed_ = value; }
 
-	void	set_xp_debt(int32 value) { std::lock_guard<std::mutex> lk(classMutex); xp_debt_ = value; }
+	void	set_xp_debt(float value) { std::lock_guard<std::mutex> lk(classMutex); xp_debt_ = value; }
 
 	void	set_xp_yellow(int16 value) { std::lock_guard<std::mutex> lk(classMutex); xp_yellow_ = value; }
 	void	set_xp_blue(int16 value) { std::lock_guard<std::mutex> lk(classMutex); xp_blue_ = value; }
@@ -891,7 +891,7 @@ private:
 	int16			absorb_;
 	int32			xp_;
 	int32			xp_needed_;
-	int32			xp_debt_;
+	float			xp_debt_;
 	int16			xp_yellow_;
 	int16			xp_yellow_vitality_bar_;
 	int16			xp_blue_vitality_bar_;
@@ -1194,6 +1194,7 @@ public:
 	void			AddHate(Entity* attacker, sint32 hate);
 	bool			CheckInterruptSpell(Entity* attacker);
 	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);
 	float           CalculateAttackSpeedMod();
 	virtual void	ProcessCombat();

+ 148 - 2
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -1007,6 +1007,19 @@ int EQ2Emu_lua_IsPlayer(lua_State* state) {
 	return 0;
 }
 
+int EQ2Emu_lua_GetCharacterID(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	Spawn* spawn = lua_interface->GetSpawn(state);
+	if (spawn && spawn->IsPlayer()) {
+		lua_interface->SetInt32Value(state, ((Player*)spawn)->GetCharacterID());
+		return 1;
+	}
+
+	lua_interface->SetInt32Value(state, 0);
+	return 1;
+}
+
 int EQ2Emu_lua_FaceTarget(lua_State* state) {
 	if (!lua_interface)
 		return 0;
@@ -10667,6 +10680,7 @@ int EQ2Emu_lua_SetAccessToEntityCommand(lua_State* state)
 	string command = lua_interface->GetStringValue(state, 3);
 	bool val = (lua_interface->GetInt8Value(state, 4) == 1);
 
+	lua_interface->ResetFunctionStack(state);
 	if (spawn && player && player->IsPlayer())
 	{
 		EntityCommand* cmd = spawn->FindEntityCommand(string(command), true);
@@ -10681,6 +10695,32 @@ int EQ2Emu_lua_SetAccessToEntityCommand(lua_State* state)
 	return 0;
 }
 
+
+int EQ2Emu_lua_SetAccessToEntityCommandByCharID(lua_State* state)
+{
+	if (!lua_interface)
+		return 0;
+
+	Spawn* spawn = lua_interface->GetSpawn(state);
+	int32 charID = lua_interface->GetInt32Value(state, 2);
+	string command = lua_interface->GetStringValue(state, 3);
+	bool val = (lua_interface->GetInt8Value(state, 4) == 1);
+
+	lua_interface->ResetFunctionStack(state);
+	if (spawn && charID)
+	{
+		EntityCommand* cmd = spawn->FindEntityCommand(string(command), true);
+		bool res = false;
+		if (cmd)
+			res = spawn->SetPermissionToEntityCommandByCharID(cmd, charID, val);
+
+		lua_interface->SetBooleanValue(state, res);
+		return 1;
+	}
+
+	return 0;
+}
+
 int EQ2Emu_lua_RemovePrimaryEntityCommand(lua_State* state)
 {
 	if (!lua_interface)
@@ -10689,6 +10729,7 @@ int EQ2Emu_lua_RemovePrimaryEntityCommand(lua_State* state)
 	Spawn* spawn = lua_interface->GetSpawn(state);
 	string command = lua_interface->GetStringValue(state, 2);
 
+	lua_interface->ResetFunctionStack(state);
 	if (spawn && command.length() > 0)
 		spawn->RemovePrimaryEntityCommand(command.c_str());
 
@@ -10705,6 +10746,7 @@ int EQ2Emu_lua_SendUpdateDefaultCommand(lua_State* state) {
 	string command = lua_interface->GetStringValue(state, 3);
 	Spawn* player = lua_interface->GetSpawn(state, 4);
 
+	lua_interface->ResetFunctionStack(state);
 	if (spawn) {
 		spawn->GetZone()->SendUpdateDefaultCommand(spawn, command.c_str(), distance, player);
 	}
@@ -10719,6 +10761,7 @@ int EQ2Emu_lua_SendTransporters(lua_State* state) {
 	Spawn* player = lua_interface->GetSpawn(state, 2);
 	int32 transport_id = lua_interface->GetInt32Value(state, 3);
 
+	lua_interface->ResetFunctionStack(state);
 	if (spawn && player && transport_id && player->IsPlayer()) {
 		Client* client = 0;
 		if (player && player->IsPlayer())
@@ -10748,6 +10791,7 @@ int EQ2Emu_lua_SetTemporaryTransportID(lua_State* state) {
 	Spawn* player = lua_interface->GetSpawn(state);
 	int32 transport_id = lua_interface->GetInt32Value(state, 2);
 
+	lua_interface->ResetFunctionStack(state);
 	if (player && player->IsPlayer()) {
 		Client* client = 0;
 		if (player && player->IsPlayer())
@@ -10765,6 +10809,8 @@ int EQ2Emu_lua_GetTemporaryTransportID(lua_State* state) {
 	if (!lua_interface)
 		return 0;
 	Spawn* player = lua_interface->GetSpawn(state);
+
+	lua_interface->ResetFunctionStack(state);
 	if (player && player->IsPlayer()) {
 		Client* client = 0;
 		if (player && player->IsPlayer())
@@ -10803,6 +10849,8 @@ int EQ2Emu_lua_SetAlignment(lua_State* state) {
 		return 0;
 	}
 
+	lua_interface->ResetFunctionStack(state);
+
 	if (spell && spell->targets.size() > 0) {
 		ZoneServer* zone = spell->caster->GetZone();
 		for (int8 i = 0; i < spell->targets.size(); i++) {
@@ -10827,6 +10875,8 @@ int EQ2Emu_lua_GetAlignment(lua_State* state) {
 		return 0;
 	Spawn* spawn = lua_interface->GetSpawn(state);
 
+	lua_interface->ResetFunctionStack(state);
+	
 	if (!spawn) {
 		lua_interface->LogError("%s: LUA GetAlignment command error: spawn is not valid", lua_interface->GetScriptName(state));
 		return 0;
@@ -11264,6 +11314,23 @@ int EQ2Emu_lua_SetInvulnerable(lua_State* state) {
 	return 0;
 }
 
+int EQ2Emu_lua_GetRuleFlagBool(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	string category = lua_interface->GetStringValue(state);
+	string name = lua_interface->GetStringValue(state, 2);
+	lua_interface->ResetFunctionStack(state);
+	Rule *ret = 0;
+	if ((ret = rule_manager.GetGlobalRule(category.c_str(), name.c_str()))) {
+		
+		lua_interface->SetBooleanValue(state, ret->GetBool());
+		return 1;
+	}
+	
+	lua_interface->LogError("%s: LUA GetRuleFlagBool Unknown rule with category '%s' and type '%s'", lua_interface->GetScriptName(state), category.c_str(), name.c_str());
+	return 0;
+}
+
 int EQ2Emu_lua_GetRuleFlagInt32(lua_State* state) {
 	if (!lua_interface)
 		return 0;
@@ -11277,7 +11344,24 @@ int EQ2Emu_lua_GetRuleFlagInt32(lua_State* state) {
 		return 1;
 	}
 	
-	lua_interface->LogError("%s: LUA GetRuleFlag Unknown rule with category '%s' and type '%s'", lua_interface->GetScriptName(state), category.c_str(), name.c_str());
+	lua_interface->LogError("%s: LUA GetRuleFlagInt32 Unknown rule with category '%s' and type '%s'", lua_interface->GetScriptName(state), category.c_str(), name.c_str());
+	return 0;
+}
+
+int EQ2Emu_lua_GetRuleFlagFloat(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	string category = lua_interface->GetStringValue(state);
+	string name = lua_interface->GetStringValue(state, 2);
+	lua_interface->ResetFunctionStack(state);
+	Rule *ret = 0;
+	if ((ret = rule_manager.GetGlobalRule(category.c_str(), name.c_str()))) {
+		
+		lua_interface->SetFloatValue(state, ret->GetFloat());
+		return 1;
+	}
+	
+	lua_interface->LogError("%s: LUA GetRuleFlagFloat Unknown rule with category '%s' and type '%s'", lua_interface->GetScriptName(state), category.c_str(), name.c_str());
 	return 0;
 }
 
@@ -11824,6 +11908,8 @@ int EQ2Emu_lua_IsOpen(lua_State* state) {
 	if (!lua_interface)
 		return 0;
 	Spawn* widget = lua_interface->GetSpawn(state);
+	lua_interface->ResetFunctionStack(state);
+
 	if (widget && widget->IsWidget())
 	{
 		lua_interface->SetBooleanValue(state, ((Widget*)widget)->IsOpen());
@@ -11838,6 +11924,8 @@ int EQ2Emu_lua_MakeRandomInt(lua_State* state) {
 
 	sint32 min = lua_interface->GetSInt32Value(state);
 	sint32 max = lua_interface->GetSInt32Value(state, 2);
+	lua_interface->ResetFunctionStack(state);
+
 	sint32 result = MakeRandomInt(min, max);
 	lua_interface->SetSInt32Value(state, result);
 	return 1;
@@ -11849,6 +11937,8 @@ int EQ2Emu_lua_MakeRandomFloat(lua_State* state) {
 
 	float min = lua_interface->GetFloatValue(state);
 	float max = lua_interface->GetFloatValue(state, 2);
+	lua_interface->ResetFunctionStack(state);
+
 	float result = MakeRandomFloat(min, max);
 	lua_interface->SetFloatValue(state, result);
 	return 1;
@@ -11896,4 +11986,60 @@ int EQ2Emu_lua_RemoveIconValue(lua_State* state) {
 	lua_interface->SetBooleanValue(state, true);
 
 	return 1;
-}
+}
+
+int EQ2Emu_lua_GetShardID(lua_State* state) {
+	Spawn* npc = lua_interface->GetSpawn(state);
+	lua_interface->ResetFunctionStack(state);
+
+	if (npc && npc->IsNPC()) {
+		NPC* shard = (NPC*)npc;
+		int32 shardid = shard->GetShardID();
+		lua_interface->SetInt32Value(state, shardid);
+		return 1;
+	}
+	lua_interface->SetInt32Value(state, 0);
+	return 1;
+}
+
+int EQ2Emu_lua_GetShardCharID(lua_State* state) {
+	Spawn* npc = lua_interface->GetSpawn(state);
+	lua_interface->ResetFunctionStack(state);
+
+	if (npc && npc->IsNPC()) {
+		NPC* shard = (NPC*)npc;
+		int32 charid = shard->GetShardCharID();
+		lua_interface->SetInt32Value(state, charid);
+		return 1;
+	}
+	lua_interface->SetInt32Value(state, 0);
+	return 1;
+}
+
+int EQ2Emu_lua_GetShardCreatedTimestamp(lua_State* state) {
+	Spawn* npc = lua_interface->GetSpawn(state);
+	lua_interface->ResetFunctionStack(state);
+
+	if (npc && npc->IsNPC()) {
+		NPC* shard = (NPC*)npc;
+		int64 timestamp = shard->GetShardCreatedTimestamp();
+		lua_interface->SetSInt64Value(state, timestamp);
+		return 1;
+	}
+	lua_interface->SetSInt64Value(state, 0);
+	return 1;
+}
+
+int EQ2Emu_lua_DeleteDBShardID(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	int32 shardid = lua_interface->GetInt32Value(state);
+
+	lua_interface->ResetFunctionStack(state);
+
+	if(shardid < 1)
+		lua_interface->SetBooleanValue(state, false);
+	else
+		lua_interface->SetBooleanValue(state, database.DeleteSpiritShard(shardid));
+	return 1;
+}

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

@@ -183,6 +183,7 @@ int EQ2Emu_lua_Shout(lua_State* state);
 int EQ2Emu_lua_SayOOC(lua_State* state);
 int EQ2Emu_lua_Emote(lua_State* state);
 int EQ2Emu_lua_IsPlayer(lua_State* state);
+int EQ2Emu_lua_GetCharacterID(lua_State* state);
 int EQ2Emu_lua_MovementLoopAdd(lua_State* state);
 int EQ2Emu_lua_GetCurrentZoneSafeLocation(lua_State* state);
 int EQ2Emu_lua_PlayFlavor(lua_State* state);
@@ -497,6 +498,7 @@ int EQ2Emu_lua_SetSeeInvis(lua_State* state);
 int EQ2Emu_lua_SetSeeHide(lua_State* state);
 
 int EQ2Emu_lua_SetAccessToEntityCommand(lua_State* state);
+int EQ2Emu_lua_SetAccessToEntityCommandByCharID(lua_State* state);
 int EQ2Emu_lua_RemovePrimaryEntityCommand(lua_State* state);
 int EQ2Emu_lua_SendUpdateDefaultCommand(lua_State* state);
 
@@ -527,7 +529,9 @@ int EQ2Emu_lua_DamageSpawn(lua_State* state);
 int EQ2Emu_lua_IsInvulnerable(lua_State* state);
 int EQ2Emu_lua_SetInvulnerable(lua_State* state);
 
+int EQ2Emu_lua_GetRuleFlagBool(lua_State* state);
 int EQ2Emu_lua_GetRuleFlagInt32(lua_State* state);
+int EQ2Emu_lua_GetRuleFlagFloat(lua_State* state);
 
 int EQ2Emu_lua_GetAAInfo(lua_State* state);
 int EQ2Emu_lua_SetAAInfo(lua_State* state);
@@ -563,4 +567,9 @@ int EQ2Emu_lua_MakeRandomFloat(lua_State* state);
 
 int EQ2Emu_lua_AddIconValue(lua_State* state);
 int EQ2Emu_lua_RemoveIconValue(lua_State* state);
+
+int EQ2Emu_lua_GetShardID(lua_State* state);
+int EQ2Emu_lua_GetShardCharID(lua_State* state);
+int EQ2Emu_lua_GetShardCreatedTimestamp(lua_State* state);
+int EQ2Emu_lua_DeleteDBShardID(lua_State* state);
 #endif

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

@@ -843,6 +843,7 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	
 	
 	lua_register(state, "IsPlayer", EQ2Emu_lua_IsPlayer);
+	lua_register(state, "GetCharacterID", EQ2Emu_lua_GetCharacterID);
 	lua_register(state, "FaceTarget", EQ2Emu_lua_FaceTarget);
 	lua_register(state, "MoveToLocation", EQ2Emu_lua_MoveToLocation);
 	lua_register(state, "ClearRunningLocations", EQ2Emu_lua_ClearRunningLocations);
@@ -1213,6 +1214,7 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	lua_register(state, "SetSeeHide", EQ2Emu_lua_SetSeeHide);
 
 	lua_register(state, "SetAccessToEntityCommand", EQ2Emu_lua_SetAccessToEntityCommand);
+	lua_register(state, "SetAccessToEntityCommandByCharID", EQ2Emu_lua_SetAccessToEntityCommandByCharID);
 	lua_register(state, "RemovePrimaryEntityCommand", EQ2Emu_lua_RemovePrimaryEntityCommand);
 	lua_register(state, "SendUpdateDefaultCommand", EQ2Emu_lua_SendUpdateDefaultCommand);
 
@@ -1241,7 +1243,9 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	lua_register(state, "IsInvulnerable", EQ2Emu_lua_IsInvulnerable);
 	lua_register(state, "SetInvulnerable", EQ2Emu_lua_SetInvulnerable);
 	
+	lua_register(state, "GetRuleFlagBool", EQ2Emu_lua_GetRuleFlagBool);
 	lua_register(state, "GetRuleFlagInt32", EQ2Emu_lua_GetRuleFlagInt32);
+	lua_register(state, "GetRuleFlagFloat", EQ2Emu_lua_GetRuleFlagFloat);
 	
 	lua_register(state, "GetAAInfo", EQ2Emu_lua_GetAAInfo);
 	lua_register(state, "SetAAInfo", EQ2Emu_lua_SetAAInfo);
@@ -1277,6 +1281,11 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	
 	lua_register(state, "AddIconValue", EQ2Emu_lua_AddIconValue);
 	lua_register(state, "RemoveIconValue", EQ2Emu_lua_RemoveIconValue);
+	
+	lua_register(state, "GetShardID", EQ2Emu_lua_GetShardID);
+	lua_register(state, "GetShardCharID", EQ2Emu_lua_GetShardCharID);
+	lua_register(state, "GetShardCreatedTimestamp", EQ2Emu_lua_GetShardCreatedTimestamp);
+	lua_register(state, "DeleteDBShardID", EQ2Emu_lua_DeleteDBShardID);
 }
 
 void LuaInterface::LogError(const char* error, ...)  {

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

@@ -143,6 +143,9 @@ void NPC::Initialize(){
 	following = false;
 	SetFollowTarget(0);
 	m_petDismissing = false;
+	m_ShardID = 0;
+	m_ShardCharID = 0;
+	m_ShardCreatedTimestamp = 0;
 }
 
 EQ2Packet* NPC::serialize(Player* player, int16 version){

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

@@ -146,6 +146,14 @@ public:
 	bool IsDismissing() { return m_petDismissing; }
 	void SetDismissing(bool val) { m_petDismissing = val; }
 
+	int32 GetShardID() { return m_ShardID; }
+	void SetShardID(int32 shardid) { m_ShardID = shardid; }
+
+	int32 GetShardCharID() { return m_ShardCharID; }
+	void SetShardCharID(int32 charid) { m_ShardCharID = charid; }
+
+	sint64 GetShardCreatedTimestamp() { return m_ShardCreatedTimestamp; }
+	void SetShardCreatedTimestamp(sint64 timestamp) { m_ShardCreatedTimestamp = timestamp; }
 private:
 	MovementLocation* runback;
 	int8	cast_percentage;
@@ -177,6 +185,10 @@ private:
 	// the brain class and not the function defined above
 	::Brain*	m_brain;
 	Mutex		MBrain;
+
+	int32		m_ShardID;
+	int32		m_ShardCharID;
+	sint64		m_ShardCreatedTimestamp;
 };
 #endif
 

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

@@ -759,7 +759,19 @@ EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyVal
 		CalculateXPPercentages();
 		packet->setDataByName("current_adv_xp", info_struct->get_xp()); // confirmed DoV
 		packet->setDataByName("needed_adv_xp", info_struct->get_xp_needed());// confirmed DoV
-		packet->setDataByName("debt_adv_xp", info_struct->get_xp_debt());//95= 9500% //confirmed DoV
+
+		if(version >= 60114)
+		{
+			// AoM ends up the debt_adv_xp field is the percentage of xp to the next level needed to advance out of debt (WHYY CANT THIS JUST BE A PERCENTAGE LIKE DOV!)
+			float currentPctOfLevel = (float)info_struct->get_xp() / (float)info_struct->get_xp_needed();
+			float neededPctAdvanceOutOfDebt = currentPctOfLevel + (info_struct->get_xp_debt() / 100.0f);
+			packet->setDataByName("debt_adv_xp", neededPctAdvanceOutOfDebt);
+		}
+		else
+		{
+			packet->setDataByName("exp_debt", (int16)(info_struct->get_xp_debt()/10.0f));//95= 9500% //confirmed DoV
+		}
+		
 		packet->setDataByName("current_trade_xp", info_struct->get_ts_xp());// confirmed DoV
 		packet->setDataByName("needed_trade_xp", info_struct->get_ts_xp_needed());// confirmed DoV
 
@@ -1431,9 +1443,12 @@ EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyVal
 			}
 		}
 		player->GetDetrimentMutex()->releasereadlock(__FUNCTION__, __LINE__);
-		packet->setDataByName("spirit_rank", 2);
-		packet->setDataByName("spirit", 1);
-		packet->setDataByName("spirit_progress", .67);
+
+		// disabling as not in use right now
+		//packet->setDataByName("spirit_rank", 2);
+		//packet->setDataByName("spirit", 1);
+		//packet->setDataByName("spirit_progress", .67);
+
 		packet->setDataByName("combat_exp_enabled", 1);
 		/*for (int i = 0; i < 12; i++) {
 			packet->setSubstructDataByName("spell_effects", "spell_id", i + 1, i);
@@ -3777,6 +3792,35 @@ float Player::CalculateTSXP(int8 level){
 	return total * world.GetXPRate() * zone_xp_modifier;
 }
 
+void Player::CalculateOfflineDebtRecovery(int32 unix_timestamp)
+{
+	float xpDebt = GetXPDebt();
+	// not a real timestamp to work with
+	if(unix_timestamp < 1 || xpDebt == 0.0f)
+		return;
+
+	uint32 diff = (Timer::GetCurrentTime2() - unix_timestamp)/1000;
+	
+	float recoveryDebtPercentage = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPercent)->GetFloat()/100.0f;
+	int32 recoveryPeriodSeconds = rule_manager.GetGlobalRule(R_Combat, ExperienceDebtRecoveryPeriod)->GetInt32();
+	if(recoveryDebtPercentage == 0.0f || recoveryPeriodSeconds < 1)
+		return;
+
+
+	float periodsPassed = (float)diff/(float)recoveryPeriodSeconds;
+
+	// not enough time passed to calculate debt xp recovered
+	if(periodsPassed < 1.0f)
+		return;
+
+	float debtToSubtract = xpDebt * ((recoveryDebtPercentage*periodsPassed)/100.0f);
+
+	if(debtToSubtract >= xpDebt)
+		GetInfoStruct()->set_xp_debt(0.0f);
+	else
+		GetInfoStruct()->set_xp_debt(xpDebt - debtToSubtract);
+}
+
 void Player::SetNeededXP(int32 val){
 	GetInfoStruct()->set_xp_needed(val);
 }
@@ -3808,7 +3852,7 @@ void Player::SetTSXP(int32 val) {
 	GetInfoStruct()->set_ts_xp(val);
 }
 
-int32 Player::GetXPDebt(){
+float Player::GetXPDebt(){
 	return GetInfoStruct()->get_xp_debt();
 }
 
@@ -3830,9 +3874,38 @@ int32 Player::GetTSXP() {
 
 bool Player::AddXP(int32 xp_amount){
 	MStats.lock();
-	xp_amount += ((xp_amount) * stats[ITEM_STAT_COMBATEXPMOD]) / 100;
+	xp_amount += (int32)(((float)xp_amount) * stats[ITEM_STAT_COMBATEXPMOD]) / 100;
 	MStats.unlock();
 
+	if(GetInfoStruct()->get_xp_debt())
+	{
+		float expRatioToDebt = rule_manager.GetGlobalRule(R_Combat, ExperienceToDebt)->GetFloat()/100.0f;
+		int32 amountToTakeFromDebt = (int32)((float)expRatioToDebt * (float)xp_amount);
+		int32 amountRequiredClearDebt = (GetInfoStruct()->get_xp_debt()/100.0f) * xp_amount;
+
+		if(amountToTakeFromDebt > amountRequiredClearDebt)
+		{
+			GetInfoStruct()->set_xp_debt(0.0f);
+			if(amountRequiredClearDebt > xp_amount)
+				xp_amount = 0;
+			else
+				xp_amount -= amountRequiredClearDebt;
+		}
+		else
+		{
+			float amountRemovedPct = ((float)amountToTakeFromDebt/(float)amountRequiredClearDebt);
+			GetInfoStruct()->set_xp_debt(GetInfoStruct()->get_xp_debt()-amountRemovedPct);
+			if(amountToTakeFromDebt > xp_amount)
+				xp_amount = 0;
+			else
+				xp_amount -= amountToTakeFromDebt;
+		}
+	}
+	
+	// used up in xp debt
+	if(!xp_amount)
+		return true;
+
 	float current_xp_percent = ((float)GetXP()/(float)GetNeededXP())*100;
 	float miniding_min_percent = ((int)(current_xp_percent/10)+1)*10;
 	while((xp_amount + GetXP()) >= GetNeededXP()){
@@ -6155,4 +6228,82 @@ void Player::SetSpawnMap(Spawn* spawn)
 
 	player_spawn_reverse_id_map.insert(make_pair(spawn,tmp_id));
 	index_mutex.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+NPC* Player::InstantiateSpiritShard(float origX, float origY, float origZ, float origHeading, int32 origGridID, ZoneServer* origZone)
+{
+		NPC* npc = new NPC();
+		string newName(GetName());
+		newName.append("'s spirit shard");
+
+		strcpy(npc->appearance.name, newName.c_str());
+		/*vector<EntityCommand*>* primary_command_list = zone->GetEntityCommandList(result.GetInt32(9));
+		vector<EntityCommand*>* secondary_command_list = zone->GetEntityCommandList(result.GetInt32(10));
+		if(primary_command_list){
+			npc->SetPrimaryCommands(primary_command_list);
+			npc->primary_command_list_id = result.GetInt32(9);
+		}
+		if(secondary_command_list){
+			npc->SetSecondaryCommands(secondary_command_list);
+			npc->secondary_command_list_id = result.GetInt32(10);
+		}*/
+		npc->appearance.level =	GetLevel();
+		npc->appearance.race = GetRace();
+		npc->appearance.gender = GetGender();
+		npc->appearance.adventure_class = GetAdventureClass();
+		 
+		//npc->appearance.lua_race_id = result.GetInt16(74);
+		npc->appearance.model_type = GetModelType();
+		npc->appearance.soga_model_type = GetSogaModelType();
+		npc->appearance.display_name = 1;
+		npc->features.hair_type = GetHairType();
+		npc->features.hair_face_type = GetFacialHairType();
+		npc->features.wing_type = GetWingType();
+		npc->features.chest_type = GetChestType();
+		npc->features.legs_type = GetLegsType();
+		npc->features.soga_hair_type = GetSogaHairType();
+		npc->features.soga_hair_face_type = GetSogaFacialHairType();
+		npc->appearance.attackable = 0;
+		npc->appearance.show_level = 1;
+		npc->appearance.targetable = 1;
+		npc->appearance.show_command_icon = 1;
+		npc->appearance.display_hand_icon = 0;
+		npc->appearance.hide_hood = GetHideHood();
+		npc->size = GetSize();
+		npc->appearance.pos.collision_radius = appearance.pos.collision_radius;
+		npc->appearance.action_state = appearance.action_state;
+		npc->appearance.visual_state = 6193; // ghostly look
+		npc->appearance.mood_state = appearance.mood_state;
+		npc->appearance.emote_state = appearance.emote_state;
+		npc->appearance.pos.state = appearance.pos.state;
+		npc->appearance.activity_status = appearance.activity_status;
+		strncpy(npc->appearance.sub_title, appearance.sub_title, sizeof(npc->appearance.sub_title));
+		npc->SetPrefixTitle(GetPrefixTitle());
+		npc->SetSuffixTitle(GetSuffixTitle());
+		npc->SetLastName(GetLastName());
+		npc->SetX(origX);
+		npc->SetY(origY);
+		npc->SetZ(origZ);
+		npc->SetHeading(origHeading);
+		npc->SetSpawnOrigX(origX);
+		npc->SetSpawnOrigY(origY);
+		npc->SetSpawnOrigZ(origZ);
+		npc->SetSpawnOrigHeading(origHeading);
+		npc->appearance.pos.grid_id = origGridID;
+		const char* script = rule_manager.GetGlobalRule(R_Combat, SpiritShardSpawnScript)->GetString();
+
+		int32 dbid = database.CreateSpiritShard(newName.c_str(), GetLevel(), GetRace(), GetGender(), GetAdventureClass(), GetModelType(), GetSogaModelType(), 
+		GetHairType(), GetFacialHairType(), GetWingType(), GetChestType(), GetLegsType(), GetSogaHairType(), GetSogaFacialHairType(), GetHideHood(),
+		GetSize(), npc->appearance.pos.collision_radius, npc->appearance.action_state, npc->appearance.visual_state, npc->appearance.mood_state, 
+		npc->appearance.emote_state, npc->appearance.pos.state, npc->appearance.activity_status, npc->appearance.sub_title, GetPrefixTitle(), GetSuffixTitle(),
+		GetLastName(), origX, origY, origZ, origHeading, origGridID, GetCharacterID(), origZone->GetZoneID(), origZone->GetInstanceID());
+
+		npc->SetShardID(dbid);
+		npc->SetShardCharID(GetCharacterID());
+		npc->SetShardCreatedTimestamp(Timer::GetCurrentTime2());
+		
+		if(script)
+			npc->SetSpawnScript(script);
+		
+		return npc;
 }

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

@@ -530,7 +530,7 @@ public:
 	void	SetNeededTSXP();
 	void	SetTSXP(int32 val);
 	int32	GetNeededXP();
-	int32	GetXPDebt();
+	float	GetXPDebt();
 	int32	GetXP();
 	int32	GetNeededTSXP();
 	int32	GetTSXP();
@@ -539,6 +539,7 @@ public:
 	bool	DoubleXPEnabled();
 	float	CalculateXP(Spawn* victim);
 	float	CalculateTSXP(int8 level);
+	void	CalculateOfflineDebtRecovery(int32 unix_timestamp);
 	void	InCombat(bool val, bool range = false);
 	void	PrepareIncomingMovementPacket(int32 len, uchar* data, int16 version);
 	uchar*	GetMovementPacketData(){
@@ -956,6 +957,7 @@ public:
 		}
 	}
 
+	NPC* InstantiateSpiritShard(float origX, float origY, float origZ, float origHeading, int32 origGridID, ZoneServer* origZone);
 
 
 

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

@@ -218,6 +218,17 @@ void RuleManager::Init()
 
 	/* COMBAT */
 	RULE_INIT(R_Combat, MaxCombatRange, "4.0");
+	RULE_INIT(R_Combat, DeathExperienceDebt, "50.00"); // divide by 100, 50/100 = .5% debt per pve death
+	RULE_INIT(R_Combat, PVPDeathExperienceDebt, "25.00"); // divide by 100, 25/100 = .25% debt per pvp death
+	RULE_INIT(R_Combat, GroupExperienceDebt, "0"); // set to 1 means we will share debt between the group
+	RULE_INIT(R_Combat, ExperienceToDebt, "50.00"); // percentage of xp earned to debt vs obtained xp 50/100 = 50% to debt
+	RULE_INIT(R_Combat, ExperienceDebtRecoveryPercent, "5.00"); // recovery percentage per period of time, 5/100 = 5% recovered (so if .5% debt, .5*.05 = .025, .5-.025=.475% debt left)
+	RULE_INIT(R_Combat, ExperienceDebtRecoveryPeriod, "600"); // every 10 minutes (x*60 seconds) recover ExperienceDebtRecoveryPercent
+	RULE_INIT(R_Combat, EnableSpiritShards, "1");
+	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
+
 	/* 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...?
 	RULE_INIT(R_Spawn, ClassicRegen, "0");

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

@@ -78,6 +78,16 @@ enum RuleType {
 
 	/* COMBAT */
 	MaxCombatRange,
+	DeathExperienceDebt,
+	GroupExperienceDebt,
+	PVPDeathExperienceDebt,
+	ExperienceToDebt,
+	ExperienceDebtRecoveryPercent,
+	ExperienceDebtRecoveryPeriod,
+	EnableSpiritShards,
+	SpiritShardSpawnScript,
+	ShardDebtRecoveryPercent,
+	ShardRecoveryByRadius,
 
 	/* SPAWN */
 	SpeedMultiplier,

+ 14 - 11
EQ2/source/WorldServer/Spawn.cpp

@@ -3849,18 +3849,21 @@ void Spawn::RemovePrimaryEntityCommand(const char* command) {
 
 bool Spawn::SetPermissionToEntityCommand(EntityCommand* command, Player* player, bool permissionValue)
 {
-	if (player != NULL)
-	{
-		map<int32, bool>::iterator itr = command->allow_or_deny.find(player->GetCharacterID());
-		if (itr == command->allow_or_deny.end())
-			command->allow_or_deny.insert(make_pair(player->GetCharacterID(), permissionValue));
-		else if (itr->second != permissionValue)
-			itr->second = permissionValue;
-
-		return true;
-	}
+	if(!player)
+		return false;
+	
+	return SetPermissionToEntityCommandByCharID(command, player->GetCharacterID(), permissionValue);
+}
 
-	return false;
+bool Spawn::SetPermissionToEntityCommandByCharID(EntityCommand* command, int32 charID, bool permissionValue)
+{
+	map<int32, bool>::iterator itr = command->allow_or_deny.find(charID);
+	if (itr == command->allow_or_deny.end())
+		command->allow_or_deny.insert(make_pair(charID, permissionValue));
+	else if (itr->second != permissionValue)
+		itr->second = permissionValue;
+	
+	return true;
 }
 
 void Spawn::RemoveSpawnFromPlayer(Player* player)

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

@@ -314,6 +314,7 @@ public:
 	void AddPrimaryEntityCommand(const char* name, float distance, const char* command, const char* error_text, int16 cast_time, int32 spell_visual, bool defaultDenyList = false, Player* player = NULL);
 	void RemovePrimaryEntityCommand(const char* command);
 	bool SetPermissionToEntityCommand(EntityCommand* command, Player* player, bool permissionValue);
+	bool SetPermissionToEntityCommandByCharID(EntityCommand* command, int32 charID, bool permissionValue);
 
 	void RemoveSpawnFromPlayer(Player* player);
 

+ 154 - 2
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -1064,6 +1064,112 @@ void WorldDatabase::LoadNPCs(ZoneServer* zone){
 	LogWrite(NPC__INFO, 0, "NPC", "--Loaded %i NPC Equipment Appearance(s).", LoadNPCAppearanceEquipmentData(zone));	
 }
 
+
+void WorldDatabase::LoadSpiritShards(ZoneServer* zone){
+	Query query;
+	MYSQL_ROW row;
+	NPC* npc = 0;
+	int32 id = 0;
+	int32 total = 0;
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT,"SELECT timestamp, name, level, race, gender, adventure_class, model_type, soga_model_type, hair_type, hair_face_type, wing_type, chest_type, legs_type, soga_hair_type, soga_hair_face_type, hide_hood, size, collision_radius, action_state, visual_state, mood_state, emote_state, pos_state, activity_status, sub_title, prefix_title, suffix_title, lastname, x, y, z, heading, gridid, id, charid\n"
+													"FROM character_spirit_shards\n"
+													"WHERE zoneid = %u and (instanceid = 0 or instanceid = %u)",
+													zone->GetZoneID(), zone->GetInstanceID());
+	while(result && (row = mysql_fetch_row(result))){
+		/*npc->SetAppearanceID(atoi(row[12]));
+		AppearanceData* appearance = world.GetNPCAppearance(npc->GetAppearanceID());
+		if(appearance)
+		memcpy(&npc->appearance, appearance, sizeof(AppearanceData));
+		*/
+		sint64 timestamp = 0;
+#ifdef WIN32
+			timestamp = _strtoui64(row[0], NULL, 10);
+#else
+			timestamp = strtoull(row[0], 0, 10);
+#endif
+		
+		if(!row[1])
+			continue;
+
+		NPC* shard = new NPC();
+		
+		shard->SetShardCreatedTimestamp(timestamp);
+		strcpy(shard->appearance.name, row[1]);
+
+		shard->appearance.level =	atoul(row[2]);
+		shard->appearance.race = atoul(row[3]);
+		shard->appearance.gender = atoul(row[4]);
+		shard->appearance.adventure_class = atoul(row[5]);
+		 
+		//shard->appearance.lua_race_id = result.GetInt16(74);
+		shard->appearance.model_type = atoul(row[6]);
+		shard->appearance.soga_model_type = atoul(row[7]);
+		shard->appearance.display_name = 1;
+		shard->features.hair_type = atoul(row[8]);
+		shard->features.hair_face_type = atoul(row[9]);
+		shard->features.wing_type = atoul(row[10]);
+		shard->features.chest_type = atoul(row[11]);
+		shard->features.legs_type = atoul(row[12]);
+		shard->features.soga_hair_type = atoul(row[13]);
+		shard->features.soga_hair_face_type = atoul(row[14]);
+		shard->appearance.attackable = 0;
+		shard->appearance.show_level = 1;
+		shard->appearance.targetable = 1;
+		shard->appearance.show_command_icon = 1;
+		shard->appearance.display_hand_icon = 0;
+		shard->appearance.hide_hood = atoul(row[15]);
+		shard->size = atoul(row[16]);
+		shard->appearance.pos.collision_radius = atoul(row[17]);
+		shard->appearance.action_state = atoul(row[18]);
+		shard->appearance.visual_state = atoul(row[19]); // ghostly look
+		shard->appearance.mood_state = atoul(row[20]);
+		shard->appearance.emote_state = atoul(row[21]);
+		shard->appearance.pos.state = atoul(row[22]);
+		shard->appearance.activity_status = atoul(row[23]);
+
+		if(row[24])
+			strncpy(shard->appearance.sub_title, row[24], sizeof(shard->appearance.sub_title));
+
+		if(row[25])
+			shard->SetPrefixTitle(row[25]);
+
+		if(row[26])
+			shard->SetSuffixTitle(row[26]);
+
+		if(row[27])
+			shard->SetLastName(row[27]);
+
+		shard->SetX(atof(row[28]));
+		shard->SetY(atof(row[29]));
+		shard->SetZ(atof(row[30]));
+		shard->SetHeading(atof(row[31]));
+		shard->SetSpawnOrigX(shard->GetX());
+		shard->SetSpawnOrigY(shard->GetY());
+		shard->SetSpawnOrigZ(shard->GetZ());
+		shard->SetSpawnOrigHeading(shard->GetHeading());
+		shard->appearance.pos.grid_id = atoul(row[32]);
+		shard->SetShardID(atoul(row[33]));
+		shard->SetShardCharID(atoul(row[34]));
+
+		const char* script = rule_manager.GetGlobalRule(R_Combat, SpiritShardSpawnScript)->GetString();
+
+		if(script)
+		{
+			shard->SetSpawnScript(script);
+			zone->CallSpawnScript(shard, SPAWN_SCRIPT_PRESPAWN);
+		}
+		
+		zone->AddSpawn(shard);
+
+		if(script)
+			zone->CallSpawnScript(shard, SPAWN_SCRIPT_SPAWN);
+		
+		total++;
+		LogWrite(NPC__DEBUG, 5, "NPC", "---Loading Player Spirit Shard: '%s' (%u)", npc->appearance.name, id);
+	}
+	LogWrite(NPC__INFO, 0, "NPC", "--Loaded %i Spirit Shard(s).", total);
+}
+
 void WorldDatabase::LoadSigns(ZoneServer* zone){
 	Query query;
 	MYSQL_ROW row;
@@ -1513,7 +1619,7 @@ bool WorldDatabase::LoadCharacterStats(int32 id, int32 account_id, Client* clien
 			if(info->get_xp_needed()== 0)
 				client->GetPlayer()->SetNeededXP();
 
-			info->set_xp_debt(result.GetInt32Str("xp_debt"));
+			info->set_xp_debt(result.GetFloatStr("xp_debt"));
 			info->set_xp_vitality(result.GetFloatStr("xp_vitality"));
 			info->set_ts_xp(result.GetInt32Str("tradeskill_xp"));
 			info->set_ts_xp_needed(result.GetInt32Str("tradeskill_xp_needed"));
@@ -3810,7 +3916,7 @@ void WorldDatabase::Save(Client* client){
 	if(client->GetCurrentZone())
 		zone_id = client->GetCurrentZone()->GetZoneID();
 	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE, "update characters set current_zone_id=%u, x=%f, y=%f, z=%f, heading=%f, level=%i,instance_id=%i,last_saved=%i, `class`=%i, `tradeskill_level`=%i, `tradeskill_class`=%i, `group_id`=%u where id = %u", zone_id, player->GetX(), player->GetY(), player->GetZ(), player->GetHeading(), player->GetLevel(), instance_id, client->GetLastSavedTimeStamp(), client->GetPlayer()->GetAdventureClass(), client->GetPlayer()->GetTSLevel(), client->GetPlayer()->GetTradeskillClass(), client->GetPlayer()->GetGroupMemberInfo() ? client->GetPlayer()->GetGroupMemberInfo()->group_id : 0, client->GetCharacterID());
-	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE, "update character_details set hp=%u, power=%u, str=%i, sta=%i, agi=%i, wis=%i, intel=%i, heat=%i, cold=%i, magic=%i, mental=%i, divine=%i, disease=%i, poison=%i, coin_copper=%u, coin_silver=%u, coin_gold=%u, coin_plat=%u, max_hp = %u, max_power=%u, xp = %u, xp_needed = %u, xp_debt = %u, xp_vitality = %f, tradeskill_xp = %u, tradeskill_xp_needed = %u, tradeskill_xp_vitality = %f, bank_copper = %u, bank_silver = %u, bank_gold = %u, bank_plat = %u, bind_zone_id=%u, bind_x = %f, bind_y = %f, bind_z = %f, bind_heading = %f, house_zone_id=%u, combat_voice = %i, emote_voice = %i, biography='%s', flags=%u, flags2=%u, last_name='%s', assigned_aa = %i, unassigned_aa = %i, tradeskill_aa = %i, unassigned_tradeskill_aa = %i, prestige_aa = %i, unassigned_prestige_aa = %i, tradeskill_prestige_aa = %i, unassigned_tradeskill_prestige_aa = %i where char_id = %u",
+	query.AddQueryAsync(client->GetCharacterID(), this, Q_UPDATE, "update character_details set hp=%u, power=%u, str=%i, sta=%i, agi=%i, wis=%i, intel=%i, heat=%i, cold=%i, magic=%i, mental=%i, divine=%i, disease=%i, poison=%i, coin_copper=%u, coin_silver=%u, coin_gold=%u, coin_plat=%u, max_hp = %u, max_power=%u, xp = %u, xp_needed = %u, xp_debt = %f, xp_vitality = %f, tradeskill_xp = %u, tradeskill_xp_needed = %u, tradeskill_xp_vitality = %f, bank_copper = %u, bank_silver = %u, bank_gold = %u, bank_plat = %u, bind_zone_id=%u, bind_x = %f, bind_y = %f, bind_z = %f, bind_heading = %f, house_zone_id=%u, combat_voice = %i, emote_voice = %i, biography='%s', flags=%u, flags2=%u, last_name='%s', assigned_aa = %i, unassigned_aa = %i, tradeskill_aa = %i, unassigned_tradeskill_aa = %i, prestige_aa = %i, unassigned_prestige_aa = %i, tradeskill_prestige_aa = %i, unassigned_tradeskill_prestige_aa = %i where char_id = %u",
 		player->GetHP(), player->GetPower(), player->GetStrBase(), player->GetStaBase(), player->GetAgiBase(), player->GetWisBase(), player->GetIntBase(), player->GetHeatResistanceBase(), player->GetColdResistanceBase(), player->GetMagicResistanceBase(),
 		player->GetMentalResistanceBase(), player->GetDivineResistanceBase(), player->GetDiseaseResistanceBase(), player->GetPoisonResistanceBase(), player->GetCoinsCopper(), player->GetCoinsSilver(), player->GetCoinsGold(), player->GetCoinsPlat(), player->GetTotalHPBase(), player->GetTotalPowerBase(), player->GetXP(), player->GetNeededXP(), player->GetXPDebt(), player->GetXPVitality(), player->GetTSXP(), player->GetNeededTSXP(), player->GetTSXPVitality(), player->GetBankCoinsCopper(),
 		player->GetBankCoinsSilver(), player->GetBankCoinsGold(), player->GetBankCoinsPlat(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneID(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneX(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneY(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneZ(), client->GetPlayer()->GetPlayerInfo()->GetBindZoneHeading(), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), 
@@ -7169,3 +7275,49 @@ void WorldDatabase::LoadStartingSpells(World* world)
 
 	world->MStartingLists.releasewritelock();
 }
+
+
+bool WorldDatabase::DeleteSpiritShard(int32 id){
+	Query query;
+	query.RunQuery2(Q_DELETE, "delete FROM character_spirit_shards where id=%u",id);
+	if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF){
+		LogWrite(WORLD__ERROR, 0, "World", "Error in DeleteSpiritShard query '%s': %s", query.GetQuery(), query.GetError());
+		return false;
+	}
+	return true;
+}
+
+int32 WorldDatabase::CreateSpiritShard(const char* name, int32 level, int8 race, int8 gender, int8 adventure_class, 
+									  int16 model_type, int16 soga_model_type, int16 hair_type, int16 hair_face_type, int16 wing_type,
+									  int16 chest_type, int16 legs_type, int16 soga_hair_type, int16 soga_hair_face_type, int8 hide_hood, 
+									  int16 size, int16 collision_radius, int16 action_state, int16 visual_state, int16 mood_state, int16 emote_state, 
+									  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) 
+{
+	LogWrite(WORLD__INFO, 3, "World", "Saving Spirit Shard %s %u", name, charid);
+
+	Query query;
+	char* name_escaped = getEscapeString(name);
+	
+	if(!sub_title)
+		sub_title = "";
+	char* subtitle_escaped = getEscapeString(sub_title);
+	char* prefix_escaped = getEscapeString(prefix_title);
+	char* suffix_escaped = getEscapeString(suffix_title);
+	char* lastname_escaped = getEscapeString(lastname);
+	string insert = string("INSERT INTO character_spirit_shards (name, level, race, gender, adventure_class, model_type, soga_model_type, hair_type, hair_face_type, wing_type, chest_type, legs_type, soga_hair_type, soga_hair_face_type, hide_hood, size, collision_radius, action_state, visual_state, mood_state, emote_state, pos_state, activity_status, sub_title, prefix_title, suffix_title, lastname, x, y, z, heading, gridid, charid, zoneid, instanceid) VALUES ('%s', %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, %u, '%s', '%s', '%s', '%s', %f, %f, %f, %f, %u, %u, %u, %u)");
+	query.RunQuery2(Q_INSERT, insert.c_str(), name_escaped, level, race, gender, adventure_class, model_type, soga_model_type, 
+																hair_type, hair_face_type, wing_type, chest_type, legs_type, soga_hair_type, 
+																soga_hair_face_type, hide_hood, size, collision_radius, action_state, visual_state, 
+																mood_state, emote_state, pos_state, activity_status, subtitle_escaped, prefix_escaped, suffix_escaped, 
+																lastname_escaped, x, y, z, heading, gridid, charid, zoneid, instanceid);
+
+	
+	safe_delete_array(name_escaped);
+	safe_delete_array(subtitle_escaped);
+	safe_delete_array(prefix_escaped);
+	safe_delete_array(suffix_escaped);
+	safe_delete_array(lastname_escaped);
+
+	return query.GetLastInsertedID();
+}

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

@@ -245,6 +245,7 @@ public:
 	void	LoadSpawns(ZoneServer* zone);
 	int8	GetAppearanceType(string type);
 	void	LoadNPCs(ZoneServer* zone);
+	void	LoadSpiritShards(ZoneServer* zone);
 	int32	LoadAppearances(ZoneServer* zone, Client* client = 0);
 	int32	LoadNPCSpells(ZoneServer* zone);
 	int32	LoadNPCSkills(ZoneServer* zone);
@@ -594,6 +595,14 @@ public:
 
 	void				LoadStartingSkills(World* world);
 	void				LoadStartingSpells(World* world);
+
+	int32				CreateSpiritShard(const char* name, int32 level, int8 race, int8 gender, int8 adventure_class, 
+									  int16 model_type, int16 soga_model_type, int16 hair_type, int16 hair_face_type, int16 wing_type,
+									  int16 chest_type, int16 legs_type, int16 soga_hair_type, int16 soga_hair_face_type, int8 hide_hood, 
+									  int16 size, int16 collision_radius, int16 action_state, int16 visual_state, int16 mood_state, int16 emote_state, 
+									  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);
 private:
 	DatabaseNew			database_new;
 	map<int32, string>	zone_names;

+ 25 - 1
EQ2/source/WorldServer/client.cpp

@@ -199,6 +199,7 @@ Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_deb
 	lastRegionRemapTime = 0;
 	regionDebugMessaging = false;
 	client_reloading_zone = false;
+	last_saved_timestamp = 0;
 }
 
 Client::~Client() {
@@ -505,6 +506,14 @@ void Client::HandlePlayerRevive(int32 point_id)
 		QueuePacket(packet->serialize());
 		safe_delete(packet);
 	}
+	float origX, origY, origZ, origHeading = 0.0f;
+
+	origX = player->GetX();
+	origY = player->GetY();
+	origZ = player->GetZ();
+	origHeading = player->GetHeading();
+	ZoneServer* originalZone = GetCurrentZone();
+	int32 origGridID = GetPlayer()->appearance.pos.grid_id;
 
 	float x, y, z, heading;
 	RevivePoint* revive_point = 0;
@@ -621,8 +630,21 @@ void Client::HandlePlayerRevive(int32 point_id)
 		QueuePacket(packet->serialize());
 		safe_delete(packet);
 	}
+	
+	if(rule_manager.GetGlobalRule(R_Combat, EnableSpiritShards)->GetBool())
+	{
+		NPC* shard = player->InstantiateSpiritShard(origX, origY, origZ, origHeading, origGridID, originalZone);
 
-	GetCurrentZone()->RemoveSpawn(player, false);
+		if(shard->GetSpawnScript() && strlen(shard->GetSpawnScript()) > 0)
+			originalZone->CallSpawnScript(shard, SPAWN_SCRIPT_PRESPAWN);
+
+		originalZone->RemoveSpawn(player, false);
+
+		originalZone->AddSpawn(shard);
+		
+		if(shard->GetSpawnScript() && strlen(shard->GetSpawnScript()) > 0)
+			originalZone->CallSpawnScript(shard, SPAWN_SCRIPT_SPAWN);
+	}
 
 	m_resurrect.writelock(__FUNCTION__, __LINE__);
 	if (current_rez.active)
@@ -3987,6 +4009,7 @@ void Client::Save() {
 
 		UpdateCharacterInstances();
 
+		this->SetLastSavedTimeStamp(Timer::GetCurrentTime2());
 		database.Save(this);
 		if (GetPlayer()->UpdateQuickbarNeeded()) {
 			database.SaveQuickBar(GetCharacterID(), GetPlayer()->GetQuickbar());
@@ -9249,6 +9272,7 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 		firstlogin = zar->isFirstLogin();
 		LogWrite(ZONE__INFO, 0, "ZoneAuth", "Access Key: %u, Character Name: %s, Account ID: %u, Client Data Version: %u", zar->GetAccessKey(), zar->GetCharacterName(), zar->GetAccountID(), version);
 		if (database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)) {
+			GetPlayer()->CalculateOfflineDebtRecovery(GetLastSavedTimeStamp());
 			GetPlayer()->vis_changed = false;
 			GetPlayer()->info_changed = false;
 

+ 11 - 0
EQ2/source/WorldServer/zoneserver.cpp

@@ -765,7 +765,12 @@ void ZoneServer::ProcessDepop(bool respawns_allowed, bool repop) {
 		DeleteData(false);
 
 	if(repop)
+	{
+		// reload spirit shards for the current zone
+		database.LoadSpiritShards(this);
+
 		LoadingData = true;
+	}
 }
 
 void ZoneServer::Depop(bool respawns, bool repop) {
@@ -1244,6 +1249,10 @@ bool ZoneServer::Process()
 			if (reloading) {
 				LogWrite(COMMAND__DEBUG, 0, "Command", "-Loading Entity Commands...");
 				database.LoadEntityCommands(this);
+				LogWrite(NPC__INFO, 0, "NPC", "-Loading Spirit Shard data...");
+				database.LoadSpiritShards(this);
+				LogWrite(NPC__INFO, 0, "NPC", "-Load Spirit Shard data complete!");
+
 				LogWrite(NPC__INFO, 0, "NPC", "-Loading NPC data...");
 				database.LoadNPCs(this);
 				LogWrite(NPC__INFO, 0, "NPC", "-Load NPC data complete!");
@@ -4195,6 +4204,8 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 			((Player*)dead)->UpdatePlayerStatistic(STAT_PLAYER_TOTAL_DEATHS, 1);
 			client = GetClientBySpawn(dead);
 
+			((Entity*)dead)->HandleDeathExperienceDebt(killer);
+
 			if(client) {
 
 				if(client->GetPlayer()->DamageEquippedItems(10, client))

+ 40 - 0
server/SpawnScripts/Generic/SpiritShard.lua

@@ -0,0 +1,40 @@
+--[[
+	Script Name	: SpawnScripts/Generic/SpiritShard.lua
+	Script Purpose	: Spirit Shard
+--]]
+
+function spawn(NPC)
+	local DebtToRemovePct = GetRuleFlagFloat("R_Combat", "ShardDebtRecoveryPercent")
+		
+	if GetRuleFlagFloat("R_Combat", "ShardRecoveryByRadius") == true then
+		SetPlayerProximityFunction(NPC, 10.0, "recovershard")
+	end
+	
+	AddPrimaryEntityCommand(nil,NPC,"Recover Spirit Shard",10.0,"recovershard","",0,0,1)
+	
+	local charID = GetShardCharID(NPC)
+	SetAccessToEntityCommandByCharID(NPC, charID, "recovershard", true)
+end
+
+function recovershard(NPC, Spawn)
+	local charid = GetShardCharID(NPC)
+	
+	if GetCharacterID(Spawn) == charid then
+		local DebtToRemovePct = GetRuleFlagFloat("R_Combat", "ShardDebtRecoveryPercent")
+		local DeathXPDebt = GetRuleFlagFloat("R_Combat", "DeathExperienceDebt")
+		
+		local debt = GetInfoStructFloat(Spawn, "xp_debt")
+		local DebtToRemove = (DebtToRemovePct/100.0)*(DeathXPDebt/100.0);
+		if debt > DebtToRemove then
+			SetInfoStructFloat(Spawn, "xp_debt", debt - DebtToRemove)
+		else
+			SetInfoStructFloat(Spawn, "xp_debt", 0.0)
+		end
+		
+		SendMessage(Spawn, "You recover a spirit shard and recovered some experience debt.", "white")
+		SetCharSheetChanged(Spawn, true)
+		local shardid = GetShardID(NPC)
+		DeleteDBShardID(shardid)
+		Despawn(NPC)
+	end
+end