diff --git a/samples/TpsPlugin/TpsPlugin.cs b/samples/TpsPlugin/TpsPlugin.cs new file mode 100644 index 00000000..824155b2 --- /dev/null +++ b/samples/TpsPlugin/TpsPlugin.cs @@ -0,0 +1,149 @@ +using Minecraft.Server.FourKit; +using Minecraft.Server.FourKit.Command; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Plugin; + +namespace TpsPlugin; + +/// +/// Minimal informational plugin. Exposes /tps (server tick rate) and /ping +/// (caller's connection latency). +/// +public class TpsPlugin : ServerPlugin +{ + public override string name => "TpsPlugin"; + public override string version => "1.0.0"; + public override string author => "LCE-Revelations"; + + public override void onEnable() + { + TpsProbe.Start(); + + var tps = FourKit.getCommand("tps"); + tps.setDescription("Show server TPS over 1s/5s/30s/60s windows."); + tps.setUsage("/tps"); + tps.setExecutor(new TpsExecutor()); + + var ping = FourKit.getCommand("ping"); + ping.setDescription("Show your connection ping in milliseconds."); + ping.setUsage("/ping"); + ping.setExecutor(new PingExecutor()); + } + + public override void onDisable() + { + TpsProbe.Stop(); + } +} + +internal static class TpsProbe +{ + private static readonly object _lock = new(); + private static readonly LinkedList<(double elapsed, int tick)> _samples = new(); + private static System.Threading.Timer? _timer; + private static System.Diagnostics.Stopwatch? _sw; + + public static void Start() + { + _sw = System.Diagnostics.Stopwatch.StartNew(); + Sample(); + _timer = new System.Threading.Timer(_ => Sample(), null, 1000, 1000); + } + + public static void Stop() + { + _timer?.Dispose(); + _timer = null; + _sw?.Stop(); + _sw = null; + lock (_lock) _samples.Clear(); + } + + private static void Sample() + { + var sw = _sw; + if (sw == null) return; + try + { + int tick = FourKit.getServerTick(); + double elapsed = sw.Elapsed.TotalSeconds; + lock (_lock) + { + _samples.AddLast((elapsed, tick)); + // 65s window so the 60s average has full data. + while (_samples.Count > 66) _samples.RemoveFirst(); + } + } + catch + { + // Probe is best-effort; never let sampling errors take down the host. + } + } + + public static (int samples, double tps1, double tps5, double tps30, double tps60) Read() + { + (double elapsed, int tick)[] arr; + lock (_lock) + { + if (_samples.Count < 2) return (_samples.Count, 0, 0, 0, 0); + arr = _samples.ToArray(); + } + + var last = arr[^1]; + + double Window(double seconds) + { + for (int j = arr.Length - 2; j >= 0; j--) + { + if (last.elapsed - arr[j].elapsed >= seconds) + { + var first = arr[j]; + double dt = last.elapsed - first.elapsed; + return dt > 0 ? (last.tick - first.tick) / dt : 0; + } + } + var oldest = arr[0]; + double dt0 = last.elapsed - oldest.elapsed; + return dt0 > 0 ? (last.tick - oldest.tick) / dt0 : 0; + } + + return (arr.Length, Window(1), Window(5), Window(30), Window(60)); + } +} + +internal sealed class TpsExecutor : CommandExecutor +{ + public bool onCommand(CommandSender sender, Command command, string label, string[] args) + { + var (samples, t1, t5, t30, t60) = TpsProbe.Read(); + if (samples < 2) + { + sender.sendMessage($"TPS probe warming up ({samples}/2 samples). Try again in a few seconds."); + return true; + } + int tick = FourKit.getServerTick(); + sender.sendMessage($"TPS 1s={t1:F2} 5s={t5:F2} 30s={t30:F2} 60s={t60:F2}"); + sender.sendMessage($"server tick={tick} samples={samples}"); + return true; + } +} + +internal sealed class PingExecutor : CommandExecutor +{ + public bool onCommand(CommandSender sender, Command command, string label, string[] args) + { + if (sender is not Player player) + { + sender.sendMessage("/ping must be run by a player."); + return true; + } + int ping = player.getPing(); + if (ping < 0) + { + sender.sendMessage("Ping unavailable."); + return true; + } + sender.sendMessage($"Your ping: {ping}ms"); + return true; + } +} diff --git a/samples/TpsPlugin/TpsPlugin.csproj b/samples/TpsPlugin/TpsPlugin.csproj new file mode 100644 index 00000000..3e53b85a --- /dev/null +++ b/samples/TpsPlugin/TpsPlugin.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + TpsPlugin + TpsPlugin + true + + + + + false + runtime + + +