diff --git a/plugins/sample_plugin/Plugin.cs b/plugins/sample_plugin/Plugin.cs new file mode 100644 index 0000000..5c0325a --- /dev/null +++ b/plugins/sample_plugin/Plugin.cs @@ -0,0 +1,36 @@ +using System; +using HarmonyLib; + +namespace SoDOff_MMO_Plugin { + // all classes from SoDOff_MMO_Plugin namespace will be created on dll load + public class SamplePlugin1 { + public SamplePlugin1() { + Console.WriteLine("SamplePlugin1 constructor"); + var harmony = new Harmony("SamplePlugin1"); + harmony.PatchAll(); + } + } + public class SamplePlugin2 { + public SamplePlugin2() { + Console.WriteLine("SamplePlugin2 constructor"); + } + } +} + +namespace Internal { + // classes from other namespace will be NOT created on dll load + public class SamplePlugin3 { + public SamplePlugin3() { + Console.WriteLine("SamplePlugin3 constructor"); + } + } +} + + +[HarmonyPatch("ChatMessageHandler", "Chat")] +public class ChatMessageHandler_Chat { + [HarmonyPrefix] + public static void Chat(sodoffmmo.Core.Client client, string message) { + Console.WriteLine($"Chat message: {message}"); + } +} diff --git a/plugins/sample_plugin/SamplePlugin.csproj b/plugins/sample_plugin/SamplePlugin.csproj new file mode 100644 index 0000000..56ee8bf --- /dev/null +++ b/plugins/sample_plugin/SamplePlugin.csproj @@ -0,0 +1,17 @@ + + + net6.0 + SamplePlugin + enable + + + + + + + + + ../../src/bin/Debug/net6.0/sodoffmmo.dll + + + diff --git a/src/CommandHandlers/ChatMessageHandler.cs b/src/CommandHandlers/ChatMessageHandler.cs index ffdacf7..d62a4d2 100644 --- a/src/CommandHandlers/ChatMessageHandler.cs +++ b/src/CommandHandlers/ChatMessageHandler.cs @@ -11,33 +11,35 @@ class ChatMessageHandler : CommandHandler { string message = receivedObject.Get("p").Get("chm"); if (ManagementCommandProcessor.ProcessCommand(message, client)) return Task.CompletedTask; - if (client.TempMuted) { - ClientMuted(client); - return Task.CompletedTask; - } if (!Configuration.ServerConfiguration.EnableChat && !client.Room.AllowChatOverride) { - ChatDisabled(client); + ChatDisabled(client, message); + } else if (Configuration.ServerConfiguration.Authentication >= AuthenticationMode.RequiredForChat && client.PlayerData.VikingId != 0) { + ChatRequiredAuthentication(client, message); + } else if (client.Muted) { + ClientMuted(client, message); } else { Chat(client, message); } return Task.CompletedTask; } - public void ChatDisabled(Client client) { + // NOTE: message is passed also for functions that refuse to send messages (this can be useful at the level of the chat moderation plugin) + + public void ChatDisabled(Client client, string message) { client.Send(Utils.BuildServerSideMessage("Unfortunately, chat has been disabled by server administrators", "Server")); } - public void ClientMuted(Client client) { + public void ChatRequiredAuthentication(Client client, string message) { + client.Send(Utils.BuildServerSideMessage("You must be authenticated to use the chat", "Server")); + } + + public void ClientMuted(Client client, string message) { client.Send(Utils.BuildServerSideMessage("You have been muted by the moderators", "Server")); } public void Chat(Client client, string message) { - if (Configuration.ServerConfiguration.Authentication >= AuthenticationMode.RequiredForChat && client.PlayerData.DiplayName == "placeholder") { - client.Send(Utils.BuildServerSideMessage("You must be authenticated to use the chat", "Server")); - return; - } - - client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DiplayName), client); + client.Room.Send(Utils.BuildChatMessage(client.PlayerData.Uid, message, client.PlayerData.DisplayName), client); + // NOTE: using DisplayName not VikingName here for consistency with client behavior, preventing "placeholder" in not authenticated modes and support for changing name while logged on MMO NetworkObject cmd = new(); NetworkObject data = new(); diff --git a/src/CommandHandlers/LoginHandler.cs b/src/CommandHandlers/LoginHandler.cs index 0714226..6620957 100644 --- a/src/CommandHandlers/LoginHandler.cs +++ b/src/CommandHandlers/LoginHandler.cs @@ -84,7 +84,8 @@ class LoginHandler : CommandHandler AuthenticationInfo info = Utils.DeserializeXml(responseString); if (info.Authenticated) { - client.PlayerData.DiplayName = info.DisplayName; + client.PlayerData.VikingName = info.VikingName; + client.PlayerData.VikingId = info.Id; client.PlayerData.Role = info.Role; return true; } diff --git a/src/Core/Client.cs b/src/Core/Client.cs index 9743489..c4f1b2d 100644 --- a/src/Core/Client.cs +++ b/src/Core/Client.cs @@ -12,7 +12,8 @@ public class Client { public PlayerData PlayerData { get; set; } = new(); public Room? Room { get; private set; } public bool OldApi { get; set; } = false; - public bool TempMuted { get; set; } = false; + + public bool Muted { get; set; } = false; private readonly Socket socket; SocketBuffer socketBuffer = new(); diff --git a/src/Core/SpecialRoom.cs b/src/Core/SpecialRoom.cs index e61e4fe..f48f510 100644 --- a/src/Core/SpecialRoom.cs +++ b/src/Core/SpecialRoom.cs @@ -90,7 +90,7 @@ public class SpecialRoom : Room { Console.WriteLine("Started event " +alert + " in room " + Name); } else { specificClient.Send(packet); - Console.WriteLine("Added " + specificClient.PlayerData.DiplayName + " to event " + alert + " with " + duration + " seconds remaining"); + Console.WriteLine("Added " + specificClient.PlayerData.DisplayName + " to event " + alert + " with " + duration + " seconds remaining"); } } diff --git a/src/Data/PlayerData.cs b/src/Data/PlayerData.cs index 47eed62..9c6bdc0 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 { @@ -35,7 +36,13 @@ public class PlayerData { // animation bitfield (animations used by avatar, e.g. mounted, swim, ...) public int Mbf { get; set; } - public string DiplayName { get; set; } = "placeholder"; + // Name of player used by MMO + public string DisplayName { get; set; } = "placeholder"; + // Name of player from API database + public string VikingName { get; set; } = ""; + // ID of player from API database, zero for not authenticated players + public int VikingId { get; set; } = 0; + // role of player for management public Role Role { get; set; } = Role.User; public long last_ue_time { get; set; } = 0; @@ -90,6 +97,18 @@ public class PlayerData { value = FixMountState(value); } + // update the playername (sent with avatardata) if it changes + // this is also sent when the player joins the room + if (varName == "A" && variables.GetValueOrDefault("A") != value) { + try { + string? name = JsonSerializer.Deserialize(value)?.DisplayName; + if (name != null && name != DisplayName) { + DisplayName = name; + } + } + catch (JsonException) {} // Don't break if the data is malformed or something. + } + // store in directory variables[varName] = value; return value; diff --git a/src/Management/Commands/TempMuteCommand.cs b/src/Management/Commands/TempMuteCommand.cs index 6479173..38fd94d 100644 --- a/src/Management/Commands/TempMuteCommand.cs +++ b/src/Management/Commands/TempMuteCommand.cs @@ -6,19 +6,27 @@ 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]); - if (target == null) { - client.Send(Utils.BuildServerSideMessage($"TempMute: user {arguments[0]} not found", "Server")); + string name = string.Join(' ', arguments); + var targets = client.Room.Clients.Where(x => x.PlayerData.DisplayName == name); + // NOTE: using DisplayName here because this is the name displayed in client-side chat history + if (targets.Count() == 0) { + client.Send(Utils.BuildServerSideMessage($"TempMute: user {name} not found", "Server")); + return; + } else if (targets.Count() > 1) { + client.Send(Utils.BuildServerSideMessage($"TempMute: found multiple users with the name: {name}", "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")); + + Client target = targets.First(); + if (target.Muted) { + client.Send(Utils.BuildServerSideMessage($"TempMute: {name} is already muted", "Server")); + } else { + target.Muted = true; + client.Send(Utils.BuildServerSideMessage($"TempMute: {name} has been temporarily muted", "Server")); + } } } diff --git a/src/Management/ManagementCommandProcessor.cs b/src/Management/ManagementCommandProcessor.cs index c4b35eb..fbab23d 100644 --- a/src/Management/ManagementCommandProcessor.cs +++ b/src/Management/ManagementCommandProcessor.cs @@ -42,7 +42,7 @@ public class ManagementCommandProcessor { if (commands.TryGetValue(new Tuple(commandName, currentRole), out Type? commandType)) { IManagementCommand command = (IManagementCommand)Activator.CreateInstance(commandType)!; - Console.WriteLine($"Management command {commandName} by {client.PlayerData.DiplayName} ({client.PlayerData.Uid}) in {client.Room.Name}"); + Console.WriteLine($"Management command {commandName} by {client.PlayerData.VikingName} ({client.PlayerData.Uid}) in {client.Room.Name}"); command.Handle(client, arguments); return true; } diff --git a/src/Program.cs b/src/Program.cs index 6878f22..75ed5d0 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,11 +1,30 @@ using sodoffmmo; using sodoffmmo.Core; using System.Net; +using System.IO; +using System.Reflection; Configuration.Initialize(); -Server server; +string pluginsDir = Path.GetFullPath("plugins"); +Console.WriteLine($"Loading plugins from '{pluginsDir}' ..."); +List plugins = new(); +try { + foreach (var dllFile in Directory.GetFiles(pluginsDir, "*.dll")) { + var dll = Assembly.LoadFrom(dllFile); + foreach(Type type in dll.GetExportedTypes()) { + if (type.Namespace == "SoDOff_MMO_Plugin") { + Console.WriteLine($" * {type} ({Path.GetFileName(dllFile)})"); + plugins.Add(Activator.CreateInstance(type)); + } + } + } +} catch (System.IO.DirectoryNotFoundException) { + Console.WriteLine(" - Skipped: plugins directory do not exist"); +} +Console.WriteLine($"Starting MMO server ..."); +Server server; if (String.IsNullOrEmpty(Configuration.ServerConfiguration.ListenIP) || Configuration.ServerConfiguration.ListenIP == "*") { server = new( IPAddress.IPv6Any, diff --git a/src/Management/AuthenticationInfo.cs b/src/Schema/AuthenticationInfo.cs similarity index 77% rename from src/Management/AuthenticationInfo.cs rename to src/Schema/AuthenticationInfo.cs index 266ebc1..d64860d 100644 --- a/src/Management/AuthenticationInfo.cs +++ b/src/Schema/AuthenticationInfo.cs @@ -8,7 +8,10 @@ public class AuthenticationInfo { public bool Authenticated { get; set; } [XmlElement] - public string DisplayName { get; set; } + public int Id { get; set; } + + [XmlElement] + public string VikingName { get; set; } [XmlElement] public Role Role { get; set; } @@ -17,4 +20,4 @@ public class AuthenticationInfo { [Serializable] public enum Role { User = 0, Moderator = 1, Admin = 2 -} \ No newline at end of file +} diff --git a/src/Schema/AvatarData.cs b/src/Schema/AvatarData.cs new file mode 100644 index 0000000..013f490 --- /dev/null +++ b/src/Schema/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/Server.cs b/src/Server.cs index 08b9d40..07795cb 100644 --- a/src/Server.cs +++ b/src/Server.cs @@ -7,18 +7,26 @@ using System.Net.Sockets; namespace sodoffmmo; public class Server { + static object lck = new(); + static readonly List clients = new(); readonly int port; readonly IPAddress ipAddress; readonly bool IPv6AndIPv4; ModuleManager moduleManager = new(); + public static int TotalClientCount => clients.Count; + public Server(IPAddress ipAdress, int port, bool IPv6AndIPv4) { this.ipAddress = ipAdress; this.port = port; this.IPv6AndIPv4 = IPv6AndIPv4; } + public static Client[] GetAllClients() { + return clients.ToArray(); + } + public async Task Run() { moduleManager.RegisterModules(); ManagementCommandProcessor.Initialize(); @@ -47,6 +55,7 @@ public class Server { private async Task HandleClient(Socket handler) { Client client = new(handler); + lock (lck) clients.Add(client); try { while (client.Connected) { await client.Receive(); @@ -60,6 +69,7 @@ public class Server { try { client.SetRoom(null); } catch (Exception) { } + lock (lck) clients.Remove(client); client.Disconnect(); Console.WriteLine("Socket disconnected IID: " + client.ClientID); }