mirror of
https://github.com/2dust/v2rayN.git
synced 2025-07-02 21:12:09 +00:00
Add speed display for sing-box , using clash api
This commit is contained in:
parent
db1b3fdaad
commit
1dce0f0366
6 changed files with 335 additions and 148 deletions
|
@ -52,7 +52,7 @@ namespace v2rayN.Handler
|
||||||
|
|
||||||
dns(node, singboxConfig);
|
dns(node, singboxConfig);
|
||||||
|
|
||||||
//statistic(singboxConfig);
|
statistic(singboxConfig);
|
||||||
|
|
||||||
msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||||
}
|
}
|
||||||
|
@ -695,13 +695,18 @@ namespace v2rayN.Handler
|
||||||
{
|
{
|
||||||
singboxConfig.experimental = new Experimental4Sbox()
|
singboxConfig.experimental = new Experimental4Sbox()
|
||||||
{
|
{
|
||||||
v2ray_api = new V2ray_Api4Sbox()
|
//v2ray_api = new V2ray_Api4Sbox()
|
||||||
|
//{
|
||||||
|
// listen = $"{Global.Loopback}:{Global.statePort}",
|
||||||
|
// stats = new Stats4Sbox()
|
||||||
|
// {
|
||||||
|
// enabled = true,
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
clash_api = new Clash_Api4Sbox()
|
||||||
{
|
{
|
||||||
listen = $"{Global.Loopback}:{Global.statePort}",
|
external_controller = $"{Global.Loopback}:{Global.statePort}",
|
||||||
stats = new Stats4Sbox()
|
store_selected = true
|
||||||
{
|
|
||||||
enabled = true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
using Grpc.Core;
|
using System.Net;
|
||||||
using Grpc.Net.Client;
|
|
||||||
using ProtosLib.Statistics;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using v2rayN.Base;
|
using v2rayN.Base;
|
||||||
using v2rayN.Mode;
|
using v2rayN.Mode;
|
||||||
|
@ -10,51 +7,40 @@ namespace v2rayN.Handler
|
||||||
{
|
{
|
||||||
internal class StatisticsHandler
|
internal class StatisticsHandler
|
||||||
{
|
{
|
||||||
private Mode.Config config_;
|
private Config _config;
|
||||||
private GrpcChannel _channel;
|
|
||||||
private StatsService.StatsServiceClient _client;
|
|
||||||
private bool _exitFlag;
|
|
||||||
private ServerStatItem? _serverStatItem;
|
private ServerStatItem? _serverStatItem;
|
||||||
private List<ServerStatItem> _lstServerStat;
|
private List<ServerStatItem> _lstServerStat;
|
||||||
public List<ServerStatItem> ServerStat => _lstServerStat;
|
|
||||||
|
|
||||||
private Action<ServerSpeedItem> _updateFunc;
|
private Action<ServerSpeedItem> _updateFunc;
|
||||||
|
private StatisticsV2ray? _statisticsV2Ray;
|
||||||
|
private StatisticsSingbox? _statisticsSingbox;
|
||||||
|
|
||||||
public bool Enable
|
public List<ServerStatItem> ServerStat => _lstServerStat;
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
|
||||||
|
public StatisticsHandler(Config config, Action<ServerSpeedItem> update)
|
||||||
{
|
{
|
||||||
get; set;
|
_config = config;
|
||||||
|
Enable = config.guiItem.enableStatistics;
|
||||||
|
if (!Enable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StatisticsHandler(Mode.Config config, Action<ServerSpeedItem> update)
|
|
||||||
{
|
|
||||||
config_ = config;
|
|
||||||
Enable = config.guiItem.enableStatistics;
|
|
||||||
_updateFunc = update;
|
_updateFunc = update;
|
||||||
_exitFlag = false;
|
|
||||||
|
|
||||||
Init();
|
Init();
|
||||||
GrpcInit();
|
|
||||||
|
|
||||||
Task.Run(Run);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GrpcInit()
|
|
||||||
{
|
|
||||||
if (_channel == null)
|
|
||||||
{
|
|
||||||
Global.statePort = GetFreePort();
|
Global.statePort = GetFreePort();
|
||||||
|
|
||||||
_channel = GrpcChannel.ForAddress($"{Global.httpProtocol}{Global.Loopback}:{Global.statePort}");
|
_statisticsV2Ray = new StatisticsV2ray(config, UpdateServerStat);
|
||||||
_client = new StatsService.StatsServiceClient(_channel);
|
_statisticsSingbox = new StatisticsSingbox(config, UpdateServerStat);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_exitFlag = true;
|
_statisticsV2Ray?.Close();
|
||||||
//channel_.ShutdownAsync();
|
_statisticsSingbox?.Close();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -62,57 +48,6 @@ namespace v2rayN.Handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Run()
|
|
||||||
{
|
|
||||||
while (!_exitFlag)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Enable && _channel.State == ConnectivityState.Ready)
|
|
||||||
{
|
|
||||||
QueryStatsResponse? res = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
res = await _client.QueryStatsAsync(new QueryStatsRequest() { Pattern = "", Reset = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
//Utils.SaveLog(ex.Message, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res != null)
|
|
||||||
{
|
|
||||||
GetServerStatItem(config_.indexId);
|
|
||||||
ParseOutput(res.Stat, out ServerSpeedItem server);
|
|
||||||
|
|
||||||
if (server.proxyUp != 0 || server.proxyDown != 0)
|
|
||||||
{
|
|
||||||
_serverStatItem.todayUp += server.proxyUp;
|
|
||||||
_serverStatItem.todayDown += server.proxyDown;
|
|
||||||
_serverStatItem.totalUp += server.proxyUp;
|
|
||||||
_serverStatItem.totalDown += server.proxyDown;
|
|
||||||
}
|
|
||||||
if (Global.ShowInTaskbar)
|
|
||||||
{
|
|
||||||
server.indexId = config_.indexId;
|
|
||||||
server.todayUp = _serverStatItem.todayUp;
|
|
||||||
server.todayDown = _serverStatItem.todayDown;
|
|
||||||
server.totalUp = _serverStatItem.totalUp;
|
|
||||||
server.totalDown = _serverStatItem.totalDown;
|
|
||||||
_updateFunc(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sleep = config_.guiItem.statisticsFreshRate < 1 ? 1 : config_.guiItem.statisticsFreshRate;
|
|
||||||
Thread.Sleep(1000 * sleep);
|
|
||||||
await _channel.ConnectAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearAllServerStatistics()
|
public void ClearAllServerStatistics()
|
||||||
{
|
{
|
||||||
SqliteHelper.Instance.Execute($"delete from ServerStatItem ");
|
SqliteHelper.Instance.Execute($"delete from ServerStatItem ");
|
||||||
|
@ -142,6 +77,28 @@ namespace v2rayN.Handler
|
||||||
_lstServerStat = SqliteHelper.Instance.Table<ServerStatItem>().ToList();
|
_lstServerStat = SqliteHelper.Instance.Table<ServerStatItem>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateServerStat(ServerSpeedItem server)
|
||||||
|
{
|
||||||
|
GetServerStatItem(_config.indexId);
|
||||||
|
|
||||||
|
if (server.proxyUp != 0 || server.proxyDown != 0)
|
||||||
|
{
|
||||||
|
_serverStatItem.todayUp += server.proxyUp;
|
||||||
|
_serverStatItem.todayDown += server.proxyDown;
|
||||||
|
_serverStatItem.totalUp += server.proxyUp;
|
||||||
|
_serverStatItem.totalDown += server.proxyDown;
|
||||||
|
}
|
||||||
|
if (Global.ShowInTaskbar)
|
||||||
|
{
|
||||||
|
server.indexId = _config.indexId;
|
||||||
|
server.todayUp = _serverStatItem.todayUp;
|
||||||
|
server.todayDown = _serverStatItem.todayDown;
|
||||||
|
server.totalUp = _serverStatItem.totalUp;
|
||||||
|
server.totalDown = _serverStatItem.totalDown;
|
||||||
|
_updateFunc(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void GetServerStatItem(string indexId)
|
private void GetServerStatItem(string indexId)
|
||||||
{
|
{
|
||||||
long ticks = DateTime.Now.Date.Ticks;
|
long ticks = DateTime.Now.Date.Ticks;
|
||||||
|
@ -177,71 +134,28 @@ namespace v2rayN.Handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseOutput(Google.Protobuf.Collections.RepeatedField<Stat> source, out ServerSpeedItem server)
|
|
||||||
{
|
|
||||||
server = new();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (Stat stat in source)
|
|
||||||
{
|
|
||||||
string name = stat.Name;
|
|
||||||
long value = stat.Value / 1024; //KByte
|
|
||||||
string[] nStr = name.Split(">>>".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
string type = "";
|
|
||||||
|
|
||||||
name = name.Trim();
|
|
||||||
|
|
||||||
name = nStr[1];
|
|
||||||
type = nStr[3];
|
|
||||||
|
|
||||||
if (name == Global.agentTag)
|
|
||||||
{
|
|
||||||
if (type == "uplink")
|
|
||||||
{
|
|
||||||
server.proxyUp = value;
|
|
||||||
}
|
|
||||||
else if (type == "downlink")
|
|
||||||
{
|
|
||||||
server.proxyDown = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (name == Global.directTag)
|
|
||||||
{
|
|
||||||
if (type == "uplink")
|
|
||||||
{
|
|
||||||
server.directUp = value;
|
|
||||||
}
|
|
||||||
else if (type == "downlink")
|
|
||||||
{
|
|
||||||
server.directDown = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
//Utils.SaveLog(ex.Message, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetFreePort()
|
private int GetFreePort()
|
||||||
{
|
{
|
||||||
int defaultPort = 28123;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// TCP stack please do me a favor
|
int defaultPort = 9090;
|
||||||
|
if (!Utils.PortInUse(defaultPort))
|
||||||
|
{
|
||||||
|
return defaultPort;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
TcpListener l = new(IPAddress.Loopback, 0);
|
TcpListener l = new(IPAddress.Loopback, 0);
|
||||||
l.Start();
|
l.Start();
|
||||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||||
l.Stop();
|
l.Stop();
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
|
catch
|
||||||
{
|
{
|
||||||
// in case access denied
|
}
|
||||||
Utils.SaveLog(ex.Message, ex);
|
return 69090;
|
||||||
return defaultPort;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
127
v2rayN/v2rayN/Handler/StatisticsSingbox.cs
Normal file
127
v2rayN/v2rayN/Handler/StatisticsSingbox.cs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using v2rayN.Mode;
|
||||||
|
|
||||||
|
namespace v2rayN.Handler
|
||||||
|
{
|
||||||
|
internal class StatisticsSingbox
|
||||||
|
{
|
||||||
|
private Config _config;
|
||||||
|
private bool _exitFlag;
|
||||||
|
private ClientWebSocket? webSocket;
|
||||||
|
private string url = string.Empty;
|
||||||
|
private Action<ServerSpeedItem> _updateFunc;
|
||||||
|
|
||||||
|
public StatisticsSingbox(Config config, Action<ServerSpeedItem> update)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_updateFunc = update;
|
||||||
|
_exitFlag = false;
|
||||||
|
|
||||||
|
Task.Run(() => Run());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Init()
|
||||||
|
{
|
||||||
|
Thread.Sleep(5000);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
url = $"ws://{Global.Loopback}:{Global.statePort}/traffic";
|
||||||
|
|
||||||
|
if (webSocket == null)
|
||||||
|
{
|
||||||
|
webSocket = new ClientWebSocket();
|
||||||
|
await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_exitFlag = true;
|
||||||
|
if (webSocket != null)
|
||||||
|
{
|
||||||
|
webSocket.Abort();
|
||||||
|
webSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Utils.SaveLog(ex.Message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Run()
|
||||||
|
{
|
||||||
|
Init();
|
||||||
|
|
||||||
|
while (!_exitFlag)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (webSocket != null)
|
||||||
|
{
|
||||||
|
if (webSocket.State == WebSocketState.Aborted
|
||||||
|
|| webSocket.State == WebSocketState.Closed)
|
||||||
|
{
|
||||||
|
webSocket.Abort();
|
||||||
|
webSocket = null;
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webSocket.State != WebSocketState.Open)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[1024];
|
||||||
|
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
while (!res.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
var result = Encoding.UTF8.GetString(buffer, 0, res.Count);
|
||||||
|
if (!string.IsNullOrEmpty(result))
|
||||||
|
{
|
||||||
|
ParseOutput(result, out ulong up, out ulong down);
|
||||||
|
|
||||||
|
_updateFunc(new ServerSpeedItem()
|
||||||
|
{
|
||||||
|
proxyUp = (long)(up / 1000),
|
||||||
|
proxyDown = (long)(down / 1000)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseOutput(string source, out ulong up, out ulong down)
|
||||||
|
{
|
||||||
|
up = 0; down = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var trafficItem = Utils.FromJson<TrafficItem>(source);
|
||||||
|
if (trafficItem != null)
|
||||||
|
{
|
||||||
|
up = trafficItem.up;
|
||||||
|
down = trafficItem.down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
120
v2rayN/v2rayN/Handler/StatisticsV2ray.cs
Normal file
120
v2rayN/v2rayN/Handler/StatisticsV2ray.cs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
using Grpc.Core;
|
||||||
|
using Grpc.Net.Client;
|
||||||
|
using ProtosLib.Statistics;
|
||||||
|
using v2rayN.Mode;
|
||||||
|
|
||||||
|
namespace v2rayN.Handler
|
||||||
|
{
|
||||||
|
internal class StatisticsV2ray
|
||||||
|
{
|
||||||
|
private Mode.Config _config;
|
||||||
|
private GrpcChannel _channel;
|
||||||
|
private StatsService.StatsServiceClient _client;
|
||||||
|
private bool _exitFlag;
|
||||||
|
private Action<ServerSpeedItem> _updateFunc;
|
||||||
|
|
||||||
|
public StatisticsV2ray(Mode.Config config, Action<ServerSpeedItem> update)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_updateFunc = update;
|
||||||
|
_exitFlag = false;
|
||||||
|
|
||||||
|
GrpcInit();
|
||||||
|
|
||||||
|
Task.Run(Run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GrpcInit()
|
||||||
|
{
|
||||||
|
if (_channel == null)
|
||||||
|
{
|
||||||
|
_channel = GrpcChannel.ForAddress($"{Global.httpProtocol}{Global.Loopback}:{Global.statePort}");
|
||||||
|
_client = new StatsService.StatsServiceClient(_channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
_exitFlag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Run()
|
||||||
|
{
|
||||||
|
while (!_exitFlag)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_channel.State == ConnectivityState.Ready)
|
||||||
|
{
|
||||||
|
QueryStatsResponse? res = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
res = await _client.QueryStatsAsync(new QueryStatsRequest() { Pattern = "", Reset = true });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res != null)
|
||||||
|
{
|
||||||
|
ParseOutput(res.Stat, out ServerSpeedItem server);
|
||||||
|
_updateFunc(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sleep = _config.guiItem.statisticsFreshRate < 1 ? 1 : _config.guiItem.statisticsFreshRate;
|
||||||
|
Thread.Sleep(1000 * sleep);
|
||||||
|
await _channel.ConnectAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseOutput(Google.Protobuf.Collections.RepeatedField<Stat> source, out ServerSpeedItem server)
|
||||||
|
{
|
||||||
|
server = new();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (Stat stat in source)
|
||||||
|
{
|
||||||
|
string name = stat.Name;
|
||||||
|
long value = stat.Value / 1024; //KByte
|
||||||
|
string[] nStr = name.Split(">>>".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string type = "";
|
||||||
|
|
||||||
|
name = name.Trim();
|
||||||
|
|
||||||
|
name = nStr[1];
|
||||||
|
type = nStr[3];
|
||||||
|
|
||||||
|
if (name == Global.agentTag)
|
||||||
|
{
|
||||||
|
if (type == "uplink")
|
||||||
|
{
|
||||||
|
server.proxyUp = value;
|
||||||
|
}
|
||||||
|
else if (type == "downlink")
|
||||||
|
{
|
||||||
|
server.proxyDown = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (name == Global.directTag)
|
||||||
|
{
|
||||||
|
if (type == "uplink")
|
||||||
|
{
|
||||||
|
server.directUp = value;
|
||||||
|
}
|
||||||
|
else if (type == "downlink")
|
||||||
|
{
|
||||||
|
server.directDown = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,4 +23,18 @@
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class TrafficItem
|
||||||
|
{
|
||||||
|
public ulong up
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong down
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -166,6 +166,7 @@
|
||||||
public class Experimental4Sbox
|
public class Experimental4Sbox
|
||||||
{
|
{
|
||||||
public V2ray_Api4Sbox v2ray_api { get; set; }
|
public V2ray_Api4Sbox v2ray_api { get; set; }
|
||||||
|
public Clash_Api4Sbox clash_api { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class V2ray_Api4Sbox
|
public class V2ray_Api4Sbox
|
||||||
|
@ -174,6 +175,12 @@
|
||||||
public Stats4Sbox stats { get; set; }
|
public Stats4Sbox stats { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Clash_Api4Sbox
|
||||||
|
{
|
||||||
|
public string external_controller { get; set; }
|
||||||
|
public bool store_selected { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class Stats4Sbox
|
public class Stats4Sbox
|
||||||
{
|
{
|
||||||
public bool enabled { get; set; }
|
public bool enabled { get; set; }
|
||||||
|
|
Loading…
Reference in a new issue