mirror of
https://git.revela.dev/itsRevela/LCE-Revelations.git
synced 2026-05-21 19:24:55 +00:00
feat: add TpsPlugin sample with /tps and /ping
This commit is contained in:
149
samples/TpsPlugin/TpsPlugin.cs
Normal file
149
samples/TpsPlugin/TpsPlugin.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal informational plugin. Exposes /tps (server tick rate) and /ping
|
||||
/// (caller's connection latency).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
17
samples/TpsPlugin/TpsPlugin.csproj
Normal file
17
samples/TpsPlugin/TpsPlugin.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>TpsPlugin</RootNamespace>
|
||||
<AssemblyName>TpsPlugin</AssemblyName>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Minecraft.Server.FourKit\Minecraft.Server.FourKit.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user