Browse Source

Complete Fix #418 item flags
- Temporary item flag support (removes item 30 minutes from camp out)
Rule R_Player, TemporaryItemLogoutTime added for seconds to deletion of item
- Heirloom item flag support added (limited to group support)
Rule R_Player, HeirloomItemShareExpiration added for seconds to inability to trade item between prior group members(tbd raid)

SQL Updates:
CREATE TABLE `character_items_group_members` (
`unique_id` int(10) unsigned NOT NULL default 0,
`character_id` int(10) unsigned NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
alter table character_items add column last_saved timestamp default current_timestamp on update current_timestamp;
alter table character_items add column created timestamp default current_timestamp;

Emagi 1 year ago
parent
commit
682e023635

+ 4 - 0
DB/character_items_group_members_june30_2022.sql

@@ -0,0 +1,4 @@
+CREATE TABLE `character_items_group_members` (
+  `unique_id` int(10) unsigned NOT NULL default 0,
+  `character_id` int(10) unsigned NOT NULL DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;

+ 1 - 0
DB/updates/character_items_updatedcolumn_june30_2022.sql

@@ -0,0 +1 @@
+alter table character_items add column last_saved timestamp default current_timestamp on update current_timestamp;

+ 1 - 0
DB/updates/starting_languages_june_28_2022.sql

@@ -0,0 +1 @@
+update starting_languages set race=2 where race=3 and language_id=3; #dwarf (2) language id of 3 is not erudite race (3)

+ 18 - 0
EQ2/source/WorldServer/Bots/Bot.cpp

@@ -57,6 +57,24 @@ void Bot::GiveItem(int32 item_id) {
 	}
 }
 
+void Bot::GiveItem(Item* item) {
+	if (item) {
+		int8 slot = GetEquipmentList()->GetFreeSlot(item);
+		if (slot != 255) {
+			GetEquipmentList()->AddItem(slot, item);
+			SetEquipment(item, slot);
+			database.SaveBotItem(BotID, item->details.item_id, slot);
+			if (slot == 0) {
+				ChangePrimaryWeapon();
+				if (IsBot())
+					LogWrite(PLAYER__ERROR, 0, "Bot", "Changing bot primary weapon.");
+			}
+
+			CalculateBonuses();
+		}
+	}
+}
+
 void Bot::RemoveItem(Item* item) {
 	int8 slot = GetEquipmentList()->GetSlotByItem(item);
 	if (slot != 255) {

+ 1 - 0
EQ2/source/WorldServer/Bots/Bot.h

@@ -15,6 +15,7 @@ public:
 	bool IsBot() { return true; }
 
 	void GiveItem(int32 item_id);
+	void GiveItem(Item* item);
 	void RemoveItem(Item* item);
 	void TradeItemAdded(Item* item);
 	void AddItemToTrade(int8 slot);

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

@@ -840,6 +840,7 @@ Item::Item(){
 	generic_info.condition = 100;
 	no_buy_back = false;
 	no_sale = false;
+	created = std::time(nullptr);
 }
 
 Item::Item(Item* in_item){
@@ -857,6 +858,8 @@ Item::Item(Item* in_item){
 	spell_tier = in_item->spell_tier;
 	no_buy_back = in_item->no_buy_back;
 	no_sale = in_item->no_sale;
+	created = in_item->created;
+	grouped_char_ids.insert(in_item->grouped_char_ids.begin(), in_item->grouped_char_ids.end());
 }
 
 Item::~Item(){

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

@@ -21,6 +21,7 @@
 #define __EQ2_ITEMS__
 #include <map>
 #include <vector>
+#include <ctime>
 #include "../../common/types.h"
 #include "../../common/DataBuffer.h"
 #include "../../common/MiscFunctions.h"
@@ -918,6 +919,10 @@ public:
 	string					item_script;
 	bool					no_buy_back;
 	bool					no_sale;
+	bool 					needs_deletion;
+	std::time_t				created;
+	std::map<int32, bool>	grouped_char_ids;
+	
 	void AddEffect(string effect, int8 percentage, int8 subbulletflag);
 	void AddBookPage(int8 page, string page_text,int8 valign, int8 halign);
 	int32 GetMaxSellValue();
@@ -996,7 +1001,6 @@ public:
 	bool CheckFlag2(int32 flag);
 	void AddSlot(int8 slot_id);
 	void SetSlots(int32 slots);
-	bool needs_deletion;
 };
 class MasterItemList{
 public:

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

@@ -27,8 +27,10 @@
 #include "../WorldDatabase.h"
 #include "Items.h"
 #include "../World.h"
+#include "../Rules/Rules.h"
 
 extern World world;
+extern RuleManager rule_manager;
 
 // handle new database class til all functions are converted
 void WorldDatabase::LoadDataFromRow(DatabaseResult* result, Item* item) 
@@ -1055,6 +1057,9 @@ void WorldDatabase::SaveItems(Client* client)
 		item = item_iter->second;
 
 		if(item) {
+			if(item->CheckFlag(TEMPORARY)) {
+					item->save_needed = true; // we need to keep updating the timestamp so it doesn't expire
+			}
 			if(item->needs_deletion || (client->IsZoning() && item->CheckFlag(NO_ZONE))) {
 				DeleteItem(client->GetCharacterID(), item, 0);
 				client->GetPlayer()->item_list.DestroyItem(item->details.index);
@@ -1080,6 +1085,9 @@ void WorldDatabase::SaveItems(Client* client)
 
 		if(item)
 		{
+			if(item->CheckFlag(TEMPORARY)) {
+					item->save_needed = true; // we need to keep updating the timestamp so it doesn't expire
+			}
 			if(item->needs_deletion || (client->IsZoning() && item->CheckFlag(NO_ZONE))) {
 				DeleteItem(client->GetCharacterID(), item, 0);
 				client->GetPlayer()->item_list.DestroyItem(item->details.index);
@@ -1107,6 +1115,9 @@ void WorldDatabase::SaveItems(Client* client)
 
 		if(item)
 		{
+			if(item->CheckFlag(TEMPORARY)) {
+					item->save_needed = true; // we need to keep updating the timestamp so it doesn't expire
+			}
 			if(item->needs_deletion || (client->IsZoning() && item->CheckFlag(NO_ZONE))) {
 				DeleteItem(client->GetCharacterID(), item, 0);
 				client->GetPlayer()->item_list.DestroyItem(item->details.index);
@@ -1126,6 +1137,9 @@ void WorldDatabase::SaveItems(Client* client)
 	for (int32 i = 0; i < overflow->size(); i++){
 		item = overflow->at(i);
 		if (item) {
+			if(item->CheckFlag(TEMPORARY)) {
+					item->save_needed = true; // we need to keep updating the timestamp so it doesn't expire
+			}
 			if(item->needs_deletion || (client->IsZoning() && item->CheckFlag(NO_ZONE))) {
 				DeleteItem(client->GetCharacterID(), item, 0);
 				client->GetPlayer()->item_list.DestroyItem(item->details.index);
@@ -1152,6 +1166,13 @@ void WorldDatabase::SaveItem(int32 account_id, int32 char_id, Item* item, const
 	string update_item = string("REPLACE INTO character_items (id, type, char_id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, no_sale, account_id, login_checksum) VALUES (%u, '%s', %u, %i, %u, '%s', %i, %i, %i, %i, %i, %i, %i, %u, %u, %u, 0)");
 	query.AddQueryAsync(char_id, this, Q_REPLACE, update_item.c_str(), item->details.unique_id, type, char_id, item->details.slot_id, item->details.item_id,
 		getSafeEscapeString(item->creator.c_str()).c_str(),item->adorn0,item->adorn1,item->adorn2, item->generic_info.condition, item->CheckFlag(ATTUNED) ? 1 : 0, item->details.inv_slot_id, item->details.count, item->GetMaxSellValue(), item->no_sale, account_id);
+	if(item->CheckFlag2(HEIRLOOM)) {
+		std::map<int32, bool>::iterator itr;
+		for(itr = item->grouped_char_ids.begin(); itr != item->grouped_char_ids.end(); itr++) {
+			string addmembers_query = string("REPLACE INTO character_items_group_members (unique_id, character_id) VALUES (%u, %u)");
+			query.AddQueryAsync(char_id, this, Q_REPLACE, addmembers_query.c_str(), item->details.unique_id, itr->first);
+		}
+	}
 }
 
 void WorldDatabase::DeleteItem(int32 char_id, Item* item, const char* type) 
@@ -1173,6 +1194,11 @@ void WorldDatabase::DeleteItem(int32 char_id, Item* item, const char* type)
 		delete_item = string("DELETE FROM character_items WHERE char_id = %u AND (id = %u OR bag_id = %u)");
 		query.RunQuery2(Q_DELETE, delete_item.c_str(), char_id, item->details.unique_id, item->details.unique_id);
 	}
+	
+	if(item->CheckFlag2(HEIRLOOM)) {
+		delete_item = string("DELETE FROM character_items_group_members WHERE unique_id = %u");
+		query.RunQuery2(Q_DELETE, delete_item.c_str(), item->details.unique_id);
+	}
 }
 
 void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Player* player, int16 version) 
@@ -1181,7 +1207,7 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 
 	Query query;
 	MYSQL_ROW row;
-	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT type, id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, no_sale FROM character_items where char_id = %u or (bag_id = -4 and account_id = %u) ORDER BY slot asc", char_id, account_id);
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT type, id, slot, item_id, creator,adorn0,adorn1,adorn2, condition_, attuned, bag_id, count, max_sell_value, no_sale, UNIX_TIMESTAMP(last_saved), UNIX_TIMESTAMP(created) FROM character_items where char_id = %u or (bag_id = -4 and account_id = %u) ORDER BY slot asc", char_id, account_id);
 
 	if(result)
 	{
@@ -1189,9 +1215,8 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 
 		while(result && (row = mysql_fetch_row(result)))
 		{
-			LogWrite(ITEM__DEBUG, 5, "Items", "\tLoading character item: %u, slot: %i", strtoul(row[1], NULL, 0), atoi(row[2]));
+			LogWrite(ITEM__DEBUG, 5, "Items", "Loading character item: %u, slot: %i", strtoul(row[1], NULL, 0), atoi(row[2]));
 			Item* master_item = master_item_list.GetItem(strtoul(row[3], NULL, 0));
-
 			if(master_item)
 			{
 				Item* item = new Item(master_item);
@@ -1203,6 +1228,19 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 
 				item->save_needed = false;
 
+				// we need the items basics (unique id slot id bag id) to continue this temporary check
+				if(item->CheckFlag(TEMPORARY)) {
+					std::time_t last_saved =  static_cast<std::time_t>(atoul(row[14]));
+					double timeInSeconds = std::difftime(std::time(nullptr), last_saved);
+						LogWrite(ITEM__INFO, 0, "Items", "Character ID %u has a temporary item %s time in seconds %f last saved.", char_id, item->name.c_str(), timeInSeconds);
+					if(timeInSeconds >= rule_manager.GetGlobalRule(R_Player, TemporaryItemLogoutTime)->GetFloat()) {
+						DeleteItem(char_id, item, 0);
+						LogWrite(ITEM__INFO, 0, "Items", "\tCharacter ID %u had a temporary item %s which was removed due to time limit.", char_id, item->name.c_str());
+						safe_delete(item);
+						continue;
+					}
+				}
+				
 				if(row[4])
 					item->creator = string(row[4]);//creator
 				item->adorn0 = atoi(row[5]); //adorn0
@@ -1220,6 +1258,21 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 
 					item->generic_info.item_flags += ATTUNED;
 				}
+				
+				if(item->CheckFlag2(HEIRLOOM)) {
+					MYSQL_ROW row2;
+					MYSQL_RES* result2 = query.RunQuery2(Q_SELECT, "SELECT character_id from character_items_group_members where unique_id = %u", item->details.unique_id);
+
+					if(result2)
+					{
+						bool ret = true;
+
+						while(result2 && (row2 = mysql_fetch_row(result2)))
+						{
+							item->grouped_char_ids.insert(std::make_pair(atoul(row2[0]),true));
+						}
+					}
+				}
 
 				item->details.inv_slot_id = atol(row[10]); //bag_id
 				item->details.count = atoi(row[11]); //count
@@ -1227,6 +1280,8 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe
 				item->no_sale = (atoul(row[13]) == 1);
 				item->details.appearance_type = 0;
 
+				// position 14 is used for the last_saved timestamp (primarily for checking temporary items on login)
+				item->created = static_cast<std::time_t>(atoul(row[15]));
 				
 				if(strncasecmp(row[0], "EQUIPPED", 8)==0)
 					ret = player->GetEquipmentList()->AddItem(item->details.slot_id, item);

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

@@ -212,7 +212,8 @@ void RuleManager::Init()
 	RULE_INIT(R_Player, MinLastNameLength, "4");
 	RULE_INIT(R_Player, DisableHouseAlignmentRequirement, "1");
 	RULE_INIT(R_Player, MentorItemDecayRate, ".05"); // 5% per level lost when mentoring
-
+	RULE_INIT(R_Player, TemporaryItemLogoutTime, "1800.0"); // time in seconds (double) for temporary item to decay after being logged out for a period of time, 30 min is the default
+	RULE_INIT(R_Player, HeirloomItemShareExpiration, "172800.0"); // 2 days ('48 hours') in seconds
 	/* PVP */
 	RULE_INIT(R_PVP, AllowPVP, "0");
 	RULE_INIT(R_PVP, LevelRange, "4");

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

@@ -72,6 +72,8 @@ enum RuleType {
 	MinLastNameLength,
 	DisableHouseAlignmentRequirement,
 	MentorItemDecayRate,
+	TemporaryItemLogoutTime,
+	HeirloomItemShareExpiration,
 
 	/* PVP */
 	AllowPVP,

+ 34 - 13
EQ2/source/WorldServer/Trade.cpp

@@ -3,9 +3,11 @@
 #include "Entity.h"
 #include "Bots/Bot.h"
 #include "../common/Log.h"
+#include "Rules/Rules.h"
 
 extern ConfigReader configReader;
 extern MasterItemList master_item_list;
+extern RuleManager rule_manager;
 
 Trade::Trade(Entity* trader1, Entity* trader2) {
 	this->trader1 = trader1;
@@ -36,7 +38,7 @@ int8 Trade::AddItemToTrade(Entity* character, Item* item, int8 quantity, int8 sl
 	}
 
 	Entity* other = GetTradee(character);
-	int8 result = CheckItem(character, item, other->IsBot());
+	int8 result = CheckItem(character, item, other);
 
 	if (result == 0) {
 		if (character == trader1) {
@@ -59,11 +61,16 @@ int8 Trade::AddItemToTrade(Entity* character, Item* item, int8 quantity, int8 sl
 	return result;
 }
 
-int8 Trade::CheckItem(Entity* trader, Item* item, bool other_is_bot) {
+int8 Trade::CheckItem(Entity* trader, Item* item, Entity* other) {
 	int8 ret = 0;
 	map<int8, TradeItemInfo>* list = 0;
 	map<int8, TradeItemInfo>::iterator itr;
 
+	bool other_is_bot = false;
+	
+	if(other)
+		other_is_bot = other->IsBot();
+	
 	if (trader == trader1)
 		list = &trader1_items;
 	else if (trader == trader2)
@@ -83,8 +90,17 @@ int8 Trade::CheckItem(Entity* trader, Item* item, bool other_is_bot) {
 			if (!other_is_bot) {
 				if (item->CheckFlag(NO_TRADE))
 					ret = 2;
-				if (item->CheckFlag2(HEIRLOOM))
-					ret = 3;
+				if (item->CheckFlag2(HEIRLOOM)) {
+					if(item->grouped_char_ids.find(((Player*)other)->GetCharacterID()) != item->grouped_char_ids.end()) {
+					double diffInSeconds = 0.0; std::difftime(std::time(nullptr), item->created);
+						if(item->CheckFlag(ATTUNED) || ((diffInSeconds = std::difftime(std::time(nullptr), item->created)) && diffInSeconds >= rule_manager.GetGlobalRule(R_Player, HeirloomItemShareExpiration)->GetFloat())) {
+							ret = 3; // denied heirloom cannot be transferred to outside of group after 48 hours (by rule setting) or if already attuned
+						}
+					}
+					else {
+						ret = 3; // not part of the group/raid
+					}
+				}
 			}
 		}
 	}
@@ -243,8 +259,8 @@ void Trade::Trader2ItemAdd(Item* item, int8 quantity, int8 slot) {
 
 void Trade::CompleteTrade() {
 	map<int8, TradeItemInfo>::iterator itr;
-	map<int32, int8> trader1_item_ids;
-	map<int32, int8>::iterator itr2;
+	vector<Item*> trader1_item_pass;
+	vector<Item*>::iterator itr2;
 	string log_string = "TradeComplete:\n";
 
 	if (trader1->IsPlayer()) {
@@ -259,14 +275,19 @@ void Trade::CompleteTrade() {
 			player->RemoveCoins(trader1_coins);
 			for (itr = trader1_items.begin(); itr != trader1_items.end(); itr++) {
 				// client->RemoveItem can delete the item so we need to store the item id's and quantity to give to trader2
-				trader1_item_ids[itr->second.item->details.item_id] = itr->second.quantity;
+				Item* newitem = new Item(itr->second.item);
+				newitem->details.count = itr->second.quantity;
+				trader1_item_pass.push_back(newitem);
+				
 				log_string += itr->second.item->name + " (" + to_string(itr->second.item->details.item_id) + ") x" + to_string(itr->second.quantity) + "\n";
 				client->RemoveItem(itr->second.item, itr->second.quantity);
 			}
 
 			player->AddCoins(trader2_coins);
 			for (itr = trader2_items.begin(); itr != trader2_items.end(); itr++) {
-				client->AddItem(itr->second.item->details.item_id, itr->second.quantity);
+				Item* newitem = new Item(itr->second.item);
+				newitem->details.count = itr->second.quantity;
+				client->AddItem(newitem, nullptr);
 			}
 
 			PacketStruct* packet = configReader.getStruct("WS_PlayerTrade", client->GetVersion());
@@ -297,8 +318,8 @@ void Trade::CompleteTrade() {
 			}
 
 			player->AddCoins(trader1_coins);
-			for (itr2 = trader1_item_ids.begin(); itr2 != trader1_item_ids.end(); itr2++) {
-				client->AddItem(itr2->first, itr2->second);
+			for (itr2 = trader1_item_pass.begin(); itr2 != trader1_item_pass.end(); itr2++) {
+				client->AddItem(*itr2, nullptr);
 			}
 
 			PacketStruct* packet = configReader.getStruct("WS_PlayerTrade", client->GetVersion());
@@ -317,9 +338,9 @@ void Trade::CompleteTrade() {
 			bot->RemoveItem(itr->second.item);
 		}
 
-		for (itr2 = trader1_item_ids.begin(); itr2 != trader1_item_ids.end(); itr2++) {
-			bot->GiveItem(itr2->first);
-		}
+		for (itr2 = trader1_item_pass.begin(); itr2 != trader1_item_pass.end(); itr2++) {
+			bot->GiveItem(*itr2);
+		}	
 		bot->FinishTrade();
 	}
 

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

@@ -26,7 +26,7 @@ public:
 	bool HasAcceptedTrade(Entity* character);
 	void CancelTrade(Entity* character);
 
-	int8 CheckItem(Entity* trader, Item* item, bool other_is_bot);
+	int8 CheckItem(Entity* trader, Item* item, Entity* other);
 
 private:
 

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

@@ -2536,6 +2536,34 @@ bool Client::HandleLootItem(Spawn* entity, Item* item) {
 	}
 	if (player->item_list.HasFreeSlot() || player->item_list.CanStack(item)) {
 		if (player->item_list.AssignItemToFreeSlot(item)) {
+			
+			if(item->CheckFlag2(HEIRLOOM)) { // TODO: RAID Support
+				GroupMemberInfo* gmi = GetPlayer()->GetGroupMemberInfo();
+				if (gmi && gmi->group_id)
+				{
+					PlayerGroup* group = world.GetGroupManager()->GetGroup(gmi->group_id);
+					if (group)
+					{
+						group->MGroupMembers.readlock(__FUNCTION__, __LINE__);
+						deque<GroupMemberInfo*>* members = group->GetMembers();
+						if(members) {
+							for (int8 i = 0; i < members->size(); i++) {
+								Entity* member = members->at(i)->member;
+
+								if ((member->GetZone() != this->GetPlayer()->GetZone()))
+									continue;
+								
+								if(member->IsPlayer()) {
+										item->grouped_char_ids.insert(std::make_pair(((Player*)member)->GetCharacterID(), true));
+										item->save_needed = true;
+								}
+							}
+						}
+						group->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__);
+					}
+				}
+			}
+			
 			int8 type = CHANNEL_LOOT;
 			if (entity) {
 				Message(type, "You loot %s from the corpse of %s", item->CreateItemLink(GetVersion()).c_str(), entity->GetName());