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