//Blizzless Project 2022 using bgs.protocol; using DiIiS_NA.Core.Extensions; using DiIiS_NA.Core.Logging; using DiIiS_NA.Core.Storage; using DiIiS_NA.Core.Storage.AccountDataBase.Entities; using DiIiS_NA.LoginServer.AccountsSystem; using DiIiS_NA.LoginServer.Base; using DiIiS_NA.LoginServer.ChannelSystem; using Google.ProtocolBuffers; using System.Collections.Generic; using System.Linq; using DateTime = System.DateTime; namespace DiIiS_NA.LoginServer.GuildSystem { public class Guild { private static readonly Logger Logger = LogManager.CreateLogger(); /// /// D3.GameMessage.GuildId encoded guild Id. /// public D3.GameMessage.GuildId GuildId { get; protected set; } public ulong PersistentId { get; protected set; } public DBGuild DBGuild { get { return DBSessions.SessionGet(this.PersistentId); } } /// /// Max number of members. /// public uint MaxMembers { get; set; } /// /// Name of the guild /// public string Name { get; set; } public string FullName { get { //return this.IsClan ? string.Format("{0} [{1}]", this.Name, this.ParagonRatings.Values.ToList().OrderByDescending(x => x).Take(12).Sum()) : this.Name; return this.Name; } } public uint Flags { get { var flags = GuildFlags.None; if (!this.Disbanded) flags |= GuildFlags.Enabled; if (this.IsLFM) flags |= GuildFlags.LookingForMembers; if (this.IsInviteRequired) flags |= GuildFlags.InviteOnly; return (uint)flags; } } /// /// Prefix of the guild (clans) /// private string _Prefix = ""; public string Prefix { get { return this._Prefix; } set { this._Prefix = value; } } public string Description { get; set; } public string MOTD { get; set; } /// /// Category of the guild (clans are always 0) /// public uint Category { get; set; } public uint Language { get; set; } public Dictionary Ranks { get; set; } public byte[] BnetRanks { get { var builder = D3.Guild.RankList.CreateBuilder(); foreach (var rank in this.Ranks) { builder.AddRanks(D3.Guild.Rank.CreateBuilder().SetRankId(rank.Key).SetRankOrder(rank.Key).SetName(rank.Value.RankName).SetPermissions(rank.Value.Privileges)); } return builder.Build().ToByteString().ToByteArray(); } } /// /// Guild channel (common one) /// public Channel Channel { get; set; } public Channel GroupChatChannel { get; set; } /// /// Dictionary of guild members + their ranks. /// public readonly Dictionary Members = new Dictionary(); public List GuildSuggestions = new List(); /// /// Guild owner. /// public GameAccount Owner { get; set; } public bool IsClan { get; set; } public bool IsLFM { get; set; } public bool IsInviteRequired { get; set; } public Dictionary ParagonRatings { get; set; } public int RatingPoints { get; set; } public ulong NewsTime { get; set; } public ulong InviteTime { get; set; } public bool Disbanded { get; set; } /// /// Creates a new guild instance /// public Guild(DBGuild dbGuild) { this.PersistentId = dbGuild.Id; this.GuildId = D3.GameMessage.GuildId.CreateBuilder().SetGuildId_(dbGuild.Id).Build(); this.IsClan = (dbGuild.Category == 0); this.IsLFM = dbGuild.IsLFM; this.IsInviteRequired = dbGuild.IsInviteRequired; this.Prefix = dbGuild.Tag; this.Name = dbGuild.Name; this.Description = dbGuild.Description; this.MOTD = dbGuild.MOTD; this.RatingPoints = dbGuild.Rating; var ranks = D3.Guild.RankList.ParseFrom(dbGuild.Ranks); this.Ranks = new Dictionary(); foreach (var rank in ranks.RanksList) this.Ranks.Add(rank.RankId, new RankDescriptor() { RankName = rank.Name, Privileges = rank.Permissions }); this.NewsTime = 0; this.InviteTime = 0; this.Owner = GameAccountManager.GetGameAccountByDBGameAccount(dbGuild.Creator); this.MaxMembers = this.IsClan ? (16U) : 100U; this.Category = (uint)dbGuild.Category; this.Language = (uint)dbGuild.Language; this.Disbanded = dbGuild.Disbanded; this.Channel = ChannelManager.CreateGuildChannel(this); this.GroupChatChannel = ChannelManager.CreateGuildGroupChannel(this); this.ParagonRatings = new Dictionary(); } public void Disband() { this.RemoveAllMembers(); this.Disbanded = true; lock (this.DBGuild) { var dbGuild = this.DBGuild; dbGuild.Disbanded = true; DBSessions.SessionUpdate(dbGuild); } } public void ActivatePremium() { if (this.Ranks.Count < 4) this.Ranks.Add(3, new RankDescriptor() { RankName = "Advanced", Privileges = 212943U }); lock (this.DBGuild) { var dbGuild = this.DBGuild; dbGuild.Ranks = this.BnetRanks; DBSessions.SessionUpdate(dbGuild); } this.UpdateChannelAttributes(); } #region member functionality public bool HasMember(GameAccount account) { return this.Members.Keys.Any(a => a == account); } public void AddMember(GameAccount account) { if (HasMember(account)) { Logger.Warn("Attempted to add account {0} to guild when it was already a member of the guild", account.Owner.BattleTag); return; } if (this.IsClan && account.Clan != null) { Logger.Warn("Attempted to add account {0} to clan when it was already a member of the another clan", account.Owner.BattleTag); return; } if (this.Members.Count + 1 > this.MaxMembers) return; var newDBGuildMember = new DBGuildMember { DBGuild = this.DBGuild, DBGameAccount = account.DBGameAccount, Note = "", Rank = (account.PersistentID == this.Owner.PersistentID ? 1 : 4) }; DBSessions.SessionSave(newDBGuildMember); this.Members.Add(account, newDBGuildMember); if (this.IsClan) { this.ParagonRatings.Add(account, account.DBGameAccount.ParagonLevel); this.AddNews(account, 2); } this.NotifyChannels(account); } public void RemoveAllMembers() { List _members = this.Members.Keys.ToList(); foreach (var account in _members) { RemoveMember(account); } } public void RemoveMember(GameAccount account) { if (account == null) return; if (!HasMember(account)) { Logger.Warn("Attempted to remove non-member account {0} from guild {1}#{2}", account.Owner.BattleTag, this.Name, this.PersistentId); return; } else if (account.Clan != this && !account.Communities.Contains(this)) { Logger.Warn("Client {0} being removed from a guild ({1}#{2}) he's not associated with.", account.Owner.BattleTag, this.Name, this.PersistentId); } this.Members.Remove(account); this.Channel.RemoveMember(account.LoggedInClient, Channel.RemoveReason.Left); if (this.IsClan) { this.ParagonRatings.Remove(account); this.AddNews(account, 3); } var dbRow = DBSessions.SessionQuerySingle(m => m.DBGuild.Id == this.PersistentId && m.DBGameAccount.Id == account.PersistentID); DBSessions.SessionDelete(dbRow); } public void AddSuggestion(GameAccount account, GameAccount inviter) { var invite = D3.Guild.Invite.CreateBuilder() .SetAccountId(account.PersistentID) .SetInviterId(inviter.PersistentID) .SetInviteTime(DateTime.Now.ToUnixTime()) .SetInviteType(1) .SetExpireTime(3600); this.GuildSuggestions.Add(invite.Build()); this.InviteTime = DateTime.Now.ToBlizzardEpoch(); this.UpdateChannelAttributes(); } public void RemoveSuggestion(GameAccount account) { this.GuildSuggestions.RemoveAll(s => s.AccountId == account.PersistentID); } public uint GetRank(ulong GameAccountId) { if (this.Members.Keys.Any(m => m.PersistentID == GameAccountId)) { return (uint)this.Members.Single(m => m.Key.PersistentID == GameAccountId).Value.Rank; } else { Logger.Warn("Client {0} called GetRank() on a guild ({1}#{2}) he's not associated with.", GameAccountId, this.Name, this.PersistentId); return 0; } } public string GetMemberNote(ulong GameAccountId) { if (this.Members.Keys.Any(m => m.PersistentID == GameAccountId)) { return this.Members.Single(m => m.Key.PersistentID == GameAccountId).Value.Note; } else { Logger.Warn("Client {0} called GetMemberNote() on a guild ({1}#{2}) he's not associated with.", GameAccountId, this.Name, this.PersistentId); return ""; } } public void SetMemberNote(GameAccount account, string note) { if (!this.HasMember(account)) return; this.Members[account].Note = note; DBSessions.SessionUpdate(this.Members[account]); this.UpdateMemberAttributes(account); } #endregion #region ranks functionality public bool HasPermission(GameAccount account, GuildPrivilegeFlags permission) { if (!this.HasMember(account)) return false; var rank = (uint)this.Members[account].Rank; if (!this.Ranks.ContainsKey(rank)) return false; var permissions = (GuildPrivilegeFlags)this.Ranks[rank].Privileges; return permissions.HasFlag(permission); } public void SetPermissions(uint rank, string name, uint permissions) { if (!this.Ranks.ContainsKey(rank)) return; this.Ranks[rank].RankName = name; this.Ranks[rank].Privileges = permissions; lock (this.DBGuild) { var dbGuild = this.DBGuild; dbGuild.Ranks = this.BnetRanks; DBSessions.SessionUpdate(dbGuild); } this.UpdateChannelAttributes(); } public void PromoteMember(GameAccount account, bool setLeader = false) { if (!this.HasMember(account)) return; var rank = (uint)this.Members[account].Rank; if (!this.Ranks.ContainsKey(rank)) return; if (rank <= 2 && !setLeader) return; this.Members[account].Rank--; if (this.Members[account].Rank == 3) this.Members[account].Rank--; DBSessions.SessionUpdate(this.Members[account]); this.UpdateMemberAttributes(account); if (setLeader) this.AddNews(account, 7); else this.AddNews(account, 4); } public void DemoteMember(GameAccount account) { if (!this.HasMember(account)) return; var rank = (uint)this.Members[account].Rank; if (!this.Ranks.ContainsKey(rank)) return; if (rank >= this.Ranks.Count) return; this.Members[account].Rank++; if (this.Members[account].Rank == 3) this.Members[account].Rank++; DBSessions.SessionUpdate(this.Members[account]); this.UpdateMemberAttributes(account); } #endregion #region guild channel /// /// bnet.protocol.channel.ChannelState message. /// public bgs.protocol.channel.v1.ChannelState ChannelState { get { var state = bgs.protocol.channel.v1.ChannelState.CreateBuilder() .SetMinMembers(0) .SetMaxMembers(this.MaxMembers) .SetName(this.Name) .SetPrivacyLevel(bgs.protocol.channel.v1.ChannelState.Types.PrivacyLevel.PRIVACY_LEVEL_OPEN) .SetChannelType(this.IsClan ? "clan" : "group") .SetProgram(17459) .SetSubscribeToPresence(true); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.GuildId").SetValue(Variant.CreateBuilder().SetUintValue(this.PersistentId))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Name").SetValue(Variant.CreateBuilder().SetStringValue(this.FullName))); if (this.IsClan) state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Tag").SetValue(Variant.CreateBuilder().SetStringValue(this.Prefix))); else { state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.TotalMembers").SetValue(Variant.CreateBuilder().SetUintValue((ulong)this.Members.Count))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.TotalMembersInChat").SetValue(Variant.CreateBuilder().SetUintValue((ulong)this.GroupChatChannel.Members.Count))); } state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Ranks").SetValue(Variant.CreateBuilder().SetMessageValue(ByteString.CopyFrom(this.BnetRanks)))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Motd").SetValue(Variant.CreateBuilder().SetStringValue(this.MOTD))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Description").SetValue(Variant.CreateBuilder().SetStringValue(this.Description))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Category").SetValue(Variant.CreateBuilder().SetUintValue(this.Category))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Creator").SetValue(Variant.CreateBuilder().SetUintValue(this.Owner.PersistentID))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Language").SetValue(Variant.CreateBuilder().SetUintValue(this.Language))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.InviteTime").SetValue(Variant.CreateBuilder().SetUintValue(this.InviteTime))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Flags").SetValue(Variant.CreateBuilder().SetUintValue(this.Flags))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.SearchCategory").SetValue(Variant.CreateBuilder().SetUintValue(this.Category))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.PostNewsTime").SetValue(Variant.CreateBuilder().SetUintValue(0))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.NewsTime").SetValue(Variant.CreateBuilder().SetUintValue(this.NewsTime))); return state.Build(); } } public bgs.protocol.channel.v1.ChannelState GroupChatChannelState { get { var state = bgs.protocol.channel.v1.ChannelState.CreateBuilder() .SetMinMembers(0) .SetMaxMembers(this.MaxMembers) .SetName(this.Name) .SetPrivacyLevel(bgs.protocol.channel.v1.ChannelState.Types.PrivacyLevel.PRIVACY_LEVEL_OPEN) .SetChannelType("group_chat") .SetProgram(17459) .SetSubscribeToPresence(true); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.GuildId").SetValue(Variant.CreateBuilder().SetUintValue(this.PersistentId))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.Guild.Name").SetValue(Variant.CreateBuilder().SetStringValue(this.Name))); return state.Build(); } } public D3.Guild.GuildSummary Summary { get { var summary = D3.Guild.GuildSummary.CreateBuilder() .SetGuildId(this.PersistentId) .SetGuildName(this.Name) .SetGuildTag(this.Prefix) .SetGuildFlags(this.Flags); return summary.Build(); } } public void NotifyChannels(GameAccount account) { var guildChannelId = bgs.protocol.Attribute.CreateBuilder() .SetName("channel_id") .SetValue(bgs.protocol.Variant.CreateBuilder().SetEntityIdValue( this.Channel.BnetEntityId ).Build()) .Build(); var notificationBuilder = bgs.protocol.notification.v1.Notification.CreateBuilder() .SetTargetId(account.BnetEntityId) .SetType("P_SUBSCRIBE_TO_CHANNEL") .AddAttribute(guildChannelId) .SetSenderAccountId(account.Owner.BnetEntityId) .SetTargetAccountId(account.Owner.BnetEntityId) .Build(); account.LoggedInClient.MakeRpc((lid) => bgs.protocol.notification.v1.NotificationListener.CreateStub(account.LoggedInClient).OnNotificationReceived(new HandlerController() { ListenerId = lid }, notificationBuilder, callback => { })); if (!this.IsClan) { var guildChatChannelId = bgs.protocol.Attribute.CreateBuilder() .SetName("channel_id") .SetValue(bgs.protocol.Variant.CreateBuilder().SetEntityIdValue( this.GroupChatChannel.BnetEntityId ).Build()) .Build(); var chatNotificationBuilder = bgs.protocol.notification.v1.Notification.CreateBuilder() .SetTargetId(account.BnetEntityId) .SetType("P_SUBSCRIBE_TO_CHANNEL") .AddAttribute(guildChatChannelId) .SetSenderAccountId(account.Owner.BnetEntityId) .SetTargetAccountId(account.Owner.BnetEntityId) .Build(); account.LoggedInClient.MakeRpc((lid) => bgs.protocol.notification.v1.NotificationListener.CreateStub(account.LoggedInClient).OnNotificationReceived(new HandlerController() { ListenerId = lid }, chatNotificationBuilder, callback => { })); } } #endregion #region guild news public void AddNews(GameAccount account, int type, byte[] data = null) { var newDBGuildNews = new DBGuildNews { DBGuild = this.DBGuild, DBGameAccount = account.DBGameAccount, Type = type, Time = DateTime.Now.ToBlizzardEpoch(), Data = data }; DBSessions.SessionSave(newDBGuildNews); this.NewsTime = DateTime.Now.ToBlizzardEpoch(); this.UpdateChannelAttributes(); } public void UpdateChannelAttributes() { var notification = bgs.protocol.channel.v1.UpdateChannelStateNotification.CreateBuilder().SetStateChange(this.ChannelState).Build(); var altnotification = bgs.protocol.channel.v1.JoinNotification.CreateBuilder().SetChannelState(this.ChannelState).Build(); foreach (var member in this.Channel.Members) //member.Key.MakeTargetedRPC(this.Channel, (lid) => bgs.protocol.channel.v1.ChannelListener.CreateStub(member.Key).OnUpdateChannelState(new HandlerController() { ListenerId = lid }, notification, callback => { })); member.Key.MakeTargetedRpc(this.Channel, (lid) => bgs.protocol.channel.v1.ChannelListener.CreateStub(member.Key).OnJoin(new HandlerController() { ListenerId = lid }, altnotification, callback => { })); } public void UpdateMemberAttributes(GameAccount member) { if (!this.HasMember(member)) return; var channelMember = bgs.protocol.channel.v1.Member.CreateBuilder(); var state = bgs.protocol.channel.v1.MemberState.CreateBuilder(); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.GuildMember.Rank").SetValue(Variant.CreateBuilder().SetIntValue(this.Members[member].Rank))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.GuildMember.Note").SetValue(Variant.CreateBuilder().SetStringValue(this.Members[member].Note))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.GuildMember.Hardcore").SetValue(bgs.protocol.Variant.CreateBuilder().SetBoolValue(true))); state.AddAttribute(Attribute.CreateBuilder().SetName("D3.GuildMember.AchievementPoints").SetValue(Variant.CreateBuilder().SetUintValue(member.AchievementPoints))); var identity = bgs.protocol.Identity.CreateBuilder().SetGameAccountId(member.BnetEntityId); channelMember.SetIdentity(identity); channelMember.SetState(state); var notification = bgs.protocol.channel.v1.UpdateMemberStateNotification.CreateBuilder() .AddStateChange(channelMember) .Build(); foreach (var mbr in this.Channel.Members) mbr.Key.MakeTargetedRpc(this.Channel, (lid) => bgs.protocol.channel.v1.ChannelListener.CreateStub(mbr.Key).OnUpdateMemberState(new HandlerController() { ListenerId = lid }, notification, callback => { })); } #endregion [System.Flags] public enum GuildFlags : uint { None = 0x00, LookingForMembers = 0x01, InviteOnly = 0x02, Enabled = 0x04 } [System.Flags] public enum GuildPrivilegeFlags : uint { None = 0x00, Enabled = 0x01, ClanChatSpeak = 0x02, IsOfficer = 0x04, OfficerChatSpeak = 0x08, Promote = 0x10, Demote = 0x20, Invite = 0x40, Kick = 0x80, SetMOTD = 0x100, AddNews = 0x200, EditMemberNotesOther = 0x400, EditMemberNotesSelf = 0x800, SeeNotes = 0x1000, ModifyPermissions = 0x2000, EditInfo = 0x8000, SeeInvites = 0x10000, CancelInvite = 0x20000, SetLanguage = 0x40000, SetPrivacy = 0x80000, } public class RankDescriptor { public string RankName; public uint Privileges; } } }