Merge branch 'feature/plugin-api' of github.com:sylvessa/MinecraftConsoles into feature/plugin-api

This commit is contained in:
sylvessa 2026-05-06 17:37:05 -05:00
commit 8e66b2c19e
13 changed files with 570 additions and 5 deletions

View file

@ -5,6 +5,7 @@ using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Event;
using Minecraft.Server.FourKit.Inventory;
using Minecraft.Server.FourKit.Plugin;
using Minecraft.Server.FourKit.Scheduler;
/// <summary>
/// The main entry point for the FourKit plugin API.
@ -388,4 +389,6 @@ public static class FourKit
/// </summary>
/// <param name="plugin">Plugin to disable.</param>
public static void disablePlugin(ServerPlugin plugin) => FourKitHost.s_loader?.DisablePlugin(plugin);
public static FourKitScheduler getScheduler() => FourKitHost.getScheduler();
}

View file

@ -1,9 +1,24 @@
using Minecraft.Server.FourKit.Scheduler;
using System.Runtime.InteropServices;
namespace Minecraft.Server.FourKit;
public static partial class FourKitHost
{
[UnmanagedCallersOnly]
public static void SetSchedulerCallbacks(IntPtr add, IntPtr remove)
{
try
{
NativeBridge.SetSchedulerCallbacks(add, remove);
}
catch (Exception ex)
{
ServerLog.Error("fourkit", $"SetSchedulerCallbacks error: {ex}");
}
}
[UnmanagedCallersOnly]
public static void SetNativeCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity)
{

View file

@ -190,6 +190,19 @@ public static partial class FourKitHost
}
}
[UnmanagedCallersOnly]
public static void FireSchedulerCallback(int currentTick) {
try
{
FourKit.getScheduler().update(currentTick);
}
catch (Exception ex)
{
ServerLog.Error("fourkit", $"FireSchedulerCallback error: {ex}");
return;
}
}
[UnmanagedCallersOnly]
public static int FirePlayerMove(int entityId,
double fromX, double fromY, double fromZ,

View file

@ -1,13 +1,15 @@
using System.Runtime.InteropServices;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Entity;
using Minecraft.Server.FourKit.Event.Inventory;
using Minecraft.Server.FourKit.Inventory;
using Minecraft.Server.FourKit.Scheduler;
using System.Runtime.InteropServices;
namespace Minecraft.Server.FourKit;
public static partial class FourKitHost
{
internal static PluginLoader? s_loader;
internal static FourKitScheduler? s_scheduler;
public static IReadOnlyList<Plugin.ServerPlugin> getLoadedPlugins() => s_loader?.Plugins ?? [];
@ -16,6 +18,9 @@ public static partial class FourKitHost
{
try
{
ServerLog.Info("fourkit", "Initializing Scheduler...");
s_scheduler = new FourKitScheduler();
ServerLog.Info("fourkit", "Initializing plugin system...");
string pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
@ -47,6 +52,17 @@ public static partial class FourKitHost
}
}
internal static FourKitScheduler getScheduler()
{
if (s_scheduler == null)
{
ServerLog.Warn("fourkit", "Scheduler accessed before initialization. Initializing now...");
s_scheduler = new FourKitScheduler();
}
return s_scheduler;
}
private static Guid ParseOrHashGuid(string s)
{

View file

@ -236,6 +236,13 @@ internal static class NativeBridge
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetBiomeIdDelegate(int dimId, int x, int z, int biomeId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeAddSchedulerDelegate(int taskid, int startDelay, int runCooldown);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeRemoveSchedulerDelegate(int taskid);
internal static NativeDamageDelegate? DamagePlayer;
internal static NativeSetHealthDelegate? SetPlayerHealth;
internal static NativeTeleportDelegate? TeleportPlayer;
@ -315,6 +322,15 @@ internal static class NativeBridge
internal static NativeGetBiomeIdDelegate? GetBiomeId;
internal static NativeSetBiomeIdDelegate? SetBiomeId;
internal static NativeAddSchedulerDelegate? AddScheduler;
internal static NativeRemoveSchedulerDelegate? RemoveScheduler;
internal static void SetSchedulerCallbacks(IntPtr addScheduler, IntPtr removeScheduler)
{
AddScheduler = Marshal.GetDelegateForFunctionPointer<NativeAddSchedulerDelegate>(addScheduler);
RemoveScheduler = Marshal.GetDelegateForFunctionPointer<NativeRemoveSchedulerDelegate>(removeScheduler);
}
internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity)
{
DamagePlayer = Marshal.GetDelegateForFunctionPointer<NativeDamageDelegate>(damage);

View file

@ -0,0 +1,245 @@
using Minecraft.Server.FourKit.Plugin;
using System;
using System.Collections.Generic;
using System.Text;
namespace Minecraft.Server.FourKit.Scheduler
{
public class FourKitScheduler
{
private Dictionary<int, FourKitTask> _taskInstanceMap = new Dictionary<int, FourKitTask>();
private Dictionary<int, Action> _taskActionMap = new Dictionary<int, Action>();
//temp task based on about intmax / 3, leaves 715m (715827882) ids for real tasks and 1.4b(1431655765) for temp tasks
//this is done to avoid needing to cleanup existing task ids, could be handled
private int _nextTempTaskId = 715827882;
private int _nextTaskId = 0;
private int _lastTick = -1;
/// <summary>
/// Removes all tasks from the scheduler.
/// </summary>
public void cancelAllTasks()
{
foreach (var task in _taskInstanceMap.Values)
{
NativeBridge.RemoveScheduler?.Invoke(task.getTaskId());
}
_taskInstanceMap.Clear();
_taskActionMap.Clear();
_nextTempTaskId = 715827882;
_nextTaskId = 0;
}
/// <summary>
/// Removes task from scheduler.
/// </summary>
/// <param name="taskId">Id number of task to be removed.</param>
public void cancelTask(int taskId)
{
if (_taskInstanceMap.ContainsKey(taskId))
{
NativeBridge.RemoveScheduler?.Invoke(taskId);
_taskInstanceMap.Remove(taskId);
_taskActionMap.Remove(taskId);
}
}
/// <summary>
/// Removes all tasks associated with a particular plugin from the scheduler.
/// </summary>
/// <param name="plugin">Owner of tasks to be removed.</param>
public void cancelTasks(ServerPlugin plugin)
{
List<int> tasksToRemove = new List<int>();
foreach (var task in _taskInstanceMap.Values)
{
if (task.getOwner() == plugin)
{
NativeBridge.RemoveScheduler?.Invoke(task.getTaskId());
tasksToRemove.Add(task.getTaskId());
}
}
foreach (var taskId in tasksToRemove)
{
_taskInstanceMap.Remove(taskId);
_taskActionMap.Remove(taskId);
}
}
/// <summary>
/// Returns a list of all pending tasks. The ordering of the tasks is not related to their order of execution.
/// </summary>
/// <returns>Active workers.</returns>
public List<FourKitTask> getPendingTasks()
{
return new List<FourKitTask>(_taskInstanceMap.Values);
}
/// <summary>
/// Check if the task currently running.
///
/// A repeating task might not be running currently, but will be running in the future.
/// A task that has finished, and does not repeat, will not be running ever again.
///
/// Explicitly, a task is running if there exists a thread for it, and that thread is alive.
/// </summary>
/// <param name="taskId">The task to check.</param>
/// <returns>If the task is currently running.</returns>
public bool isCurrentlyRunning(int taskId)
{
if (_taskInstanceMap.ContainsKey(taskId))
{
return _taskInstanceMap[taskId].startedRunning;
}
return false;
}
/// <summary>
/// Check if the task queued to be run later.
///
/// If a repeating task is currently running, it might not be queued now but could be in the future.
/// A task that is not queued, and not running, will not be queued again.
/// </summary>
/// <param name="taskId">The task to check.</param>
/// <returns>If the task is queued to be run.</returns>
public bool isQueued(int taskId)
{
if (_taskInstanceMap.ContainsKey(taskId))
{
return !_taskInstanceMap[taskId].startedRunning;
}
return false;
}
/// <summary>
/// Returns a task that will run on the next server tick.
/// </summary>
/// <param name="plugin">The reference to the plugin scheduling task.</param>
/// <param name="task">The task to be run.</param>
/// <returns>A task that contains the id number.</returns>
/// <exception cref="ArgumentNullException">If plugin is null.</exception>
/// <exception cref="ArgumentNullException">If task is null.</exception>
public FourKitTask runTask(ServerPlugin plugin, Action task)
{
FourKitTask fourKitTask = new FourKitTask(plugin, _nextTempTaskId++, 0, -1);
startTask(fourKitTask, task);
return fourKitTask;
}
/// <summary>
/// Returns a task that will run after the specified number of server ticks.
/// </summary>
/// <param name="plugin">The reference to the plugin scheduling task.</param>
/// <param name="task">The task to be run.</param>
/// <param name="delay">The ticks to wait before running the task.</param>
/// <returns>A task that contains the id number.</returns>
/// <exception cref="ArgumentNullException">If plugin is null.</exception>
/// <exception cref="ArgumentNullException">If task is null.</exception>
public FourKitTask runTaskLater(ServerPlugin plugin, Action task, int delay)
{
FourKitTask fourKitTask = new FourKitTask(plugin, _nextTempTaskId++, delay, -1);
startTask(fourKitTask, task);
return fourKitTask;
}
/// <summary>
/// Returns a task that will repeatedly run until cancelled, starting after the specified number of server ticks.
/// </summary>
/// <param name="plugin">The reference to the plugin scheduling task.</param>
/// <param name="task">The task to be run.</param>
/// <param name="delay">The ticks to wait before running the task.</param>
/// <param name="period">The ticks to wait between runs.</param>
/// <returns>A task that contains the id number.</returns>
/// <exception cref="ArgumentNullException">If plugin is null.</exception>
/// <exception cref="ArgumentNullException">If task is null.</exception>
public FourKitTask runTaskTimer(ServerPlugin plugin, Action task, int delay, int period)
{
FourKitTask fourKitTask = new FourKitTask(plugin, _nextTempTaskId++, delay, period);
startTask(fourKitTask, task);
return fourKitTask;
}
/// <summary>
/// Starts tracking a task instance and registers it with the native scheduler bridge.
/// </summary>
/// <param name="task">The task instance to schedule.</param>
/// <param name="action">The callback action executed when the task runs.</param>
internal void startTask(FourKitTask task, Action action)
{
task.startedRunning = true;
_taskInstanceMap[task.getTaskId()] = task;
_taskActionMap[task.getTaskId()] = action;
NativeBridge.AddScheduler?.Invoke(task.getTaskId(), task.startDelay, task.runCooldown);
}
/// <summary>
/// Updates scheduled tasks for the current server tick and runs due task callbacks.
/// </summary>
/// <param name="currentTick">The current server tick.</param>
internal void update(int currentTick)
{
if (_lastTick == -1) _lastTick = currentTick;
List<int> tasksToRemove = new List<int>();
foreach (var task in _taskInstanceMap.Values)
{
if (!task.shouldRun)
{
tasksToRemove.Add(task.getTaskId());
continue;
}
if (task.startDelay > 0)
{
task.startDelay -= (currentTick - _lastTick);
if (task.startDelay <= 0)
{
task.startDelay = 0;
_taskActionMap[task.getTaskId()]?.Invoke();
}
continue;
}
if (task.runCooldown == -1)
{
task.lastRunTick = currentTick;
_taskActionMap[task.getTaskId()]?.Invoke();
tasksToRemove.Add(task.getTaskId());
}
else
{
int lastTaskTick = task.lastRunTick;
if (lastTaskTick == -1 || (lastTaskTick + task.runCooldown) <= currentTick)
{
task.lastRunTick = currentTick;
_taskActionMap[task.getTaskId()]?.Invoke();
}
}
}
foreach (var taskId in tasksToRemove)
{
_taskInstanceMap.Remove(taskId);
_taskActionMap.Remove(taskId);
}
}
}
}

View file

@ -0,0 +1,52 @@
using Minecraft.Server.FourKit.Plugin;
using System;
using System.Collections.Generic;
using System.Text;
namespace Minecraft.Server.FourKit.Scheduler
{
public class FourKitTask
{
private ServerPlugin owner;
private int taskId;
internal bool shouldRun = true;
internal int startDelay;
internal int runCooldown;
internal int lastRunTick = -1;
internal bool startedRunning = false;
/// <summary>
/// Initializes a new task instance with owner, task id, start delay, and run cooldown.
/// </summary>
/// <param name="owner">The plugin that owns this task.</param>
/// <param name="taskId">Task id number.</param>
/// <param name="startDelay">Delay in server ticks before executing the task.</param>
/// <param name="runCooldown">Period in server ticks between runs, or -1 for one-shot tasks.</param>
internal FourKitTask(ServerPlugin owner, int taskId, int startDelay, int runCooldown)
{
this.owner = owner;
this.taskId = taskId;
this.startDelay = startDelay;
this.runCooldown = runCooldown;
}
/// <summary>
/// Will attempt to cancel this task.
/// </summary>
public void cancel() { shouldRun = false; }
/// <summary>
/// Returns the Plugin that owns this task.
/// </summary>
/// <returns>The Plugin that owns the task.</returns>
public ServerPlugin getOwner() { return owner; }
/// <summary>
/// Returns the taskId for the task.
/// </summary>
/// <returns>Task id number.</returns>
public int getTaskId() { return taskId; }
}
}

View file

@ -0,0 +1,98 @@
@page scheduler-programming Scheduler Programming
@section introduction Introduction
Usually when we code in FourKit, everything is ran linearly. Despite this you may wish to schedule some code to be executed at a later point of time.
Do NOT use the built in C# Thread system, it is not safe for FourKit.
@section getting_started Getting Started
To get started we either need the FourKitScheduler instance. You can get this from the server instance.
```csharp
FourKitScheduler scheduler = FourKit.getScheduler();
```
When scheduling a task, you will also need to pass your main plugin class instance. Here is an example of how it can be done:
```csharp
public class ExampleOne
{
// you can also use a getter
public static ExampleOne Instance { get; private set; }
public void OnEnable()
{
Instance = this;
}
}
// then
public class Other
{
private readonly ExampleOne plugin = ExampleOne.Instance;
}
```
@section scheduling_delayed_task Scheduling a Delayed Task
The scheduling itself is based on ticks, the Minecraft time unit. 1 tick = 0.05 seconds or 1 second = 20 ticks.
Let's say you want to schedule a task to run 30 seconds later which broadcasts a message:
```csharp
FourKitScheduler scheduler = FourKit.getScheduler();
scheduler.runTaskLater(plugin, () => {
FourKit.broadcastMessage("Mooooo!");
}, 20 * 30 /*<-- the delay */);
```
@section scheduling_repeating_task Scheduling a Repeating Task
Repeating tasks are tasks that can reschedule themselves.
Let's say you want to schedule a task to run 10 seconds later then after that it should repeat itself a finite amount of times with an interval of 5 seconds between each consecutive run:
```csharp
FourKitScheduler scheduler = FourKit.getScheduler();
scheduler.runTaskTimer(plugin, () => {
FourKit.broadcastMessage("Mooooo!");
}, 20 * 10 /*<-- the initial delay */, 20 * 5 /*<-- the interval */);
```
@section run_task_next_tick Running a Task on the Next Tick
Sometimes we just want to run some code on the next tick:
```csharp
FourKitScheduler scheduler = FourKit.getScheduler();
scheduler.runTask(plugin, () => {
FourKit.broadcastMessage("Mooooo again!");
});
```
@section canceling_tasks Canceling Tasks
Sometimes we want to just cancel a task!
```csharp
FourKitScheduler scheduler = FourKit.getScheduler();
// Cancel outside
FourKitTask task = scheduler.runTaskLater(plugin, () => {
FourKit.broadcastMessage("Mooooo again!");
}, 20 * 60);
// Cancel inside
scheduler.runTaskTimer(plugin, () => {
if (FourKit.getOnlinePlayers().Count == 0) {
task.cancel(); // <--
return;
}
FourKit.broadcastMessage("Mooooo again!");
}, 0, 20 * 60);
// then
task.cancel(); // <--
```