C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ShadowStatus", "Eclipsa", "1.2.2")]
[Description("Отправка FPS, нагрузки плагинов и ошибок на панель ShadowStatus")]
public class ShadowStatusPlugin : RustPlugin
{
private Configuration _config;
private Timer _reportTimer;
private DateTime _lastSend = DateTime.MinValue;
private string _serverId;
private readonly Queue<Dictionary<string, object>> _pendingErrors = new Queue<Dictionary<string, object>>();
private readonly Queue<Dictionary<string, object>> _pendingSlowHooks = new Queue<Dictionary<string, object>>();
private const int MaxQueue = 50;
private static readonly Regex SlowHookRegex = new Regex(
@"Calling\s+'([^']+)'\s+on\s+'([^']+)'\s+took\s+([\d.]+)ms",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private class Configuration
{
[JsonProperty("API URL")]
public string ApiUrl = "http://176.212.127.83:32457";
[JsonProperty("API Key")]
public string ApiKey = "shadowstatus-default-key";
[JsonProperty("Report interval (seconds)")]
public float ReportIntervalSeconds = 10f;
[JsonProperty("Server display name (empty = hostname)")]
public string ServerDisplayName = "";
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
LoadDefaultConfig();
}
catch
{
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
private void Init()
{
_serverId = $"{ConVar.Server.hostname}_{ConVar.Server.port}".GetHashCode().ToString("X8");
Application.logMessageReceived += OnUnityLog;
}
private void Unload()
{
Application.logMessageReceived -= OnUnityLog;
_reportTimer?.Destroy();
}
private void OnServerInitialized()
{
_reportTimer?.Destroy();
var interval = Mathf.Max(5f, _config.ReportIntervalSeconds);
_reportTimer = timer.Every(interval, SendReport);
Puts($"ShadowStatus → {_config.ApiUrl} (каждые {interval}s)");
}
private void OnUnityLog(string condition, string stackTrace, LogType type)
{
if (!string.IsNullOrEmpty(condition) && SlowHookRegex.IsMatch(condition))
{
var m = SlowHookRegex.Match(condition);
EnqueueSlowHook(m.Groups[2].Value, m.Groups[1].Value, float.Parse(m.Groups[3].Value));
}
if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert)
return;
EnqueueError(type.ToString(), condition, stackTrace);
}
private void EnqueueError(string level, string message, string stack)
{
if (string.IsNullOrEmpty(message))
return;
if (_pendingErrors.Count >= MaxQueue)
_pendingErrors.Dequeue();
_pendingErrors.Enqueue(new Dictionary<string, object>
{
["level"] = level,
["message"] = message.Length > 2000 ? message.Substring(0, 2000) : message,
["stack"] = stack ?? ""
});
}
private void EnqueueSlowHook(string plugin, string hook, float ms)
{
if (_pendingSlowHooks.Count >= MaxQueue)
_pendingSlowHooks.Dequeue();
_pendingSlowHooks.Enqueue(new Dictionary<string, object>
{
["plugin"] = plugin,
["hook"] = hook,
["ms"] = ms
});
}
private void SendReport()
{
var perf = Performance.current;
var report = Performance.report;
var metrics = new Dictionary<string, object>
{
["fps"] = report.frameRate,
["fps_avg"] = report.frameRateAverage,
["frame_time"] = perf.frameTime,
["frame_time_avg"] = perf.frameTimeAverage,
["memory_used"] = perf.memoryUsageSystem,
["gc_collections"] = perf.memoryCollections,
["entities"] = BaseNetworkable.serverEntities.Count,
["players"] = BasePlayer.activePlayerList.Count,
["sleepers"] = BasePlayer.sleepingPlayerList.Count,
["hook_time_total"] = GetTotalHookTimeMs(),
["tasks_load_balancer"] = perf.loadBalancerTasks,
["tasks_invoke_handler"] = perf.invokeHandlerTasks,
["network_in"] = GetNetworkIn(),
["network_out"] = GetNetworkOut(),
["packet_loss"] = GetPacketLoss()
};
var pluginList = new List<Dictionary<string, object>>();
foreach (var plugin in plugins.GetAll().OrderByDescending(p => p.TotalHookTime).Take(40))
{
if (plugin == null || plugin.IsCorePlugin || plugin.Name == Name)
continue;
pluginList.Add(new Dictionary<string, object>
{
["name"] = plugin.Name,
["version"] = plugin.Version.ToString(),
["hook_time_ms"] = Math.Round(plugin.TotalHookTime, 4),
["hook_memory_bytes"] = plugin.TotalHookMemory
});
}
var errors = DrainQueue(_pendingErrors);
var slowHooks = DrainQueue(_pendingSlowHooks);
var payload = new Dictionary<string, object>
{
["server_id"] = _serverId,
["server_name"] = GetServerDisplayName(),
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["metrics"] = metrics,
["plugins"] = pluginList,
["slow_hooks"] = slowHooks,
["errors"] = errors
};
var json = JsonConvert.SerializeObject(payload);
var headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json",
["X-Api-Key"] = _config.ApiKey
};
var url = _config.ApiUrl.TrimEnd('/') + "/api/ingest";
webrequest.Enqueue(url, json, (code, response) =>
{
if (code == 200)
_lastSend = DateTime.UtcNow;
else if (code != 0)
PrintWarning($"ShadowStatus ingest HTTP {code}: {response}");
}, this, Core.Libraries.RequestMethod.POST, headers, 15f);
}
private static List<Dictionary<string, object>> DrainQueue(Queue<Dictionary<string, object>> q)
{
var list = new List<Dictionary<string, object>>();
while (q.Count > 0)
list.Add(q.Dequeue());
return list;
}
private double GetTotalHookTimeMs()
{
double total = 0;
foreach (var p in plugins.GetAll())
{
if (p != null && !p.IsCorePlugin)
total += p.TotalHookTime;
}
return Math.Round(total, 2);
}
private string GetServerDisplayName()
{
if (!string.IsNullOrWhiteSpace(_config.ServerDisplayName))
return _config.ServerDisplayName;
return $"{ConVar.Server.hostname} :{ConVar.Server.port}";
}
// Безопасное получение сетевых метрик (без прямого доступа к Facepunch.Network)
private long GetNetworkIn()
{
try
{
// Попытка через рефлексию (если Net.sv существует)
var netSv = Type.GetType("Network.Net, Assembly-CSharp")?.GetProperty("sv")?.GetValue(null);
if (netSv != null)
{
var method = netSv.GetType().GetMethod("GetStat");
if (method != null)
{
var statType = Type.GetType("Network.StatTypeLong, Assembly-CSharp");
if (statType != null)
{
var enumValue = Enum.Parse(statType, "BytesReceived_LastSecond");
return (long)method.Invoke(netSv, new[] { enumValue });
}
}
}
}
catch { }
return 0;
}
private long GetNetworkOut()
{
try
{
var netSv = Type.GetType("Network.Net, Assembly-CSharp")?.GetProperty("sv")?.GetValue(null);
if (netSv != null)
{
var method = netSv.GetType().GetMethod("GetStat");
if (method != null)
{
var statType = Type.GetType("Network.StatTypeLong, Assembly-CSharp");
if (statType != null)
{
var enumValue = Enum.Parse(statType, "BytesSent_LastSecond");
return (long)method.Invoke(netSv, new[] { enumValue });
}
}
}
}
catch { }
return 0;
}
private long GetPacketLoss()
{
try
{
var netSv = Type.GetType("Network.Net, Assembly-CSharp")?.GetProperty("sv")?.GetValue(null);
if (netSv != null)
{
var method = netSv.GetType().GetMethod("GetStat");
if (method != null)
{
var statType = Type.GetType("Network.StatTypeLong, Assembly-CSharp");
if (statType != null)
{
var enumValue = Enum.Parse(statType, "PacketLossLastSecond");
return (long)method.Invoke(netSv, new[] { enumValue });
}
}
}
}
catch { }
return 0;
}
[ConsoleCommand("shadowstatus.status")]
private void CmdStatus(ConsoleSystem.Arg arg)
{
var ago = _lastSend == DateTime.MinValue
? "никогда"
: $"{(DateTime.UtcNow - _lastSend).TotalSeconds:F0}s назад";
arg.ReplyWith($"ShadowStatus API: {_config.ApiUrl} | последняя отправка: {ago} | id: {_serverId}");
}
}
}