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

@ -63,6 +63,10 @@
#include "Durango\Network\NetworkPlayerDurango.h"
#endif
#if defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\FourKitNatives.h"
#endif
#define DEBUG_SERVER_DONT_SPAWN_MOBS 0
//4J Added
@ -2208,6 +2212,12 @@ void MinecraftServer::tick()
connection->tick();
PIXEndNamedEvent();
#if defined(MINECRAFT_SERVER_BUILD)
PIXBeginNamedEvent(0, "FourKit Scheduler tick");
FourKitBridge::ServerTickCallback(tickCount);
PIXEndNamedEvent();
#endif
// 4J - removed
#if 0
for (size_t i = 0; i < tickables.size(); i++) {

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(); // <--
```

View file

@ -30,6 +30,8 @@ typedef int(__stdcall *fn_fire_player_move)(int entityId,
double fromX, double fromY, double fromZ,
double toX, double toY, double toZ,
double *outCoords);
typedef void(__stdcall* fn_set_scheduler_callbacks)(void* add, void* remove);
typedef void(__stdcall* fn_fire_scheduler_callback)(int currentTick);
typedef void(__stdcall *fn_set_native_callbacks)(void *damage, void *setHealth, void *teleport, void *setGameMode, void *broadcastMessage, void *setFallDistance, void *getPlayerSnapshot, void *sendMessage, void *setWalkSpeed, void *teleportEntity);
typedef void(__stdcall *fn_set_world_callbacks)(void *getTileId, void *getTileData, void *setTile, void *setTileData, void *breakBlock, void *getHighestBlockY, void *getWorldInfo, void *setWorldTime, void *setWeather, void *createExplosion, void *strikeLightning, void *setSpawnLocation, void *dropItem);
typedef void(__stdcall *fn_update_entity_id)(int oldEntityId, int newEntityId);
@ -128,6 +130,8 @@ static fn_update_entity_id s_managedUpdateEntityId = nullptr;
static fn_fire_player_quit s_managedFireQuit = nullptr;
static fn_fire_player_kick s_managedFireKick = nullptr;
static fn_fire_player_move s_managedFireMove = nullptr;
static fn_set_scheduler_callbacks s_managedSetSchedulerCallbacks = nullptr;
static fn_fire_scheduler_callback s_managedFireSchedulerCallback = nullptr;
static fn_set_native_callbacks s_managedSetCallbacks = nullptr;
static fn_set_world_callbacks s_managedSetWorldCallbacks = nullptr;
static fn_fire_structure_grow s_managedFireStructureGrow = nullptr;
@ -211,6 +215,8 @@ void Initialize()
{L"FirePlayerQuit", (void **)&s_managedFireQuit},
{L"FirePlayerKick", (void **)&s_managedFireKick},
{L"FirePlayerMove", (void **)&s_managedFireMove},
{L"SetSchedulerCallbacks", (void**)&s_managedSetSchedulerCallbacks},
{L"FireSchedulerCallback", (void**)&s_managedFireSchedulerCallback},
{L"SetNativeCallbacks", (void **)&s_managedSetCallbacks},
{L"SetWorldCallbacks", (void **)&s_managedSetWorldCallbacks},
{L"UpdatePlayerEntityId", (void **)&s_managedUpdateEntityId},
@ -270,9 +276,11 @@ void Initialize()
return;
}
s_initialized = true;
s_managedInit();
s_managedSetSchedulerCallbacks(
(void*)&NativeAddScheduler,
(void*)&NativeRemoveScheduler
);
s_managedSetCallbacks(
(void *)&NativeDamagePlayer,
@ -375,6 +383,10 @@ void Initialize()
(void *)&NativeGetWorldEntities,
(void *)&NativeGetChunkEntities);
//we should init after setting callbacks so we have access inside the plugins on enable
s_initialized = true;
s_managedInit();
LogInfo("fourkit", "FourKit initialized successfully.");
}
@ -515,6 +527,16 @@ void UpdatePlayerEntityId(int oldEntityId, int newEntityId)
LogDebugf("fourkit", "UpdatePlayerEntityId: %d -> %d", oldEntityId, newEntityId);
}
void FireSchedulerCallback(int currentTick)
{
if (!s_initialized || !s_managedFireSchedulerCallback)
{
return;
}
s_managedFireSchedulerCallback(currentTick);
}
bool FirePlayerMove(int entityId,
double fromX, double fromY, double fromZ,
double toX, double toY, double toZ,

View file

@ -12,6 +12,7 @@ namespace FourKitBridge
bool FirePlayerQuit(int entityId);
bool FirePlayerKick(int entityId, int disconnectReason,
std::wstring &outLeaveMessage);
void FireSchedulerCallback(int currentTick);
bool FirePlayerMove(int entityId,
double fromX, double fromY, double fromZ,
double toX, double toY, double toZ,

View file

@ -47,6 +47,7 @@
#include "Common\NetworkUtils.h"
#include "ServerLogManager.h"
#include "../Minecraft.World/ItemInstance.cpp"
#include <mutex>
namespace
{
@ -104,12 +105,79 @@ class VirtualContainer : public SimpleContainer
return m_containerType;
}
};
class NativeFourKitTask;
static int64_t STATIC_lastTick = -1;
static std::unordered_map<int, std::shared_ptr<NativeFourKitTask>> _taskCache;
static std::mutex _taskMutex;
class NativeFourKitTask {
public:
int startDelay;
int runCooldown;
int lastRunTick;
NativeFourKitTask(int _startDelay, int _runCooldown) : startDelay(_startDelay), runCooldown(_runCooldown), lastRunTick(-1) {};
};
}
namespace FourKitBridge
{
void __cdecl NativeDamagePlayer(int entityId, float amount)
void __cdecl NativeAddScheduler(int taskid, int startDelay, int runCooldown)
{
std::lock_guard<std::mutex> g(_taskMutex);
_taskCache.emplace(taskid, std::make_shared<NativeFourKitTask>(startDelay, runCooldown));
}
void __cdecl NativeRemoveScheduler(int taskid)
{
std::lock_guard<std::mutex> g(_taskMutex);
auto it = _taskCache.find(taskid);
if (it != _taskCache.end()) {
_taskCache.erase(it);
}
}
void ServerTickCallback(int currentTick)
{
bool callManagedFunction = false;
if (STATIC_lastTick == -1) {
callManagedFunction = true;
STATIC_lastTick = currentTick;
}
{
std::lock_guard<std::mutex> g(_taskMutex);
for (const auto& [taskid, task] : _taskCache)
{
NativeFourKitTask* taskPointer = task.get();
if (taskPointer->startDelay > 0) {
taskPointer->startDelay -= (currentTick - STATIC_lastTick);
if (taskPointer->startDelay <= 0) {
callManagedFunction = true; //make c# update the tasks so its not queued anymore but now its running
taskPointer->startDelay = 0; //ensure it stays 0
}
continue;
}
int lastTaskTick = taskPointer->lastRunTick;
if (lastTaskTick == -1 || (lastTaskTick + taskPointer->runCooldown) <= currentTick) {
callManagedFunction = true;
taskPointer->lastRunTick = currentTick;
}
}
}
if (callManagedFunction) FireSchedulerCallback(currentTick);
STATIC_lastTick = currentTick;
}
void __cdecl NativeDamagePlayer(int entityId, float amount)
{
auto player = FindPlayer(entityId);
if (player)

View file

@ -3,6 +3,12 @@
namespace FourKitBridge
{
//scheduler
void __cdecl NativeAddScheduler(int taskid, int startDelay, int runCooldown);
void __cdecl NativeRemoveScheduler(int taskid);
void ServerTickCallback(int currentTick);
// core
void __cdecl NativeDamagePlayer(int entityId, float amount);
void __cdecl NativeSetPlayerHealth(int entityId, float health);