moved heartbeat to timer instead of manual task/thread

GameEventHandler uses ConcurrentQueue for events
exception handlers for events and log reading
added IW4ScriptCommands plugin
fixed stats
lots of little fixes
This commit is contained in:
RaidMax 2018-04-28 00:22:18 -05:00
parent 2c2c442ba7
commit bb90a807b7
21 changed files with 445 additions and 134 deletions

View File

@ -8,9 +8,13 @@ using SharedLibraryCore;
namespace IW4MAdmin.Application.API.Master
{
public class HeartbeatState
{
public bool Connected { get; set; }
}
public class Heartbeat
{
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
{
var api = Endpoint.Get();

View File

@ -1,6 +1,7 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
@ -8,12 +9,14 @@ namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
private Queue<GameEvent> EventQueue;
private ConcurrentQueue<GameEvent> EventQueue;
private ConcurrentQueue<GameEvent> StatusSensitiveQueue;
private IManager Manager;
public GameEventHandler(IManager mgr)
{
EventQueue = new Queue<GameEvent>();
EventQueue = new ConcurrentQueue<GameEvent>();
StatusSensitiveQueue = new ConcurrentQueue<GameEvent>();
Manager = mgr;
}
@ -22,7 +25,21 @@ namespace IW4MAdmin.Application
#if DEBUG
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
#endif
EventQueue.Enqueue(gameEvent);
// we need this to keep accurate track of the score
if (gameEvent.Type == GameEvent.EventType.Script ||
gameEvent.Type == GameEvent.EventType.Kill)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Added sensitive event to queue");
#endif
StatusSensitiveQueue.Enqueue(gameEvent);
return;
}
else
{
EventQueue.Enqueue(gameEvent);
}
#if DEBUG
Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue");
#endif
@ -34,13 +51,43 @@ namespace IW4MAdmin.Application
throw new NotImplementedException();
}
public GameEvent GetNextSensitiveEvent()
{
if (StatusSensitiveQueue.Count > 0)
{
if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing");
}
else
{
return newEvent;
}
}
return null;
}
public GameEvent GetNextEvent()
{
if (EventQueue.Count > 0)
{
#if DEBUG
Manager.GetLogger().WriteDebug("Getting next event to be processed");
Manager.GetLogger().WriteDebug("Getting next event to be processed");
#endif
if (!EventQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue event for processing");
}
return EventQueue.Count > 0 ? EventQueue.Dequeue() : null;
else
{
return newEvent;
}
}
return null;
}
}
}

View File

@ -11,41 +11,46 @@ namespace IW4MAdmin.Application.IO
{
class GameLogEvent
{
FileSystemWatcher LogPathWatcher;
Server Server;
long PreviousFileSize;
GameLogReader Reader;
Timer RefreshInfoTimer;
string GameLogFile;
class EventState
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEvent(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
RefreshInfoTimer = new Timer((sender) =>
RefreshInfoTimer = new Timer(OnEvent, new EventState()
{
long newLength = new FileInfo(GameLogFile).Length;
UpdateLogEvents(newLength);
}, null, 0, 100);
/*LogPathWatcher = new FileSystemWatcher()
{
Path = gameLogPath.Replace(gameLogName, ""),
Filter = gameLogName,
NotifyFilter = (NotifyFilters)383,
InternalBufferSize = 4096
};
// LogPathWatcher.Changed += LogPathWatcher_Changed;
LogPathWatcher.EnableRaisingEvents = true;*/
Log = server.Manager.GetLogger(),
ServerId = server.ToString()
}, 0, 100);
}
/*
~GameLogEvent()
private void OnEvent(object state)
{
LogPathWatcher.EnableRaisingEvents = false;
}*/
long newLength = new FileInfo(GameLogFile).Length;
try
{
UpdateLogEvents(newLength);
}
catch (Exception e)
{
((EventState)state).Log.WriteWarning($"Failed to update log event for {((EventState)state).ServerId}");
((EventState)state).Log.WriteDebug($"Exception: {e.Message}");
((EventState)state).Log.WriteDebug($"StackTrace: {e.StackTrace}");
}
}
private void UpdateLogEvents(long fileSize)
{

View File

@ -127,7 +127,19 @@ namespace IW4MAdmin.Application
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
Task.Run(() =>
{
try
{
WebfrontCore.Program.Init(ServerManager);
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning("Webfront had unhandled exception");
ServerManager.Logger.WriteDebug(e.Message);
}
});
}
}

View File

@ -19,6 +19,7 @@ using SharedLibraryCore.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;
using IW4MAdmin.Application.API.Master;
namespace IW4MAdmin.Application
{
@ -43,12 +44,7 @@ namespace IW4MAdmin.Application
EventApi Api;
GameEventHandler Handler;
ManualResetEventSlim OnEvent;
Timer StatusUpdateTimer;
#if FTP_LOG
const int UPDATE_FREQUENCY = 700;
#else
const int UPDATE_FREQUENCY = 2000;
#endif
Timer HeartbeatTimer;
private ApplicationManager()
{
@ -90,16 +86,62 @@ namespace IW4MAdmin.Application
return Instance ?? (Instance = new ApplicationManager());
}
public void UpdateStatus(object state)
public async Task UpdateStatus(object state)
{
var taskList = new List<Task>();
foreach (var server in Servers)
while (Running)
{
taskList.Add(Task.Run(() => server.ProcessUpdatesAsync(new CancellationToken())));
}
taskList.Clear();
foreach (var server in Servers)
{
taskList.Add(Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(new CancellationToken());
}
Task.WaitAll(taskList.ToArray());
catch (Exception e)
{
Logger.WriteWarning($"Failed to update status for {server}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"StackTrace: {e.StackTrace}");
}
}));
}
#if DEBUG
Logger.WriteDebug($"{taskList.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
await Task.WhenAll(taskList.ToArray());
GameEvent sensitiveEvent;
while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null)
{
try
{
await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent);
#if DEBUG
Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}");
#endif
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
sensitiveEvent.OnProcessed.Set();
continue;
}
}
await Task.Delay(5000);
}
}
public async Task Init()
@ -286,41 +328,35 @@ namespace IW4MAdmin.Application
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
// start polling servers
StatusUpdateTimer = new Timer(UpdateStatus, null, 0, 10000);
#endregion
Running = true;
}
private void HeartBeatThread()
private void SendHeartbeat(object state)
{
bool successfulConnection = false;
restartConnection:
while (!successfulConnection)
var heartbeatState = (HeartbeatState)state;
if (!heartbeatState.Connected)
{
try
{
API.Master.Heartbeat.Send(this, true).Wait();
successfulConnection = true;
Heartbeat.Send(this, true).Wait();
heartbeatState.Connected = true;
}
catch (Exception e)
{
successfulConnection = false;
heartbeatState.Connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
Thread.Sleep(30000);
}
while (Running)
else
{
Logger.WriteDebug("Sending heartbeat...");
try
{
API.Master.Heartbeat.Send(this).Wait();
Heartbeat.Send(this).Wait();
}
catch (System.Net.Http.HttpRequestException e)
{
@ -336,8 +372,7 @@ namespace IW4MAdmin.Application
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
successfulConnection = false;
goto restartConnection;
heartbeatState.Connected = false;
}
}
}
@ -347,55 +382,65 @@ namespace IW4MAdmin.Application
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
successfulConnection = false;
goto restartConnection;
heartbeatState.Connected = false;
}
}
Thread.Sleep(30000);
}
}
public void Start()
{
Task.Run(() => HeartBeatThread());
GameEvent newEvent;
while (Running)
{
// wait for new event to be added
OnEvent.Wait();
// todo: sequencially or parallelize?
while ((newEvent = Handler.GetNextEvent()) != null)
{
try
{
newEvent.Owner.ExecuteEvent(newEvent).Wait();
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
newEvent.OnProcessed.Set();
continue;
}
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
}
// signal that all events have been processed
OnEvent.Reset();
}
#if !DEBUG
// start heartbeat
HeartbeatTimer = new Timer(SendHeartbeat, new HeartbeatState(), 0, 30000);
#endif
// start polling servers
// StatusUpdateTimer = new Timer(UpdateStatus, null, 0, 5000);
Task.Run(() => UpdateStatus(null));
GameEvent newEvent;
Task.Run(async () =>
{
while (Running)
{
// wait for new event to be added
OnEvent.Wait();
// todo: sequencially or parallelize?
while ((newEvent = Handler.GetNextEvent()) != null)
{
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
newEvent.OnProcessed.Set();
continue;
}
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
}
// signal that all events have been processed
OnEvent.Reset();
}
#if !DEBUG
HeartbeatTimer.Change(0, Timeout.Infinite);
foreach (var S in Servers)
S.Broadcast(Utilities.CurrentLocalization.LocalizationSet["BROADCAST_OFFLINE"]).Wait();
#endif
_servers.Clear();
_servers.Clear();
}).Wait();
}

View File

@ -91,7 +91,7 @@ namespace Application.RconParsers
int Ping = -1;
Int32.TryParse(playerInfo[2], out Ping);
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();
long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}|bot[0-9]+", RegexOptions.IgnoreCase).Value.ConvertLong();
int.TryParse(playerInfo[0], out cID);
var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
int cIP = regex.Value.Split(':')[0].ConvertToIP();
@ -105,8 +105,9 @@ namespace Application.RconParsers
IPAddress = cIP,
Ping = Ping,
Score = score,
IsBot = npID == 0
IsBot = cIP == 0
};
StatusPlayers.Add(P);
}
}

View File

@ -147,7 +147,7 @@ namespace Application.RconParsers
regex = Regex.Match(responseLine, @" +(\d+ +){3}");
int score = Int32.Parse(regex.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0]);
StatusPlayers.Add(new Player()
var p = new Player()
{
Name = name,
NetworkId = networkId,
@ -156,7 +156,12 @@ namespace Application.RconParsers
Ping = Ping,
Score = score,
IsBot = networkId == 0
});
};
StatusPlayers.Add(p);
if (p.IsBot)
p.NetworkId = -p.ClientNumber;
}
}

View File

@ -177,8 +177,7 @@ namespace Application.RconParsers
int score = 0;
// todo: fix this when T6M score is valid ;)
//int score = Int32.Parse(playerInfo[1]);
StatusPlayers.Add(new Player()
var p = new Player()
{
Name = name,
NetworkId = networkId,
@ -187,7 +186,12 @@ namespace Application.RconParsers
Ping = Ping,
Score = score,
IsBot = networkId == 0
});
};
if (p.IsBot)
p.NetworkId = -p.ClientNumber;
StatusPlayers.Add(p);
}
}

View File

@ -54,7 +54,7 @@ namespace IW4MAdmin
{
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
polledPlayer.Ping < 1 || polledPlayer.ClientNumber > (MaxClients) ||
polledPlayer.Ping < 1 ||
polledPlayer.ClientNumber < 0)
{
//Logger.WriteDebug($"Skipping client not in connected state {P}");
@ -415,7 +415,7 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Connect)
{
// special case for IW5 when connect is from the log
if (E.Extra != null)
if (E.Extra != null && GameName == Game.IW5)
{
var logClient = (Player)E.Extra;
var client = (await this.GetStatusAsync())
@ -573,10 +573,12 @@ namespace IW4MAdmin
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
#endif
Throttled = false;
for (int i = 0; i < Players.Count; i++)
var clients = GetPlayersAsList();
foreach(var client in clients)
{
if (CurrentPlayers.Find(p => p.ClientNumber == i) == null && Players[i] != null)
await RemovePlayer(i);
if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId))
await RemovePlayer(client.ClientNumber);
}
for (int i = 0; i < CurrentPlayers.Count; i++)
@ -673,15 +675,15 @@ namespace IW4MAdmin
}
return true;
}
catch (NetworkException)
{
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
return false;
}
catch (InvalidOperationException)
// this one is ok
catch (ServerException e)
{
Logger.WriteWarning("Event could not parsed properly");
if (e is NetworkException)
{
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
}
return false;
}
@ -778,7 +780,7 @@ namespace IW4MAdmin
CustomCallback = await ScriptLoaded();
string mainPath = EventParser.GetGameDir();
#if DEBUG
basepath.Value = @"\\192.168.88.253\Call of Duty 4\";
basepath.Value = @"\\192.168.88.253\mw2";
#endif
string logPath;
if (GameName == Game.IW5)

View File

@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject
_commands.gsc = _commands.gsc
_customcallbacks.gsc = _customcallbacks.gsc
README.md = README.md
version.txt = version.txt
@ -28,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Logi
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -260,6 +263,30 @@ Global
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -270,6 +297,7 @@ Global
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -7,8 +7,7 @@
<ProjectGuid>f5051a32-6bd0-4128-abba-c202ee15fc5c</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>
</StartupFile>
<StartupFile>master\runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
@ -19,6 +18,11 @@
<Name>Master</Name>
<RootNamespace>Master</RootNamespace>
<InterpreterId>MSBuild|dev_env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<PythonRunWebServerCommand>master\runserver</PythonRunWebServerCommand>
<PythonDebugWebServerCommand>master\runserver</PythonDebugWebServerCommand>
<PythonRunWebServerCommandType>module</PythonRunWebServerCommandType>
<PythonDebugWebServerCommandType>module</PythonDebugWebServerCommandType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
@ -73,6 +77,9 @@
<Compile Include="master\routes.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\runserver.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\schema\instanceschema.py">
<SubType>Code</SubType>
</Compile>

View File

@ -0,0 +1,9 @@
"""
This script runs the Master application using a development server.
"""
from os import environ
from master import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=True)

View File

@ -0,0 +1,19 @@
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using System.Threading.Tasks;
namespace IW4ScriptCommands.Commands
{
class Balance : Command
{
public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null)
{
}
public override async Task ExecuteAsync(GameEvent E)
{
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
await E.Origin.Tell("Balance command sent");
}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace IW4ScriptCommands
{
class Plugin : IPlugin
{
public string Name => "IW4 Script Commands";
public float Version => 1.0f;
public string Author => "RaidMax";
public Task OnEventAsync(GameEvent E, Server S) => Task.CompletedTask;
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public Task OnUnloadAsync() => Task.CompletedTask;
}
}

View File

@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
stats.Kills = 0;
stats.SPM = 0.0;
stats.Skill = 0.0;
stats.TimePlayed = 0;
// reset the cached version
Plugin.Manager.ResetStats(E.Origin.ClientId, E.Owner.GetHashCode());

View File

@ -190,15 +190,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// get individual client's stats
var clientStats = playerStats[pl.ClientId];
// sync their score
clientStats.SessionScore += (pl.Score - clientStats.LastScore);
/*// sync their score
clientStats.SessionScore += pl.Score;*/
// remove the client from the stats dictionary as they're leaving
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3);
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4);
// sync their stats before they leave
clientStats = UpdateStats(clientStats);
/* // sync their stats before they leave
clientStats = UpdateStats(clientStats);*/
// todo: should this be saved every disconnect?
statsSvc.ClientStatSvc.Update(clientStats);
@ -229,8 +229,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (FormatException)
{
Log.WriteWarning("Could not parse kill or death origin vector");
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin}");
Log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAgnel - {viewAngles}");
await AddStandardKill(attacker, victim);
return;
}
@ -342,15 +342,27 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return;
}
#if DEBUG
Log.WriteDebug("Calculating standard kill");
#endif
// update the total stats
Servers[serverId].ServerStatistics.TotalKills += 1;
attackerStats.SessionScore += (attacker.Score - attackerStats.LastScore);
victimStats.SessionScore += (victim.Score - victimStats.LastScore);
// this happens when the round has changed
if (attackerStats.SessionScore == 0)
attackerStats.LastScore = 0;
if (victimStats.SessionScore == 0)
victimStats.LastScore = 0;
attackerStats.SessionScore = attacker.Score;
victimStats.SessionScore = victim.Score;
// calculate for the clients
CalculateKill(attackerStats, victimStats);
// this should fix the negative SPM
// updates their last score after being calculated
attackerStats.LastScore = attacker.Score;
victimStats.LastScore = victim.Score;
@ -427,14 +439,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// prevent NaN or inactive time lowering SPM
if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 ||
(DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 ||
clientStats.SessionScore < 1)
clientStats.SessionScore == 0)
{
// prevents idle time counting
clientStats.LastStatCalculation = DateTime.UtcNow;
return clientStats;
}
double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0;
// calculate the players Score Per Minute for the current session
int scoreDifference = clientStats.LastScore == 0 ? 0 : clientStats.SessionScore - clientStats.LastScore;
int scoreDifference = clientStats.RoundScore - clientStats.LastScore;
double killSPM = scoreDifference / timeSinceLastCalc;
// calculate how much the KDR should weigh
@ -442,9 +458,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
double kdr = clientStats.Deaths == 0 ? clientStats.Kills : clientStats.KDR;
double KDRWeight = Math.Round(Math.Pow(kdr, 1.637 / Math.E), 3);
// if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute
//double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM;
// calculate the weight of the new play time against last 10 hours of gameplay
int totalPlayTime = (clientStats.TimePlayed == 0) ?
(int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds :
@ -504,10 +517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
var serverStats = Servers[serverId];
foreach (var stat in serverStats.PlayerStats.Values)
{
stat.KillStreak = 0;
stat.DeathStreak = 0;
}
stat.StartNewSession();
}
public void ResetStats(int clientId, int serverId)
@ -517,6 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
stats.Deaths = 0;
stats.SPM = 0;
stats.Skill = 0;
stats.TimePlayed = 0;
}
public async Task AddMessageAsync(int clientId, int serverId, string message)

View File

@ -56,7 +56,34 @@ namespace IW4MAdmin.Plugins.Stats.Models
public int LastScore { get; set; }
[NotMapped]
public DateTime LastActive { get; set; }
public void StartNewSession()
{
KillStreak = 0;
DeathStreak = 0;
LastScore = 0;
SessionScores.Add(0);
}
[NotMapped]
public int SessionScore { get; set; }
public int SessionScore
{
set
{
SessionScores[SessionScores.Count - 1] = value;
}
get
{
return SessionScores.Sum();
}
}
[NotMapped]
public int RoundScore
{
get
{
return SessionScores[SessionScores.Count - 1];
}
}
[NotMapped]
private List<int> SessionScores = new List<int>() { 0 };
}
}

View File

@ -262,14 +262,17 @@ namespace SharedLibraryCore.Services
{
p.Active = false;
// reset the player levels
if (p.Type == Objects.Penalty.PenaltyType.Ban)
if (p.Type == Penalty.PenaltyType.Ban)
{
using (var internalContext = new DatabaseContext())
{
await internalContext.Clients
.Where(c => c.AliasLinkId == p.LinkId)
.ForEachAsync(c => c.Level = Objects.Player.Permission.User);
await internalContext.SaveChangesAsync();
}
}
});

View File

@ -183,6 +183,9 @@ namespace SharedLibraryCore
{
if (Int64.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long id))
return id;
var bot = Regex.Match(str, @"bot[0-9]+").Value;
if (!string.IsNullOrEmpty(bot))
return -Convert.ToInt64(bot.Substring(3));
return 0;
}

30
_commands.gsc Normal file
View File

@ -0,0 +1,30 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
level thread WaitForCommand();
}
WaitForCommand()
{
for(;;)
{
command = getDvar("sv_iw4madmin_command");
switch(command)
{
case "balance":
if (isRoundBased())
{
iPrintLnBold("Balancing Teams..");
level maps\mp\gametypes\_teams::balanceTeams();
}
break;
}
setDvar("sv_iw4madmin_command", "");
wait(1);
}
}