diff --git a/src/CommandHandlers/ChatMessageHandler.cs b/src/CommandHandlers/ChatMessageHandler.cs index ffdacf7..6fa22e2 100644 --- a/src/CommandHandlers/ChatMessageHandler.cs +++ b/src/CommandHandlers/ChatMessageHandler.cs @@ -11,7 +11,7 @@ class ChatMessageHandler : CommandHandler { string message = receivedObject.Get("p").Get("chm"); if (ManagementCommandProcessor.ProcessCommand(message, client)) return Task.CompletedTask; - if (client.TempMuted) { + if (client.Muted()) { ClientMuted(client); return Task.CompletedTask; } diff --git a/src/CommandHandlers/LoginHandler.cs b/src/CommandHandlers/LoginHandler.cs index 0714226..110f688 100644 --- a/src/CommandHandlers/LoginHandler.cs +++ b/src/CommandHandlers/LoginHandler.cs @@ -86,6 +86,11 @@ class LoginHandler : CommandHandler if (info.Authenticated) { client.PlayerData.DiplayName = info.DisplayName; client.PlayerData.Role = info.Role; + client.PlayerData.VikingId = info.Id; + if (info.Id != null) { + client.RealMuted = Client.MutedList.ContainsKey((int)info.Id); + client.Banned = Client.BannedList.ContainsKey((int)info.Id); + } return true; } } catch (Exception ex) { diff --git a/src/Core/Client.cs b/src/Core/Client.cs index 0ff7659..1511b75 100644 --- a/src/Core/Client.cs +++ b/src/Core/Client.cs @@ -2,17 +2,45 @@ using System; using System.Net.Sockets; using System.Runtime.CompilerServices; +using System.Text.Json; namespace sodoffmmo.Core; public class Client { static int id; static object lck = new(); + // VikingId, Reason (nullable) + public static readonly Dictionary MutedList; + public static readonly Dictionary BannedList; + + static Client() { // Runs when Client class is loaded + if (File.Exists("muted_banned_users.json")) { + try { + MutedBannedData? data = JsonSerializer.Deserialize(File.ReadAllText("muted_banned_users.json")); + if (data != null) { + MutedList = data.MutedList ?? new(); + BannedList = data.BannedList ?? new(); + return; + } + } catch { + Console.WriteLine("Data for Muted/Banned users wasn't read. Is it corrupted?"); + } + } + MutedList = new(); + BannedList = new(); + } + public int ClientID { get; private set; } public PlayerData PlayerData { get; set; } = new(); public Room? Room { get; private set; } + // Used by ban system for smooth unban transitions. + public Room? ReturnRoomOnPardon { get; private set; } = null; public bool OldApi { get; set; } = false; + public bool TempMuted { get; set; } = false; + public bool RealMuted { get; set; } = false; + public bool Banned { get; set; } = false; + public bool Muted() => TempMuted || RealMuted || Banned; private readonly Socket socket; SocketBuffer socketBuffer = new(); @@ -51,7 +79,8 @@ public class Client { // set variable player data as not valid, but do not reset all player data PlayerData.IsValid = false; - if (Room != null) { + ReturnRoomOnPardon = room; // This is needed so that users are put where they're supposed to when being unbanned. + if (Room != null && (!Banned || room?.Name == "LIMBO" || !Room.Name.StartsWith("BannedUserRoom_"))) { // If user is in the ban room they're just fine exactly where they are. Console.WriteLine($"Leave room: {Room.Name} (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}"); Room.RemoveClient(this); @@ -61,16 +90,32 @@ public class Client { Room.Send(NetworkObject.WrapObject(0, 1004, data).Serialize()); } - // set new room (null when SetRoom is used as LeaveRoom) - Room = room; + if (Banned && room?.Name != "LIMBO") { + if (room != null) { + Room = Room.GetOrAdd("BannedUserRoom_" + ClientID, autoRemove: true); + if (Room.Clients.Contains(this)) { + Send(Room.RespondJoinRoom()); + } else { + Room.AddClient(this); + } + Send(Room.SubscribeRoom()); + } else { + Room?.RemoveClient(this); + Room = null; + } + } else { + // set new room (null when SetRoom is used as LeaveRoom) + Room = room; - if (Room != null) { - Console.WriteLine($"Join room: {Room.Name} RoomID (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}"); - Room.AddClient(this); + if (Room != null) { + Console.WriteLine($"Join room: {Room.Name} RoomID (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}"); + Room.AddClient(this); - Send(Room.SubscribeRoom()); - if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo + Send(Room.SubscribeRoom()); + if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo + } } + } } @@ -112,4 +157,17 @@ public class Client { return socket.Connected && !scheduledDisconnect; } } + public static void SaveMutedBanned() { + File.WriteAllText("muted_banned_users.json", JsonSerializer.Serialize(new MutedBannedData { + MutedList = MutedList, + BannedList = BannedList + }, new JsonSerializerOptions { + WriteIndented = true + })); + } +} + +internal sealed class MutedBannedData { + public Dictionary? MutedList { get; set; } + public Dictionary? BannedList { get; set; } } diff --git a/src/Core/Room.cs b/src/Core/Room.cs index 5e59735..b379f44 100644 --- a/src/Core/Room.cs +++ b/src/Core/Room.cs @@ -104,8 +104,14 @@ public class Room { NetworkObject obj = new(); NetworkArray roomInfo = new(); roomInfo.Add(Id); - roomInfo.Add(Name); // Room Name - roomInfo.Add(Group); // Group Name + if (Name.StartsWith("BannedUserRoom_")) { + // Send these fake names to the client. + roomInfo.Add("YouAreBanned"); // Room Name + roomInfo.Add("YouAreBanned"); // Group Name + } else { + roomInfo.Add(Name); // Room Name + roomInfo.Add(Group); // Group Name + } roomInfo.Add(true); // is game roomInfo.Add(false); // is hidden roomInfo.Add(false); // is password protected @@ -136,8 +142,14 @@ public class Room { NetworkArray r1 = new(); r1.Add(Id); - r1.Add(Name); // Room Name - r1.Add(Group); // Group Name + if (Name.StartsWith("BannedUserRoom_")) { + // Send these fake names to the client. + r1.Add("YouAreBanned"); // Room Name + r1.Add("YouAreBanned"); // Group Name + } else { + r1.Add(Name); // Room Name + r1.Add(Group); // Group Name + } r1.Add(true); r1.Add(false); r1.Add(false); diff --git a/src/Data/PlayerData.cs b/src/Data/PlayerData.cs index 47eed62..d958f04 100644 --- a/src/Data/PlayerData.cs +++ b/src/Data/PlayerData.cs @@ -2,6 +2,7 @@ using System.Globalization; using sodoffmmo.Core; using sodoffmmo.Management; +using System.Text.Json; namespace sodoffmmo.Data; public class PlayerData { @@ -37,6 +38,7 @@ public class PlayerData { public string DiplayName { get; set; } = "placeholder"; public Role Role { get; set; } = Role.User; + public int? VikingId { get; set; } public long last_ue_time { get; set; } = 0; @@ -89,6 +91,20 @@ public class PlayerData { if (varName == "FP") { value = FixMountState(value); } + + // update the playername (sent with avatardata) if it changes + // this is also hit when the player joins the room + if (varName == "A" && variables.GetValueOrDefault("A") != value) { + try { + // Gonna surround this in a try/catch in case for some + // reason it breaks so that it doesn't break MMO. + string? name = JsonSerializer.Deserialize(value)?.DisplayName; + if (name != null && name != DiplayName) { + DiplayName = name; + } + } + catch (Exception) {} // Pokemon-style exception handling + } // store in directory variables[varName] = value; diff --git a/src/Management/AuthenticationInfo.cs b/src/Management/AuthenticationInfo.cs index 266ebc1..921edd1 100644 --- a/src/Management/AuthenticationInfo.cs +++ b/src/Management/AuthenticationInfo.cs @@ -12,6 +12,9 @@ public class AuthenticationInfo { [XmlElement] public Role Role { get; set; } + + [XmlElement] + public int? Id { get; set; } } [Serializable] diff --git a/src/Management/AvatarData.cs b/src/Management/AvatarData.cs new file mode 100644 index 0000000..013f490 --- /dev/null +++ b/src/Management/AvatarData.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace sodoffmmo.Management; + +public class AvatarData { + public string? DisplayName { get; set; } +} \ No newline at end of file diff --git a/src/Management/Commands/BanCommand.cs b/src/Management/Commands/BanCommand.cs new file mode 100644 index 0000000..eadd73b --- /dev/null +++ b/src/Management/Commands/BanCommand.cs @@ -0,0 +1,82 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using System.Numerics; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("ban", Role.Moderator)] +class BanCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length < 1) { + client.Send(Utils.BuildServerSideMessage("Ban: Invalid number of arguments", "Server")); + return; + } + string name = string.Join(' ', arguments); + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == name); // Find in this room. + if (target == null) { // Find through all the rooms. + foreach (Room room in Room.AllRooms()) { + if (room == client.Room) continue; // Skip; we already checked here. + target = room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == name); + if (target != null) break; + } + } + if (target == null) { // Failure + client.Send(Utils.BuildServerSideMessage($"Ban: user {name} not found", "Server")); + return; + } + + if (target.Banned) { + client.Send(Utils.BuildServerSideMessage($"Ban: {name} is already banned", "Server")); + } else { + target.Banned = true; + target.SetRoom(target.Room); // This will run the client through the 'if Banned' conditions. + client.Send(Utils.BuildServerSideMessage($"Ban: {name} has been banned", "Server")); + if (target.PlayerData.VikingId != null) { + Client.BannedList.Add((int)target.PlayerData.VikingId, target.PlayerData.DiplayName); + Client.SaveMutedBanned(); + } else { + client.Send(Utils.BuildServerSideMessage($"Mute: This user is not authenticated. Their ban will not be permanent!", "Server")); + + } + } + } +} + +[ManagementCommand("unban", Role.Moderator)] +class UnbanCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length < 1) { + client.Send(Utils.BuildServerSideMessage("Unban: Invalid number of arguments", "Server")); + return; + } + string name = string.Join(' ', arguments); + int? id = null; + if (Client.BannedList.ContainsValue(name)) id = Client.BannedList.FirstOrDefault(p => p.Value == name).Key; // Find in list in case they're offline. + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.VikingId == id || x.PlayerData.DiplayName == name); // Find in this room. + if (target == null) { // Find through all the rooms. + foreach (Room room in Room.AllRooms()) { + if (room == client.Room) continue; // Skip; we already checked here. + target = room.Clients.FirstOrDefault(x => x.PlayerData.VikingId == id || x.PlayerData.DiplayName == name); + if (target != null) break; + } + } + if (target == null && id == null) { // Failure + client.Send(Utils.BuildServerSideMessage($"Unban: user {name} not found", "Server")); + return; + } + + if (target?.Muted() == false || (id != null && !Client.BannedList.ContainsKey((int)id))) { + client.Send(Utils.BuildServerSideMessage($"Unban: {name} is not banned", "Server")); + } else { + if (target != null) { + target.Banned = false; + target.SetRoom(target.ReturnRoomOnPardon); // Put the target back. + } + client.Send(Utils.BuildServerSideMessage($"Unban: {name} has been unbanned", "Server")); + if (id != null) { + Client.BannedList.Remove((int)id); + Client.SaveMutedBanned(); + } + } + } +} \ No newline at end of file diff --git a/src/Management/Commands/MuteCommand.cs b/src/Management/Commands/MuteCommand.cs new file mode 100644 index 0000000..d5cd3a9 --- /dev/null +++ b/src/Management/Commands/MuteCommand.cs @@ -0,0 +1,80 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; + +namespace sodoffmmo.Management.Commands; + +[ManagementCommand("mute", Role.Moderator)] +class MuteCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length < 1) { + client.Send(Utils.BuildServerSideMessage("Mute: Invalid number of arguments", "Server")); + return; + } + string name = string.Join(' ', arguments); + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == name); // Find in this room. + if (target == null) { // Find through all the rooms. + foreach (Room room in Room.AllRooms()) { + if (room == client.Room) continue; // Skip; we already checked here. + target = room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == name); + if (target != null) break; + } + } + if (target == null) { // Failure + client.Send(Utils.BuildServerSideMessage($"Mute: user {name} not found", "Server")); + return; + } + + if (target.RealMuted) { + client.Send(Utils.BuildServerSideMessage($"Mute: {name} is already muted", "Server")); + } else { + target.RealMuted = true; + client.Send(Utils.BuildServerSideMessage($"Mute: {name} has been muted", "Server")); + if (target.PlayerData.VikingId != null) { + Client.MutedList.Add((int)target.PlayerData.VikingId, target.PlayerData.DiplayName); + Client.SaveMutedBanned(); + } else { + client.Send(Utils.BuildServerSideMessage($"Mute: This user is not authenticated. Their mute will not be permanent!", "Server")); + + } + } + } +} + +[ManagementCommand("unmute", Role.Moderator)] +class UnmuteCommand : IManagementCommand { + public void Handle(Client client, string[] arguments) { + if (arguments.Length < 1) { + client.Send(Utils.BuildServerSideMessage("Unmute: Invalid number of arguments", "Server")); + return; + } + string name = string.Join(' ', arguments); + int? id = null; + if (Client.MutedList.ContainsValue(name)) id = Client.MutedList.FirstOrDefault(p => p.Value == name).Key; // Find in list in case they're offline. + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.VikingId == id || x.PlayerData.DiplayName == name); // Find in this room. + if (target == null) { // Find through all the rooms. + foreach (Room room in Room.AllRooms()) { + if (room == client.Room) continue; // Skip; we already checked here. + target = room.Clients.FirstOrDefault(x => x.PlayerData.VikingId == id || x.PlayerData.DiplayName == name); + if (target != null) break; + } + } + if (target == null && id == null) { // Failure + client.Send(Utils.BuildServerSideMessage($"Unmute: user {name} not found", "Server")); + return; + } + + if (target?.Muted() == false || (id != null && !Client.MutedList.ContainsKey((int)id))) { + client.Send(Utils.BuildServerSideMessage($"Unmute: {name} is not muted", "Server")); + } else { + if (target != null) { + target.RealMuted = false; + target.TempMuted = false; // Undo this too I guess. + } + client.Send(Utils.BuildServerSideMessage($"Unmute: {name} has been unmuted", "Server")); + if (id != null) { + Client.MutedList.Remove((int)id); + Client.SaveMutedBanned(); + } + } + } +} \ No newline at end of file diff --git a/src/Management/Commands/TempMuteCommand.cs b/src/Management/Commands/TempMuteCommand.cs index 6479173..1303685 100644 --- a/src/Management/Commands/TempMuteCommand.cs +++ b/src/Management/Commands/TempMuteCommand.cs @@ -6,19 +6,25 @@ namespace sodoffmmo.Management.Commands; [ManagementCommand("tempmute", Role.Moderator)] class TempMuteCommand : IManagementCommand { public void Handle(Client client, string[] arguments) { - if (arguments.Length != 1) { + if (arguments.Length < 1) { client.Send(Utils.BuildServerSideMessage("TempMute: Invalid number of arguments", "Server")); return; } - Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == arguments[0]); + string name = string.Join(' ', arguments); + Client? target = client.Room.Clients.FirstOrDefault(x => x.PlayerData.DiplayName == name); if (target == null) { - client.Send(Utils.BuildServerSideMessage($"TempMute: user {arguments[0]} not found", "Server")); + client.Send(Utils.BuildServerSideMessage($"TempMute: user {name} not found", "Server")); return; } - target.TempMuted = !target.TempMuted; - if (target.TempMuted) - client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been temporarily muted", "Server")); - else - client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been unmuted", "Server")); + + if (target.RealMuted) { + client.Send(Utils.BuildServerSideMessage($"TempMute: {name} is already regular muted", "Server")); + } else { + target.TempMuted = !target.TempMuted; + if (target.TempMuted) + client.Send(Utils.BuildServerSideMessage($"TempMute: {name} has been temporarily muted", "Server")); + else + client.Send(Utils.BuildServerSideMessage($"TempMute: {name} has been unmuted", "Server")); + } } }