4 Commits 875be485de ... 852f035b45

Author SHA1 Message Date
  Image 852f035b45 Exe Updates 3 years ago
  Image 82ccc26642 Item/Merchant/Scribing/Inventory Updates 3 years ago
  Image 017a9b80ee Struct updates for merchants (red text description) and item display options (scribed/need other spell scribed first) 3 years ago
  Image 9f9d61fbe4 Login Server updates 3 years ago

+ 5 - 0
DB/updates/ls_table_update_mar21_2021.sql

@@ -0,0 +1,5 @@
+CREATE TABLE `login_bannedips` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `ip` varchar(64) not null default '',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;

+ 40 - 39
EQ2/source/LoginServer/LWorld.cpp

@@ -64,6 +64,13 @@ LWorld::LWorld(TCPConnection* in_con, bool in_OutgoingLoginUplink, int32 iIP, in
 		ip = iIP;
 	else
 		ip = in_con->GetrIP();
+
+	struct in_addr  in;
+	in.s_addr = in_con->GetrIP();
+	char* ipadd = inet_ntoa(in);
+	if(ipadd)
+		strncpy(IPAddr,ipadd,64);
+
 	if (iPort)
 		port = iPort;
 	else
@@ -100,7 +107,6 @@ LWorld::LWorld(TCPConnection* in_con, bool in_OutgoingLoginUplink, int32 iIP, in
 		OutgoingUplink = false;
 	}
 
-	struct in_addr  in;
 	in.s_addr = GetIP();
 	strcpy(address, inet_ntoa(in));
 	isaddressip = true;
@@ -115,6 +121,7 @@ LWorld::LWorld(int32 in_accountid, char* in_accountname, char* in_worldname, int
 	ip = 0;
 	port = 0;
 	ID = 0;
+	strcpy(IPAddr,"");
 	pClientPort = 0;
 	memset(account, 0, sizeof(account));
 	memset(address, 0, sizeof(address));
@@ -151,6 +158,16 @@ LWorld::LWorld(TCPConnection* in_RemoteLink, int32 in_ip, int32 in_RemoteID, int
 	RemoteID = in_RemoteID;
 	LinkWorldID = iLinkWorldID;
 	ip = in_ip;
+	
+	struct in_addr  in;
+	if(in_RemoteLink)
+		in.s_addr = in_RemoteLink->GetrIP();
+	else if (in_ip)
+		in.s_addr = in_ip;
+	char* ipadd = inet_ntoa(in);
+	if(ipadd)
+		strncpy(IPAddr,ipadd,64);
+
 	port = 0;
 	ID = 0;
 	pClientPort = 0;
@@ -234,7 +251,15 @@ bool LWorld::Process() {
 		return true;
 
 	if(pStatsTimer && pStatsTimer->Check())
+	{
+		if(isAuthenticated && (database.IsServerAccountDisabled(account) || database.IsIPBanned(IPAddr)))
+		{	
+			this->Kick(ERROR_BADPASSWORD);
+			return false;
+		}
+
 		database.UpdateWorldServerStats(this, GetStatus());
+	}
 
 	ServerPacket* pack = 0;
 	while (ret && (pack = Link->PopPacket())) {
@@ -302,6 +327,10 @@ bool LWorld::Process() {
 			UsertoWorldResponse_Struct* seps = (UsertoWorldResponse_Struct*) pack->pBuffer;
 			if (seps->ToID) {
 				LWorld* world = world_list.FindByID(seps->ToID);
+
+				if (this->GetType() != Login) {
+					break;
+				}
 				if (world) {
 					seps->ToID = world->GetRemoteID();
 					world->SendPacket(pack);
@@ -453,41 +482,6 @@ bool LWorld::Process() {
 			break;
 								  }
 		case ServerOP_WorldListUpdate: {
-			if (this->GetType() != Login) {
-				break;
-			}
-			if (pack->size != sizeof(ServerSyncWorldList_Struct)) {
-				break;
-			}
-			ServerSyncWorldList_Struct* sswls = (ServerSyncWorldList_Struct*) pack->pBuffer;
-			if (!CheckServerName(sswls->name)) {
-				//struct in_addr  in;
-				//in.s_addr = sswls->ip;
-				break; // Someone needs to tell the other login server to update it's exe!
-			}
-			LWorld* world = world_list.FindByLink(this->Link, sswls->RemoteID);
-			if (world) {
-				world->SetRemoteInfo(sswls->ip, sswls->accountid, sswls->account, sswls->name, sswls->address, sswls->status, sswls->adminid, sswls->num_players, sswls->num_zones);
-			}
-			else {
-				world = world_list.FindByAccount(sswls->accountid, World);
-				if (world == 0 || sswls->placeholder == false) {
-					if (world) {
-#ifdef _DEBUG
-						cout << "Kick(" << world->GetID() << ") in ServerOP_WorldListUpdate" << endl;
-#endif
-						world->Kick();
-					}
-					world = new LWorld(this->Link, sswls->ip, sswls->RemoteID, sswls->accountid, sswls->account, sswls->name, sswls->address, sswls->status, sswls->adminid, sswls->showdown, sswls->authlevel, sswls->placeholder, this->GetID());
-					LWorld* world2=world_list.FindByID(sswls->RemoteID);
-					if(!world2)
-						world_list.Add(world);
-				}
-			}
-			sswls->RemoteID = world->GetID();
-			if (net.GetLoginMode() != Mesh)
-				world_list.SendPacketLogin(pack, this);
-			cout << "Got world update for '" << sswls->name << "', #" << world->GetID() << endl;
 			break;
 									   }
 		case ServerOP_WorldListRemove: {
@@ -664,16 +658,24 @@ bool LWorld::SetupWorld(char* in_worldname, char* in_worldaddress, char* in_acco
 		strcpy(address, in_worldaddress);
 	}
 	if (strlen(in_worldname) > 3) {
-		int32 id = database.CheckServerAccount(in_account, in_password);
+		char tmpAccount[30];
+		memcpy(tmpAccount, in_account, 29);
+		tmpAccount[29] = '\0';
+
+		int32 id = database.CheckServerAccount(tmpAccount, in_password);
 
 		if(id == 0)
 			return false;
+		if(database.IsServerAccountDisabled(tmpAccount) || database.IsIPBanned(IPAddr) || (isaddressip && database.IsIPBanned(address)))
+			return false;
+		
 		LWorld* world = world_list.FindByID(id);
 		if(world)
 			world->Kick("Ghost Kick!");
 
 		ID = id;
 		accountid = id;
+		strncpy(account,tmpAccount,30);
 		char* name = database.GetServerAccountName(id);
 		if(name)
 			snprintf(worldname, (sizeof(worldname)) - 1, "%s", name);
@@ -684,7 +686,6 @@ bool LWorld::SetupWorld(char* in_worldname, char* in_worldaddress, char* in_acco
 			return false;
 		}
 		//world_list.KickGhostIP(GetIP(), this);
-		account[0] = 0;
 		IsInit = true;
 		ptype = World;
 		world_list.SendWorldChanged(id, true);
@@ -734,7 +735,7 @@ void LWorld::ChangeToPlaceholder() {
 void LWorld::SetRemoteInfo(int32 in_ip, int32 in_accountid, char* in_account, char* in_name, char* in_address, int32 in_status, int32 in_adminid, sint32 in_players, sint32 in_zones) {
 	ip = in_ip;
 	accountid = in_accountid;
-	strcpy(account, in_account);
+//	strcpy(account, in_account);
 	strcpy(worldname, in_name);
 	strcpy(address, in_address);
 	status = in_status;

+ 1 - 0
EQ2/source/LoginServer/LWorld.h

@@ -115,6 +115,7 @@ protected:
 private:
 	int32	ID;
 	int32	ip;
+	char	IPAddr[64];
 	int16	port;
 	bool	kicked;
 	bool	pNeverKick;

+ 40 - 1
EQ2/source/LoginServer/LoginDatabase.cpp

@@ -395,6 +395,7 @@ int32 LoginDatabase::SaveCharacter(PacketStruct* create, LoginAccount* acct, int
 	}
 	else {
 		SaveCharacterColors(char_id, "skin_color", create->getType_EQ2_Color_ByName("skin_color"));
+		SaveCharacterColors(char_id, "model_color", create->getType_EQ2_Color_ByName("model_color"));
 		SaveCharacterColors(char_id, "eye_color", create->getType_EQ2_Color_ByName("eye_color"));
 		SaveCharacterColors(char_id, "hair_color1", create->getType_EQ2_Color_ByName("hair_color1"));
 		SaveCharacterColors(char_id, "hair_color2", create->getType_EQ2_Color_ByName("hair_color2"));
@@ -412,6 +413,7 @@ int32 LoginDatabase::SaveCharacter(PacketStruct* create, LoginAccount* acct, int
 		SaveCharacterColors(char_id, "unknown9", create->getType_EQ2_Color_ByName("unknown9"));		
 
 		SaveCharacterColors(char_id, "soga_skin_color", create->getType_EQ2_Color_ByName("soga_skin_color"));
+		SaveCharacterColors(char_id, "soga_model_color", create->getType_EQ2_Color_ByName("soga_model_color"));
 		SaveCharacterColors(char_id, "soga_eye_color", create->getType_EQ2_Color_ByName("soga_eye_color"));
 		SaveCharacterColors(char_id, "soga_hair_color1", create->getType_EQ2_Color_ByName("soga_hair_color1"));
 		SaveCharacterColors(char_id, "soga_hair_color2", create->getType_EQ2_Color_ByName("soga_hair_color2"));
@@ -597,7 +599,7 @@ int32 LoginDatabase::CheckServerAccount(char* name, char* passwd){
 	Query query;
 	MYSQL_ROW row;
 	query.escaped_name = getEscapeString(name);
-	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT lower(password), id from login_worldservers where account='%s'", query.escaped_name);
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT lower(password), id from login_worldservers where account='%s' and disabled = 0", query.escaped_name);
 
 	LogWrite(LOGIN__INFO, 0, "Login", "WorldServer CheckServerAccount Account=%s\nSHA=%s", (char*)query.escaped_name, passwd);
 	if(result && mysql_num_rows(result) == 1){
@@ -613,6 +615,43 @@ int32 LoginDatabase::CheckServerAccount(char* name, char* passwd){
 	}
 	return id;
 }
+
+bool LoginDatabase::IsServerAccountDisabled(char* name){
+	Query query;
+	MYSQL_ROW row;
+	query.escaped_name = getEscapeString(name);
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT id from login_worldservers where account='%s' and disabled = 1", query.escaped_name);
+
+	LogWrite(LOGIN__INFO, 0, "Login", "WorldServer IsServerAccountDisabled Account=%s", (char*)query.escaped_name);
+	if(result && mysql_num_rows(result) > 0){
+		row = mysql_fetch_row(result);
+
+		LogWrite(LOGIN__INFO, 0, "Login", "WorldServer IsServerAccountDisabled Match Account=%s", (char*)query.escaped_name);
+
+		return true;
+	}
+	return false;
+}
+
+bool LoginDatabase::IsIPBanned(char* ipaddr){
+	if(!ipaddr)
+		return false;
+
+	Query query;
+	MYSQL_ROW row;
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT ip from login_bannedips where '%s' LIKE CONCAT(ip ,'%%')", ipaddr);
+
+	LogWrite(LOGIN__INFO, 0, "Login", "WorldServer IsServerIPBanned IPPartial=%s", (char*)ipaddr);
+	if(result && mysql_num_rows(result) > 0){
+		row = mysql_fetch_row(result);
+
+		LogWrite(LOGIN__INFO, 0, "Login", "WorldServer IsServerIPBanned Match IPBan=%s", row[0]);
+
+		return true;
+	}
+	return false;
+}
+
 void LoginDatabase::GetServerAccounts(vector<LWorld*>* server_list){
 	Query query;
 	MYSQL_ROW row;

+ 2 - 0
EQ2/source/LoginServer/LoginDatabase.h

@@ -38,6 +38,8 @@ public:
 	LoginAccount* LoadAccount(const char* name, const char* password, bool attemptAccountCreation=true);
 	int32 GetAccountIDByName(const char* name);
 	int32 CheckServerAccount(char* name, char* passwd);
+	bool IsServerAccountDisabled(char* name);
+	bool IsIPBanned(char* ipaddr);
 	void  GetServerAccounts(vector<LWorld*>* server_list);
 	char* GetServerAccountName(int32 id);
 	bool  VerifyDelete(int32 account_id, int32 character_id, const char* name);

+ 2 - 0
EQ2/source/LoginServer/PacketHeaders.cpp

@@ -41,6 +41,8 @@ EQ2Packet* LS_CharSelectList::serialize(int16 version){
 		for (int i = 0; i < 3; i++)
 			account_info.unknown5[i] = 0xFFFFFFFF;
 		account_info.unknown5[3] = 0;
+		account_info.unknown6 = 0xFFFF; // sets Veteran Bonus under 'Select Character' yellow (vs greyed out), adventure/tradeskill bonus 200%
+		account_info.unknown7 = 0; // when 1 (count?) provides free upgrade option for character to lvl 90 (heroic character) -- its a green 'Free' up arrow next to the character that is selected in char select
 		AddData(account_info);
 	}	
 	return new EQ2Packet(OP_AllCharactersDescReplyMsg, getData(), getDataSize());

+ 17 - 1
EQ2/source/LoginServer/client.cpp

@@ -465,6 +465,22 @@ void Client::CharacterApproved(int32 server_id,int32 char_id)
 			database.LoadCharacters(GetLoginAccount(), GetVersion());
 			
 			SendCharList();
+
+			if (GetVersion() <= 546)
+			{
+				pending_play_char_id = char_id;
+				ServerPacket* outpack = new ServerPacket(ServerOP_UsertoWorldReq, sizeof(UsertoWorldRequest_Struct));
+				UsertoWorldRequest_Struct* req = (UsertoWorldRequest_Struct*)outpack->pBuffer;
+				req->char_id = char_id;
+				req->lsaccountid = GetAccountID();
+				req->worldid = server_id;
+
+				struct in_addr in;
+				in.s_addr = GetIP();
+				strcpy(req->ip_address, inet_ntoa(in));
+				world_server->SendPacket(outpack);
+				delete outpack;
+			}
 		}
 	}
 	else{
@@ -752,4 +768,4 @@ void Client::StartDisconnectTimer() {
 		disconnectTimer = new Timer(1000);
 		disconnectTimer->Start();
 	}
-}
+}

+ 1 - 1
EQ2/source/LoginServer/login_structs.h

@@ -54,7 +54,7 @@ struct LS_CharListAccountInfo{
 	int8	unknown4;
 	int32	unknown5[4];
 	int16	unknown6;
-//	int8	unknown7; // adds 'free' option..
+	int8	unknown7; // adds 'free' option..
 };
 #pragma pack()
 

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

@@ -1759,7 +1759,11 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						Spell* spell = master_spell_list.GetSpell(item->skill_info->spell_id, item->skill_info->spell_tier);
 						int8 old_slot = 0;
 						if (spell) {
-							if (!player->HasSpell(spell->GetSpellID(), spell->GetSpellTier(), true)) {
+
+							int16 tier_up = player->GetTierUp(spell->GetSpellTier());
+							if (rule_manager.GetGlobalRule(R_Spells, RequirePreviousTierScribe)->GetInt8() && !player->HasSpell(spell->GetSpellID(), tier_up, false, true))
+								client->SimpleMessage(CHANNEL_COLOR_RED, "You have not scribed the required previous version of this ability.");
+							else if (!player->HasSpell(spell->GetSpellID(), spell->GetSpellTier(), true)) {
 								old_slot = player->GetSpellSlot(spell->GetSpellID());
 								player->RemoveSpellBookEntry(spell->GetSpellID());
 								player->AddSpellBookEntry(spell->GetSpellID(), spell->GetSpellTier(), old_slot, spell->GetSpellData()->spell_book_type, spell->GetSpellData()->linked_timer, true);

+ 13 - 5
EQ2/source/WorldServer/Items/Items.cpp

@@ -31,6 +31,7 @@
 #include <algorithm>
 #include <sstream>
 #include <boost/algorithm/string.hpp>
+#include "../Rules/Rules.h"
 
 extern World world;
 extern MasterSpellList master_spell_list;
@@ -38,6 +39,7 @@ extern MasterQuestList master_quest_list;
 extern MasterRecipeList master_recipe_list;
 extern ConfigReader configReader;
 extern LuaInterface* lua_interface;
+extern RuleManager rule_manager;
 
 MasterItemList::MasterItemList(){
 	AddMappedItemStat(ITEM_STAT_ADORNING, std::string("adorning"));
@@ -2184,16 +2186,22 @@ void Item::serialize(PacketStruct* packet, bool show_name, Player* player, int16
 							packet->setSubstructDataByName("header_info", "footer_type", 0);
 							
 							spell->SetPacketInformation(packet, player->GetZone()->GetClientBySpawn(player));
-							if (player->HasSpell(skill_info->spell_id, skill_info->spell_tier))
+							if (player->HasSpell(skill_info->spell_id, skill_info->spell_tier, true))
 								packet->setDataByName("scribed", 1);
-							else
+
 								if (packet->GetVersion() >= 927){
 									if (player->HasSpell(skill_info->spell_id, skill_info->spell_tier, true))
-										packet->setAddToPacketByName("better_version", 1);// need to confirm
+										packet->setAddToPacketByName("scribed_better_version", 1);// need to confirm
 								}
 								else
-									packet->setAddToPacketByName("better_version", 0); //if not scribed
-							packet->setDataByName("require_previous", 0);
+									packet->setAddToPacketByName("scribed_better_version", 0); //if not scribed
+							
+							// either don't require previous tier or check that we have the lower tier spells potentially
+							int32 tier_up = player->GetTierUp(skill_info->spell_tier);
+							if (!rule_manager.GetGlobalRule(R_Spells, RequirePreviousTierScribe)->GetInt8() || player->HasSpell(skill_info->spell_id, tier_up, false, true))
+								packet->setDataByName("require_previous", 1, 0);
+							// membership required
+							//packet->setDataByName("unknown_1188_2_MJ", 1, 1);
 							
 						}
 						else {							

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

@@ -629,6 +629,33 @@ void LuaInterface::RemoveSpawnScript(const char* name) {
 	MSpawnScripts.releasewritelock(__FUNCTION__, __LINE__);
 }
 
+bool LuaInterface::CallItemScript(lua_State* state, int8 num_parameters, std::string* returnValue) {
+	if(shutting_down)
+		return false;
+	if(!state || lua_pcall(state, num_parameters, 1, 0) != 0){
+		if (state){
+			const char* err = lua_tostring(state, -1);
+			LogError("%s: %s", GetScriptName(state), err);
+			lua_pop(state, 1);
+		}
+		return false;
+	}
+
+	std::string result = std::string("");
+	
+	if(lua_isstring(state, -1)){
+		size_t size = 0;
+		const char* str = lua_tolstring(state, -1, &size);
+		if(str)
+			result = string(str);
+	}
+	
+	if(returnValue)
+		*returnValue = std::string(result);
+	
+	return true;
+}
+
 bool LuaInterface::CallItemScript(lua_State* state, int8 num_parameters, sint64* returnValue) {
 	if(shutting_down)
 		return false;
@@ -2088,6 +2115,47 @@ bool LuaInterface::RunItemScript(string script_name, const char* function_name,
 		return false;
 }
 
+bool LuaInterface::RunItemScriptWithReturnString(string script_name, const char* function_name, Item* item, Spawn* spawn, std::string* returnValue) {
+	if(!item)
+		return false;
+	lua_State* state = GetItemScript(script_name.c_str(), true, true);
+	if(state){
+		Mutex* mutex = GetItemScriptMutex(script_name.c_str());
+		if(mutex)
+			mutex->readlock(__FUNCTION__, __LINE__);
+		else{
+			LogError("Error getting lock for '%s'", script_name.c_str());
+			UseItemScript(script_name.c_str(), state, false);
+			return false;
+		}
+		lua_getglobal(state, function_name);
+		if (!lua_isfunction(state, lua_gettop(state))){
+			lua_pop(state, 1);
+			mutex->releasereadlock(__FUNCTION__);
+			UseItemScript(script_name.c_str(), state, false);
+			return false;
+		}
+		SetItemValue(state, item);
+		int8 num_parms = 1;
+		if(spawn){
+			SetSpawnValue(state, spawn);
+			num_parms++;
+		}
+		if(!CallItemScript(state, num_parms, returnValue)){
+			if(mutex)
+				mutex->releasereadlock(__FUNCTION__, __LINE__);
+			UseItemScript(script_name.c_str(), state, false);
+			return false;
+		}
+		if(mutex)
+			mutex->releasereadlock(__FUNCTION__, __LINE__);
+		UseItemScript(script_name.c_str(), state, false);
+		return true;
+	}
+	else
+		return false;
+}
+
 
 bool LuaInterface::RunSpawnScript(string script_name, const char* function_name, Spawn* npc, Spawn* spawn, const char* message, bool is_door_open) {
 	if(!npc || spawn_scripts_reloading)

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

@@ -251,6 +251,8 @@ public:
 
 	void			RemoveSpawnScript(const char* name);
 	bool			RunItemScript(string script_name, const char* function_name, Item* item, Spawn* spawn = 0, sint64* returnValue = 0);
+	bool			RunItemScriptWithReturnString(string script_name, const char* function_name, Item* item, Spawn* spawn = 0, std::string* returnValue = 0);
+	bool			CallItemScript(lua_State* state, int8 num_parameters, std::string* returnValue = 0);
 	bool			CallItemScript(lua_State* state, int8 num_parameters, sint64* returnValue = 0);
 	bool			RunSpawnScript(string script_name, const char* function_name, Spawn* npc, Spawn* spawn = 0, const char* message = 0, bool is_door_open = false);
 	bool			CallSpawnScript(lua_State* state, int8 num_parameters);

+ 21 - 4
EQ2/source/WorldServer/Player.cpp

@@ -1828,8 +1828,8 @@ vector<EQ2Packet*>	Player::UnequipItem(int16 index, sint32 bag_id, int8 slot, in
 				bag_id = 0;
 		}
 
-		if (item_list.items.count(bag_id) > 0 && item_list.items[bag_id][appearance_type].count(slot) > 0)
-			to_item = item_list.items[bag_id][appearance_type][slot];
+		if (item_list.items.count(bag_id) > 0 && item_list.items[bag_id][BASE_EQUIPMENT].count(slot) > 0)
+			to_item = item_list.items[bag_id][BASE_EQUIPMENT][slot];
 		if (to_item && equipList->CanItemBeEquippedInSlot(to_item, ConvertSlotFromClient(item->details.slot_id, version))) {
 			equipList->RemoveItem(index);
 			if(item->details.appearance_type)
@@ -2829,14 +2829,31 @@ vector<SpellBookEntry*>* Player::GetSpellsSaveNeeded(){
 	return ret;
 }
 
-bool Player::HasSpell(int32 spell_id, int8 tier, bool include_higher_tiers){
+int16 Player::GetTierUp(int16 tier)
+{
+	switch(tier)
+	{
+		case 0:
+			break;
+		case 7:
+		case 9:
+			tier -= 2;
+			break;
+		default:
+			tier -= 1;
+		break;
+	}
+
+	return tier;
+}
+bool Player::HasSpell(int32 spell_id, int8 tier, bool include_higher_tiers, bool include_possible_scribe){
 	bool ret = false;
 	vector<SpellBookEntry*>::iterator itr;
 	MSpellsBook.lock();
 	SpellBookEntry* spell = 0;
 	for(itr = spells.begin(); itr != spells.end(); itr++){
 		spell = *itr;
-		if(spell->spell_id == spell_id && (tier == 255 || spell->tier == tier || (include_higher_tiers && spell->tier > tier))){
+		if(spell->spell_id == spell_id && (tier == 255 || spell->tier == tier || (include_higher_tiers && spell->tier > tier) || (include_possible_scribe && tier <= spell->tier))){
 			ret = true;
 			break;
 		}

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

@@ -682,7 +682,8 @@ public:
 	void				RemovePendingLootItem(int32 id, int32 item_id);
 	void				RemovePendingLootItems(int32 id);
 	void				AddPendingLootItems(int32 id, vector<Item*>* items);
-	bool				HasSpell(int32 spell_id, int8 tier = 255, bool include_higher_tiers = false);
+	int16				GetTierUp(int16 tier);
+	bool				HasSpell(int32 spell_id, int8 tier = 255, bool include_higher_tiers = false, bool include_possible_scribe = false);
 	bool				HasRecipeBook(int32 recipe_id);
 	void				AddPlayerStatistic(int32 stat_id, sint32 stat_value, int32 stat_date);
 	void				UpdatePlayerStatistic(int32 stat_id, sint32 stat_value, bool overwrite = false);

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

@@ -333,6 +333,7 @@ void RuleManager::Init()
 	RULE_INIT(R_Spells, EnableCrossZoneTargetBuffs, "0"); // enables/disables allowing cross zone target buffs
 	RULE_INIT(R_Spells, PlayerSpellSaveStateWaitInterval, "100"); // time in milliseconds we wait before performing a save when the spell save trigger is activated, allows additional actions to take place until the cap is hit
 	RULE_INIT(R_Spells, PlayerSpellSaveStateCap, "1000"); // sets a maximum wait time before we queue a spell state save to the DB, given a lot can go on in a short period with players especially in combat, maybe good to have this at a higher interval.
+	RULE_INIT(R_Spells, RequirePreviousTierScribe, "0"); // requires step up apprentice -> apprentice (handcrafted?) -> journeyman (handcrafted?) -> adept -> expert -> master
 
 	RULE_INIT(R_Expansion, GlobalExpansionFlag, "0");
 	RULE_INIT(R_Expansion, GlobalHolidayFlag, "0");

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

@@ -182,6 +182,7 @@ enum RuleType {
 	EnableCrossZoneTargetBuffs,
 	PlayerSpellSaveStateWaitInterval,
 	PlayerSpellSaveStateCap,
+	RequirePreviousTierScribe,
 
 	/* ZONE TIMERS */
 	RegenTimer,

+ 6 - 11
EQ2/source/WorldServer/client.cpp

@@ -7009,10 +7009,7 @@ void Client::SendBuyMerchantList(bool sell) {
 
 					sint64 overrideValue = 0;
 					if (item->GetItemScript() && lua_interface && lua_interface->RunItemScript(item->GetItemScript(), "item_difficulty", item, player, &overrideValue))
-					{
 						item_difficulty = (sint8)overrideValue;
-						printf("Override difficulty: %i\n",item_difficulty);
-					}
 
 					item_difficulty -= 6;
 					if (item_difficulty < 0)
@@ -7022,8 +7019,12 @@ void Client::SendBuyMerchantList(bool sell) {
 					packet->setArrayDataByName("quantity", ItemInfo.quantity, i);
 					packet->setArrayDataByName("unknown5", 255, i);
 					packet->setArrayDataByName("stack_size2", item->stack_count, i);
-					if (GetVersion() <= 1096)
-						packet->setArrayDataByName("description", item->description.c_str(), i);
+					
+					std::string overrideValueStr;
+					// classic client isn't properly tracking this field, DoF we don't have it identified yet, but no field to cause any issues (can add later if identified)
+					if (GetVersion() >= 546 && item->GetItemScript() && lua_interface && lua_interface->RunItemScriptWithReturnString(item->GetItemScript(), "item_description", item, player, &overrideValueStr))
+						packet->setArrayDataByName("description", overrideValueStr.c_str(), i);
+					
 					// If no price set in the merchant_inventory table then use the old method
 					if (ItemInfo.price_item_id == 0 && ItemInfo.price_item2_id == 0 && ItemInfo.price_coins == 0 && ItemInfo.price_status == 0 && ItemInfo.price_stationcash == 0) {
 						sell_price = (int32)(item->sell_price * multiplier);
@@ -7191,10 +7192,7 @@ void Client::SendSellMerchantList(bool sell) {
 					
 					sint64 overrideValue = 0;
 					if (item->GetItemScript() && lua_interface && lua_interface->RunItemScript(item->GetItemScript(), "item_difficulty", item, player, &overrideValue))
-					{
 						item_difficulty = (sint8)overrideValue;
-						printf("Override difficulty: %i\n",item_difficulty);
-					}
 					
 					item_difficulty -= 6;
 					if (item_difficulty < 0)
@@ -7271,10 +7269,7 @@ void Client::SendBuyBackList(bool sell) {
 					
 					sint64 overrideValue = 0;
 					if (master_item->GetItemScript() && lua_interface && lua_interface->RunItemScript(master_item->GetItemScript(), "item_difficulty", master_item, player, &overrideValue))
-					{
 						item_difficulty = (sint8)overrideValue;
-						printf("Override difficulty: %i\n",item_difficulty);
-					}
 
 					item_difficulty -= 6;
 					if (item_difficulty < 0)

+ 78 - 9
EQ2/source/WorldServer/zoneserver.cpp

@@ -261,6 +261,9 @@ void ZoneServer::Init()
 	spawn_update.Start(rule_manager.GetGlobalRule(R_Zone, SpawnUpdateTimer)->GetInt16());
 	LogWrite(ZONE__DEBUG, 0, "Zone", "SpawnUpdateTimer: %ims", rule_manager.GetGlobalRule(R_Zone, SpawnUpdateTimer)->GetInt16());
 
+	queue_updates.Start(rule_manager.GetGlobalRule(R_Zone, SpawnUpdateTimer)->GetInt16());
+	LogWrite(ZONE__DEBUG, 0, "Zone", "QueueUpdateTimer(inherits SpawnUpdateTimer): %ims", rule_manager.GetGlobalRule(R_Zone, SpawnUpdateTimer)->GetInt16());
+
 	spawn_delete_timer = rule_manager.GetGlobalRule(R_Zone, SpawnDeleteTimer)->GetInt32();
 	LogWrite(ZONE__DEBUG, 0, "Zone", "SpawnDeleteTimer: %ums", spawn_delete_timer);
 
@@ -1596,6 +1599,8 @@ bool ZoneServer::SpawnProcess(){
 			movementMgr->Process();
 		MSpawnList.releasereadlock(__FUNCTION__, __LINE__);
 
+		if(queue_updates.Check())
+			ProcessQueuedStateCommands();
 		// Do other loops for spawns
 		// tracking, client loop with spawn loop for each client that is tracking, change to a spawn_range_map loop instead of using the main spawn list?
 		//if (tracking_timer.Check())
@@ -5459,16 +5464,8 @@ void ZoneServer::SendUpdateDefaultCommand(Spawn* spawn, const char* command, flo
 		return;
 	}
 
-	Client* client = 0;
-	PacketStruct* packet = 0;
-	vector<Client*>::iterator client_itr;
+	QueueDefaultCommand(spawn->GetID(), std::string(command), distance);
 
-	MClientList.readlock(__FUNCTION__, __LINE__);
-	for (client_itr = clients.begin(); client_itr != clients.end(); client_itr++) {
-		client = *client_itr;
-		client->SendDefaultCommand(spawn, command, distance);
-	}
-	
 	if (strlen(command)>0)
 		spawn->SetPrimaryCommand(command, command, distance);
 	MClientList.releasereadlock(__FUNCTION__, __LINE__);
@@ -7896,4 +7893,76 @@ void ZoneServer::AddSpawnToGroup(Spawn* spawn, int32 group_id)
 	}
 	groupList->Add(spawn->GetID());
 	spawn->SetSpawnGroupID(group_id);
+}
+
+void ZoneServer::QueueStateCommandToClients(int32 spawn_id, int32 state)
+{
+	if(spawn_id < 1)
+		return;
+
+	MLuaQueueStateCmd.lock();
+	lua_queued_state_commands.insert(make_pair(spawn_id, state));
+	MLuaQueueStateCmd.unlock();
+}
+
+void ZoneServer::QueueDefaultCommand(int32 spawn_id, std::string command, float distance)
+{
+	if(spawn_id < 1)
+		return;
+
+	MLuaQueueStateCmd.lock();
+	lua_spawn_update_command[spawn_id].insert(make_pair(command,distance));
+	MLuaQueueStateCmd.unlock();
+}
+
+void ZoneServer::ProcessQueuedStateCommands() // in a client list lock only
+{
+	vector<Client*>::iterator itr;
+
+	MLuaQueueStateCmd.lock();
+
+	if(lua_queued_state_commands.size() > 0)
+	{
+		std::map<int32, int32>::iterator statecmds;
+		for(statecmds = lua_queued_state_commands.begin(); statecmds != lua_queued_state_commands.end(); statecmds++)
+		{
+			Spawn* spawn = GetSpawnByID(statecmds->first, false);
+			if(!spawn)
+				continue;
+			
+			MClientList.readlock(__FUNCTION__, __LINE__);
+			for (itr = clients.begin(); itr != clients.end(); itr++) {
+				Client* client = *itr;
+				if (client && client->GetPlayer()->WasSentSpawn(spawn->GetID()) && !client->GetPlayer()->WasSpawnRemoved(spawn))
+					ClientPacketFunctions::SendStateCommand(client, client->GetPlayer()->GetIDWithPlayerSpawn(spawn), statecmds->second);
+			}
+			MClientList.releasereadlock(__FUNCTION__, __LINE__);
+		}
+		lua_queued_state_commands.clear();
+	}
+
+	if(lua_spawn_update_command.size() > 0)
+	{
+		std::map<int32, std::map<std::string,float>>::iterator updatecmds;
+		for(updatecmds = lua_spawn_update_command.begin(); updatecmds != lua_spawn_update_command.end(); updatecmds++)
+		{
+			Spawn* spawn = GetSpawnByID(updatecmds->first, false);
+			if(!spawn)
+				continue;
+			
+			std::map<std::string,float>::iterator innermap;
+			for(innermap = lua_spawn_update_command[updatecmds->first].begin(); innermap != lua_spawn_update_command[updatecmds->first].end(); innermap++)
+			{
+				MClientList.readlock(__FUNCTION__, __LINE__);
+				for (itr = clients.begin(); itr != clients.end(); itr++) {
+					Client* client = *itr;
+					if (client && client->GetPlayer()->WasSentSpawn(spawn->GetID()) && !client->GetPlayer()->WasSpawnRemoved(spawn))
+						client->SendDefaultCommand(spawn, innermap->first.c_str(), innermap->second);
+				}
+				MClientList.releasereadlock(__FUNCTION__, __LINE__);
+			}
+		}
+		lua_spawn_update_command.clear();
+	}
+	MLuaQueueStateCmd.unlock();
 }

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

@@ -669,6 +669,10 @@ public:
 	bool	SendRemoveSpawn(Client* client, Spawn* spawn, PacketStruct* packet = 0, bool delete_spawn = false);
 
 	void	AddSpawnToGroup(Spawn* spawn, int32 group_id);
+
+	void	QueueStateCommandToClients(int32 spawn_id, int32 state);
+	void	QueueDefaultCommand(int32 spawn_id, std::string command, float distance);
+	void	ProcessQueuedStateCommands();
 private:
 #ifndef WIN32
 	pthread_t ZoneThread;
@@ -843,6 +847,7 @@ private:
 	Timer	tracking_timer;
 	Timer	weatherTimer;
 	Timer	widget_timer;
+	Timer	queue_updates;
 	
 	/* Enums */
 	Instance_Type InstanceType;
@@ -947,6 +952,10 @@ private:
 
 	vector<int32> m_pendingSpawnRemove;
 	Mutex MPendingSpawnRemoval;
+
+	std::map<int32, int32> lua_queued_state_commands;
+	std::map<int32, std::map<std::string, float>> lua_spawn_update_command;
+	std::mutex MLuaQueueStateCmd;
 public:
 	Spawn*				GetSpawn(int32 id);
 

BIN
server/EQ2Login__Debug64.exe


BIN
server/EQ2World__Debug_x64.exe


+ 13 - 7
server/ItemStructs.xml

@@ -6572,8 +6572,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="57048" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -6775,8 +6776,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="57107" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -6977,8 +6979,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="58571" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -7179,8 +7182,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="58617" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -7382,8 +7386,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="60024" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -7585,8 +7590,9 @@
 <Data ElementName="spell_info" Substruct="WS_SpellInfo" Size="1" />
 <Data ElementName="scribed" Type="int8" Size="1" />
 <Data ElementName="scribed_better_version" Type="int8" Size="1" IfVariableSet="scribed" />
+<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="1" />
 <Data ElementName="require_previous" Type="int8" Size="1" /> <!-- added on 4/26/19 -->
-<Data ElementName="unknown_1188_2_MJ" Type="int8" Size="2" />
+<Data ElementName="require_membership" Type="int8" Size="1" />
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
 <Struct Name="WS_ItemRecipeBook" ClientVersion="60055" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqExamineInfoCmd">
@@ -12033,4 +12039,4 @@
 </Data>
 <Data ElementName="footer" Substruct="Substruct_ItemFooter" Size="1" />
 </Struct>
-</EQ2Emulator>
+</EQ2Emulator>

+ 2 - 4
server/WorldStructs.xml

@@ -9449,8 +9449,7 @@ to zero and treated like placeholders." />
     <Data ElementName="token_id2" Type="sint32" Size =" 1" />
     <Data ElementName="token_name" Type="EQ2_16Bit_String" Size =" 1" />
   </Data>
-  <Data ElementName="description" Type="EQ2_8Bit_String" Size="1" />
-  <Data ElementName="unknown" Type="int8" Size="1" />
+  <Data ElementName="description" Type="EQ2_16Bit_String" Size="1" />
 </Data>
 <Data ElementName="type" Type="int32" />
 <Data ElementName="unknown8" Type="int8" Size="43" />
@@ -9487,8 +9486,7 @@ to zero and treated like placeholders." />
     <Data ElementName="token_id2" Type="sint32" Size =" 1" />
     <Data ElementName="token_name" Type="EQ2_16Bit_String" Size =" 1" />
   </Data>
-  <Data ElementName="description" Type="EQ2_8Bit_String" Size="1" />
-  <Data ElementName="unknown" Type="int8" Size="1" />
+  <Data ElementName="description" Type="EQ2_16Bit_String" Size="1" />
 </Data>
 <Data ElementName="type" Type="int32" />
 <Data ElementName="unknown8a" Type="int16" Size="20" />