diff --git a/Maple2.Model/Error/GroupChatError.cs b/Maple2.Model/Error/GroupChatError.cs new file mode 100644 index 000000000..dcd9843b3 --- /dev/null +++ b/Maple2.Model/Error/GroupChatError.cs @@ -0,0 +1,17 @@ +// ReSharper disable InconsistentNaming, IdentifierTypo + +using System.ComponentModel; + +namespace Maple2.Model.Error; + +public enum GroupChatError { + none = 0, + [Description("You cannot invite that player to the group chat.")] + s_err_groupchat_null_target_user = 3, + [Description("You cannot invite that player to the group chat.")] + s_err_groupchat_add_member_target = 8, + [Description("{0} is already participating in 3 group chats.")] + s_err_groupchat_maxgroup = 10, + [Description("\"{0}\" contains inappropriate words.\\nPlease enter another name.")] + s_change_charname_err_bad_words = 13, +} diff --git a/Maple2.Model/Game/GroupChat/GroupChat.cs b/Maple2.Model/Game/GroupChat/GroupChat.cs new file mode 100644 index 000000000..acbc2267b --- /dev/null +++ b/Maple2.Model/Game/GroupChat/GroupChat.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Maple2.Model.Game.GroupChat; + +public class GroupChat { + public required int Id { get; init; } + public readonly ConcurrentDictionary Members; + + [SetsRequiredMembers] + public GroupChat(int id) { + Id = id; + Members = new ConcurrentDictionary(); + } +} diff --git a/Maple2.Model/Game/GroupChat/GroupChatMember.cs b/Maple2.Model/Game/GroupChat/GroupChatMember.cs new file mode 100644 index 000000000..256c7b9ea --- /dev/null +++ b/Maple2.Model/Game/GroupChat/GroupChatMember.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using Maple2.PacketLib.Tools; +using Maple2.Tools; +using Maple2.Tools.Extensions; + +namespace Maple2.Model.Game.GroupChat; + +public class GroupChatMember : IDisposable { + public required PlayerInfo Info; + public long CharacterId => Info.CharacterId; + public string Name => Info.Name; + + public CancellationTokenSource? TokenSource; + + public void Dispose() { + TokenSource?.Cancel(); + TokenSource?.Dispose(); + TokenSource = null; + } +} diff --git a/Maple2.Model/Game/Party/Party.cs b/Maple2.Model/Game/Party/Party.cs index d045bcfe9..f0be2e7b0 100644 --- a/Maple2.Model/Game/Party/Party.cs +++ b/Maple2.Model/Game/Party/Party.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; -namespace Maple2.Model.Game; +namespace Maple2.Model.Game.Party; public class Party : IByteSerializable { private int capacity = Constant.PartyMaxCapacity; diff --git a/Maple2.Model/Game/Party/PartyInvite.cs b/Maple2.Model/Game/Party/PartyInvite.cs index e2b967ed7..170a1d6c3 100644 --- a/Maple2.Model/Game/Party/PartyInvite.cs +++ b/Maple2.Model/Game/Party/PartyInvite.cs @@ -1,4 +1,4 @@ -namespace Maple2.Model.Game; +namespace Maple2.Model.Game.Party; public class PartyInvite { public enum Response : byte { diff --git a/Maple2.Model/Game/Party/PartyMember.cs b/Maple2.Model/Game/Party/PartyMember.cs index 8a5239004..edb71166e 100644 --- a/Maple2.Model/Game/Party/PartyMember.cs +++ b/Maple2.Model/Game/Party/PartyMember.cs @@ -7,7 +7,7 @@ using Maple2.Tools; using Maple2.Tools.Extensions; -namespace Maple2.Model.Game; +namespace Maple2.Model.Game.Party; public class PartyMember : IByteSerializable, IDisposable { public long PartyId { get; init; } @@ -22,56 +22,7 @@ public class PartyMember : IByteSerializable, IDisposable { public CancellationTokenSource? TokenSource; public void WriteTo(IByteWriter writer) { - writer.WriteLong(Info.AccountId); - writer.WriteLong(Info.CharacterId); - writer.WriteUnicodeString(Info.Name); - writer.Write(Info.Gender); - writer.WriteByte(1); - writer.WriteLong(); - writer.WriteInt(); - writer.WriteInt(Info.MapId); - writer.WriteInt(Info.MapId); - writer.WriteInt(Info.PlotMapId); - writer.WriteShort(Info.Level); - writer.WriteShort(Info.Channel); - writer.WriteInt((int) Info.Job.Code()); - writer.Write(Info.Job); - writer.WriteInt((int) Info.CurrentHp); - writer.WriteInt((int) Info.TotalHp); - writer.WriteShort(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteInt(); - writer.Write(default); - writer.WriteInt(Info.GearScore); - writer.Write(default); - writer.WriteLong(); - writer.Write(default); - writer.WriteLong(); - writer.WriteUnicodeString(); - writer.WriteUnicodeString(Info.Motto); - writer.WriteUnicodeString(Info.Picture); - writer.WriteByte(); - writer.WriteByte(); - writer.WriteClass(new Mastery()); - writer.WriteUnicodeString(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteLong(); - writer.WriteInt(); - writer.WriteByte(); - writer.WriteBool(false); - writer.WriteLong(); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteLong(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); - writer.WriteInt(); - writer.WriteLong(); - writer.WriteInt(); - writer.WriteInt(); - writer.WriteShort(); - writer.WriteLong(); + writer.WriteClass(Info); } public void WriteDungeonEligibility(IByteWriter writer) { diff --git a/Maple2.Model/Game/User/PlayerInfo.cs b/Maple2.Model/Game/User/PlayerInfo.cs index 7c8ed8bc1..cf283d15a 100644 --- a/Maple2.Model/Game/User/PlayerInfo.cs +++ b/Maple2.Model/Game/User/PlayerInfo.cs @@ -1,9 +1,14 @@ -using System.Linq; +using System; +using System.Numerics; +using Maple2.Model.Common; using Maple2.Model.Enum; +using Maple2.PacketLib.Tools; +using Maple2.Tools; +using Maple2.Tools.Extensions; namespace Maple2.Model.Game; -public class PlayerInfo : CharacterInfo, IPlayerInfo { +public class PlayerInfo : CharacterInfo, IPlayerInfo, IByteSerializable { // Home/Plot public string HomeName { get; set; } public int PlotMapId { get; set; } @@ -31,6 +36,59 @@ public PlayerInfo(CharacterInfo character, string homeName, AchievementInfo achi public PlayerInfo Clone() { return (PlayerInfo) MemberwiseClone(); } + + public void WriteTo(IByteWriter writer) { + writer.WriteLong(AccountId); + writer.WriteLong(CharacterId); + writer.WriteUnicodeString(Name); + writer.Write(Gender); + writer.WriteByte(1); + writer.WriteLong(); + writer.WriteInt(); + writer.WriteInt(MapId); + writer.WriteInt(MapId); + writer.WriteInt(PlotMapId); + writer.WriteShort(Level); + writer.WriteShort(Channel); + writer.WriteInt((int) Job.Code()); + writer.Write(Job); + writer.WriteInt((int) CurrentHp); + writer.WriteInt((int) TotalHp); + writer.WriteShort(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteInt(); + writer.Write(default); + writer.WriteInt(GearScore); + writer.Write(default); + writer.WriteLong(); + writer.Write(default); + writer.WriteLong(); + writer.WriteUnicodeString(); + writer.WriteUnicodeString(Motto); + writer.WriteUnicodeString(Picture); + writer.WriteByte(); + writer.WriteByte(); + writer.WriteClass(new Mastery()); + writer.WriteUnicodeString(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteLong(); + writer.WriteInt(); + writer.WriteByte(); + writer.WriteBool(false); + writer.WriteLong(); + writer.WriteInt(); + writer.WriteInt(); + writer.WriteLong(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + writer.WriteInt(); + writer.WriteLong(); + writer.WriteInt(); + writer.WriteInt(); + writer.WriteShort(); + writer.WriteLong(); + } } public class CharacterInfo { @@ -92,4 +150,6 @@ public static implicit operator CharacterInfo(Player player) { Channel = player.Character.Channel, }; } + + } diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index bc67b8e5f..71f690922 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -79,6 +79,8 @@ public static class Constant { public const int FishingNpcId = 11001609; public const int PartyMaxCapacity = 10; public const int PartyMinCapacity = 4; + public const int GroupChatMaxCapacity = 20; + public const int GroupChatMaxCount = 3; public const long FurnishingBaseId = 2870000000000000000; diff --git a/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs b/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs index 471993ab3..908ce8328 100644 --- a/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs +++ b/Maple2.Server.Core/Sync/PlayerInfoUpdateEvent.cs @@ -21,6 +21,7 @@ public enum UpdateField { Buddy = Profile | Job | Level | Map | Channel | Home | Trophy, Guild = Profile | Job | Level | GearScore | Map | Channel | Home | Trophy, Party = Profile | Job | Level | GearScore | Health | Map | Channel | Home, + GroupChat = Profile | Job | Level | Map, All = int.MaxValue, } diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index 77b565b5f..946913842 100644 --- a/Maple2.Server.Core/proto/channel/channel.proto +++ b/Maple2.Server.Core/proto/channel/channel.proto @@ -17,6 +17,8 @@ service Channel { rpc Guild(GuildRequest) returns (GuildResponse); // Manage party. rpc Party(PartyRequest) returns (PartyResponse); + // Manage group chat. + rpc GroupChat(GroupChatRequest) returns (GroupChatResponse); // Notify character about new mail. rpc MailNotification(maple2.MailNotificationRequest) returns (maple2.MailNotificationResponse); diff --git a/Maple2.Server.Core/proto/common.proto b/Maple2.Server.Core/proto/common.proto index a7c045da3..d56aeb6a0 100644 --- a/Maple2.Server.Core/proto/common.proto +++ b/Maple2.Server.Core/proto/common.proto @@ -79,3 +79,62 @@ message BuddyRequest { message BuddyResponse { int32 channel = 1; } + +message GroupChatInfo { + message Member { + int64 character_id = 1; + } + + int32 id = 1; + repeated Member members = 2; +} + +message GroupChatRequest { + message Create {} + + message Invite { + int64 receiver_id = 1; + string receiver_name = 2; + } + + message Leave {} + + message RemoveMember { + int64 character_id = 1; + } + + message AddMember { + int64 requester_id = 1; + string requester_name = 2; + int64 character_id = 3; + } + + message Chat { + string message = 1; + string requester_name = 2; + } + + message Disband {} + + int32 group_chat_id = 1; + int64 requester_id = 2; + repeated int64 receiver_ids = 3; + oneof group_chat { + Create create = 4; + Invite invite = 5; + Leave leave = 6; + RemoveMember remove_member = 7; + AddMember add_member = 8; + Chat chat = 9; + Disband disband = 10; + } +} + +message GroupChatResponse { + oneof Info { + int32 group_chat_id = 1; + GroupChatInfo group_chat = 2; + } + + int32 error = 3; +} diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index 3b91d20ec..f06f63765 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -24,6 +24,9 @@ service World { // Manage party. rpc PartyInfo(PartyInfoRequest) returns (PartyInfoResponse); rpc Party(PartyRequest) returns (PartyResponse); + // Manage group chat. + rpc GroupChatInfo(GroupChatInfoRequest) returns (GroupChatInfoResponse); + rpc GroupChat(GroupChatRequest) returns (GroupChatResponse); // Retrieve player info from online player. rpc PlayerInfo(maple2.PlayerInfoRequest) returns (maple2.PlayerInfoResponse); // Update player info @@ -177,6 +180,15 @@ message GuildResponse { int32 error = 3; } +message GroupChatInfoRequest { + optional int32 group_chat_id = 1; + optional int64 character_id = 2; +} + +message GroupChatInfoResponse { + repeated GroupChatInfo infos = 3; +} + message PartyInfo { message Member { int64 character_id = 1; diff --git a/Maple2.Server.Game/Manager/GroupChatManager.cs b/Maple2.Server.Game/Manager/GroupChatManager.cs new file mode 100644 index 000000000..7d99ea91a --- /dev/null +++ b/Maple2.Server.Game/Manager/GroupChatManager.cs @@ -0,0 +1,178 @@ +using System; +using System.Linq; +using System.Threading; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Maple2.Server.Core.Sync; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; +using Maple2.Server.Game.Util.Sync; +using Maple2.Server.World.Service; +using Maple2.Tools.Extensions; +using Serilog; + +namespace Maple2.Server.Game.Manager; + +public class GroupChatManager : IDisposable { + private readonly GameSession session; + + private GroupChat? groupChat; + + private readonly CancellationTokenSource tokenSource; + + private readonly ILogger logger = Log.Logger.ForContext(); + + public GroupChatManager(GroupChatInfo info, GameSession session) { + this.session = session; + tokenSource = new CancellationTokenSource(); + SetGroupChat(info); + } + + public void Dispose() { + session.Dispose(); + tokenSource.Dispose(); + + if (groupChat != null) { + foreach (GroupChatMember member in groupChat.Members.Values) { + member.Dispose(); + } + } + } + + public void Load() { + if (groupChat == null) { + return; + } + session.Send(GroupChatPacket.Load(groupChat)); + } + + public bool SetGroupChat(GroupChatInfo info) { + if (this.groupChat != null) { + return false; + } + + GroupChatMember[] members = info.Members.Select(member => { + if (!session.PlayerInfo.GetOrFetch(member.CharacterId, out PlayerInfo? playerInfo)) { + return null; + } + + var result = new GroupChatMember { + Info = playerInfo.Clone(), + }; + return result; + }).WhereNotNull().ToArray(); + + var groupChat = new GroupChat(info.Id); + foreach (GroupChatMember member in members) { + if (groupChat.Members.TryAdd(member.CharacterId, member)) { + BeginListen(member); + } + } + + this.groupChat = groupChat; + + session.Send(GroupChatPacket.Load(this.groupChat)); + return true; + } + + public bool AddMember(GroupChatMember sender, GroupChatMember receiver) { + if (groupChat == null) { + return false; + } + if (!groupChat.Members.TryAdd(receiver.CharacterId, receiver)) { + return false; + } + + BeginListen(receiver); + session.Send(GroupChatPacket.AddMember(receiver.Info, sender.Name, groupChat.Id)); + return true; + } + + public bool RemoveMember(long characterId) { + if (groupChat == null) { + return false; + } + if (!groupChat.Members.TryRemove(characterId, out GroupChatMember? member)) { + return false; + } + EndListen(member); + + session.Send(characterId == session.CharacterId ? GroupChatPacket.Leave(groupChat.Id) : + GroupChatPacket.RemoveMember(member.Name, groupChat.Id)); + return true; + } + + + public bool Disband() { + if (groupChat == null) { + return false; + } + + foreach (GroupChatMember member in groupChat.Members.Values) { + EndListen(member); + } + + groupChat = null; + return true; + } + + public void CheckDisband() { + if (groupChat == null) { + return; + } + + // Checking less or equal to 1 because member logging off is still online at this point + if (groupChat.Members.Values.Count(member => member.Info.Online) <= 1) { + session.World.GroupChat(new GroupChatRequest { + Disband = new GroupChatRequest.Types.Disband(), + GroupChatId = groupChat.Id, + RequesterId = session.CharacterId, + }); + } + } + + public GroupChatMember? GetMember(long characterId) { + if (groupChat?.Members.TryGetValue(characterId, out GroupChatMember? member) == true) { + return member; + } + + return null; + } + + #region PlayerInfo Events + private void BeginListen(GroupChatMember member) { + // Clean up previous token if necessary + if (member.TokenSource != null) { + logger.Warning("BeginListen called on Member {Id} that was already listening", member.CharacterId); + EndListen(member); + } + + member.TokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token); + CancellationToken token = member.TokenSource.Token; + var listener = new PlayerInfoListener(UpdateField.GroupChat, (type, info) => SyncUpdate(token, member.CharacterId, type, info)); + session.PlayerInfo.Listen(member.Info.CharacterId, listener); + } + + private void EndListen(GroupChatMember member) { + member.TokenSource?.Cancel(); + member.TokenSource?.Dispose(); + member.TokenSource = null; + } + + private bool SyncUpdate(CancellationToken cancel, long characterId, UpdateField type, IPlayerInfo info) { + if (cancel.IsCancellationRequested || groupChat == null || !groupChat.Members.TryGetValue(characterId, out GroupChatMember? member)) { + return true; + } + + bool wasOnline = member.Info.Online; + member.Info.Update(type, info); + + if (member.Info.Online != wasOnline) { + session.Send(member.Info.Online + ? GroupChatPacket.LoginNotice(member.Name, groupChat.Id) + : GroupChatPacket.LogoutNotice(member.Name, groupChat.Id)); + } + return false; + } + #endregion +} diff --git a/Maple2.Server.Game/Manager/PartyManager.cs b/Maple2.Server.Game/Manager/PartyManager.cs index dca48c2da..3a5aa5573 100644 --- a/Maple2.Server.Game/Manager/PartyManager.cs +++ b/Maple2.Server.Game/Manager/PartyManager.cs @@ -3,6 +3,7 @@ using System.Threading; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.Server.Core.Sync; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; diff --git a/Maple2.Server.Game/PacketHandlers/ChatStickerHandler.cs b/Maple2.Server.Game/PacketHandlers/ChatStickerHandler.cs index d0e2661c9..0bf03d1e0 100644 --- a/Maple2.Server.Game/PacketHandlers/ChatStickerHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/ChatStickerHandler.cs @@ -33,6 +33,9 @@ public override void Handle(GameSession session, IByteReader packet) { case Command.Use: HandleUse(session, packet); break; + case Command.GroupChat: + HandleGroupChat(session, packet); + break; case Command.Favorite: HandleFavorite(session, packet); break; @@ -53,6 +56,17 @@ private void HandleUse(GameSession session, IByteReader packet) { session.Send(ChatStickerPacket.Use(stickerId, html)); } + private void HandleGroupChat(GameSession session, IByteReader packet) { + int stickerId = packet.ReadInt(); + string groupChatName = packet.ReadUnicodeString(); + + if (!TryUseSticker(session.Player.Value.Unlock, stickerId)) { + return; + } + + session.Send(ChatStickerPacket.GroupChat(stickerId, groupChatName)); + } + private void HandleFavorite(GameSession session, IByteReader packet) { int stickerId = packet.ReadInt(); diff --git a/Maple2.Server.Game/PacketHandlers/GroupChatHandler.cs b/Maple2.Server.Game/PacketHandlers/GroupChatHandler.cs new file mode 100644 index 000000000..5e2f2e132 --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/GroupChatHandler.cs @@ -0,0 +1,150 @@ +using System; +using Grpc.Core; +using Maple2.Database.Storage; +using Maple2.Model.Error; +using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Game.Manager; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; +using Maple2.Server.World.Service; +using WorldClient = Maple2.Server.World.Service.World.WorldClient; + +namespace Maple2.Server.Game.PacketHandlers; + +public class GroupChatHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.GroupChat; + + private enum Command : byte { + Create = 1, + Invite = 2, + Leave = 4, + Chat = 10, + } + + #region Autofac Autowired + // ReSharper disable MemberCanBePrivate.Global + public required WorldClient World { private get; init; } + // ReSharper restore All + #endregion + + public override void Handle(GameSession session, IByteReader packet) { + var command = packet.Read(); + switch (command) { + case Command.Create: + HandleCreate(session); + break; + case Command.Invite: + HandleInvite(session, packet); + break; + case Command.Leave: + HandleLeave(session, packet); + break; + case Command.Chat: + HandleChat(session, packet); + break; + } + } + + private void HandleCreate(GameSession session) { + if (session.GroupChats.Count >= Constant.GroupChatMaxCapacity) { + session.Send(GroupChatPacket.Error(GroupChatError.s_err_groupchat_maxgroup, session.PlayerName, session.PlayerName)); + return; + } + + GroupChatResponse response = World.GroupChat(new GroupChatRequest { + Create = new GroupChatRequest.Types.Create(), + RequesterId = session.CharacterId, + }); + + var error = (GroupChatError) response.Error; + if (error != GroupChatError.none) { + session.Send(GroupChatPacket.Error(error, session.PlayerName, string.Empty)); + return; + } + + if (!session.GroupChats.TryAdd(response.GroupChat.Id, new GroupChatManager(response.GroupChat, session))) { + throw new InvalidOperationException($"Failed to add group chat: {response.GroupChat.Id}"); + } + + session.Send(GroupChatPacket.Create(response.GroupChat.Id)); + } + + private void HandleInvite(GameSession session, IByteReader packet) { + string targetPlayerName = packet.ReadUnicodeString(); + int groupChatId = packet.ReadInt(); + + using GameStorage.Request db = session.GameStorage.Context(); + long characterId = db.GetCharacterId(targetPlayerName); + if (characterId == 0) { + session.Send(GroupChatPacket.Error(GroupChatError.s_err_groupchat_null_target_user, session.PlayerName, targetPlayerName)); + return; + } + + try { + GroupChatInfoResponse infoResponse = World.GroupChatInfo(new GroupChatInfoRequest { + CharacterId = characterId, + }); + + if (infoResponse.Infos.Count >= Constant.GroupChatMaxCount) { + session.Send(GroupChatPacket.Error(GroupChatError.s_err_groupchat_maxgroup, session.PlayerName, targetPlayerName)); + return; + } + + session.Send(GroupChatPacket.Invite(session.PlayerName, targetPlayerName, groupChatId)); + var request = new GroupChatRequest { + Invite = new GroupChatRequest.Types.Invite { + ReceiverId = characterId, + }, + GroupChatId = groupChatId, + RequesterId = session.CharacterId, + }; + + GroupChatResponse response = World.GroupChat(request); + var error = (GroupChatError) response.Error; + if (error != GroupChatError.none) { + session.Send(GroupChatPacket.Error(error, session.PlayerName, targetPlayerName)); + } + } catch (RpcException ex) { + Logger.Error(ex, "Failed to invite {Name} to group chat", targetPlayerName); + session.Send(GroupChatPacket.Error(GroupChatError.s_err_groupchat_add_member_target, session.PlayerName, targetPlayerName)); + } + } + + private void HandleLeave(GameSession session, IByteReader packet) { + int groupChatId = packet.ReadInt(); + + if (!session.GroupChats.ContainsKey(groupChatId)) { + return; + } + + try { + World.GroupChat(new GroupChatRequest { + Leave = new GroupChatRequest.Types.Leave(), + GroupChatId = groupChatId, + RequesterId = session.CharacterId, + }); + } catch (RpcException ex) { + Logger.Error(ex, "Failed to leave group chat {GroupChatId}", groupChatId); + } + } + + private void HandleChat(GameSession session, IByteReader packet) { + string message = packet.ReadUnicodeString(); + int groupChatId = packet.ReadInt(); + + if (!session.GroupChats.ContainsKey(groupChatId)) { + return; + } + + World.GroupChat(new GroupChatRequest { + Chat = new GroupChatRequest.Types.Chat { + Message = message, + }, + GroupChatId = groupChatId, + RequesterId = session.CharacterId, + }); + } +} diff --git a/Maple2.Server.Game/PacketHandlers/InstrumentHandler.cs b/Maple2.Server.Game/PacketHandlers/InstrumentHandler.cs index da4f1d8fb..c6030dcbe 100644 --- a/Maple2.Server.Game/PacketHandlers/InstrumentHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/InstrumentHandler.cs @@ -6,6 +6,7 @@ using Maple2.Model; using Maple2.Model.Enum; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; diff --git a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs index d8f47d9cb..9ed1e60e7 100644 --- a/Maple2.Server.Game/PacketHandlers/PartyHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/PartyHandler.cs @@ -4,6 +4,7 @@ using Maple2.Model.Enum; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; diff --git a/Maple2.Server.Game/Packets/ChatStickerPacket.cs b/Maple2.Server.Game/Packets/ChatStickerPacket.cs index d123385cd..33bdcc2b9 100644 --- a/Maple2.Server.Game/Packets/ChatStickerPacket.cs +++ b/Maple2.Server.Game/Packets/ChatStickerPacket.cs @@ -43,7 +43,7 @@ public static ByteWriter Add(Item item, ChatSticker sticker) { return pWriter; } - + public static ByteWriter Use(int stickerId, string html) { var pWriter = Packet.Of(SendOp.ChatStamp); pWriter.Write(Command.Use); @@ -53,7 +53,16 @@ public static ByteWriter Use(int stickerId, string html) { return pWriter; } - + + public static ByteWriter GroupChat(int stickerId, string groupChatName) { + var pWriter = Packet.Of(SendOp.ChatStamp); + pWriter.Write(Command.GroupChat); + pWriter.WriteInt(stickerId); + pWriter.WriteUnicodeString(groupChatName); + + return pWriter; + } + public static ByteWriter Favorite(int stickerId) { var pWriter = Packet.Of(SendOp.ChatStamp); pWriter.Write(Command.Favorite); @@ -61,7 +70,7 @@ public static ByteWriter Favorite(int stickerId) { return pWriter; } - + public static ByteWriter Unfavorite(int stickerId) { var pWriter = Packet.Of(SendOp.ChatStamp); pWriter.Write(Command.Unfavorite); diff --git a/Maple2.Server.Game/Packets/GroupChatPacket.cs b/Maple2.Server.Game/Packets/GroupChatPacket.cs new file mode 100644 index 000000000..6e430095c --- /dev/null +++ b/Maple2.Server.Game/Packets/GroupChatPacket.cs @@ -0,0 +1,134 @@ +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; +using Maple2.Tools.Extensions; + +namespace Maple2.Server.Game.Packets; + +public static class GroupChatPacket { + private enum Command : byte { + Load = 0, + Create = 1, + Invite = 2, + Join = 3, + Leave = 4, + AddMember = 6, + RemoveMember = 7, + LoginNotice = 8, + LogoutNotice = 9, + Chat = 10, + Error = 13, + } + + public static ByteWriter Load(GroupChat groupChat) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Load); + pWriter.WriteInt(groupChat.Id); + pWriter.WriteByte((byte) groupChat.Members.Count); + foreach ((long characterId, GroupChatMember member) in groupChat.Members) { + pWriter.WriteBool(true); // is member? + pWriter.WriteClass(member.Info); + } + + return pWriter; + } + + public static ByteWriter Create(int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Create); + pWriter.WriteInt(groupChatId); + + return pWriter; + } + + public static ByteWriter Invite(string memberName, string targetName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Invite); + pWriter.WriteUnicodeString(memberName); + pWriter.WriteUnicodeString(targetName); + pWriter.WriteInt(groupChatId); + + return pWriter; + } + + public static ByteWriter Join(string senderMemberName, string receiverTargetName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Join); + pWriter.WriteUnicodeString(senderMemberName); + pWriter.WriteUnicodeString(receiverTargetName); + pWriter.WriteInt(groupChatId); + + return pWriter; + } + + public static ByteWriter Leave(int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Leave); + pWriter.WriteInt(groupChatId); + + return pWriter; + } + + public static ByteWriter AddMember(PlayerInfo targetCharacter, string inviterName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.AddMember); + pWriter.WriteInt(groupChatId); + pWriter.WriteUnicodeString(inviterName); + pWriter.WriteBool(true); // is member? + pWriter.WriteClass(targetCharacter); + + return pWriter; + } + + public static ByteWriter RemoveMember(string memberName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.RemoveMember); + pWriter.WriteInt(groupChatId); + pWriter.WriteBool(false); // is not member? + pWriter.WriteUnicodeString(memberName); + + return pWriter; + } + + public static ByteWriter LoginNotice(string memberName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.LoginNotice); + pWriter.WriteInt(groupChatId); + pWriter.WriteUnicodeString(memberName); + + return pWriter; + } + + public static ByteWriter LogoutNotice(string memberName, int groupChatId) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.LogoutNotice); + pWriter.WriteInt(groupChatId); + pWriter.WriteUnicodeString(memberName); + + return pWriter; + } + + public static ByteWriter Chat(string memberName, int groupChatId, string message) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Chat); + pWriter.WriteInt(groupChatId); + pWriter.WriteUnicodeString(memberName); + pWriter.WriteUnicodeString(message); + + return pWriter; + } + + public static ByteWriter Error(GroupChatError error, string memberName, string targetName) { + var pWriter = Packet.Of(SendOp.GroupChat); + pWriter.Write(Command.Error); + pWriter.WriteByte(2); // Unknown + pWriter.Write(error); + pWriter.WriteUnicodeString(memberName); + pWriter.WriteUnicodeString(targetName); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/ItemDropNoticePacket.cs b/Maple2.Server.Game/Packets/ItemDropNoticePacket.cs new file mode 100644 index 000000000..de21ea9c3 --- /dev/null +++ b/Maple2.Server.Game/Packets/ItemDropNoticePacket.cs @@ -0,0 +1,18 @@ +using Maple2.Model.Game; +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class ItemDropNoticePacket { + public static ByteWriter Notice(string playerName, Item item) { + var pWriter = Packet.Of(SendOp.ItemDropNotice); + pWriter.WriteUnicodeString(playerName); + pWriter.WriteInt(item.Amount); + pWriter.WriteInt(item.Rarity); + pWriter.WriteLong(item.Uid); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Packets/PartyPacket.cs b/Maple2.Server.Game/Packets/PartyPacket.cs index 32530fdf4..bb03c3094 100644 --- a/Maple2.Server.Game/Packets/PartyPacket.cs +++ b/Maple2.Server.Game/Packets/PartyPacket.cs @@ -5,6 +5,7 @@ using Maple2.Server.Core.Packets; using Maple2.Tools.Extensions; using System; +using Maple2.Model.Game.Party; namespace Maple2.Server.Game.Packets; diff --git a/Maple2.Server.Game/Packets/PlayerKillNoticePacket.cs b/Maple2.Server.Game/Packets/PlayerKillNoticePacket.cs new file mode 100644 index 000000000..c4fb13ea9 --- /dev/null +++ b/Maple2.Server.Game/Packets/PlayerKillNoticePacket.cs @@ -0,0 +1,21 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; + +namespace Maple2.Server.Game.Packets; + +public static class PlayerKillNoticePacket { + private enum Command : byte { + Notice = 0, + Unknown = 1, // Error? + } + + public static ByteWriter Notice(string attacker, string victim) { + var pWriter = Packet.Of(SendOp.ItemDropNotice); + pWriter.Write(Command.Notice); + pWriter.WriteUnicodeString(attacker); + pWriter.WriteUnicodeString(victim); + + return pWriter; + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.GroupChat.cs b/Maple2.Server.Game/Service/ChannelService.GroupChat.cs new file mode 100644 index 000000000..1010fc4bf --- /dev/null +++ b/Maple2.Server.Game/Service/ChannelService.GroupChat.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Maple2.Server.Game.Manager; +using Maple2.Server.Game.Packets; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.Service; + +public partial class ChannelService { + public override Task GroupChat(GroupChatRequest request, ServerCallContext context) { + switch (request.GroupChatCase) { + case GroupChatRequest.GroupChatOneofCase.AddMember: + return Task.FromResult(AddGroupChatMember(request.GroupChatId, request.ReceiverIds, request.AddMember)); + case GroupChatRequest.GroupChatOneofCase.RemoveMember: + return Task.FromResult(RemoveGroupChatMember(request.GroupChatId, request.ReceiverIds, request.RemoveMember)); + case GroupChatRequest.GroupChatOneofCase.Chat: + return Task.FromResult(ChatGroupChat(request.GroupChatId, request.ReceiverIds, request.Chat)); + case GroupChatRequest.GroupChatOneofCase.Disband: + return Task.FromResult(DisbandGroupChat(request.GroupChatId, request.ReceiverIds, request.Disband)); + default: + return Task.FromResult(new GroupChatResponse { Error = (int) GroupChatError.s_err_groupchat_null_target_user }); + } + } + + private GroupChatResponse AddGroupChatMember(int groupChatId, IEnumerable receiverIds, GroupChatRequest.Types.AddMember add) { + if (!playerInfos.GetOrFetch(add.CharacterId, out PlayerInfo? info)) { + return new GroupChatResponse { Error = (int) GroupChatError.s_err_groupchat_null_target_user }; + } + + IEnumerable characterIds = receiverIds.ToList(); + foreach (long characterId in characterIds) { + if (!server.GetSession(characterId, out GameSession? session)) { + continue; + } + + if (characterId == add.CharacterId) { + JoinGroupChat(session, add.RequesterName, characterIds, ToGroupChatInfo(groupChatId, characterIds)); + continue; + } + + if (!session.GroupChats.TryGetValue(groupChatId, out GroupChatManager? manager)) { + continue; + } + + GroupChatMember? sender = manager.GetMember(add.RequesterId); + if (sender == null) { + continue; + } + manager.AddMember(sender, new GroupChatMember { + Info = info.Clone(), + }); + } + + return new GroupChatResponse(); + } + + private void JoinGroupChat(GameSession session, string inviterName, IEnumerable receiverIds, GroupChatInfo groupChatInfo) { + GroupChat groupChat = new GroupChat(groupChatInfo.Id); + foreach (long receiverId in receiverIds) { + if (!session.PlayerInfo.GetOrFetch(receiverId, out PlayerInfo? playerInfo)) { + continue; + } + groupChat.Members.TryAdd(receiverId, new GroupChatMember { + Info = playerInfo.Clone(), + }); + } + + session.GroupChats.TryAdd(groupChatInfo.Id, new GroupChatManager(groupChatInfo, session)); + session.Send(GroupChatPacket.Join(inviterName, session.PlayerName, groupChatInfo.Id)); + } + + private GroupChatResponse RemoveGroupChatMember(int groupChatId, IEnumerable receiverIds, GroupChatRequest.Types.RemoveMember remove) { + foreach (long characterId in receiverIds) { + if (!server.GetSession(characterId, out GameSession? session)) { + continue; + } + + if (!session.GroupChats.TryGetValue(groupChatId, out GroupChatManager? manager)) { + continue; + } + + manager.RemoveMember(remove.CharacterId); + if (characterId == remove.CharacterId) { + session.GroupChats.Remove(groupChatId, out _); + } + } + + return new GroupChatResponse(); + } + + private GroupChatResponse ChatGroupChat(int groupChatId, IEnumerable receiverIds, GroupChatRequest.Types.Chat chat) { + foreach (long characterId in receiverIds) { + if (!server.GetSession(characterId, out GameSession? session)) { + continue; + } + + session.Send(GroupChatPacket.Chat(chat.RequesterName, groupChatId, chat.Message)); + } + return new GroupChatResponse(); + } + + private GroupChatResponse DisbandGroupChat(int groupChatId, IEnumerable receiverIds, GroupChatRequest.Types.Disband disband) { + foreach (long characterId in receiverIds) { + if (!server.GetSession(characterId, out GameSession? session) || !session.GroupChats.TryGetValue(groupChatId, out GroupChatManager? manager)) { + continue; + } + + manager.Disband(); + } + + return new GroupChatResponse(); + } + + private static GroupChatInfo ToGroupChatInfo(int groupChatId, IEnumerable receiverIds) { + return new GroupChatInfo { + Id = groupChatId, + Members = { + receiverIds.Select(characterId => new GroupChatInfo.Types.Member { + CharacterId = characterId, + }), + }, + }; + } +} + diff --git a/Maple2.Server.Game/Service/ChannelService.Party.cs b/Maple2.Server.Game/Service/ChannelService.Party.cs index 700c2a20a..cf16b1335 100644 --- a/Maple2.Server.Game/Service/ChannelService.Party.cs +++ b/Maple2.Server.Game/Service/ChannelService.Party.cs @@ -4,6 +4,7 @@ using Grpc.Core; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.Server.Channel.Service; using Maple2.Server.Game.Packets; using Maple2.Server.Game.Session; diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 757416112..9c33ec6e7 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,6 +12,7 @@ using Maple2.Model.Error; using Maple2.Model.Game; using Maple2.Model.Game.Event; +using Maple2.Model.Game.GroupChat; using Maple2.Model.Game.Shop; using Maple2.Model.Metadata; using Maple2.PacketLib.Tools; @@ -89,6 +91,7 @@ public sealed partial class GameSession : Core.Network.Session { public FieldManager? Field { get; set; } public FieldPlayer Player { get; private set; } public PartyManager Party { get; set; } + public ConcurrentDictionary GroupChats { get; set; } public GameSession(TcpClient tcpClient, GameServer server, IComponentContext context) : base(tcpClient) { this.server = server; @@ -98,6 +101,7 @@ public GameSession(TcpClient tcpClient, GameServer server, IComponentContext con Scheduler.ScheduleRepeated(() => Send(TimeSyncPacket.Request()), 1000); OnLoop += Scheduler.InvokeAll; + GroupChats = new ConcurrentDictionary(); } public bool FindSession(long characterId, [NotNullWhen(true)] out GameSession? other) { @@ -144,6 +148,15 @@ public bool EnterServer(long accountId, long characterId, Guid machineId) { UgcMarket = new UgcMarketManager(this); Party = new PartyManager(World, this); + GroupChatInfoResponse groupChatInfoRequest = World.GroupChatInfo(new GroupChatInfoRequest { + CharacterId = CharacterId, + }); + + foreach(GroupChatInfo groupChatInfo in groupChatInfoRequest.Infos) { + GroupChatManager manager = new GroupChatManager(groupChatInfo, this); + GroupChats.TryAdd(groupChatInfo.Id, manager); + } + if (!PrepareField(player.Character.MapId)) { Send(MigrationPacket.MoveResult(MigrationError.s_move_err_default)); return false; @@ -168,6 +181,9 @@ public bool EnterServer(long accountId, long characterId, Guid machineId) { // UserConditionEvent // PCBangBonus Guild.Load(); + foreach ((int id, GroupChatManager groupChat) in GroupChats) { + groupChat.Load(); + } // Club Buddy.Load(); Party.Load(); @@ -442,6 +458,9 @@ protected override void Dispose(bool disposing) { Guild.Dispose(); Buddy.Dispose(); Party.Dispose(); + foreach ((int groupChatId, GroupChatManager groupChat) in GroupChats) { + groupChat.CheckDisband(); + } using (GameStorage.Request db = GameStorage.Context()) { db.BeginTransaction(); diff --git a/Maple2.Server.World/Containers/GroupChatLookup.cs b/Maple2.Server.World/Containers/GroupChatLookup.cs new file mode 100644 index 000000000..2ccfecbc1 --- /dev/null +++ b/Maple2.Server.World/Containers/GroupChatLookup.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Serilog; + +namespace Maple2.Server.World.Containers; + +public class GroupChatLookup : IDisposable { + private readonly ChannelClientLookup channelClients; + private readonly PlayerInfoLookup playerLookup; + + private readonly ConcurrentDictionary groupChats; + private int nextGroupChatId = 1; + + public GroupChatLookup(ChannelClientLookup channelClients, PlayerInfoLookup playerLookup) { + this.channelClients = channelClients; + this.playerLookup = playerLookup; + + groupChats = new ConcurrentDictionary(); + } + + public void Dispose() { + foreach (GroupChatManager manager in groupChats.Values) { + manager.Dispose(); + } + } + + public bool TryGet(int groupChatId, [NotNullWhen(true)] out GroupChatManager? groupChat) { + return groupChats.TryGetValue(groupChatId, out groupChat); + } + + public IEnumerable TryGetMany(long characterId) { + foreach (GroupChatManager manager in groupChats.Values) { + if (manager.GroupChat.Members.TryGetValue(characterId, out GroupChatMember? member)) { + yield return manager; + } + } + } + + public GroupChatError Create(long requesterId, out int groupChatId) { + groupChatId = nextGroupChatId++; + + PlayerInfo? requesterInfo = playerLookup.GetPlayerInfo(requesterId); + if (requesterInfo == null) { + return GroupChatError.s_err_groupchat_null_target_user; + } + + var groupChat = new GroupChat(groupChatId); + var manager = new GroupChatManager(groupChat) { + ChannelClients = channelClients, + }; + + if (!groupChats.TryAdd(groupChatId, manager)) { + return GroupChatError.s_err_groupchat_null_target_user; + } + + manager.Create(requesterInfo); + return GroupChatError.none; + } + + public void Disband(int groupChatId) { + if (!TryGet(groupChatId, out GroupChatManager? manager)) { + Log.Error("Failed to disband group chat {GroupChatId} because it was not found", groupChatId); + return; + } + + if (!groupChats.TryRemove(groupChatId, out manager)) { + Log.Error("Unable to remove group chat {GroupChatId} in World", groupChatId); + return; + } + manager.Dispose(); + } +} diff --git a/Maple2.Server.World/Containers/GroupChatManager.cs b/Maple2.Server.World/Containers/GroupChatManager.cs new file mode 100644 index 000000000..8a8b17b3a --- /dev/null +++ b/Maple2.Server.World/Containers/GroupChatManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Maple2.Model.Metadata; +using Maple2.Server.Channel.Service; +using Serilog; +using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; + +namespace Maple2.Server.World.Containers; + +public class GroupChatManager : IDisposable { + public required ChannelClientLookup ChannelClients { get; init; } + public readonly GroupChat GroupChat; + + public GroupChatManager(GroupChat groupChat) { + GroupChat = groupChat; + } + + public void Dispose() { + Broadcast(new GroupChatRequest { + Disband = new GroupChatRequest.Types.Disband { }, + }); + } + + public void Broadcast(GroupChatRequest request) { + if (request.GroupChatId > 0 && request.GroupChatId != GroupChat.Id) { + throw new InvalidOperationException($"Broadcasting {request.GroupChatCase} for incorrect group chat: {request.GroupChatId} => {GroupChat.Id}"); + } + + request.GroupChatId = GroupChat.Id; + foreach (IGrouping group in GroupChat.Members.Values.GroupBy(member => member.Info.Channel)) { + if (!ChannelClients.TryGetClient(group.Key, out ChannelClient? client)) { + continue; + } + + request.ReceiverIds.Clear(); + request.ReceiverIds.AddRange(group.Select(member => member.Info.CharacterId)); + + try { + client.GroupChat(request); + } catch { } + } + } + + private bool CheckForDisband() { + if (GroupChat.Members.Count <= 1 || + GroupChat.Members.Values.Count(member => member.Info.Online) <= 1) { + Dispose(); + return true; + } + return false; + } + + public GroupChatError Invite(long requestorId, PlayerInfo info) { + if (!GroupChat.Members.TryGetValue(requestorId, out GroupChatMember? requester)) { + return GroupChatError.s_err_groupchat_null_target_user; + } + if (GroupChat.Members.ContainsKey(info.CharacterId)) { + return GroupChatError.s_err_groupchat_add_member_target; + } + if (!ChannelClients.TryGetClient(info.Channel, out ChannelClient? client)) { + return GroupChatError.s_err_groupchat_add_member_target; + } + + GroupChatMember member = new GroupChatMember { + Info = info.Clone(), + }; + GroupChat.Members.TryAdd(info.CharacterId, member); + + Broadcast(new GroupChatRequest { + AddMember = new GroupChatRequest.Types.AddMember { + CharacterId = info.CharacterId, + RequesterName = requester.Name, + RequesterId = requester.CharacterId, + }, + GroupChatId = GroupChat.Id, + }); + + return GroupChatError.none; + } + + public bool Create(PlayerInfo info) { + var member = new GroupChatMember { + Info = info.Clone(), + }; + + GroupChat.Members.TryAdd(info.CharacterId, member); + return true; + } + + public void Leave(long characterId) { + if (!GroupChat.Members.TryGetValue(characterId, out GroupChatMember? member)) { + Log.Error("Failed to remove member {CharacterId} from group chat {GroupChatId} because they were not found", characterId, GroupChat.Id); + return; + } + + Broadcast(new GroupChatRequest { + RemoveMember = new GroupChatRequest.Types.RemoveMember { + CharacterId = member.CharacterId, + }, + }); + GroupChat.Members.TryRemove(member.CharacterId, out _); + + CheckForDisband(); + } +} diff --git a/Maple2.Server.World/Containers/PartyLookup.cs b/Maple2.Server.World/Containers/PartyLookup.cs index 0504d64de..0c94c31cf 100644 --- a/Maple2.Server.World/Containers/PartyLookup.cs +++ b/Maple2.Server.World/Containers/PartyLookup.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; namespace Maple2.Server.World.Containers; diff --git a/Maple2.Server.World/Containers/PartyManager.cs b/Maple2.Server.World/Containers/PartyManager.cs index 1078bea32..036f44397 100644 --- a/Maple2.Server.World/Containers/PartyManager.cs +++ b/Maple2.Server.World/Containers/PartyManager.cs @@ -5,6 +5,7 @@ using Grpc.Core; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.Server.Channel.Service; using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; diff --git a/Maple2.Server.World/Program.cs b/Maple2.Server.World/Program.cs index 97c42bcdd..abfeac058 100644 --- a/Maple2.Server.World/Program.cs +++ b/Maple2.Server.World/Program.cs @@ -59,6 +59,8 @@ .SingleInstance(); autofac.RegisterType() .SingleInstance(); + autofac.RegisterType() + .SingleInstance(); }); WebApplication app = builder.Build(); diff --git a/Maple2.Server.World/Service/WorldService.GroupChat.cs b/Maple2.Server.World/Service/WorldService.GroupChat.cs new file mode 100644 index 000000000..44bff0061 --- /dev/null +++ b/Maple2.Server.World/Service/WorldService.GroupChat.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using Maple2.Model.Error; +using Maple2.Model.Game; +using Maple2.Model.Game.GroupChat; +using Maple2.Server.World.Containers; +using ChannelPartyRequest = Maple2.Server.Channel.Service.PartyRequest; + +namespace Maple2.Server.World.Service; + +public partial class WorldService { + public override Task GroupChatInfo(GroupChatInfoRequest request, ServerCallContext context) { + IEnumerable groupChatManagers = groupChatLookup.TryGetMany(request.CharacterId); + var groupChats = new List(); + foreach (GroupChatManager manager in groupChatManagers) { + groupChats.Add(ToGroupChatInfo(manager.GroupChat)); + } + + return Task.FromResult(new GroupChatInfoResponse { + Infos = { + groupChats + }, + }); + } + + public override Task GroupChat(GroupChatRequest request, ServerCallContext context) { + switch (request.GroupChatCase) { + case GroupChatRequest.GroupChatOneofCase.Create: + return Task.FromResult(CreateGroupChat(request.RequesterId, request.Create)); + case GroupChatRequest.GroupChatOneofCase.Invite: + return Task.FromResult(InviteGroupChat(request.GroupChatId, request.RequesterId, request.Invite)); + case GroupChatRequest.GroupChatOneofCase.Leave: + return Task.FromResult(LeaveGroupChat(request.GroupChatId, request.RequesterId, request.Leave)); + case GroupChatRequest.GroupChatOneofCase.Chat: + return Task.FromResult(ChatGroupChat(request.GroupChatId, request.RequesterId, request.Chat)); + case GroupChatRequest.GroupChatOneofCase.Disband: + return Task.FromResult(DisbandGroupChat(request.GroupChatId, request.Disband)); + default: + return Task.FromResult(new GroupChatResponse { + Error = (int) GroupChatError.none, + }); + } + } + + private GroupChatResponse CreateGroupChat(long requesterId, GroupChatRequest.Types.Create create) { + GroupChatError error = groupChatLookup.Create(requesterId, out int groupChatId); + if (error != GroupChatError.none) { + return new GroupChatResponse { + Error = (int) error, + }; + } + if (!groupChatLookup.TryGet(groupChatId, out GroupChatManager? manager)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + + if (!playerLookup.TryGet(requesterId, out PlayerInfo? info)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + + return new GroupChatResponse { + GroupChat = ToGroupChatInfo(manager.GroupChat), + }; + } + + private GroupChatResponse InviteGroupChat(int groupChatId, long requestorId, GroupChatRequest.Types.Invite invite) { + if (!groupChatLookup.TryGet(groupChatId, out GroupChatManager? manager)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + if (!playerLookup.TryGet(invite.ReceiverId, out PlayerInfo? info)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + + GroupChatError error = manager.Invite(requestorId, info); + return new GroupChatResponse { + Error = (int) error + }; + } + + private GroupChatResponse LeaveGroupChat(int groupChatId, long requestorId, GroupChatRequest.Types.Leave leave) { + if (!groupChatLookup.TryGet(groupChatId, out GroupChatManager? manager)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + + manager.Leave(requestorId); + return new GroupChatResponse(); + } + + private GroupChatResponse ChatGroupChat(int groupChatId, long requesterId, GroupChatRequest.Types.Chat chat) { + if (!groupChatLookup.TryGet(groupChatId, out GroupChatManager? manager) || !manager.GroupChat.Members.TryGetValue(requesterId, out GroupChatMember? member)) { + return new GroupChatResponse { + Error = (int) GroupChatError.s_err_groupchat_null_target_user, + }; + } + + manager.Broadcast(new GroupChatRequest { + Chat = new GroupChatRequest.Types.Chat { + Message = chat.Message, + RequesterName = member.Name, + }, + }); + + return new GroupChatResponse { + GroupChatId = groupChatId, + }; + } + + private GroupChatResponse DisbandGroupChat(int groupChatId, GroupChatRequest.Types.Disband disband) { + if (!groupChatLookup.TryGet(groupChatId, out GroupChatManager? _)) { + return new GroupChatResponse(); + } + + groupChatLookup.Disband(groupChatId); + return new GroupChatResponse(); + } + + private static GroupChatInfo ToGroupChatInfo(GroupChat groupChat) { + return new GroupChatInfo { + Id = groupChat.Id, + Members = { + groupChat.Members.Values.Select(member => new GroupChatInfo.Types.Member { + CharacterId = member.CharacterId, + }), + }, + }; + } +} diff --git a/Maple2.Server.World/Service/WorldService.Party.cs b/Maple2.Server.World/Service/WorldService.Party.cs index d6091005c..b17a49e8d 100644 --- a/Maple2.Server.World/Service/WorldService.Party.cs +++ b/Maple2.Server.World/Service/WorldService.Party.cs @@ -3,6 +3,7 @@ using Grpc.Core; using Maple2.Model.Error; using Maple2.Model.Game; +using Maple2.Model.Game.Party; using Maple2.Server.World.Containers; using ChannelPartyRequest = Maple2.Server.Channel.Service.PartyRequest; diff --git a/Maple2.Server.World/Service/WorldService.cs b/Maple2.Server.World/Service/WorldService.cs index 894788ea9..43d3bc69e 100644 --- a/Maple2.Server.World/Service/WorldService.cs +++ b/Maple2.Server.World/Service/WorldService.cs @@ -11,14 +11,16 @@ public partial class WorldService : World.WorldBase { private readonly PlayerInfoLookup playerLookup; private readonly GuildLookup guildLookup; private readonly PartyLookup partyLookup; + private readonly GroupChatLookup groupChatLookup; private readonly ILogger logger = Log.Logger.ForContext(); - public WorldService(IMemoryCache tokenCache, ChannelClientLookup channelClients, PlayerInfoLookup playerLookup, GuildLookup guildLookup, PartyLookup partyLookup) { + public WorldService(IMemoryCache tokenCache, ChannelClientLookup channelClients, PlayerInfoLookup playerLookup, GuildLookup guildLookup, PartyLookup partyLookup, GroupChatLookup groupChatLookup) { this.tokenCache = tokenCache; this.channelClients = channelClients; this.playerLookup = playerLookup; this.guildLookup = guildLookup; this.partyLookup = partyLookup; + this.groupChatLookup = groupChatLookup; } public override Task Channels(ChannelsRequest request, ServerCallContext context) {