final changes for 2.0 release

This commit is contained in:
RaidMax 2018-04-21 17:18:20 -05:00
parent 30ec0527d2
commit c60a1f3d33
20 changed files with 230 additions and 87 deletions

View File

@ -18,10 +18,12 @@
<ApplicationIcon />
<AssemblyName>IW4MAdmin</AssemblyName>
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
@ -34,6 +36,21 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

View File

@ -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);

View File

@ -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<Player> StatusPlayers = new List<Player>();
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);
}

View File

@ -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
});
}
}

View File

@ -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}" :

View File

@ -36,6 +36,9 @@
<Compile Include="master\context\base.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\context\history.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\context\__init__.py">
<SubType>Code</SubType>
</Compile>
@ -51,6 +54,9 @@
<Compile Include="Master\resources\authenticate.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\history_graph.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\instance.py">
<SubType>Code</SubType>
</Compile>
@ -88,37 +94,12 @@
<Folder Include="master\schema\" />
<Folder Include="Master\resources\" />
<Folder Include="Master\static\" />
<Folder Include="Master\static\content\" />
<Folder Include="Master\static\fonts\" />
<Folder Include="Master\static\scripts\" />
<Folder Include="Master\templates\" />
</ItemGroup>
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="master\config\master.json" />
<Content Include="requirements.txt" />
<Content Include="Master\static\content\bootstrap.css" />
<Content Include="Master\static\content\bootstrap.min.css" />
<Content Include="Master\static\content\site.css" />
<Content Include="Master\static\fonts\glyphicons-halflings-regular.eot" />
<Content Include="Master\static\fonts\glyphicons-halflings-regular.svg" />
<Content Include="Master\static\fonts\glyphicons-halflings-regular.ttf" />
<Content Include="Master\static\fonts\glyphicons-halflings-regular.woff" />
<Content Include="Master\static\scripts\bootstrap.js" />
<Content Include="Master\static\scripts\bootstrap.min.js" />
<Content Include="Master\static\scripts\jquery-1.10.2.intellisense.js" />
<Content Include="Master\static\scripts\jquery-1.10.2.js" />
<Content Include="Master\static\scripts\jquery-1.10.2.min.js" />
<Content Include="Master\static\scripts\jquery-1.10.2.min.map" />
<Content Include="Master\static\scripts\jquery.validate-vsdoc.js" />
<Content Include="Master\static\scripts\jquery.validate.js" />
<Content Include="Master\static\scripts\jquery.validate.min.js" />
<Content Include="Master\static\scripts\jquery.validate.unobtrusive.js" />
<Content Include="Master\static\scripts\jquery.validate.unobtrusive.min.js" />
<Content Include="Master\static\scripts\modernizr-2.6.2.js" />
<Content Include="Master\static\scripts\respond.js" />
<Content Include="Master\static\scripts\respond.min.js" />
<Content Include="Master\static\scripts\_references.js" />
<Content Include="Master\templates\index.html" />
<Content Include="Master\templates\layout.html" />
</ItemGroup>

View File

@ -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

View File

@ -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())
})

View File

@ -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

View File

@ -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/<string:id>')
api.add_resource(Version, '/version')
api.add_resource(Authenticate, '/authenticate')
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')

View File

@ -1,5 +1,46 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<div class="col-12">
<figure>
<div id="history_graph">{{history_graph|safe}}</div>
<figcaption class="float-right pr-3 mr-4">
<span id="history_graph_zoom_out" class="h4 oi oi-zoom-out text-muted" style="cursor:pointer;"></span>
<span id="history_graph_zoom_in" class="h4 oi oi-zoom-in text-muted" style="cursor:pointer;"></span>
</figcaption>
</figure>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
<script>
let dataPoints = {{data_points}};
let zoomLevel = Math.ceil(dataPoints / 2);
//console.log(dataPoints);
function updateHistoryGraph() {
$.get('/history/' + zoomLevel)
.done(function (content) {
$('#history_graph').html(content.message);
});
}
setInterval(updateHistoryGraph, 30000);
$('#history_graph_zoom_out').click(function () {
// console.log(zoomLevel);
zoomLevel = zoomLevel * 2 < dataPoints ? Math.ceil(zoomLevel * 2) : dataPoints;
updateHistoryGraph();
});
$('#history_graph_zoom_in').click(function () {
// console.log(zoomLevel);
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
updateHistoryGraph();
});
</script>
{% endblock %}

View File

@ -1,44 +1,32 @@
<!DOCTYPE html>
<html>
<html class="bg-dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IW4MAdmin Master | {{ title }}</title>
<link rel="stylesheet" type="text/css" href="/static/content/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/static/content/site.css" />
<script src="/static/scripts/modernizr-2.6.2.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css" integrity="sha256-BJ/G+e+y7bQdrYkS2RBTyNfBHpA9IuGaPmf9htub5MQ=" crossorigin="anonymous" />
<style type="text/css">
.active {
stroke-width: initial !important;
}
.oi:hover {
color: #fff !important;
}
</style>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">Application name</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('home') }}">Home</a></li>
</ul>
</div>
</div>
</div>
<body class="bg-dark">
<div class="container body-content">
<div class="container body-content bg-dark">
{% block content %}{% endblock %}
<hr />
<footer>
</footer>
</div>
<script src="/static/scripts/jquery-1.10.2.js"></script>
<script src="/static/scripts/bootstrap.js"></script>
<script src="/static/scripts/respond.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -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,
title='API Overview',
history_graph = _history_graph[0]['message'],
data_points = _history_graph[0]['data_points']
)

View File

@ -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.

View File

@ -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<EFClient> db_players = (await (E.Owner.Manager.GetClientService() as ClientService)
.GetClientByName(E.Data))
.OrderByDescending(p => p.LastConnection)

View File

@ -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<ServerConfiguration> 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)

View File

@ -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;
}

View File

@ -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");

View File

@ -226,6 +226,9 @@ namespace SharedLibraryCore.Services
public async Task<IList<EFClient>> GetClientByName(string name)
{
if (name.Length < 3)
return new List<EFClient>();
using (var context = new DatabaseContext())
{
var iqClients = (from alias in context.Aliases

View File

@ -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)