Browse Source

Group fixes/support - requires characters table update

Fix #66

- Fixed crashing of the world server when a client in a group zones
- Tracking of players group_id in the characters table (for both sanity and tracking cross zone)
- Clients zoning now properly 'rejoin' their group
- Mutex locks added to group code, this makes groups more reliably work across different zones as they run on different threads (some behavior for example was some players would not see chat and others would)
image 3 years ago
parent
commit
41716c32fa

+ 1 - 0
DB/updates/characters_table_update_groupid_aug21_2020.sql

@@ -0,0 +1 @@
+alter table characters add column group_id int(10) unsigned not null default '0';

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

@@ -62,7 +62,10 @@ bool PlayerGroup::AddMember(Entity* member) {
 
 	member->SetGroupMemberInfo(gmi);
 	member->UpdateGroupMemberInfo();
+
+	MGroupMembers.writelock();
 	m_members.push_back(gmi);
+	MGroupMembers.releasewritelock();
 
 	SendGroupUpdate();
 	return true;
@@ -77,6 +80,7 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 	bool ret = false;
 
 	member->SetGroupMemberInfo(0);
+	MGroupMembers.writelock();
 	deque<GroupMemberInfo*>::iterator erase_itr = m_members.end();
 	deque<GroupMemberInfo*>::iterator itr;
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
@@ -89,6 +93,7 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 		ret = true;
 		m_members.erase(erase_itr);
 	}
+	MGroupMembers.releasewritelock();
 
 	safe_delete(gmi);
 	if (member->IsBot())
@@ -99,6 +104,7 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 
 void PlayerGroup::Disband() {
 	deque<GroupMemberInfo*>::iterator itr;
+	MGroupMembers.writelock();
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
 		if ((*itr)->member) {
 			(*itr)->member->SetGroupMemberInfo(0);
@@ -112,37 +118,45 @@ void PlayerGroup::Disband() {
 	}
 
 	m_members.clear();
+	MGroupMembers.releasewritelock();
 }
 
 void PlayerGroup::SendGroupUpdate(Client* exclude) {
 	deque<GroupMemberInfo*>::iterator itr;
+	MGroupMembers.readlock();
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
 		GroupMemberInfo* gmi = *itr;
 		if (gmi->client && gmi->client != exclude && !gmi->client->IsZoning())
 			gmi->client->GetPlayer()->SetCharSheetChanged(true);
 	}
+	MGroupMembers.releasereadlock();
 }
 
 void PlayerGroup::SimpleGroupMessage(const char* message) {
 	deque<GroupMemberInfo*>::iterator itr;
+	MGroupMembers.readlock();
 	for(itr = m_members.begin(); itr != m_members.end(); itr++) {
 		GroupMemberInfo* info = *itr;
 		if(info->client)
 			info->client->SimpleMessage(CHANNEL_GROUP, message);
 	}
+	MGroupMembers.releasereadlock();
 }
 
 void PlayerGroup::GroupChatMessage(Spawn* from, const char* message) {
 	deque<GroupMemberInfo*>::iterator itr;
+	MGroupMembers.readlock();
 	for(itr = m_members.begin(); itr != m_members.end(); itr++) {
 		GroupMemberInfo* info = *itr;
 		if(info && info->client && info->client->GetCurrentZone())
 			info->client->GetCurrentZone()->HandleChatMessage(info->client, from, 0, CHANNEL_GROUP_SAY, message, 0);
 	}
+	MGroupMembers.releasereadlock();
 }
 
 void PlayerGroup::MakeLeader(Entity* new_leader) {
 	deque<GroupMemberInfo*>::iterator itr;
+	MGroupMembers.readlock();
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
 		GroupMemberInfo* info = *itr;
 		if (info->leader) {
@@ -150,6 +164,7 @@ void PlayerGroup::MakeLeader(Entity* new_leader) {
 			break;
 		}
 	}
+	MGroupMembers.releasereadlock();
 
 	new_leader->GetGroupMemberInfo()->leader = true;
 	SendGroupUpdate();
@@ -388,6 +403,13 @@ void PlayerGroupManager::SendGroupUpdate(int32 group_id, Client* exclude) {
 	MGroups.releasewritelock(__FUNCTION__, __LINE__);
 }
 
+PlayerGroup* PlayerGroupManager::GetGroup(int32 group_id) {
+	if (m_groups.count(group_id) > 0)
+		return m_groups[group_id];
+
+	return 0;
+}
+
 deque<GroupMemberInfo*>* PlayerGroupManager::GetGroupMembers(int32 group_id) {
 	if (m_groups.count(group_id) > 0)
 		return m_groups[group_id]->GetMembers();
@@ -479,6 +501,7 @@ void PlayerGroupManager::SendGroupQuests(int32 group_id, Client* client) {
 	GroupMemberInfo* info = 0;
 	MGroups.readlock(__FUNCTION__, __LINE__);
 	if (m_groups.count(group_id) > 0) {
+		m_groups[group_id]->MGroupMembers.readlock();
 		deque<GroupMemberInfo*>* members = m_groups[group_id]->GetMembers();
 		deque<GroupMemberInfo*>::iterator itr;
 		for (itr = members->begin(); itr != members->end(); itr++) {
@@ -489,6 +512,7 @@ void PlayerGroupManager::SendGroupQuests(int32 group_id, Client* client) {
 				client->SendQuestJournal(false, info->client);
 			}
 		}
+		m_groups[group_id]->MGroupMembers.releasereadlock();
 	}
 	MGroups.releasereadlock(__FUNCTION__, __LINE__);
 }
@@ -560,6 +584,7 @@ void PlayerGroupManager::UpdateGroupBuffs() {
 		/* loop through the group members and see if any of them have any maintained spells that are group buffs and friendly.
 		if so, update the list of targets and apply/remove effects as needed */
 
+		group->MGroupMembers.readlock();
 		for (member_itr = group->GetMembers()->begin(); member_itr != group->GetMembers()->end(); member_itr++) {
 			if ((*member_itr)->client)
 				caster = (*member_itr)->client->GetPlayer();
@@ -695,6 +720,7 @@ void PlayerGroupManager::UpdateGroupBuffs() {
 			}
 			caster->GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__);
 		}
+		group->MGroupMembers.releasereadlock();
 	}
 }
 
@@ -704,6 +730,7 @@ bool PlayerGroupManager::IsInGroup(int32 group_id, Entity* member) {
 	MGroups.readlock(__FUNCTION__, __LINE__);
 
 	if (m_groups.count(group_id) > 0) {
+		m_groups[group_id]->MGroupMembers.readlock();
 		deque<GroupMemberInfo*>* members = m_groups[group_id]->GetMembers();
 		for (int8 i = 0; i < members->size(); i++) {
 			if (member == members->at(i)->member) {
@@ -711,6 +738,7 @@ bool PlayerGroupManager::IsInGroup(int32 group_id, Entity* member) {
 				break;
 			}
 		}
+		m_groups[group_id]->MGroupMembers.releasereadlock();
 	}
 
 	MGroups.releasereadlock(__FUNCTION__, __LINE__);

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

@@ -83,6 +83,7 @@ public:
 	void GroupChatMessage(Spawn* from, const char* message);
 	void MakeLeader(Entity* new_leader);
 
+	Mutex MGroupMembers;				// Mutex for the group members
 private:
 	int32					m_id;		// ID of this group
 	deque<GroupMemberInfo*>	m_members;	// List of members in this group
@@ -140,6 +141,9 @@ public:
 	/// <param name='exclude'>Client* to exclude from the update, usually the one that triggers the update</param>
 	void SendGroupUpdate(int32 group_id, Client* exclude = 0);
 
+
+	PlayerGroup* GetGroup(int32 group_id);
+
 	/// <summary>
 	/// Gets the group members for the given group, be sure to call GroupLock() before calling this and ReleaseGroupLock() after you 
 	/// are done with the list.  This is for reading purposes only, the list should never be altered using this function

+ 50 - 18
EQ2/source/WorldServer/World.cpp

@@ -1459,27 +1459,59 @@ void World::SendGroupQuests(PlayerGroup* group, Client* client){
 	}
 }*/
 
-void World::RejoinGroup(Client* client){
-	/*map<GroupMemberInfo*, int32>::iterator itr;
-	GroupMemberInfo* found = 0;
+void World::RejoinGroup(Client* client, int32 group_id){
+	if (!group_id) // no need if no group id!
+		return;
+
+	world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__);
+	PlayerGroup* group = world.GetGroupManager()->GetGroup(group_id);
+	deque<GroupMemberInfo*>* members = 0;
+	if (group)
+		members = group->GetMembers();
+
 	string name = string(client->GetPlayer()->GetName());
-	MGroups.readlock(__FUNCTION__, __LINE__);
-	PlayerGroup* group = 0;
-	for(int i = player_groups.size()-1;!found && i >= 0;i--){
-		group = player_groups[i];
-		for(int x=group->members.size()-1;x>=0; x--){
-			if(group->members[x]->name == name){
-				found = group->members[x];
-				break;
-			}
+	if (!members)
+	{
+		// group does not exist!
+
+		Query query;
+		query.AddQueryAsync(client->GetCharacterID(), &database, Q_INSERT, "UPDATE characters set group_id = 0 where id = %u",
+			client->GetCharacterID());
+		LogWrite(PLAYER__ERROR, 0, "Player", "Group did not exist for player %s to group id %i, async query to group_id = 0.", name.c_str(), group_id);
+		world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
+		return;
+	}
+	deque<GroupMemberInfo*>::iterator itr;
+	GroupMemberInfo* info = 0;
+
+	bool match = false;
+	group->MGroupMembers.writelock();
+	for (itr = members->begin(); itr != members->end(); itr++) {
+
+		info = *itr;
+
+		if (info && info->name == name)
+		{
+			info->client = client;
+			info->member = client->GetPlayer();
+			client->GetPlayer()->SetGroup(group);
+			client->GetPlayer()->SetGroupMemberInfo(info);
+			client->GetPlayer()->UpdateGroupMemberInfo();
+			LogWrite(PLAYER__DEBUG, 0, "Player", "Identified group match for player %s to group id %u", name.c_str(), group_id);
+			match = true;
+			break;
 		}
 	}
-	MGroups.releasereadlock(__FUNCTION__, __LINE__);
-	if(found){
-		found->client = client;
-		client->GetPlayer()->SetGroup(found->group);
-		client->GetPlayer()->SetGroupMemberInfo(found);
-	}*/
+	group->MGroupMembers.releasewritelock();
+
+	// must be done after cause it needs a readlock
+	if (match)
+		group->SendGroupUpdate();
+
+	if (!match)
+		LogWrite(PLAYER__ERROR, 0, "Player", "Identified group match for player %s to group id %u, however the player name was not present in the group!  May be an old group id that has been re-used.", name.c_str(), group_id);
+	
+	world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
 }
 
 

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

@@ -563,7 +563,7 @@ public:
 	//void GroupReadUnLock();
 	//void CheckRemoveGroupedPlayer();
 	//void SendGroupUpdate(PlayerGroup* group, Client* exclude = 0);
-	void RejoinGroup(Client* client);
+	void RejoinGroup(Client* client, int32 group_id);
 	//bool MakeLeader(Client* leader, string new_leader);
 	
 	void AddBonuses(ItemStatsValues* values, int16 type, sint32 value, Entity* entity);

+ 9 - 5
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -1530,7 +1530,7 @@ bool WorldDatabase::loadCharacter(const char* ch_name, int32 account_id, Client*
 	MYSQL_ROW row, row4;
 	int32 id = 0;
 	query.escaped_name = getEscapeString(ch_name);
-	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT id, current_zone_id, x, y, z, heading, admin_status, race, model_type, class, deity, level, gender, tradeskill_class, tradeskill_level, wing_type, hair_type, chest_type, legs_type, soga_wing_type, soga_hair_type, soga_chest_type, soga_legs_type, 0xFFFFFFFF - crc32(name), facial_hair_type, soga_facial_hair_type,instance_id,last_saved, DATEDIFF(curdate(), created_date) as accage FROM characters where name='%s' and account_id=%i AND deleted = 0", query.escaped_name, account_id);
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT id, current_zone_id, x, y, z, heading, admin_status, race, model_type, class, deity, level, gender, tradeskill_class, tradeskill_level, wing_type, hair_type, chest_type, legs_type, soga_wing_type, soga_hair_type, soga_chest_type, soga_legs_type, 0xFFFFFFFF - crc32(name), facial_hair_type, soga_facial_hair_type, instance_id, group_id, last_saved, DATEDIFF(curdate(), created_date) as accage FROM characters where name='%s' and account_id=%i AND deleted = 0", query.escaped_name, account_id);
 	// no character found
 	if ( result == NULL ) {
 		LogWrite(PLAYER__ERROR, 0, "Player", "Error loading character for '%s'", ch_name);
@@ -1578,6 +1578,10 @@ bool WorldDatabase::loadCharacter(const char* ch_name, int32 account_id, Client*
 		client->GetPlayer()->features.hair_face_type = atoi(row[24]);
 		client->GetPlayer()->features.soga_hair_face_type = atoi(row[25]);
 		int32 instanceid = atoi(row[26]);
+
+		int32 groupid = atoi(row[27]);
+		client->SetRejoinGroupID(groupid);
+
 		int32 zoneid = atoul(row[1]);
 /*
 JA Notes on SOGA: I think there are many more settings to add than were commented out here,
@@ -1596,11 +1600,11 @@ SOGA chars looked ok in LoginServer screen tho... odd.
 
 
 
-		int32 lastsavedtime = atoi(row[27]);
+		int32 lastsavedtime = atoi(row[28]);
 		client->SetLastSavedTimeStamp(lastsavedtime);
 
-		if (row[28])
-			client->GetPlayer()->GetPlayerInfo()->SetAccountAge(atoi(row[28]));
+		if (row[29])
+			client->GetPlayer()->GetPlayerInfo()->SetAccountAge(atoi(row[29]));
 
 		LoadCharacterFriendsIgnoreList(client->GetPlayer());
 		MYSQL_RES* result4 = query4.RunQuery2(Q_SELECT, "SELECT `guild_id` FROM `guild_members` WHERE `char_id`=%u", id);
@@ -3697,7 +3701,7 @@ void WorldDatabase::Save(Client* client){
 	int32 zone_id = 0;
 	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 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->GetCharacterID());
+	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' 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(),

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

@@ -194,6 +194,7 @@ Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_deb
 	placement_unique_item_id = 0;
 	SetHasOwnerOrEditAccess(false);
 	temporary_transport_id = 0;
+	rejoin_group_id = 0;
 }
 
 Client::~Client() {
@@ -201,6 +202,7 @@ Client::~Client() {
 		if (player->GetGroupMemberInfo() && (player->GetActivityStatus() & ACTIVITY_STATUS_LINKDEAD) > 0)
 			world.GetGroupManager()->RemoveGroupMember(player->GetGroupMemberInfo()->group_id, player);
 		world.GetGroupManager()->ClearPendingInvite(player);
+
 	}
 	if (lua_interface)
 		lua_interface->RemoveDebugClients(this);
@@ -3506,6 +3508,17 @@ void Client::Zone(ZoneServer* new_zone, bool set_coords) {
 		LogWrite(CCLIENT__DEBUG, 0, "Client", "Zone Request Denied! No 'new_zone' value");
 		return;
 	}
+
+	// block out the member info for the group
+	if (this->GetPlayer()->GetGroupMemberInfo())
+	{
+		PlayerGroup* group = world.GetGroupManager()->GetGroup(this->GetPlayer()->GetGroupMemberInfo()->group_id);
+		group->MGroupMembers.writelock();
+		this->GetPlayer()->GetGroupMemberInfo()->client = 0;
+		this->GetPlayer()->GetGroupMemberInfo()->member = 0;
+		group->MGroupMembers.releasewritelock();
+	}
+
 	client_zoning = true;
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "%s: Setting player Resurrecting to 'true'", __FUNCTION__);
 	player->SetResurrecting(true);
@@ -8693,7 +8706,7 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 				new_client_login = true;
 				GetCurrentZone()->AddClient(this); //add to zones client list
 
-				world.RejoinGroup(this);
+				world.RejoinGroup(this, rejoin_group_id);
 				zone_list.AddClientToMap(player->GetName(), this);
 			}
 			else {

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

@@ -433,6 +433,8 @@ public:
 
 	void SetTemporaryTransportID(int32 id) { temporary_transport_id = id; }
 	int32 GetTemporaryTransportID() { return temporary_transport_id; }
+
+	void SetRejoinGroupID(int32 id) { rejoin_group_id = id; }
 private:
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
@@ -534,6 +536,8 @@ private:
 	bool hasOwnerOrEditAccess;
 
 	int32 temporary_transport_id;
+
+	int32 rejoin_group_id;
 };
 
 class ClientList {