diff --git a/samples/FourKitTestPlugin/FourKitTestPlugin.cs b/samples/FourKitTestPlugin/FourKitTestPlugin.cs
new file mode 100644
index 00000000..79d5257f
--- /dev/null
+++ b/samples/FourKitTestPlugin/FourKitTestPlugin.cs
@@ -0,0 +1,382 @@
+using Minecraft.Server.FourKit;
+using Minecraft.Server.FourKit.Command;
+using Minecraft.Server.FourKit.Enchantments;
+using Minecraft.Server.FourKit.Entity;
+using Minecraft.Server.FourKit.Event;
+using Minecraft.Server.FourKit.Event.World;
+using Minecraft.Server.FourKit.Inventory;
+using Minecraft.Server.FourKit.Inventory.Meta;
+using Minecraft.Server.FourKit.Plugin;
+
+namespace FourKitTestPlugin;
+
+///
+/// Exercises the new FourKit APIs added by the recent upstream sync:
+/// chunk additions, chunk events, ender chest inventory, and the
+/// disenchant fix on ItemStack.setDurability.
+///
+/// Use the /fktest command in-game (or from the console) to run
+/// each subtest. Run /fktest help for the full menu.
+///
+public class FourKitTestPlugin : ServerPlugin
+{
+ public override string name => "FourKitTestPlugin";
+ public override string version => "1.0.0";
+ public override string author => "LCE-Revelations";
+
+ private static int _chunkLoadCount;
+ private static int _chunkUnloadCount;
+
+ private static string? _logPath;
+ private static readonly object _logLock = new();
+
+ public override void onEnable()
+ {
+ _logPath = ResolveLogPath(serverDirectory, dataDirectory);
+ Log("FourKitTestPlugin enabled.");
+ Log($"Plugin log file: {_logPath}");
+ FourKit.addListener(new ChunkEventLogger());
+
+ var cmd = FourKit.getCommand("fktest");
+ cmd.setDescription("FourKit API smoke tests.");
+ cmd.setUsage("/fktest ");
+ cmd.setExecutor(new TestExecutor());
+ }
+
+ public override void onDisable()
+ {
+ Log("FourKitTestPlugin disabled.");
+ }
+
+ internal static int ChunkLoadCount => _chunkLoadCount;
+ internal static int ChunkUnloadCount => _chunkUnloadCount;
+ internal static void IncChunkLoad() => Interlocked.Increment(ref _chunkLoadCount);
+ internal static void IncChunkUnload() => Interlocked.Increment(ref _chunkUnloadCount);
+
+ ///
+ /// Writes a line both to the live server console and to a persistent log
+ /// file so test results are recoverable after the server window closes.
+ /// Tries server.log first using shared-write mode; falls back to a
+ /// sibling fkplugin.log in the server root if the C++ host has the
+ /// main log opened exclusively.
+ ///
+ internal static void Log(string message)
+ {
+ string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}][INFO][fkplugin] {message}";
+ Console.WriteLine(line);
+
+ if (_logPath == null) return;
+ try
+ {
+ lock (_logLock)
+ {
+ using var fs = new FileStream(_logPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
+ using var sw = new StreamWriter(fs);
+ sw.WriteLine(line);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[fkplugin] WARN: failed to append to {_logPath}: {ex.Message}");
+ }
+ }
+
+ private static string ResolveLogPath(string serverDir, string dataDir)
+ {
+ string serverLog = Path.Combine(serverDir, "server.log");
+ try
+ {
+ using var fs = new FileStream(serverLog, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
+ return serverLog;
+ }
+ catch (IOException)
+ {
+ string fallback = Path.Combine(serverDir, "fkplugin.log");
+ Console.WriteLine($"[fkplugin] server.log is locked; writing to {fallback} instead.");
+ return fallback;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ string fallback = Path.Combine(dataDir, "fkplugin.log");
+ Console.WriteLine($"[fkplugin] server.log not writable; writing to {fallback} instead.");
+ return fallback;
+ }
+ }
+}
+
+internal sealed class ChunkEventLogger : Listener
+{
+ [EventHandler(Priority = EventPriority.Monitor)]
+ public void onChunkLoad(ChunkLoadEvent e)
+ {
+ FourKitTestPlugin.IncChunkLoad();
+ var chunk = e.getChunk();
+ FourKitTestPlugin.Log($"ChunkLoadEvent dim={chunk.getWorld().getDimensionId()} ({chunk.getX()},{chunk.getZ()}) new={e.isNewChunk()}");
+ }
+
+ [EventHandler(Priority = EventPriority.Monitor)]
+ public void onChunkUnload(ChunkUnloadEvent e)
+ {
+ FourKitTestPlugin.IncChunkUnload();
+ var chunk = e.getChunk();
+ FourKitTestPlugin.Log($"ChunkUnloadEvent dim={chunk.getWorld().getDimensionId()} ({chunk.getX()},{chunk.getZ()})");
+ }
+}
+
+internal sealed class TestExecutor : CommandExecutor
+{
+ private static void Reply(CommandSender sender, string message)
+ {
+ sender.sendMessage(message);
+ FourKitTestPlugin.Log($"[/fktest] {message}");
+ }
+
+ public bool onCommand(CommandSender sender, Command command, string label, string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Reply(sender,"Usage: /fktest ");
+ return true;
+ }
+
+ try
+ {
+ switch (args[0].ToLowerInvariant())
+ {
+ case "help": SendHelp(sender); return true;
+ case "world": return TestWorld(sender);
+ case "chunks": return TestLoadedChunks(sender);
+ case "snapshot": return TestChunkSnapshot(sender, args);
+ case "entities": return TestChunkEntities(sender, args);
+ case "loadchunk": return TestLoadUnloadChunk(sender, args);
+ case "enderchest": return TestEnderChest(sender);
+ case "disenchant": return TestDisenchant(sender);
+ case "events":
+ Reply(sender,$"Chunk loads observed: {FourKitTestPlugin.ChunkLoadCount}");
+ Reply(sender,$"Chunk unloads observed: {FourKitTestPlugin.ChunkUnloadCount}");
+ return true;
+ default:
+ Reply(sender,$"Unknown subcommand '{args[0]}'. Try /fktest help");
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Reply(sender, $"Test threw: {ex.GetType().Name}: {ex.Message}");
+ FourKitTestPlugin.Log($"Exception: {ex}");
+ return true;
+ }
+ }
+
+ private static void SendHelp(CommandSender sender)
+ {
+ Reply(sender,"FourKit test commands:");
+ Reply(sender,"/fktest world - World/spawn/seed lookup");
+ Reply(sender,"/fktest chunks - List loaded chunks in your world");
+ Reply(sender,"/fktest snapshot - Snapshot of chunk under your feet");
+ Reply(sender,"/fktest entities - Entities in chunk under your feet");
+ Reply(sender,"/fktest loadchunk [dx dz] - Load/unload a chunk relative to you");
+ Reply(sender,"/fktest enderchest - Probe ender chest inventory");
+ Reply(sender,"/fktest disenchant - Verify setDurability preserves enchants");
+ Reply(sender,"/fktest events - Show observed chunk-event counters");
+ }
+
+ private static Player? RequirePlayer(CommandSender sender)
+ {
+ if (sender is Player p) return p;
+ Reply(sender,"This subcommand must be run by a player.");
+ return null;
+ }
+
+ private static bool TestWorld(CommandSender sender)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var loc = player.getLocation();
+ var world = loc?.getWorld();
+ if (world == null)
+ {
+ Reply(sender,"Could not resolve player world.");
+ return true;
+ }
+
+ var spawn = world.getSpawnLocation();
+ Reply(sender,$"World name={world.getName()} dim={world.getDimensionId()}");
+ Reply(sender,$"Spawn = ({spawn.getBlockX()},{spawn.getBlockY()},{spawn.getBlockZ()}) seed={world.getSeed()} time={world.getTime()}");
+ Reply(sender,$"Players in world: {world.getPlayers().Count}");
+ return true;
+ }
+
+ private static bool TestLoadedChunks(CommandSender sender)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var world = player.getLocation()?.getWorld();
+ if (world == null) { Reply(sender,"No world."); return true; }
+
+ var chunks = world.getLoadedChunks();
+ Reply(sender,$"Loaded chunks in {world.getName()}: {chunks.Length}");
+ int sample = Math.Min(5, chunks.Length);
+ for (int i = 0; i < sample; i++)
+ {
+ var c = chunks[i];
+ bool inUse = world.isChunkInUse(c.getX(), c.getZ());
+ Reply(sender,$" ({c.getX()},{c.getZ()}) loaded={c.isLoaded()} inUse={inUse}");
+ }
+ return true;
+ }
+
+ private static bool TestChunkSnapshot(CommandSender sender, string[] args)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var loc = player.getLocation();
+ var world = loc?.getWorld();
+ if (world == null || loc == null) { Reply(sender,"No world."); return true; }
+
+ bool includeBiome = args.Length > 1 && args[1].Equals("biome", StringComparison.OrdinalIgnoreCase);
+
+ var chunk = world.getChunkAt(loc);
+ var snap = chunk.getChunkSnapshot(includeBiome, includeBiome);
+
+ int blocksUnderFeet = 0;
+ int lx = loc.getBlockX() & 0xF;
+ int lz = loc.getBlockZ() & 0xF;
+ for (int ly = 0; ly < 128; ly++)
+ {
+ if (snap.getBlockTypeId(lx, ly, lz) != 0) blocksUnderFeet++;
+ }
+
+ Reply(sender,$"Snapshot of chunk ({chunk.getX()},{chunk.getZ()}) in '{snap.getWorldName()}' captured at tick {snap.getCaptureFullTime()}.");
+ Reply(sender,$"Non-air column at ({lx},{lz}): {blocksUnderFeet} blocks. Highest = y{snap.getHighestBlockYAt(lx, lz)}.");
+ if (includeBiome)
+ {
+ var biome = snap.getBiome(lx, lz);
+ Reply(sender,$"Biome at ({lx},{lz}) = {biome}, temp={snap.getRawBiomeTemperature(lx, lz):0.00}, rain={snap.getRawBiomeRainfall(lx, lz):0.00}");
+ }
+ return true;
+ }
+
+ private static bool TestChunkEntities(CommandSender sender, string[] args)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var loc = player.getLocation();
+ var world = loc?.getWorld();
+ if (world == null || loc == null) { Reply(sender,"No world."); return true; }
+
+ var chunk = world.getChunkAt(loc);
+ var entities = chunk.getEntities();
+ Reply(sender,$"Entities in chunk ({chunk.getX()},{chunk.getZ()}): {entities.Length}");
+ int sample = Math.Min(8, entities.Length);
+ for (int i = 0; i < sample; i++)
+ {
+ var ent = entities[i];
+ Reply(sender,$" id={ent.getEntityId()} type={ent.GetType()}");
+ }
+ return true;
+ }
+
+ private static bool TestLoadUnloadChunk(CommandSender sender, string[] args)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var loc = player.getLocation();
+ var world = loc?.getWorld();
+ if (world == null || loc == null) { Reply(sender,"No world."); return true; }
+
+ int dx = 0, dz = 0;
+ if (args.Length >= 3 && int.TryParse(args[1], out var px) && int.TryParse(args[2], out var pz))
+ {
+ dx = px; dz = pz;
+ }
+
+ int cx = (loc.getBlockX() >> 4) + dx;
+ int cz = (loc.getBlockZ() >> 4) + dz;
+
+ bool wasLoaded = world.isChunkLoaded(cx, cz);
+ Reply(sender,$"Chunk ({cx},{cz}) loaded? {wasLoaded}");
+
+ if (!wasLoaded)
+ {
+ bool ok = world.loadChunk(cx, cz, true);
+ Reply(sender,$"loadChunk -> {ok}; nowLoaded={world.isChunkLoaded(cx, cz)}");
+ }
+ else
+ {
+ bool inUse = world.isChunkInUse(cx, cz);
+ if (inUse)
+ {
+ Reply(sender,$"Chunk in use by a player; requesting unsafe unload would refuse. Trying unloadChunkRequest(safe=true).");
+ bool queued = world.unloadChunkRequest(cx, cz, true);
+ Reply(sender,$"unloadChunkRequest -> {queued}");
+ }
+ else
+ {
+ bool unloaded = world.unloadChunk(cx, cz, true, true);
+ Reply(sender,$"unloadChunk -> {unloaded}");
+ }
+ }
+ return true;
+ }
+
+ private static bool TestEnderChest(CommandSender sender)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var ender = player.getEnderChest();
+ Reply(sender,$"Ender chest size = {ender.getSize()} type={ender.getType()} name='{ender.getName()}'");
+
+ int filled = 0;
+ for (int i = 0; i < ender.getSize(); i++)
+ {
+ var item = ender.getItem(i);
+ if (item != null && item.getAmount() > 0)
+ {
+ filled++;
+ if (filled <= 4)
+ {
+ Reply(sender,$" slot {i}: {item.getType()} x{item.getAmount()} dur={item.getDurability()}");
+ }
+ }
+ }
+ Reply(sender,$"Non-empty slots: {filled}");
+ return true;
+ }
+
+ private static bool TestDisenchant(CommandSender sender)
+ {
+ var player = RequirePlayer(sender);
+ if (player == null) return true;
+
+ var pickaxe = new ItemStack(Material.DIAMOND_PICKAXE, 1, 0);
+ var meta = pickaxe.getItemMeta();
+ meta.addEnchant(EnchantmentType.DIG_SPEAD, 5, true);
+ meta.addEnchant(EnchantmentType.DURABILITY, 3, true);
+ pickaxe.setItemMeta(meta);
+
+ int beforeCount = pickaxe.getItemMeta().getEnchants().Count;
+ Reply(sender,$"Before setDurability: {beforeCount} enchants.");
+
+ pickaxe.setDurability(123);
+
+ var after = pickaxe.getItemMeta().getEnchants();
+ Reply(sender,$"After setDurability(123): {after.Count} enchants, durability={pickaxe.getDurability()}");
+ foreach (var kv in after)
+ {
+ Reply(sender,$" {kv.Key} lvl {kv.Value}");
+ }
+
+ bool ok = after.Count == beforeCount;
+ Reply(sender,ok
+ ? "PASS: setDurability preserved enchantments."
+ : "FAIL: setDurability dropped enchantments.");
+ return true;
+ }
+}
diff --git a/samples/FourKitTestPlugin/FourKitTestPlugin.csproj b/samples/FourKitTestPlugin/FourKitTestPlugin.csproj
new file mode 100644
index 00000000..e9d453ed
--- /dev/null
+++ b/samples/FourKitTestPlugin/FourKitTestPlugin.csproj
@@ -0,0 +1,17 @@
+
+
+ net10.0
+ enable
+ enable
+ FourKitTestPlugin
+ FourKitTestPlugin
+ true
+
+
+
+
+ false
+ runtime
+
+
+