diff --git a/Application/Application.csproj b/Application/Application.csproj index 10cbc9f74..95623f247 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -18,10 +18,12 @@ IW4MAdmin Debug;Release;Prerelease + + @@ -34,6 +36,21 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + Always diff --git a/Application/Manager.cs b/Application/Manager.cs index 36bea63b6..5bbbc33e9 100644 --- a/Application/Manager.cs +++ b/Application/Manager.cs @@ -18,6 +18,7 @@ using WebfrontCore; using SharedLibraryCore.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Text; namespace IW4MAdmin.Application { @@ -161,6 +162,9 @@ namespace IW4MAdmin.Application else if (config.Servers.Count == 0) throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Utilities.EncodingType = Encoding.GetEncoding(config.CustomParserEncoding ?? "windows-1252"); + #endregion #region PLUGINS SharedLibraryCore.Plugins.PluginImporter.Load(this); diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs index 0184f4aad..df7d275eb 100644 --- a/Application/RconParsers/IW4RConParser.cs +++ b/Application/RconParsers/IW4RConParser.cs @@ -32,7 +32,7 @@ namespace Application.RconParsers { string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, dvarName); - if (LineSplit.Length != 3) + if (LineSplit.Length < 3) { var e = new DvarException($"DVAR \"{dvarName}\" does not exist"); e.Data["dvar_name"] = dvarName; @@ -41,7 +41,7 @@ namespace Application.RconParsers string[] ValueSplit = LineSplit[1].Split(new char[] { '"' }, StringSplitOptions.RemoveEmptyEntries); - if (ValueSplit.Length != 5) + if (ValueSplit.Length < 5) { var e = new DvarException($"DVAR \"{dvarName}\" does not exist"); e.Data["dvar_name"] = dvarName; @@ -75,17 +75,20 @@ namespace Application.RconParsers { List StatusPlayers = new List(); + if (Status.Length < 4) + throw new ServerException("Unexpected status response received"); + foreach (String S in Status) { String responseLine = S.Trim(); - if (Regex.Matches(responseLine, @"^\d+", RegexOptions.IgnoreCase).Count > 0) + if (Regex.Matches(responseLine, @" *^\d+", RegexOptions.IgnoreCase).Count > 0) { String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int cID = -1; int Ping = -1; Int32.TryParse(playerInfo[2], out Ping); - String cName = Encoding.UTF8.GetString(Encoding.Convert(Encoding.UTF7, Encoding.UTF8, Encoding.UTF7.GetBytes(responseLine.Substring(46, 18).StripColors().Trim()))); + String cName = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(responseLine.Substring(46, 18).StripColors().Trim()))); long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}", RegexOptions.IgnoreCase).Value.ConvertLong(); int.TryParse(playerInfo[0], out cID); var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}"); @@ -99,7 +102,8 @@ namespace Application.RconParsers ClientNumber = cID, IPAddress = cIP, Ping = Ping, - Score = score + Score = score, + IsBot = npID == -1 }; StatusPlayers.Add(P); } diff --git a/Application/RconParsers/T6MRConParser.cs b/Application/RconParsers/T6MRConParser.cs index cd2d8ced5..0bbdb58b4 100644 --- a/Application/RconParsers/T6MRConParser.cs +++ b/Application/RconParsers/T6MRConParser.cs @@ -166,7 +166,7 @@ namespace Application.RconParsers Int32.TryParse(playerInfo[3], out Ping); var regex = Regex.Match(responseLine, @"\^7.*\ +0 "); - string name = Encoding.UTF8.GetString(Encoding.Convert(Encoding.UTF7, Encoding.UTF8, Encoding.UTF7.GetBytes(regex.Value.Substring(0, regex.Value.Length - 2).StripColors().Trim()))); + string name = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(regex.Value.Substring(0, regex.Value.Length - 2).StripColors().Trim()))); long networkId = playerInfo[4].ConvertLong(); int.TryParse(playerInfo[0], out clientId); regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}"); @@ -175,7 +175,9 @@ namespace Application.RconParsers #endif int ipAddress = regex.Value.Split(':')[0].ConvertToIP(); regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+"); - int score = Int32.Parse(playerInfo[1]); + int score = 0; + // todo: fix this when T6M score is valid ;) + //int score = Int32.Parse(playerInfo[1]); StatusPlayers.Add(new Player() { @@ -184,7 +186,8 @@ namespace Application.RconParsers ClientNumber = clientId, IPAddress = ipAddress, Ping = Ping, - Score = score + Score = score, + IsBot = networkId < 1 }); } } diff --git a/Application/Server.cs b/Application/Server.cs index 8283f4cf3..346bfb5a1 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -151,6 +151,7 @@ namespace IW4MAdmin // Do the player specific stuff player.ClientNumber = polledPlayer.ClientNumber; + player.IsBot = polledPlayer.IsBot; player.Score = polledPlayer.Score; player.CurrentServer = this; Players[player.ClientNumber] = player; @@ -648,7 +649,7 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); string mainPath = EventParser.GetGameDir(); #if DEBUG - basepath.Value = @"D:\"; + basepath.Value = @"\\192.168.88.253\Call of Duty Black Ops II"; #endif string logPath = game.Value == string.Empty ? $"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" : diff --git a/Master/Master.pyproj b/Master/Master.pyproj index d7626837d..2a4ebb44e 100644 --- a/Master/Master.pyproj +++ b/Master/Master.pyproj @@ -36,6 +36,9 @@ Code + + Code + Code @@ -51,6 +54,9 @@ Code + + Code + Code @@ -88,37 +94,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Master/master/context/base.py b/Master/master/context/base.py index 6439c56a3..a6e16547d 100644 --- a/Master/master/context/base.py +++ b/Master/master/context/base.py @@ -1,11 +1,15 @@ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger +from master.context.history import History + +from master.schema.instanceschema import InstanceSchema + import time class Base(): def __init__(self): + self.history = History() self.instance_list = {} - self.server_list = {} self.token_list = {} self.scheduler = BackgroundScheduler() self.scheduler.start() @@ -16,6 +20,23 @@ class Base(): name='Remove stale instances if no heartbeat in 120 seconds', replace_existing=True ) + self.scheduler.add_job( + func=self._update_history_count, + trigger=IntervalTrigger(seconds=30), + id='update history', + name='update client and instance count every 30 seconds', + replace_existing=True + ) + + def _update_history_count(self): + servers = [instance.servers for instance in self.instance_list.values()] + servers = [inner for outer in servers for inner in outer] + client_num = 0 + # force it being a number + for server in servers: + client_num += server.clientnum + self.history.add_client_history(client_num) + self.history.add_instance_history(len(self.instance_list)) def _remove_staleinstances(self): for key, value in list(self.instance_list.items()): @@ -28,9 +49,6 @@ class Base(): def get_instances(self): return self.instance_list.values() - def get_server_count(self): - return self.server_list.count - def get_instance_count(self): return self.instance_list.count diff --git a/Master/master/context/history.py b/Master/master/context/history.py new file mode 100644 index 000000000..f5319bada --- /dev/null +++ b/Master/master/context/history.py @@ -0,0 +1,23 @@ +import time +from random import randint + +class History(): + def __init__(self): + self.client_history = list() + self.instance_history = list() + + def add_client_history(self, client_num): + if len(self.client_history) > 1440: + self.client_history = self.client_history[1:] + self.client_history.append({ + 'count' : client_num, + 'time' : int(time.time()) + }) + + def add_instance_history(self, instance_num): + if len(self.instance_history) > 1440: + self.instance_history = self.instance_history[1:] + self.instance_history.append({ + 'count' : instance_num, + 'time' : int(time.time()) + }) diff --git a/Master/master/resources/history_graph.py b/Master/master/resources/history_graph.py new file mode 100644 index 000000000..a5691aad3 --- /dev/null +++ b/Master/master/resources/history_graph.py @@ -0,0 +1,46 @@ +from flask_restful import Resource +from pygal.style import Style +from master import ctx +import pygal +import timeago +from math import ceil + +class HistoryGraph(Resource): + def get(self, history_count): + try: + custom_style = Style( + background='transparent', + plot_background='transparent', + foreground='rgba(109, 118, 126, 0.3)', + foreground_strong='rgba(109, 118, 126, 0.3)', + foreground_subtle='rgba(109, 118, 126, 0.3)', + opacity='0.1', + opacity_hover='0.2', + transition='100ms ease-in', + colors=('#007acc', '#749363') + ) + + graph = pygal.StackedLine( + interpolate='cubic', + interpolation_precision=3, + #x_labels_major_every=100, + #x_labels_major_count=500, + stroke_style={'width': 0.4}, + show_dots=False, + show_legend=False, + fill=True, + style=custom_style, + disable_xml_declaration=True) + + instance_count = [history['time'] for history in ctx.history.instance_history][-history_count:] + + if len(instance_count) > 0: + graph.x_labels = [ timeago.format(instance_count[0])] + + graph.add('Instance Count', [history['count'] for history in ctx.history.instance_history][-history_count:]) + graph.add('Client Count', [history['count'] for history in ctx.history.client_history][-history_count:]) + return { 'message' : graph.render(), + 'data_points' : len(instance_count) + }, 200 + except Exception as e: + return { 'message' : str(e) }, 500 diff --git a/Master/master/routes.py b/Master/master/routes.py index 84285775a..93ac61ad8 100644 --- a/Master/master/routes.py +++ b/Master/master/routes.py @@ -4,8 +4,10 @@ from master.resources.null import Null from master.resources.instance import Instance from master.resources.authenticate import Authenticate from master.resources.version import Version +from master.resources.history_graph import HistoryGraph api.add_resource(Null, '/null') api.add_resource(Instance, '/instance/', '/instance/') api.add_resource(Version, '/version') -api.add_resource(Authenticate, '/authenticate') \ No newline at end of file +api.add_resource(Authenticate, '/authenticate') +api.add_resource(HistoryGraph, '/history/', '/history/') \ No newline at end of file diff --git a/Master/master/templates/index.html b/Master/master/templates/index.html index 5c9f5a162..2867a6df2 100644 --- a/Master/master/templates/index.html +++ b/Master/master/templates/index.html @@ -1,5 +1,46 @@ {% extends "layout.html" %} {% block content %} - +
+
+
+
{{history_graph|safe}}
+
+ + +
+
+ +
+
+{% endblock %} + +{% block scripts %} + + {% endblock %} diff --git a/Master/master/templates/layout.html b/Master/master/templates/layout.html index aa1843f4e..ddc75d238 100644 --- a/Master/master/templates/layout.html +++ b/Master/master/templates/layout.html @@ -1,44 +1,32 @@  - + IW4MAdmin Master | {{ title }} - - - + + + - - + -
+
{% block content %}{% endblock %} -
-
-
+
- - - + {% block scripts %}{% endblock %} - diff --git a/Master/master/views.py b/Master/master/views.py index 7b27e6a8e..b5ca7b57f 100644 --- a/Master/master/views.py +++ b/Master/master/views.py @@ -5,13 +5,14 @@ Routes and views for the flask application. from datetime import datetime from flask import render_template from master import app +from master.resources.history_graph import HistoryGraph @app.route('/') -@app.route('/home') def home(): - """Renders the home page.""" + _history_graph = HistoryGraph().get(500) return render_template( 'index.html', - title='Home Page', - year=datetime.now().year, - ) \ No newline at end of file + title='API Overview', + history_graph = _history_graph[0]['message'], + data_points = _history_graph[0]['data_points'] + ) diff --git a/README.md b/README.md index c091ca6f8..c10fe2f96 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ + # IW4MAdmin ### Quick Start Guide ### Version 2.0 @@ -40,6 +41,10 @@ When **IW4MAdmin** is launched for the _first time_, you will be prompted to set * Shows a link to your server's discord on the webfront * _This feature requires an invite link to your discord server_ +`Use Custom Encoding Parser` +* Allows alternative encodings to be used for parsing game information and events +* **Russian users should use this and then specify** `windows-1251` **as the encoding string** + #### Advanced Configuration If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor. diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 0bc343c15..a30d3eaa8 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -214,11 +214,11 @@ namespace SharedLibraryCore.Commands public class CUnban : Command { public CUnban() : - base("unban", "unban player by database id", "ub", Player.Permission.SeniorAdmin, true, new CommandArgument[] + base("unban", "unban player by client id", "ub", Player.Permission.SeniorAdmin, true, new CommandArgument[] { new CommandArgument() { - Name = "databaseID", + Name = "client id", Required = true, }, new CommandArgument() @@ -576,6 +576,12 @@ namespace SharedLibraryCore.Commands public override async Task ExecuteAsync(GameEvent E) { + if (E.Data.Length < 3) + { + await E.Origin.Tell("Please enter at least 3 characters"); + return; + } + IList db_players = (await (E.Owner.Manager.GetClientService() as ClientService) .GetClientByName(E.Data)) .OrderByDescending(p => p.LastConnection) diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 75d9509f8..0915e0cbd 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -17,6 +17,7 @@ namespace SharedLibraryCore.Configuration public string DiscordInviteCode { get; set; } public string IPHubAPIKey { get; set; } public string WebfrontBindUrl { get; set; } + public string CustomParserEncoding { get; set; } public string Id { get; set; } public List Servers { get; set; } public int AutoMessagePeriod { get; set; } @@ -32,6 +33,10 @@ namespace SharedLibraryCore.Configuration EnableSteppedHierarchy = Utilities.PromptBool("Enable stepped privilege hierarchy"); EnableCustomSayName = Utilities.PromptBool("Enable custom say name"); + bool useCustomParserEncoding = Utilities.PromptBool("Use custom encoding parser"); + CustomParserEncoding = useCustomParserEncoding ? Utilities.PromptString("Enter encoding string") : "windows-1252"; + + WebfrontBindUrl = "http://127.0.0.1:1624"; if (EnableCustomSayName) diff --git a/SharedLibraryCore/File.cs b/SharedLibraryCore/File.cs index ba275e89d..31ffcb2a6 100644 --- a/SharedLibraryCore/File.cs +++ b/SharedLibraryCore/File.cs @@ -39,7 +39,7 @@ namespace SharedLibraryCore if (fileName != string.Empty) { Name = fileName; - Handle = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true), Encoding.UTF8); + Handle = new StreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true), Utilities.EncodingType); sze = Handle.BaseStream.Length; } diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index 8664b2463..9d1e8c6fb 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -130,7 +130,7 @@ namespace SharedLibraryCore.RCon #if DEBUG Log.WriteDebug($"Received {bytesRead} bytes from {ServerConnection.RemoteEndPoint}"); #endif - connectionState.ResponseString.Append(Encoding.UTF7.GetString(connectionState.Buffer, 0, bytesRead).TrimEnd('\0') + '\n'); + connectionState.ResponseString.Append(Utilities.EncodingType.GetString(connectionState.Buffer, 0, bytesRead).TrimEnd('\0') + '\n'); if (!connectionState.Buffer.Take(4).ToArray().SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })) throw new NetworkException("Unexpected packet received"); diff --git a/SharedLibraryCore/Services/ClientService.cs b/SharedLibraryCore/Services/ClientService.cs index 5295c7849..8966a23e4 100644 --- a/SharedLibraryCore/Services/ClientService.cs +++ b/SharedLibraryCore/Services/ClientService.cs @@ -226,6 +226,9 @@ namespace SharedLibraryCore.Services public async Task> GetClientByName(string name) { + if (name.Length < 3) + return new List(); + using (var context = new DatabaseContext()) { var iqClients = (from alias in context.Aliases diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index fed971eb0..4dce4a886 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -20,6 +20,7 @@ namespace SharedLibraryCore { public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar; public static readonly Task CompletedTask = Task.FromResult(false); + public static Encoding EncodingType; //Get string with specified number of spaces -- really only for visual output public static String GetSpaces(int Num) @@ -197,15 +198,9 @@ namespace SharedLibraryCore public static int ConvertToIP(this string str) { - try - { - return BitConverter.ToInt32(System.Net.IPAddress.Parse(str).GetAddressBytes(), 0); - } + System.Net.IPAddress.TryParse(str, out System.Net.IPAddress ip); - catch (FormatException) - { - return 0; - } + return ip == null ? 0 : BitConverter.ToInt32(ip.GetAddressBytes(), 0); } public static string ConvertIPtoString(this int ip)