diff --git a/.idea/.idea.DaemonMC/.idea/.gitignore b/.idea/.idea.DaemonMC/.idea/.gitignore new file mode 100644 index 0000000..04e8cc3 --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.DaemonMC.iml +/projectSettingsUpdater.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.DaemonMC/.idea/.name b/.idea/.idea.DaemonMC/.idea/.name new file mode 100644 index 0000000..4f63fc8 --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/.name @@ -0,0 +1 @@ +DaemonMC \ No newline at end of file diff --git a/.idea/.idea.DaemonMC/.idea/AndroidProjectSystem.xml b/.idea/.idea.DaemonMC/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..e82600c --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.DaemonMC/.idea/encodings.xml b/.idea/.idea.DaemonMC/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.DaemonMC/.idea/indexLayout.xml b/.idea/.idea.DaemonMC/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.DaemonMC/.idea/vcs.xml b/.idea/.idea.DaemonMC/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.DaemonMC/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DaemonMC/CommandManager.cs b/DaemonMC/CommandManager.cs index f91a4b3..e5cbce2 100644 --- a/DaemonMC/CommandManager.cs +++ b/DaemonMC/CommandManager.cs @@ -1,6 +1,9 @@ using System.Numerics; +using DaemonMC; using DaemonMC.Network; +using DaemonMC.Network.Bedrock; using DaemonMC.Network.Enumerations; +using DaemonMC.Plugin; using DaemonMC.Utils; using DaemonMC.Utils.Game; using DaemonMC.Utils.Text; @@ -9,7 +12,7 @@ namespace DaemonMC { public class CommandManager { - public static List AvailableCommands { get; set; } = new List(); + public static Dictionary AvailableCommands { get; set; } = new(); public static List EnumValues { get; set; } = new List(); public static List RealEnums { get; set; } = new List(); @@ -30,30 +33,57 @@ public class CommandManager { typeof(Player), "target" }, { typeof(Vector3), "x y z" }, }; + + public static List GetAvailableCommands() { + return AvailableCommands.Values.Select(x => x.Command).ToList(); + } + + public static List GetCommandsByPlugin(Plugin.Plugin plugin) { + return AvailableCommands.Where(kvp => kvp.Value.Plugin == plugin).Select(kvp => kvp.Value.Command).ToList(); + } - public static void Register(Command command, Action commandFunction) - { - var regCommand = AvailableCommands.FirstOrDefault(p => p.Name == command.Name); + public static void Register(Plugin.Plugin plugin, Command command, Action commandFunction) { + + if (plugin == null!) { + Log.warn($"Cannot register command '{command.Name}' without a plugin reference."); + return; + } + + var existingEntry = AvailableCommands.FirstOrDefault(p => p.Value.Plugin == plugin && p.Value.Command.Name == command.Name); - if (regCommand != null) + if (existingEntry.Key != null) { - bool overloadExists = regCommand.Overloads.Any(existingOverload => existingOverload.Count == command.Parameters.Count && !existingOverload.Where((param, index) => param.Name != command.Parameters[index].Name || param.Type != command.Parameters[index].Type).Any()); + var existingCommand = existingEntry.Value.Command; + bool overloadExists = existingCommand.Overloads.Any(existingOverload => + existingOverload.Count == command.Parameters.Count && + !existingOverload.Where((param, index) => + param.Name != command.Parameters[index].Name || + param.Type != command.Parameters[index].Type).Any()); + if (overloadExists) { - Log.warn($"Couldn't register {command.Name}. Command already registered."); + Log.warn($"Couldn't register {command.Name}. Command already registered by plugin {plugin.GetName()}."); return; } else { - regCommand.Overloads.Add(command.Parameters); - regCommand.CommandFunction.Add(commandFunction); + existingCommand.Overloads.Add(command.Parameters); + existingCommand.CommandFunction.Add(commandFunction); } } else { + // Cerca se il comando esiste per altri plugin + var conflict = AvailableCommands.FirstOrDefault(p => p.Value.Command.Name == command.Name); + if (conflict.Key != null) + { + Log.warn($"Command name '{command.Name}' is already registered by plugin {conflict.Value.Plugin.GetName()}. Use a different name."); + return; + } + command.Overloads.Add(command.Parameters); command.CommandFunction.Add(commandFunction); - AvailableCommands.Add(command); + AvailableCommands.Add(command.Name, (plugin, command)); } foreach (var param in command.Parameters) @@ -71,13 +101,31 @@ public static void Register(Command command, Action commandFuncti public static void Unregister(string commandName) { - var cmd = AvailableCommands.FirstOrDefault(p => p.Name == commandName); - if (cmd == null) + if (AvailableCommands.Remove(commandName)) + { + Log.debug($"Command '{commandName}' unregistered successfully."); + } + else { Log.warn($"Couldn't unregister {commandName}. Command not found."); - return; } - AvailableCommands.Remove(cmd); + } + + public static void UnregisterAll(Plugin.Plugin plugin) + { + if (plugin == null) return; + + var commandsToRemove = AvailableCommands + .Where(kvp => kvp.Value.Plugin == plugin) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var commandName in commandsToRemove) + { + AvailableCommands.Remove(commandName); + } + + Log.debug($"Unregistered {commandsToRemove.Count} commands for plugin {plugin.GetName()}"); } public static int GetSymbol(Type type, int enumIndex = -1) @@ -97,6 +145,7 @@ public static int GetSymbol(Type type, int enumIndex = -1) public static void Execute(string command, Player player) { + if (string.IsNullOrWhiteSpace(command)) return; string[] commandParts = command.Split(' '); @@ -105,13 +154,14 @@ public static void Execute(string command, Player player) string commandName = commandParts[0]; string[] argParts = commandParts.Skip(1).ToArray(); - var regCommand = AvailableCommands.FirstOrDefault(c => c.Name == commandName); - if (regCommand == null) + if (!AvailableCommands.TryGetValue(commandName, out var commandEntry)) { player.SendMessage($"§cUnknown command: {commandName}"); return; } + var regCommand = commandEntry.Command; + for (int i = 0; i < regCommand.Overloads.Count; i++) { var overload = regCommand.Overloads[i]; @@ -169,22 +219,26 @@ public static void Execute(string command, Player player) } Log.debug($"Failed command request: {command}"); - player.SendMessage("§cIncorrect usage. Available parameters:"); + player.SendMessage($"{TextFormat.Red}Incorrect usage. Available parameters:"); foreach (var overload in regCommand.Overloads) { var parameters = string.Join(" ", overload.Select(p => $"<{p.Name}: {(p.Type == typeof(EnumP) ? string.Join(" | ", p.Values) : typeStringMap[p.Type])}>")); - player.SendMessage($"§f/{regCommand.Name} §a{parameters}"); + player.SendMessage($"{TextFormat.White}/{regCommand.Name} {TextFormat.Green}{parameters}"); } } - public static void RegisterBuiltinCommands() - { - Register(new Command("about", "Information about the server"), about); - Register(new Command("pos", "Your current position"), position); + public static void RegisterBuiltinCommands() { + RegisterInternal(new Command("about", "Information about the server"), about); + RegisterInternal(new Command("pos", "Your current position"), position); + } + + private static void RegisterInternal(Command command, Action commandFunction) { + command.Overloads.Add(command.Parameters); + command.CommandFunction.Add(commandFunction); + AvailableCommands.Add(command.Name, (null!, command)); } - public static void about(CommandAction action) - { + public static void about(CommandAction action) { action.Player.SendMessage($"§k§r§7§lDaemon§8MC§r§k§r {DaemonMC.Version} \n§r§fProject URL: §agithub.com/laz1444/DaemonMC \n§r§fGit hash: §a{DaemonMC.GitHash} \n§r§fEnvironment: §a.NET{Environment.Version}, {Environment.OSVersion} \n§r§fSupported MCBE versions: §a{string.Join(", ", Info.ProtocolVersion)}"); } @@ -217,4 +271,4 @@ public CommandEnum(string name, List values) Values = values; } } -} +} \ No newline at end of file diff --git a/DaemonMC/Config.cs b/DaemonMC/Config.cs index c59a780..e7bf2b0 100644 --- a/DaemonMC/Config.cs +++ b/DaemonMC/Config.cs @@ -18,6 +18,7 @@ public class Config public bool ForcePacks { get; set; } = false; public bool XboxAuth { get; set; } = true; public bool Debug { get; set; } = false; + public bool HotReloading { get; set; } = true; public static void Set() { @@ -57,6 +58,7 @@ public static void Set() DaemonMC.GameMode = ToGameMode(config.DefaultGamemode); DaemonMC.DrawDistance = config.DrawDistance; DaemonMC.UnloadChunks = config.UnloadChunks; + DaemonMC.HotReloading = config.HotReloading; Server.Port = config.Port; JWT.XboxAuth = config.XboxAuth; ResourcePackManager.ForcePacks = config.ForcePacks; diff --git a/DaemonMC/DaemonMC.cs b/DaemonMC/DaemonMC.cs index 0464b32..961b84f 100644 --- a/DaemonMC/DaemonMC.cs +++ b/DaemonMC/DaemonMC.cs @@ -5,6 +5,7 @@ using DaemonMC.Utils.Game; using System.Numerics; using System.Reflection; +using DaemonMC.Plugin; namespace DaemonMC { @@ -17,6 +18,7 @@ public static class DaemonMC public static int GameMode = 0; public static int DrawDistance = 10; public static bool UnloadChunks = true; + public static bool HotReloading = true; internal static string Version = "unknown"; internal static string GitHash = "unknown"; public static void Main() @@ -69,17 +71,17 @@ static void OnExit(object? sender, EventArgs e) Server.ServerClose(); } - static void Command() - { - string cmd = Console.ReadLine(); - switch (cmd) - { + private static void Command() { + + var cmd = Console.ReadLine(); + switch (cmd) { case "/help": Log.line(); Log.info("/shutdown - Close server"); Log.info("/dev - Debugging mode"); Log.info("/list - Player list"); Log.info("/liste - Entity list"); + Log.info("/plugins - Plugins list"); Log.line(); break; case "/list": @@ -91,6 +93,10 @@ static void Command() case "/shutdown": Server.ServerClose(); break; + case "/plugins": + var plugins = PluginManager.GetLoadedPlugins(); + Log.info($"Plugins list ({plugins.Count}): " + string.Join(", ", plugins.Select(x => $"{x.PluginInstance.GetName()} v{x.PluginInstance.GetVersion()}"))); + break; case "/dev": Log.line(); Log.warn("================== DaemonMC Debugging mode =================="); diff --git a/DaemonMC/DaemonMC.csproj b/DaemonMC/DaemonMC.csproj index ade32fd..f1fc539 100644 --- a/DaemonMC/DaemonMC.csproj +++ b/DaemonMC/DaemonMC.csproj @@ -35,8 +35,10 @@ + + diff --git a/DaemonMC/Network/Bedrock/BedrockPacketProcessor.cs b/DaemonMC/Network/Bedrock/BedrockPacketProcessor.cs index c057553..b360285 100644 --- a/DaemonMC/Network/Bedrock/BedrockPacketProcessor.cs +++ b/DaemonMC/Network/Bedrock/BedrockPacketProcessor.cs @@ -162,7 +162,7 @@ internal static void HandlePacket(Packet packet, IPEndPoint clientEp) { EnumValues = CommandManager.EnumValues, Enums = CommandManager.RealEnums, - Commands = CommandManager.AvailableCommands + Commands = CommandManager.GetAvailableCommands() }; player.Send(commands); diff --git a/DaemonMC/Network/Packet.cs b/DaemonMC/Network/Packet.cs index c87a752..78191cd 100644 --- a/DaemonMC/Network/Packet.cs +++ b/DaemonMC/Network/Packet.cs @@ -1,5 +1,5 @@ using DaemonMC.Network.Bedrock; -using DaemonMC.Plugin.Plugin; +using DaemonMC.Plugin; using DaemonMC.Utils.Text; namespace DaemonMC.Network diff --git a/DaemonMC/Player.cs b/DaemonMC/Player.cs index fe85732..68a4c4e 100644 --- a/DaemonMC/Player.cs +++ b/DaemonMC/Player.cs @@ -8,10 +8,10 @@ using DaemonMC.Network.Enumerations; using DaemonMC.Network.RakNet; using DaemonMC.Utils.Game; -using DaemonMC.Plugin.Plugin; using DaemonMC.Entities; using DaemonMC.Blocks; using DaemonMC.Forms; +using DaemonMC.Plugin; using Newtonsoft.Json; namespace DaemonMC @@ -139,25 +139,23 @@ public void SendGameRules() Send(packet); } - public void Kick(string msg) - { - _ = Task.Run(async () => { - await Task.Delay(1000); - - var packet = new Disconnect - { - Message = msg - }; - Send(packet); - - PacketEncoder encoder2 = PacketEncoderPool.Get(this); - var packet2 = new RakDisconnect - { - }; - packet2.Encode(encoder2); - Server.RemovePlayer(EntityID); - RakSessionManager.deleteSession(ep); - }); + public void Kick(string msg) { + + var packet = new Disconnect { + Message = msg + }; + + Send(packet); + + var encoder2 = PacketEncoderPool.Get(this); + var packet2 = new RakDisconnect { + id = 0 + }; + + packet2.Encode(encoder2); + + Server.RemovePlayer(EntityID); + RakSessionManager.deleteSession(ep); } public void SendMessage(string message) @@ -604,7 +602,9 @@ internal void HandlePacket(Packet packet) } if (Vector3.Distance(Position, playerAuthInput.Position) > 0.01f || Vector2.Distance(Rotation, playerAuthInput.Rotation) > 0.01f) { - PluginManager.PlayerMove(this); + if (!PluginManager.PlayerMove(this)) { + return; + } Position = playerAuthInput.Position; Rotation = playerAuthInput.Rotation; @@ -688,10 +688,13 @@ internal void HandlePacket(Packet packet) _player.SendChunks(); } - if (packet is TextMessage textMessage) - { - var msg = new TextMessage - { + if (packet is TextMessage textMessage) { + + if (!PluginManager.PlayerSentMessage(this, textMessage)) { + return; + } + + var msg = new TextMessage { MessageType = 1, Username = Username, Message = textMessage.Message @@ -699,28 +702,24 @@ internal void HandlePacket(Packet packet) CurrentWorld.Send(msg); } - if (packet is ServerboundLoadingScreen serverboundLoadingScreen) - { - if (serverboundLoadingScreen.ScreenType == 4) - { - PluginManager.PlayerJoined(this); + if (packet is ServerboundLoadingScreen { ScreenType: 4 }) { + + if (PluginManager.PlayerJoined(this)) { SendEntities(); } + + return; } - if (packet is PlayerSkin playerSkin) - { - var pk = new PlayerSkin - { - UUID = UUID, - Skin = playerSkin.Skin, - Name = playerSkin.Name, - OldName = Skin.SkinId, - Trusted = playerSkin.Trusted, - }; + if (packet is PlayerSkin playerSkin) { + + var pk = new PlayerSkin { UUID = UUID, Skin = Skin, Name = Skin.SkinId, OldName = Skin.SkinId, Trusted = true }; + if (PluginManager.PlayerSkinChanged(this, playerSkin)) { + pk = new PlayerSkin { UUID = UUID, Skin = playerSkin.Skin, Name = playerSkin.Name, OldName = Skin.SkinId, Trusted = playerSkin.Trusted }; + Skin = playerSkin.Skin; + } + CurrentWorld.Send(pk); - - Skin = playerSkin.Skin; } if (packet is CommandRequest commandRequest) @@ -771,18 +770,24 @@ internal void HandlePacket(Packet packet) Log.debug($"{Username} animation action:{animate.Action}"); } - if (packet is InventoryTransaction inventoryTransaction) - { - if (inventoryTransaction.Transaction.Type is TransactionType.ItemUseOnEntityTransaction) - { - if (CurrentWorld.Entities.TryGetValue(inventoryTransaction.Transaction.EntityId, out Entity entity)) - { - PluginManager.EntityAttack(this, entity); + if (packet is InventoryTransaction { Transaction.Type: TransactionType.ItemUseOnEntityTransaction } inventoryTransaction) { + + if (CurrentWorld.Entities.TryGetValue(inventoryTransaction.Transaction.EntityId, out Entity entity)) { + + if (!PluginManager.PlayerAttackedEntity(this, entity)) { + return; } - if (CurrentWorld.OnlinePlayers.TryGetValue(inventoryTransaction.Transaction.EntityId, out Player player)) - { - PluginManager.PlayerAttack(this, player); + + // TODO PvP mechanics, and health manager... + } + + if (CurrentWorld.OnlinePlayers.TryGetValue(inventoryTransaction.Transaction.EntityId, out Player player)) { + + if (!PluginManager.PlayerAttackedPlayer(this, player)) { + return; } + + // TODO PvP mechanics, and health manager... } } diff --git a/DaemonMC/Plugin/Events/Event.cs b/DaemonMC/Plugin/Events/Event.cs new file mode 100644 index 0000000..f543abb --- /dev/null +++ b/DaemonMC/Plugin/Events/Event.cs @@ -0,0 +1,10 @@ +namespace DaemonMC.Plugin.Events; + +public abstract class Event { + + public bool IsCancelled { get; private set; } + + public void Cancel() { + IsCancelled = true; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerAttackedEntityEvent.cs b/DaemonMC/Plugin/Events/PlayerAttackedEntityEvent.cs new file mode 100644 index 0000000..b90c3ec --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerAttackedEntityEvent.cs @@ -0,0 +1,17 @@ +using DaemonMC.Entities; + +namespace DaemonMC.Plugin.Events; + +public class PlayerAttackedEntityEvent(Player player, Entity entity) : Event { + + private Player Player { get; } = player; + private Entity Entity { get; } = entity; + + public Player GetPlayer() { + return Player; + } + + public Entity GetEntity() { + return Entity; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerAttackedPlayerEvent.cs b/DaemonMC/Plugin/Events/PlayerAttackedPlayerEvent.cs new file mode 100644 index 0000000..41b59e8 --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerAttackedPlayerEvent.cs @@ -0,0 +1,15 @@ +namespace DaemonMC.Plugin.Events; + +public class PlayerAttackedPlayerEvent(Player attacker, Player victim) : Event { + + private Player Attacker { get; } = attacker; + private Player Victim { get; } = victim; + + public Player GetAttacker() { + return Attacker; + } + + public Player GetVictim() { + return Victim; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerJoinedEvent.cs b/DaemonMC/Plugin/Events/PlayerJoinedEvent.cs new file mode 100644 index 0000000..51aeded --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerJoinedEvent.cs @@ -0,0 +1,30 @@ +using DaemonMC.Utils; + +namespace DaemonMC.Plugin.Events; + +public class PlayerJoinedEvent(Player player) : Event { + + private Player Player { get; } = player; + private string JoinMessage { get; set; } = $"{TextFormat.Yellow}{player.Username} joined the server!"; + private bool _DisableJoinMessage { get; set; } + + public Player GetPlayer() { + return Player; + } + + public string GetJoinMessage() { + return JoinMessage; + } + + public void SetJoinMessage(string message) { + JoinMessage = message; + } + + public bool IsJoinMessageEnabled() { + return !_DisableJoinMessage; + } + + public void DisableJoinMessage() { + _DisableJoinMessage = true; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerLeavedEvent.cs b/DaemonMC/Plugin/Events/PlayerLeavedEvent.cs new file mode 100644 index 0000000..df9b2a5 --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerLeavedEvent.cs @@ -0,0 +1,21 @@ +using DaemonMC.Utils; + +namespace DaemonMC.Plugin.Events; + +public class PlayerLeavedEvent(Player player) : Event { + + private Player Player { get; } = player; + private string LeaveMessage { get; set; } = $"{TextFormat.Yellow}{player.Username} left the server!"; + + public Player GetPlayer() { + return Player; + } + + public string GetLeaveMessage() { + return LeaveMessage; + } + + public void SetLeaveMessage(string message) { + LeaveMessage = message; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerMoveEvent.cs b/DaemonMC/Plugin/Events/PlayerMoveEvent.cs new file mode 100644 index 0000000..1a2a1be --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerMoveEvent.cs @@ -0,0 +1,10 @@ +namespace DaemonMC.Plugin.Events; + +public class PlayerMoveEvent(Player player) : Event { + + private Player Player { get; } = player; + + public Player GetPlayer() { + return Player; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerSentMessageEvent.cs b/DaemonMC/Plugin/Events/PlayerSentMessageEvent.cs new file mode 100644 index 0000000..0aa9271 --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerSentMessageEvent.cs @@ -0,0 +1,22 @@ +using DaemonMC.Network.Bedrock; + +namespace DaemonMC.Plugin.Events; + +public class PlayerSentMessageEvent(Player player, TextMessage textMessage) : Event { + + private Player Player { get; } = player; + private string UncensoredMessage { get; set; } = textMessage.Message; + private string CensoredMessage { get; set; } = textMessage.FilteredMessage; + + public Player GetPlayer() { + return Player; + } + + public string GetUncensoredMessage() { + return UncensoredMessage; + } + + public string GetCensoredMessage() { + return CensoredMessage; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Events/PlayerSkinChangedEvent.cs b/DaemonMC/Plugin/Events/PlayerSkinChangedEvent.cs new file mode 100644 index 0000000..ebbdf99 --- /dev/null +++ b/DaemonMC/Plugin/Events/PlayerSkinChangedEvent.cs @@ -0,0 +1,29 @@ +using DaemonMC.Network.Bedrock; +using DaemonMC.Utils; + +namespace DaemonMC.Plugin.Events; + +public class PlayerSkinChangedEvent(Player player, PlayerSkin playerSkin) : Event { + + private Player Player { get; } = player; + + public Player GetPlayer() { + return Player; + } + + public Skin GetSkin() { + return playerSkin.Skin; + } + + public string GetSkinName() { + return playerSkin.Name; + } + + public string GetOldSkinName() { + return playerSkin.OldName; + } + + public bool IsSkinTrusted() { + return playerSkin.Trusted; + } +} \ No newline at end of file diff --git a/DaemonMC/Plugin/Plugin.cs b/DaemonMC/Plugin/Plugin.cs index ff82b44..526f1e0 100644 --- a/DaemonMC/Plugin/Plugin.cs +++ b/DaemonMC/Plugin/Plugin.cs @@ -1,33 +1,59 @@ using System.Net; -using DaemonMC.Entities; using DaemonMC.Network; +using DaemonMC.Plugin.Events; -namespace DaemonMC.Plugin.Plugin -{ - public interface IPlugin - { - void OnLoad(); - void OnUnload(); +namespace DaemonMC.Plugin; - void OnPlayerJoined(Player player); - void OnPlayerLeaved(Player player); - void OnPlayerMove(Player player); - bool OnPacketReceived(IPEndPoint ep, Packet packet); - bool OnPacketSent(IPEndPoint ep, Packet packet); - void OnEntityAttack(Player player, Entity entity); - void OnPlayerAttack(Player player, Player victim); +public interface IPlugin { + void OnLoad(); + void OnUnload(); + + void OnPlayerJoined(PlayerJoinedEvent ev); + void OnPlayerLeaved(PlayerLeavedEvent ev); + void OnPlayerMove(PlayerMoveEvent ev); + void OnPlayerAttackedEntity(PlayerAttackedEntityEvent ev); + void OnPlayerAttackedPlayer(PlayerAttackedPlayerEvent ev); + void OnPlayerSentMessage(PlayerSentMessageEvent ev); + void OnPlayerSkinChanged(PlayerSkinChangedEvent ev); + bool OnPacketReceived(IPEndPoint ep, Packet packet); + bool OnPacketSent(IPEndPoint ep, Packet packet); +} + +public abstract class Plugin : IPlugin { + + private string Name { get; set; } = "Plugin"; + private string Version { get; set; } = "1.0.0"; + internal PluginResources Resources { get; set; } = null!; + + public virtual void OnLoad() { } + public virtual void OnUnload() { } + public virtual void OnPlayerJoined(PlayerJoinedEvent ev) { } + public virtual void OnPlayerLeaved(PlayerLeavedEvent ev) { } + public virtual void OnPlayerMove(PlayerMoveEvent ev) { } + public virtual void OnPlayerAttackedEntity(PlayerAttackedEntityEvent ev) { } + public virtual void OnPlayerAttackedPlayer(PlayerAttackedPlayerEvent ev) { } + public virtual void OnPlayerSentMessage(PlayerSentMessageEvent ev) { } + public virtual void OnPlayerSkinChanged(PlayerSkinChangedEvent ev) { } + public virtual bool OnPacketReceived(IPEndPoint ep, Packet packet) { return true; } + public virtual bool OnPacketSent(IPEndPoint ep, Packet packet) { return true; } + + public PluginResources GetPluginResources() { + return Resources; } - public abstract class Plugin : IPlugin - { - public virtual void OnLoad() { } - public virtual void OnUnload() { } - public virtual void OnPlayerJoined(Player player) { } - public virtual void OnPlayerLeaved(Player player) { } - public virtual void OnPlayerMove(Player player) { } - public virtual bool OnPacketReceived(IPEndPoint ep, Packet packet) { return true; } - public virtual bool OnPacketSent(IPEndPoint ep, Packet packet) { return true; } - public virtual void OnEntityAttack(Player player, Entity entity) { } - public virtual void OnPlayerAttack(Player player, Player victim) { } + public void SetName(string name) { + Name = name; + } + + public void SetVersion(string version) { + Version = version; + } + + public string GetName() { + return Name; + } + + public string GetVersion() { + return Version; } } diff --git a/DaemonMC/Plugin/PluginManager.cs b/DaemonMC/Plugin/PluginManager.cs index 1c2b04f..9721710 100644 --- a/DaemonMC/Plugin/PluginManager.cs +++ b/DaemonMC/Plugin/PluginManager.cs @@ -1,246 +1,705 @@ -using System.Net; +using System.Collections.Concurrent; +using System.Net; using System.Reflection; using System.Runtime.Loader; using DaemonMC.Entities; using DaemonMC.Network; -using DaemonMC.Utils.Text; - -namespace DaemonMC.Plugin.Plugin -{ - public class PluginManager - { - private static readonly List _plugins = new List(); - private static readonly Dictionary _reloadDelays = new Dictionary(); - private static readonly object _watcherLock = new(); - private static FileSystemWatcher? _watcher; - - public static void LoadPlugins(string pluginDirectory) - { - if (!Directory.Exists(pluginDirectory)) - { - Log.warn($"{pluginDirectory}/ not found. Creating new folder..."); - Directory.CreateDirectory(pluginDirectory); - } - - foreach (var file in Directory.GetFiles(pluginDirectory, "*.dll")) - { - LoadPlugin(file); - } - - _watcher = new FileSystemWatcher(pluginDirectory, "*.dll") - { - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, - EnableRaisingEvents = true, - IncludeSubdirectories = false - }; +using DaemonMC.Network.Bedrock; +using DaemonMC.Plugin.Events; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Log = DaemonMC.Utils.Text.Log; + +namespace DaemonMC.Plugin; + +public abstract class PluginManager { + + private static readonly List Plugins = []; + private static readonly ConcurrentDictionary LastFileEdit = new(); + private static readonly Timer CleanupTimer = new(CleanupFileEditCache, null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + private static readonly FileSystemWatcher Watcher = new(); + + public static void LoadPlugins(string pluginDirectory) { + + if (!Directory.Exists(pluginDirectory)) { + Log.warn($"{pluginDirectory}/ not found. Creating new folder..."); + Directory.CreateDirectory(pluginDirectory); + Directory.CreateDirectory(Path.Combine(pluginDirectory, "SharedLibraries")); + return; + } - _watcher.Changed += (s, e) => HandlePluginChange(e.FullPath); - _watcher.Created += (s, e) => HandlePluginChange(e.FullPath); - _watcher.Deleted += (s, e) => HandlePluginRemoval(e.FullPath); - _watcher.Renamed += (s, e) => HandlePluginRenamed(e.OldFullPath, e.FullPath); + foreach (var dir in Directory.GetDirectories(pluginDirectory)) { + + if (dir.EndsWith("SharedLibraries")) { + continue; + } + + var files = Directory.GetFiles(Path.GetFullPath(dir), "*.dll"); + if (files.Length > 0) { + LoadPlugin(files.First()); + continue; + } + + var sourcePath = Path.Combine(Path.GetFullPath(dir), "Program.cs"); + if (!File.Exists(sourcePath)) { + Log.warn($"Unable to load plugin {dir}! Program.cs or DLL not found."); + continue; + } + + LoadSourcePlugin(sourcePath); } - public static void LoadPlugin(string filePath) - { - Log.info($"Loading plugin: {filePath}"); + if (DaemonMC.HotReloading) { + EnableWatcher(pluginDirectory); + } + } + + private static void CleanupFileEditCache(object? state) { + + var cutoff = DateTime.Now.AddMinutes(-5); + foreach (var kvp in LastFileEdit.Where(kvp => kvp.Value < cutoff).ToList()) { + LastFileEdit.TryRemove(kvp.Key, out _); + } + } - var fullPath = Path.GetFullPath(filePath); - var loadContext = new PluginLoadContext(fullPath); + private static void OnFileRemoved(object source, FileSystemEventArgs e) { + + var extension = Path.GetExtension(e.FullPath).ToLower(); + if (extension != ".cs" && extension != ".dll") { + return; + } + + var pluginToRemove = Plugins.FirstOrDefault(p => p.Path.Equals(e.FullPath, StringComparison.OrdinalIgnoreCase)); + if (pluginToRemove != null) { + UnloadPlugin(pluginToRemove); + Plugins.Remove(pluginToRemove); + return; + } + + var affectedPlugin = FindPluginUsingDll(e.FullPath); + if (affectedPlugin != null) { + UnloadPlugin(affectedPlugin); + Plugins.Remove(affectedPlugin); + return; + } - using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using var ms = new MemoryStream(); - fs.CopyTo(ms); - ms.Position = 0; + Log.warn($"File removed but no associated plugin found: {e.FullPath}"); + } - var assembly = loadContext.LoadFromStream(ms); + private static void OnFileChanged(object source, FileSystemEventArgs e) { + + var extension = Path.GetExtension(e.FullPath).ToLower(); + if (extension != ".cs" && extension != ".dll") { + return; + } - foreach (var type in assembly.GetTypes()) - { - if (typeof(Plugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract) - { - var pluginInstance = (Plugin)Activator.CreateInstance(type)!; - pluginInstance.OnLoad(); + if (LastFileEdit.ContainsKey(e.FullPath)) { + if (DateTime.Now - LastFileEdit[e.FullPath] < TimeSpan.FromSeconds(1)) { + return; + } + } + + LastFileEdit[e.FullPath] = DateTime.Now; + if (IsFilePartOfPlugin(e.FullPath, out var plugin) && extension == ".cs") { + if (plugin == null!) { + LoadSourcePlugin(FindPluginProgramCs(e.FullPath)!); + return; + } + + ReloadPlugin(plugin); + } else if (extension == ".dll") { + + var possiblePlugin = FindPluginUsingDll(e.FullPath); + if (possiblePlugin != null) { + Log.info($"Dependency edited for plugin '{possiblePlugin.PluginInstance.GetType().Name}': {e.FullPath}"); + ReloadPlugin(possiblePlugin); + } else { + LoadPlugin(e.FullPath); + } + } + } + + public static string? FindPluginProgramCs(string filePath) { + + try { + + var currentDir = Path.GetDirectoryName(Path.GetFullPath(filePath)); + while (!string.IsNullOrEmpty(currentDir)) { + + var programPath = Path.Combine(currentDir, "Program.cs"); + if (File.Exists(programPath)) { + return programPath; + } - _plugins.Add(new LoadedPlugin - { - PluginInstance = pluginInstance, - LoadContext = loadContext, - Path = fullPath - }); + if (string.Equals(Path.GetFileName(currentDir), "Plugins", StringComparison.OrdinalIgnoreCase)) { + break; } + + currentDir = Directory.GetParent(currentDir)?.FullName; } + } catch (Exception ex) { + Log.error($"FindPluginProgramCs: {ex.Message}"); } - public static void ReloadPlugin(string file) - { - UnloadPlugin(file); - LoadPlugin(file); - } + return null; + } - public static void UnloadPlugins() - { - Log.info("Unloading plugins..."); - foreach (var plugin in _plugins) - { - try - { - plugin.PluginInstance.OnUnload(); - plugin.LoadContext.Unload(); - } - catch (Exception ex) - { - Log.error($"Failed to unload plugin: {ex.Message}"); + private static LoadedPlugin? FindPluginUsingDll(string dllPath) { + + foreach (var plugin in Plugins) { + try { + foreach (var assembly in plugin.LoadContext.Assemblies) { + if (assembly.Location.Equals(dllPath, StringComparison.OrdinalIgnoreCase)) { + return plugin; + } } + } catch { // Ignore + } + } + return null; + } + + private static bool IsFilePartOfPlugin(string filePath, out LoadedPlugin? affectedPlugin) { + + affectedPlugin = null; + + filePath = Path.GetFullPath(filePath); + var fileDir = Path.GetDirectoryName(filePath); + + if (string.IsNullOrEmpty(fileDir)) + return false; + + foreach (var plugin in Plugins) { + + var pluginDir = Path.GetDirectoryName(Path.GetFullPath(plugin.Path)); + if (string.IsNullOrEmpty(pluginDir) || (!fileDir.Equals(pluginDir, StringComparison.OrdinalIgnoreCase) && !fileDir.StartsWith(pluginDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))) { + continue; + } + + affectedPlugin = plugin; + return true; + } + + var currentDir = fileDir; + while (currentDir != null) { + + var programCsPath = Path.Combine(currentDir, "Program.cs"); + if (File.Exists(programCsPath)) { + return true; } - _plugins.Clear(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); + if (Path.GetFileName(currentDir).Equals("Plugins", StringComparison.OrdinalIgnoreCase)) { + break; + } - Log.info("Plugins successfully unloaded."); + currentDir = Directory.GetParent(currentDir)?.FullName; } - public static void UnloadPlugin(string file) - { - var plugin = _plugins.FirstOrDefault(p => p.Path == Path.GetFullPath(file)); - if (plugin == null) return; + return false; + } + public static void UnloadPlugins() { + + Log.info("Unloading plugins..."); + foreach (var plugin in Plugins) { + UnloadPlugin(plugin); + } + + DisableWatcher(); + Plugins.Clear(); + + Log.info("Plugins successfully unloaded."); + } + + public static void UnloadPlugin(LoadedPlugin plugin) { + + try { + + var commands = CommandManager.GetCommandsByPlugin(plugin.PluginInstance); + foreach (var command in commands) { + CommandManager.Unregister(command.Name); + } + plugin.PluginInstance.OnUnload(); + if (plugin.PluginInstance.Resources is IDisposable disposable) { + disposable.Dispose(); + } + + plugin.PluginInstance = null!; plugin.LoadContext.Unload(); - _plugins.Remove(plugin); + + for (var i = 0; i < 5; i++) { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); + GC.WaitForPendingFinalizers(); + } + + Log.info($"Unloaded plugin: {Path.GetFileName(plugin.Path)}"); + UpdateCommandsForAllPlayers(); + } catch (Exception ex) { + Log.error($"Error unloading plugin {plugin.Path}: {ex}"); + } + } - GC.Collect(); - GC.WaitForPendingFinalizers(); + private static void UpdateCommandsForAllPlayers() { - Log.info($"Plugin unloaded: {file}"); + foreach (var onlinePlayer in Server.GetOnlinePlayers()) { + onlinePlayer.Send(new AvailableCommands { + EnumValues = CommandManager.EnumValues, + Enums = CommandManager.RealEnums, + Commands = CommandManager.GetAvailableCommands() + }); + } + } + + private static void ReloadPlugin(LoadedPlugin plugin) { + + Plugins.Remove(plugin); + UnloadPlugin(plugin); + + if (plugin.SourcePlugin) { + LoadSourcePlugin(plugin.Path); + return; } + + LoadPlugin(plugin.Path); + } + + public static void LoadPlugin(string sourcePath) { + + try { + + var pluginDirectory = Path.GetDirectoryName(sourcePath)!; + var sharedLibsPath = Path.Combine(Directory.GetParent(pluginDirectory)!.FullName, "SharedLibraries"); + var globalSharedLibsPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins", "SharedLibraries"); + + byte[] assemblyBytes = []; + var retryCount = 0; + + while (retryCount < 3) { + + try { + using var fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + assemblyBytes = new byte[fs.Length]; + fs.ReadExactly(assemblyBytes, 0, assemblyBytes.Length); + break; + } catch (IOException) when (retryCount < 2) { + retryCount++; + Thread.Sleep(100); + } + } + + if (assemblyBytes.Length == 0) { + Log.error($"Failed to read plugin assembly from {sourcePath}"); + return; + } - private static void HandlePluginChange(string fullPath) - { - lock (_watcherLock) - { - var now = DateTime.Now; - if (_reloadDelays.TryGetValue(fullPath, out var lastTime)) - { - if ((now - lastTime).TotalMilliseconds < 500) - return; + var loadContext = new PluginLoadContext(sourcePath); + var assembly = loadContext.LoadFromStream(new MemoryStream(assemblyBytes)); + + void LoadDependency(string dllPath) { + + try { + + byte[] bytes; + using (var fs = new FileStream(dllPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { + bytes = new byte[fs.Length]; + fs.ReadExactly(bytes, 0, bytes.Length); + } + + loadContext.LoadFromStream(new MemoryStream(bytes)); + Log.debug($"Loaded shared library: {Path.GetFileName(dllPath)}"); + } catch (Exception ex) { + Log.error($"Failed to load shared library {dllPath}: {ex.Message}"); } - _reloadDelays[fullPath] = now; } - Task.Delay(500).ContinueWith(_ => - { - try - { - ReloadPlugin(fullPath); + if (Directory.Exists(sharedLibsPath)) { + + foreach (var dll in Directory.GetFiles(sharedLibsPath, "*.dll")) { + LoadDependency(dll); } - catch (Exception ex) - { - Log.error($"Failed to reload plugin: {ex}"); + } + + if (Directory.Exists(globalSharedLibsPath)) { + + foreach (var dll in Directory.GetFiles(globalSharedLibsPath, "*.dll")) { + LoadDependency(dll); + } + } + + var pluginLoaded = false; + foreach (var type in assembly.GetTypes()) { + + if (!typeof(Plugin).IsAssignableFrom(type) || type is { IsInterface: true, IsAbstract: true }) { + continue; } - }); - } - private static void HandlePluginRenamed(string oldPath, string newPath) - { - UnloadPlugin(oldPath); + try { + + var pluginInstance = (Plugin)Activator.CreateInstance(type)!; + pluginInstance.Resources = new PluginResources(Path.GetDirectoryName(sourcePath)!); + + pluginInstance.Resources.CreateResourcesFolder(); + pluginInstance.OnLoad(); - if (Path.GetExtension(newPath).Equals(".dll", StringComparison.OrdinalIgnoreCase)) - { - Task.Delay(500).ContinueWith(_ => LoadPlugin(newPath)); + Plugins.Add(new LoadedPlugin { + PluginInstance = pluginInstance, + Path = sourcePath, + SourcePlugin = false, + LoadContext = loadContext + }); + + pluginLoaded = true; + Log.info($"Loaded plugin: {type.Name} from {Path.GetFileName(sourcePath)}"); + + UpdateCommandsForAllPlayers(); + } catch (Exception ex) { + Log.error($"Failed to instantiate plugin type {type.Name} from {sourcePath}: {ex.Message}"); + } } - } - private static void HandlePluginRemoval(string path) - { - UnloadPlugin(path); + if (pluginLoaded) { + return; + } + + Log.warn($"No valid plugin class found in: {sourcePath}"); + loadContext.Unload(); + } catch (Exception ex) { + Log.error($"Error loading plugin from DLL {sourcePath}: {ex}"); } + } + + public static void LoadSourcePlugin(string sourcePath) { + + try { + + var pluginDir = Path.GetDirectoryName(sourcePath)!; + var allCsFiles = Directory.GetFiles(pluginDir, "*.cs"); + var syntaxTrees = new List(); + + foreach (var file in allCsFiles) { + try { + var code = File.ReadAllText(file); + syntaxTrees.Add(CSharpSyntaxTree.ParseText(code)); + Log.debug($"Parsed source file: {Path.GetFileName(file)}"); + } catch (Exception ex) { + Log.error($"Error parsing {file}: {ex.Message}"); + } + } - public static void PlayerJoined(Player player) - { - foreach (var plugin in _plugins) - { - plugin.PluginInstance.OnPlayerJoined(player); + if (syntaxTrees.Count == 0) { + Log.error($"No valid C# files found in {pluginDir}"); + return; } - } - public static void PlayerLeaved(Player player) - { - foreach (var plugin in _plugins) - { - plugin.PluginInstance.OnPlayerLeaved(player); + var assemblyName = Path.GetRandomFileName(); + var references = new List { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location), + MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location) + }; + + references.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)).Select(a => MetadataReference.CreateFromFile(a.Location))); + references.Add(MetadataReference.CreateFromFile(typeof(Plugin).Assembly.Location)); + + var globalSharedLibsPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins", "SharedLibraries"); + if (Directory.Exists(globalSharedLibsPath)) { + references.AddRange(Directory.GetFiles(globalSharedLibsPath, "*.dll").Select(dll => MetadataReference.CreateFromFile(dll))); } - } - public static void PlayerMove(Player player) - { - foreach (var plugin in _plugins) - { - plugin.PluginInstance.OnPlayerMove(player); + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release, concurrentBuild: true, metadataImportOptions: MetadataImportOptions.Public); + var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, compilationOptions); + + using var ms = new MemoryStream(); + var result = compilation.Emit(ms); + + if (!result.Success) { + + Log.error($"Failed to compile plugin from {sourcePath}"); + foreach (var diagnostic in result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) { + Log.error(diagnostic.ToString()); + } + + return; } + + ms.Seek(0, SeekOrigin.Begin); + + var loadContext = new PluginLoadContext(sourcePath); + var assembly = loadContext.LoadFromStream(ms); + + foreach (var type in assembly.GetTypes()) { + + if (!typeof(Plugin).IsAssignableFrom(type) || type is { IsInterface: true, IsAbstract: true }) { + continue; + } + + try { + + var pluginInstance = (Plugin)Activator.CreateInstance(type)!; + pluginInstance.Resources = new PluginResources(Path.GetDirectoryName(sourcePath)!); + + pluginInstance.Resources.CreateResourcesFolder(); + pluginInstance.OnLoad(); + + Plugins.Add(new LoadedPlugin { + PluginInstance = pluginInstance, + Path = sourcePath, + SourcePlugin = true, + LoadContext = loadContext + }); + + Log.info($"Loaded plugin: {type.Name} from {Path.GetFileName(sourcePath)}"); + UpdateCommandsForAllPlayers(); + } catch (Exception ex) { + Log.error($"Failed to instantiate plugin {type.Name}: {ex.Message}"); + } + } + } catch (Exception ex) { + Log.error($"Error loading source plugin from {sourcePath}: {ex}"); } + } + + private static void EnableWatcher(string dir) { + + Watcher.Path = Path.GetFullPath(dir); + Watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; + + Watcher.IncludeSubdirectories = true; + Watcher.Filter = "*.*"; + + Watcher.Changed += OnFileChanged; + Watcher.Renamed += OnFileChanged; + + Watcher.Created += OnFileChanged; + Watcher.Deleted += OnFileRemoved; + + Watcher.EnableRaisingEvents = true; + } - public static bool PacketReceived(IPEndPoint ep, Packet packet) - { - foreach (var plugin in _plugins) - { - return plugin.PluginInstance.OnPacketReceived(ep, packet); + private static void DisableWatcher() { + + Watcher.EnableRaisingEvents = false; + Watcher.Changed -= OnFileChanged; + Watcher.Renamed -= OnFileChanged; + Watcher.Created -= OnFileChanged; + Watcher.Deleted -= OnFileRemoved; + Watcher.Dispose(); + } + + public static bool PlayerJoined(Player player) { + + var ev = new PlayerJoinedEvent(player); + var playerKicked = false; + + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerJoined(ev); + if (!ev.IsCancelled) { + continue; } + + if (playerKicked) { + continue; + } + + player.Kick("Disconnected"); + playerKicked = true; + } + + if (ev.IsCancelled) { + return false; + } + + if (!ev.IsJoinMessageEnabled()) { return true; } + + foreach (var onlinePlayer in Server.GetOnlinePlayers()) { + onlinePlayer.SendMessage(ev.GetJoinMessage()); + } + + return true; + } - public static bool PacketSent(IPEndPoint ep, Packet packet) - { - foreach (var plugin in _plugins) - { - return plugin.PluginInstance.OnPacketSent(ep, packet); + public static void PlayerLeaved(Player player) { + + var ev = new PlayerLeavedEvent(player); + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerLeaved(ev); + if (!ev.IsCancelled) { + continue; } - return true; + + Log.warn("You can't cancel a PlayerLeavedEvent!"); } + + foreach (var onlinePlayer in Server.GetOnlinePlayers()) { + onlinePlayer.SendMessage(ev.GetLeaveMessage()); + } + } - public static void EntityAttack(Player player, Entity entity) - { - foreach (var plugin in _plugins) - { - plugin.PluginInstance.OnEntityAttack(player, entity); + public static bool PlayerMove(Player player) { + + var oldPosition = player.Position; + var ev = new PlayerMoveEvent(player); + + var playerMoved = false; + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerMove(ev); + if (!ev.IsCancelled) { + continue; } + + if (playerMoved) { + continue; + } + + player.MoveTo(oldPosition); + playerMoved = true; } - public static void PlayerAttack(Player player, Player victim) - { - foreach (var plugin in _plugins) - { - plugin.PluginInstance.OnPlayerAttack(player, victim); + return !playerMoved; + } + + public static bool PlayerAttackedEntity(Player player, Entity entity) { + + var ev = new PlayerAttackedEntityEvent(player, entity); + var damageNotSent = false; + + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerAttackedEntity(ev); + if (!ev.IsCancelled) { + continue; } + + damageNotSent = true; } + + return !damageNotSent; } - public class PluginLoadContext : AssemblyLoadContext - { - private AssemblyDependencyResolver _resolver; + public static bool PlayerAttackedPlayer(Player attacker, Player victim) { + + var ev = new PlayerAttackedPlayerEvent(attacker, victim); + var damageNotSent = false; + + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerAttackedPlayer(ev); + if (!ev.IsCancelled) { + continue; + } + + damageNotSent = true; + } - public PluginLoadContext(string pluginPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(pluginPath); + return !damageNotSent; + } + + public static bool PlayerSentMessage(Player player, TextMessage textMessage) { + + var ev = new PlayerSentMessageEvent(player, textMessage); + var messageSent = false; + + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerSentMessage(ev); + if (!ev.IsCancelled) { + continue; + } + + messageSent = true; } - protected override Assembly? Load(AssemblyName assemblyName) - { - string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); - if (assemblyPath != null) - { - return LoadFromAssemblyPath(assemblyPath); + return !messageSent; + } + + public static bool PlayerSkinChanged(Player player, PlayerSkin playerSkin) { + + var ev = new PlayerSkinChangedEvent(player, playerSkin); + var skinChanged = false; + + foreach (var plugin in Plugins) { + + plugin.PluginInstance.OnPlayerSkinChanged(ev); + if (!ev.IsCancelled) { + continue; } - return null; + + skinChanged = true; + } + + return !skinChanged; + } + + public static bool PacketReceived(IPEndPoint ep, Packet packet) { + + foreach (var plugin in Plugins) { + return plugin.PluginInstance.OnPacketReceived(ep, packet); + } + + return true; + } + + public static bool PacketSent(IPEndPoint ep, Packet packet) { + + foreach (var plugin in Plugins) { + return plugin.PluginInstance.OnPacketSent(ep, packet); } + + return true; + } + + public static List GetLoadedPlugins() { + return Plugins; + } +} + +public class LoadedPlugin { + + public required Plugin PluginInstance { get; set; } + public required string Path { get; set; } + public required bool SourcePlugin { get; set; } + public required PluginLoadContext LoadContext { get; set; } +} + +public class PluginLoadContext : AssemblyLoadContext { + + private readonly AssemblyDependencyResolver _resolver; + private readonly string _sharedLibrariesPath; + private readonly string _globalSharedLibrariesPath; + + public PluginLoadContext(string pluginPath) : base(isCollectible: true) { + + _resolver = new AssemblyDependencyResolver(pluginPath); + + var pluginDir = Path.GetDirectoryName(pluginPath)!; + _sharedLibrariesPath = Path.Combine(Directory.GetParent(pluginDir)!.FullName, "SharedLibraries"); + _globalSharedLibrariesPath = Path.Combine(Directory.GetCurrentDirectory(), "Plugins", "SharedLibraries"); } - public class LoadedPlugin - { - public Plugin PluginInstance { get; set; } - public PluginLoadContext LoadContext { get; set; } - public string Path { get; set; } + protected override Assembly? Load(AssemblyName assemblyName) { + + var path = _resolver.ResolveAssemblyToPath(assemblyName); + if (path != null) { + return LoadFromAssemblyPath(path); + } + + var localLib = Path.Combine(_sharedLibrariesPath, $"{assemblyName.Name}.dll"); + if (File.Exists(localLib)) { + return LoadFromAssemblyPath(localLib); + } + + var globalLib = Path.Combine(_globalSharedLibrariesPath, $"{assemblyName.Name}.dll"); + return File.Exists(globalLib) ? LoadFromAssemblyPath(globalLib) : null; + } + + public void UnloadPlugin() { + Unload(); } } diff --git a/DaemonMC/Plugin/PluginResources.cs b/DaemonMC/Plugin/PluginResources.cs new file mode 100644 index 0000000..4945fc3 --- /dev/null +++ b/DaemonMC/Plugin/PluginResources.cs @@ -0,0 +1,16 @@ +namespace DaemonMC.Plugin; + +public class PluginResources(string path) { + + public void CreateResourcesFolder() { + + var resourcesPath = Path.Combine(path, "Resources"); + if (!Directory.Exists(resourcesPath)) { + Directory.CreateDirectory(resourcesPath); + } + } + + public string GetResourcesPath() { + return Path.Combine(path, "Resources"); + } +} \ No newline at end of file diff --git a/DaemonMC/Server.cs b/DaemonMC/Server.cs index 6d1a33b..27dc3a9 100644 --- a/DaemonMC/Server.cs +++ b/DaemonMC/Server.cs @@ -4,8 +4,8 @@ using DaemonMC.Network; using DaemonMC.Network.RakNet; using DaemonMC.Level; -using DaemonMC.Plugin.Plugin; using DaemonMC.Blocks; +using DaemonMC.Plugin; namespace DaemonMC { diff --git a/DaemonMC/Utils/TextFormat.cs b/DaemonMC/Utils/TextFormat.cs new file mode 100644 index 0000000..2329827 --- /dev/null +++ b/DaemonMC/Utils/TextFormat.cs @@ -0,0 +1,31 @@ +namespace DaemonMC.Utils; + + +public static class TextFormat { + + private const string Section = "§"; + + public static string Black => Section + "0"; + public static string DarkBlue => Section + "1"; + public static string DarkGreen => Section + "2"; + public static string DarkAqua => Section + "3"; + public static string DarkRed => Section + "4"; + public static string DarkPurple => Section + "5"; + public static string Gold => Section + "6"; + public static string Gray => Section + "7"; + public static string DarkGray => Section + "8"; + public static string Blue => Section + "9"; + public static string Green => Section + "a"; + public static string Aqua => Section + "b"; + public static string Red => Section + "c"; + public static string LightPurple => Section + "d"; + public static string Yellow => Section + "e"; + public static string White => Section + "f"; + + public static string MinecoinGold => Section + "g"; + + public static string Obfuscated => Section + "k"; + public static string Bold => Section + "l"; + public static string Italic => Section + "o"; + public static string Reset => Section + "r"; +} \ No newline at end of file