diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs
index db3f08f82..0a72afc70 100644
--- a/Application/EventParsers/IW4EventParser.cs
+++ b/Application/EventParsers/IW4EventParser.cs
@@ -140,26 +140,26 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType == "Q")
- {
- var regexMatch = Regex.Match(logLine, @"^(Q;)(.{1,32});([0-9]+);(.*)$");
- if (regexMatch.Success)
- {
- return new GameEvent()
- {
- Type = GameEvent.EventType.Quit,
- Data = logLine,
- Owner = server,
- Origin = new Player()
- {
- Name = regexMatch.Groups[4].ToString().StripColors(),
- NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
- ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
- State = Player.ClientState.Connecting
- }
- };
- }
- }
+ //if (eventType == "Q")
+ //{
+ // var regexMatch = Regex.Match(logLine, @"^(Q;)(.{1,32});([0-9]+);(.*)$");
+ // if (regexMatch.Success)
+ // {
+ // return new GameEvent()
+ // {
+ // Type = GameEvent.EventType.Quit,
+ // Data = logLine,
+ // Owner = server,
+ // Origin = new Player()
+ // {
+ // Name = regexMatch.Groups[4].ToString().StripColors(),
+ // NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
+ // ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
+ // State = Player.ClientState.Connecting
+ // }
+ // };
+ // }
+ //}
if (eventType.Contains("ExitLevel"))
{
diff --git a/Application/IO/GameLogEvent.cs b/Application/IO/GameLogEventDetection.cs
similarity index 79%
rename from Application/IO/GameLogEvent.cs
rename to Application/IO/GameLogEventDetection.cs
index 7785b0a4c..61933bf2e 100644
--- a/Application/IO/GameLogEvent.cs
+++ b/Application/IO/GameLogEventDetection.cs
@@ -6,11 +6,11 @@ using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
- class GameLogEvent
+ class GameLogEventDetection
{
Server Server;
long PreviousFileSize;
- GameLogReader Reader;
+ IGameLogReader Reader;
readonly string GameLogFile;
class EventState
@@ -19,14 +19,22 @@ namespace IW4MAdmin.Application.IO
public string ServerId { get; set; }
}
- public GameLogEvent(Server server, string gameLogPath, string gameLogName)
+ public GameLogEventDetection(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
- Reader = new GameLogReader(gameLogPath, server.EventParser);
+ // todo: abtract this more
+ if (gameLogPath.StartsWith("http"))
+ {
+ Reader = new GameLogReaderHttp(gameLogPath, server.EventParser);
+ }
+ else
+ {
+ Reader = new GameLogReader(gameLogPath, server.EventParser);
+ }
Server = server;
Task.Run(async () =>
- {
+ {
while (!server.Manager.ShutdownRequested())
{
if ((server.Manager as ApplicationManager).IsInitialized)
@@ -44,7 +52,7 @@ namespace IW4MAdmin.Application.IO
private void OnEvent(object state)
{
- long newLength = new FileInfo(GameLogFile).Length;
+ long newLength = Reader.Length;
try
{
diff --git a/Application/IO/GameLogReader.cs b/Application/IO/GameLogReader.cs
index c5df1fe56..1b4ea78b9 100644
--- a/Application/IO/GameLogReader.cs
+++ b/Application/IO/GameLogReader.cs
@@ -7,11 +7,15 @@ using System.Text;
namespace IW4MAdmin.Application.IO
{
- class GameLogReader
+ class GameLogReader : IGameLogReader
{
IEventParser Parser;
readonly string LogFile;
+ public long Length => new FileInfo(LogFile).Length;
+
+ public int UpdateInterval => 100;
+
public GameLogReader(string logFile, IEventParser parser)
{
LogFile = logFile;
diff --git a/Application/IO/GameLogReaderHttp.cs b/Application/IO/GameLogReaderHttp.cs
new file mode 100644
index 000000000..b132e3144
--- /dev/null
+++ b/Application/IO/GameLogReaderHttp.cs
@@ -0,0 +1,84 @@
+using SharedLibraryCore;
+using SharedLibraryCore.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+
+namespace IW4MAdmin.Application.IO
+{
+ ///
+ /// provides capibility of reading log files over HTTP
+ ///
+ class GameLogReaderHttp : IGameLogReader
+ {
+ readonly IEventParser Parser;
+ readonly string LogFile;
+
+ public GameLogReaderHttp(string logFile, IEventParser parser)
+ {
+ LogFile = logFile;
+ Parser = parser;
+ }
+
+ public long Length
+ {
+ get
+ {
+ using (var cl = new HttpClient())
+ {
+ using (var re = cl.GetAsync($"{LogFile}?length=1").Result)
+ {
+ using (var content = re.Content)
+ {
+ return Convert.ToInt64(content.ReadAsStringAsync().Result ?? "0");
+ }
+ }
+ }
+ }
+ }
+
+ public int UpdateInterval => 1000;
+
+ public ICollection EventsFromLog(Server server, long fileSizeDiff, long startPosition)
+ {
+ string log;
+ using (var cl = new HttpClient())
+ {
+ using (var re = cl.GetAsync($"{LogFile}?start={fileSizeDiff}").Result)
+ {
+ using (var content = re.Content)
+ {
+ log = content.ReadAsStringAsync().Result;
+ }
+ }
+ }
+
+ List events = new List();
+
+ // parse each line
+ foreach (string eventLine in log.Split(Environment.NewLine))
+ {
+ if (eventLine.Length > 0)
+ {
+ try
+ {
+ // todo: catch elsewhere
+ events.Add(Parser.GetEvent(server, eventLine));
+ }
+
+ catch (Exception e)
+ {
+ Program.ServerManager.GetLogger().WriteWarning("Could not properly parse event line");
+ Program.ServerManager.GetLogger().WriteDebug(e.Message);
+ Program.ServerManager.GetLogger().WriteDebug(eventLine);
+ }
+ }
+ }
+
+ return events;
+ }
+ }
+}
diff --git a/Application/Main.cs b/Application/Main.cs
index 21e535c79..560993ce3 100644
--- a/Application/Main.cs
+++ b/Application/Main.cs
@@ -50,9 +50,6 @@ namespace IW4MAdmin.Application
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
loc = Utilities.CurrentLocalization.LocalizationIndex;
- using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
- new ContextSeed(db).Seed().Wait();
-
var api = API.Master.Endpoint.Get();
var version = new API.Master.VersionInfo()
diff --git a/Application/Manager.cs b/Application/Manager.cs
index 8bc28edfd..b86e8b295 100644
--- a/Application/Manager.cs
+++ b/Application/Manager.cs
@@ -21,6 +21,7 @@ using Newtonsoft.Json.Linq;
using System.Text;
using IW4MAdmin.Application.API.Master;
using System.Reflection;
+using SharedLibraryCore.Database;
namespace IW4MAdmin.Application
{
@@ -36,7 +37,7 @@ namespace IW4MAdmin.Application
// define what the delagate function looks like
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
// expose the event handler so we can execute the events
- public OnServerEventEventHandler OnServerEvent { get; private set; }
+ public OnServerEventEventHandler OnServerEvent { get; set; }
public DateTime StartTime { get; private set; }
static ApplicationManager Instance;
@@ -46,10 +47,10 @@ namespace IW4MAdmin.Application
ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
- BaseConfigurationHandler ConfigHandler;
+ public BaseConfigurationHandler ConfigHandler;
EventApi Api;
GameEventHandler Handler;
- ManualResetEventSlim OnEvent;
+ ManualResetEventSlim OnQuit;
readonly IPageList PageList;
public class GameEventArgs : System.ComponentModel.AsyncCompletedEventArgs
@@ -78,7 +79,7 @@ namespace IW4MAdmin.Application
//ServerEventOccurred += Api.OnServerEvent;
ConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
- OnEvent = new ManualResetEventSlim();
+ OnQuit = new ManualResetEventSlim();
PageList = new PageList();
OnServerEvent += OnServerEventAsync;
}
@@ -110,15 +111,16 @@ namespace IW4MAdmin.Application
await newEvent.Owner.ExecuteEvent(newEvent);
//// todo: this is a hacky mess
- if (newEvent.Origin?.DelayedEvents?.Count > 0 &&
+ if (newEvent.Origin?.DelayedEvents.Count > 0 &&
newEvent.Origin?.State == Player.ClientState.Connected)
{
var events = newEvent.Origin.DelayedEvents;
// add the delayed event to the queue
- while (events?.Count > 0)
+ while(events.Count > 0)
{
var e = events.Dequeue();
+
e.Origin = newEvent.Origin;
// check if the target was assigned
if (e.Target != null)
@@ -133,9 +135,12 @@ namespace IW4MAdmin.Application
continue;
}
}
+ Logger.WriteDebug($"Adding delayed event of type {e.Type} for {e.Origin} back for processing");
this.GetEventHandler().AddEvent(e);
}
}
+
+ Api.OnServerEvent(this, newEvent);
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif
@@ -248,6 +253,11 @@ namespace IW4MAdmin.Application
Running = true;
#region DATABASE
+ using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString))
+ {
+ await new ContextSeed(db).Seed();
+ }
+
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new
{
@@ -513,8 +523,8 @@ namespace IW4MAdmin.Application
while (Running)
{
- OnEvent.Wait();
- OnEvent.Reset();
+ OnQuit.Wait();
+ OnQuit.Reset();
}
_servers.Clear();
}
@@ -558,7 +568,7 @@ namespace IW4MAdmin.Application
public void SetHasEvent()
{
- OnEvent.Set();
+ OnQuit.Set();
}
public IList GetPluginAssemblies() => SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs
index ead23c3d1..0348808f4 100644
--- a/Application/RconParsers/IW4RConParser.cs
+++ b/Application/RconParsers/IW4RConParser.cs
@@ -27,7 +27,7 @@ namespace IW4MAdmin.Application.RconParsers
public async Task ExecuteCommandAsync(Connection connection, string command)
{
- var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
+ var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
}
@@ -117,7 +117,6 @@ namespace IW4MAdmin.Application.RconParsers
IsBot = ip == 0,
State = Player.ClientState.Connecting
};
-
StatusPlayers.Add(P);
}
}
diff --git a/Application/RconParsers/IW5MRConParser.cs b/Application/RconParsers/IW5MRConParser.cs
index baec8387f..b0948610e 100644
--- a/Application/RconParsers/IW5MRConParser.cs
+++ b/Application/RconParsers/IW5MRConParser.cs
@@ -10,7 +10,7 @@ using SharedLibraryCore.Objects;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
-namespace IW4MAdmin.WApplication.RconParsers
+namespace IW4MAdmin.Application.RconParsers
{
public class IW5MRConParser : IRConParser
{
diff --git a/Application/Server.cs b/Application/Server.cs
index dc633944f..6fee44ce1 100644
--- a/Application/Server.cs
+++ b/Application/Server.cs
@@ -20,14 +20,13 @@ using IW4MAdmin.Application.RconParsers;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Core;
-using IW4MAdmin.WApplication.RconParsers;
namespace IW4MAdmin
{
public class IW4MServer : Server
{
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
- private GameLogEvent LogEvent;
+ private GameLogEventDetection LogEvent;
private ClientAuthentication AuthQueue;
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg)
@@ -56,8 +55,11 @@ namespace IW4MAdmin
public async Task OnPlayerJoined(Player logClient)
{
- if (Players[logClient.ClientNumber] == null ||
- Players[logClient.ClientNumber].NetworkId != logClient.NetworkId)
+ var existingClient = Players[logClient.ClientNumber];
+
+ if (existingClient == null ||
+ (existingClient.NetworkId != logClient.NetworkId &&
+ existingClient.State != Player.ClientState.Connected))
{
Logger.WriteDebug($"Log detected {logClient} joining");
Players[logClient.ClientNumber] = logClient;
@@ -68,9 +70,8 @@ namespace IW4MAdmin
override public async Task AddPlayer(Player polledPlayer)
{
- //if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
- // polledPlayer.Ping < 1 ||
- if (
+ if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
+ polledPlayer.Ping < 1 ||
polledPlayer.ClientNumber < 0)
{
//Logger.WriteDebug($"Skipping client not in connected state {P}");
@@ -78,7 +79,7 @@ namespace IW4MAdmin
}
// set this when they are waiting for authentication
- if (Players[polledPlayer.ClientNumber] == null &&
+ if (Players[polledPlayer.ClientNumber] == null &&
polledPlayer.State == Player.ClientState.Connecting)
{
Players[polledPlayer.ClientNumber] = polledPlayer;
@@ -186,6 +187,8 @@ namespace IW4MAdmin
player.IsBot = polledPlayer.IsBot;
player.Score = polledPlayer.Score;
player.CurrentServer = this;
+
+ player.DelayedEvents = (Players[player.ClientNumber]?.DelayedEvents) ?? new Queue();
Players[player.ClientNumber] = player;
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress);
@@ -278,7 +281,6 @@ namespace IW4MAdmin
public override async Task ExecuteEvent(GameEvent E)
{
bool canExecuteCommand = true;
- Manager.GetEventApi().OnServerEvent(this, E);
await ProcessEvent(E);
Command C = null;
@@ -387,15 +389,16 @@ namespace IW4MAdmin
}
}
- if (E.Type == GameEvent.EventType.Connect)
+ else if (E.Type == GameEvent.EventType.Connect)
{
E.Origin.State = Player.ClientState.Authenticated;
// add them to the server
if (!await AddPlayer(E.Origin))
{
- throw new ServerException("Player didn't pass authorization, so we are discontinuing event");
+ E.Origin.State = Player.ClientState.Connecting;
+ throw new ServerException("client didn't pass authorization, so we are discontinuing event");
}
- // hack makes the event propgate with the correct info
+ // hack: makes the event propgate with the correct info
E.Origin = Players[E.Origin.ClientNumber];
ChatHistory.Add(new ChatInfo()
@@ -416,27 +419,27 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.Quit)
{
- var origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId);
+ //var origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId);
- if (origin != null &&
- // we only want to forward the event if they are connected.
- origin.State == Player.ClientState.Connected)
- {
- var e = new GameEvent()
- {
- Type = GameEvent.EventType.Disconnect,
- Origin = origin,
- Owner = this
- };
+ //if (origin != null &&
+ // // we only want to forward the event if they are connected.
+ // origin.State == Player.ClientState.Connected)
+ //{
+ // var e = new GameEvent()
+ // {
+ // Type = GameEvent.EventType.Disconnect,
+ // Origin = origin,
+ // Owner = this
+ // };
- Manager.GetEventHandler().AddEvent(e);
- }
+ // Manager.GetEventHandler().AddEvent(e);
+ //}
- else if (origin != null &&
- origin.State != Player.ClientState.Connected)
- {
- await RemovePlayer(origin.ClientNumber);
- }
+ //else if (origin != null &&
+ // origin.State != Player.ClientState.Connected)
+ //{
+ // await RemovePlayer(origin.ClientNumber);
+ //}
}
else if (E.Type == GameEvent.EventType.Disconnect)
@@ -448,7 +451,13 @@ namespace IW4MAdmin
Time = DateTime.UtcNow
});
+ var currentState = E.Origin.State;
await RemovePlayer(E.Origin.ClientNumber);
+
+ if (currentState != Player.ClientState.Connected)
+ {
+ throw new ServerException("Disconnecting player was not in a connected state");
+ }
}
if (E.Type == GameEvent.EventType.Say)
@@ -555,7 +564,7 @@ namespace IW4MAdmin
#endif
Throttled = false;
- foreach(var client in polledClients)
+ foreach (var client in polledClients)
{
// todo: move out somehwere
var existingClient = Players[client.ClientNumber] ?? client;
@@ -564,7 +573,7 @@ namespace IW4MAdmin
}
var disconnectingClients = currentClients.Except(polledClients);
- var connectingClients = polledClients.Except(currentClients);
+ var connectingClients = polledClients.Except(currentClients.Where(c => c.State == Player.ClientState.Connected));
return new List[] { connectingClients.ToList(), disconnectingClients.ToList() };
}
@@ -634,8 +643,8 @@ namespace IW4MAdmin
}
// wait for all the connect tasks to finish
- await Task.WhenAll(waiterList.Select(t => Task.Run(() => t.Wait())));
-
+ await Task.WhenAll(waiterList.Select(t => Task.Run(() => t.Wait(5000))));
+
if (ConnectionErrors > 0)
{
Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}");
@@ -806,10 +815,10 @@ namespace IW4MAdmin
CustomCallback = await ScriptLoaded();
string mainPath = EventParser.GetGameDir();
#if DEBUG
- basepath.Value = @"";
+ basepath.Value = @"D:\";
#endif
string logPath;
- if (GameName == Game.IW5)
+ if (GameName == Game.IW5 || ServerConfig.ManualLogPath?.Length > 0)
{
logPath = ServerConfig.ManualLogPath;
}
@@ -831,11 +840,13 @@ namespace IW4MAdmin
Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}");
#if !DEBUG
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
+#else
+ LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
#endif
}
else
{
- LogEvent = new GameLogEvent(this, logPath, logfile.Value);
+ LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
}
Logger.WriteInfo($"Log file is {logPath}");
diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs
index f1d4391de..ad220b3a1 100644
--- a/Plugins/Stats/Helpers/StatManager.cs
+++ b/Plugins/Stats/Helpers/StatManager.cs
@@ -625,7 +625,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
int individualClientRanking = await ctx.Set()
.Where(c => c.ServerId == clientStats.ServerId)
.Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned)
- .Where(r => r.ActivityAmount > 3600)
+ .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime)
.Where(r => r.RatingHistory.Client.LastConnection > thirtyDaysAgo)
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(r => r.Newest)
@@ -670,11 +670,16 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
});
// weight the overall performance based on play time
- var performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
+ double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
+
+ if (double.IsNaN(performanceAverage))
+ {
+ performanceAverage = clientStatsList.Average(p => p.Performance);
+ }
int overallClientRanking = await ctx.Set()
.Where(r => r.RatingHistory.Client.Level != Player.Permission.Banned)
- .Where(r => r.ActivityAmount > 3600)
+ .Where(r => r.ActivityAmount > Plugin.Config.Configuration().TopPlayersMinPlayTime)
.Where(r => r.RatingHistory.Client.LastConnection > thirtyDaysAgo)
.Where(r => r.RatingHistory.ClientId != client.ClientId)
.Where(r => r.ServerId == null)
diff --git a/Plugins/Tests/ManagerFixture.cs b/Plugins/Tests/ManagerFixture.cs
new file mode 100644
index 000000000..66e806a68
--- /dev/null
+++ b/Plugins/Tests/ManagerFixture.cs
@@ -0,0 +1,61 @@
+using IW4MAdmin.Application;
+using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Tests
+{
+ public class ManagerFixture : IDisposable
+ {
+ public ApplicationManager Manager { get; private set; }
+
+ public ManagerFixture()
+ {
+
+ File.WriteAllText("test_mp.log", "TEST_LOG_FILE");
+
+ Manager = Program.ServerManager;
+
+ var config = new ApplicationConfiguration
+ {
+ Servers = new List()
+ {
+ new ServerConfiguration()
+ {
+ AutoMessages = new List(),
+ IPAddress = "127.0.0.1",
+ Password = "test",
+ Port = 28963,
+ Rules = new List(),
+ ManualLogPath = "https://raidmax.org/IW4MAdmin/getlog.php"
+ }
+ },
+ AutoMessages = new List(),
+ GlobalRules = new List(),
+ Maps = new List(),
+ RConPollRate = 10000
+ };
+ Manager.ConfigHandler = new BaseConfigurationHandler("Test.json");
+ Manager.ConfigHandler.Set(config);
+
+ Manager.Init().Wait();
+ Task.Run(() => Manager.Start());
+ }
+
+ public void Dispose()
+ {
+ Manager.Stop();
+ }
+ }
+
+ [CollectionDefinition("ManagerCollection")]
+ public class ManagerCollection : ICollectionFixture
+ {
+
+ }
+}
diff --git a/Plugins/Tests/ManagerTests.cs b/Plugins/Tests/ManagerTests.cs
new file mode 100644
index 000000000..1f08b075e
--- /dev/null
+++ b/Plugins/Tests/ManagerTests.cs
@@ -0,0 +1,194 @@
+using IW4MAdmin.Application;
+using SharedLibraryCore;
+using SharedLibraryCore.Interfaces;
+using SharedLibraryCore.Objects;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Xunit;
+
+namespace Tests
+{
+ [Collection("ManagerCollection")]
+ public class ManagerTests
+ {
+ readonly ApplicationManager Manager;
+
+ public ManagerTests(ManagerFixture fixture)
+ {
+ Manager = fixture.Manager;
+ }
+
+ [Fact]
+ public void AreCommandNamesUnique()
+ {
+ bool test = Manager.GetCommands().Count == Manager.GetCommands().Select(c => c.Name).Distinct().Count();
+ Assert.True(test, "command names are not unique");
+ }
+
+ [Fact]
+ public void AreCommandAliasesUnique()
+ {
+ var mgr = IW4MAdmin.Application.Program.ServerManager;
+ bool test = mgr.GetCommands().Count == mgr.GetCommands().Select(c => c.Alias).Distinct().Count();
+
+ Assert.True(test, "command aliases are not unique");
+ }
+
+ [Fact]
+ public void AddAndRemoveClientsViaJoinShouldSucceed()
+ {
+ var server = Manager.GetServers().First();
+ var waiters = new Queue();
+
+ int clientStartIndex = 4;
+ int clientNum = 10;
+
+ for (int i = clientStartIndex; i < clientStartIndex + clientNum; i++)
+ {
+ var e = new GameEvent()
+ {
+ Type = GameEvent.EventType.Join,
+ Origin = new Player()
+ {
+ Name = $"Player{i}",
+ NetworkId = i,
+ ClientNumber = i - 1
+ },
+ Owner = server
+ };
+
+ server.Manager.GetEventHandler().AddEvent(e);
+ waiters.Enqueue(e.OnProcessed);
+ }
+
+ while (waiters.Count > 0)
+ {
+ waiters.Dequeue().Wait();
+ }
+
+ Assert.True(server.ClientNum == clientNum, $"client num does not match added client num [{server.ClientNum}:{clientNum}]");
+
+ for (int i = clientStartIndex; i < clientStartIndex + clientNum; i++)
+ {
+ var e = new GameEvent()
+ {
+ Type = GameEvent.EventType.Disconnect,
+ Origin = new Player()
+ {
+ Name = $"Player{i}",
+ NetworkId = i,
+ ClientNumber = i - 1
+ },
+ Owner = server
+ };
+
+ server.Manager.GetEventHandler().AddEvent(e);
+ waiters.Enqueue(e.OnProcessed);
+ }
+
+ while (waiters.Count > 0)
+ {
+ waiters.Dequeue().Wait();
+ }
+
+ Assert.True(server.ClientNum == 0, "there are still clients connected");
+ }
+
+ [Fact]
+ public void AddAndRemoveClientsViaRconShouldSucceed()
+ {
+ var server = Manager.GetServers().First();
+ var waiters = new Queue();
+
+ int clientIndexStart = 1;
+ int clientNum = 8;
+
+ for (int i = clientIndexStart; i < clientNum + clientIndexStart; i++)
+ {
+ var e = new GameEvent()
+ {
+ Type = GameEvent.EventType.Connect,
+ Origin = new Player()
+ {
+ Name = $"Player{i}",
+ NetworkId = i,
+ ClientNumber = i - 1,
+ IPAddress = i,
+ Ping = 50,
+ CurrentServer = server
+ },
+ Owner = server,
+ };
+
+ Manager.GetEventHandler().AddEvent(e);
+ waiters.Enqueue(e.OnProcessed);
+ }
+
+ while (waiters.Count > 0)
+ {
+ waiters.Dequeue().Wait();
+ }
+
+ int actualClientNum = server.GetPlayersAsList().Count(p => p.State == Player.ClientState.Connected);
+ Assert.True(actualClientNum == clientNum, $"client connected states don't match [{actualClientNum}:{clientNum}");
+
+ for (int i = clientIndexStart; i < clientNum + clientIndexStart; i++)
+ {
+ var e = new GameEvent()
+ {
+ Type = GameEvent.EventType.Disconnect,
+ Origin = new Player()
+ {
+ Name = $"Player{i}",
+ NetworkId = i,
+ ClientNumber = i - 1,
+ IPAddress = i,
+ Ping = 50,
+ CurrentServer = server
+ },
+ Owner = server,
+ };
+
+ Manager.GetEventHandler().AddEvent(e);
+ waiters.Enqueue(e.OnProcessed);
+ }
+
+ while (waiters.Count > 0)
+ {
+ waiters.Dequeue().Wait();
+ }
+
+ actualClientNum = server.ClientNum;
+ Assert.True(actualClientNum == 0, "there are clients still connected");
+ }
+
+
+ [Fact]
+ public void AddClientViaLog()
+ {
+ var resetEvent = new ManualResetEventSlim();
+ resetEvent.Reset();
+
+ Manager.OnServerEvent += (sender, eventArgs) =>
+ {
+ if (eventArgs.Event.Type == GameEvent.EventType.Join)
+ {
+ eventArgs.Event.OnProcessed.Wait();
+ Assert.True(false);
+ }
+ };
+
+ File.AppendAllText("test_mp.log", " 2:33 J;224b3d0bc64ab4f9;0;goober");
+
+
+ resetEvent.Wait(5000);
+
+ }
+ }
+}
+
diff --git a/Plugins/Tests/Plugin.cs b/Plugins/Tests/Plugin.cs
deleted file mode 100644
index 8aaab03cb..000000000
--- a/Plugins/Tests/Plugin.cs
+++ /dev/null
@@ -1,192 +0,0 @@
-#if DEBUG
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-
-using SharedLibraryCore;
-using SharedLibraryCore.Interfaces;
-using SharedLibraryCore.Helpers;
-using SharedLibraryCore.Objects;
-
-namespace IW4MAdmin.Plugins
-{
- public class Tests : IPlugin
- {
- public string Name => "Dev Tests";
-
- public float Version => 0.1f;
-
- public string Author => "RaidMax";
-
- public async Task OnEventAsync(GameEvent E, Server S)
- {
- return;
- if (E.Type == GameEvent.EventType.Start)
- {
- #region UNIT_TEST_LOG_CONNECT
- for (int i = 1; i <= 8; i++)
- {
- var e = new GameEvent()
- {
- Type = GameEvent.EventType.Join,
- Origin = new Player()
- {
- Name = $"Player{i}",
- NetworkId = i,
- ClientNumber = i - 1
- },
- Owner = S
- };
-
- S.Manager.GetEventHandler().AddEvent(e);
- e.OnProcessed.Wait();
- }
-
- S.Logger.WriteAssert(S.ClientNum == 8, "UNIT_TEST_LOG_CONNECT failed client num check");
- #endregion
-
- #region UNIT_TEST_RCON_AUTHENTICATE
- for (int i = 1; i <= 8; i++)
- {
- var e = new GameEvent()
- {
- Type = GameEvent.EventType.Connect,
- Origin = new Player()
- {
- Name = $"Player{i}",
- NetworkId = i,
- ClientNumber = i - 1,
- IPAddress = i,
- Ping = 50,
- CurrentServer = S
- },
- Owner = S,
- };
-
- S.Manager.GetEventHandler().AddEvent(e);
- e.OnProcessed.Wait();
- }
-
- S.Logger.WriteAssert(S.GetPlayersAsList().Count(p => p.State == Player.ClientState.Connected) == 8,
- "UNIT_TEST_RCON_AUTHENTICATE failed client num connected state check");
- #endregion
- }
- //if (E.Type == GameEvent.EventType.Start)
- //{
- // #region PLAYER_HISTORY
- // var rand = new Random(GetHashCode());
- // var time = DateTime.UtcNow;
-
- // await Task.Run(() =>
- // {
- // if (S.PlayerHistory.Count > 0)
- // return;
-
- // while (S.PlayerHistory.Count < 144)
- // {
- // S.PlayerHistory.Enqueue(new PlayerHistory(time, rand.Next(7, 18)));
- // time = time.AddMinutes(PlayerHistory.UpdateInterval);
- // }
- // });
- // #endregion
-
- // #region PLUGIN_INFO
- // Console.WriteLine("|Name |Alias|Description |Requires Target|Syntax |Required Level|");
- // Console.WriteLine("|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|");
- // foreach (var command in S.Manager.GetCommands().OrderByDescending(c => c.Permission).ThenBy(c => c.Name))
- // {
- // Console.WriteLine($"|{command.Name}|{command.Alias}|{command.Description}|{command.RequiresTarget}|{command.Syntax.Substring(8).EscapeMarkdown()}|{command.Permission}|");
- // }
- // #endregion
- //}
- }
-
- public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
-
- public Task OnTickAsync(Server S)
- {
- return Task.CompletedTask;
- /*
- if ((DateTime.Now - Interval).TotalSeconds > 1)
- {
- var rand = new Random();
- int index = rand.Next(0, 17);
- var p = new Player()
- {
- Name = $"Test_{index}",
- NetworkId = (long)$"_test_{index}".GetHashCode(),
- ClientNumber = index,
- Ping = 1,
- IPAddress = $"127.0.0.{index}".ConvertToIP()
- };
-
- if (S.Players.ElementAt(index) != null)
- await S.RemovePlayer(index);
- // await S.AddPlayer(p);
-
-
- Interval = DateTime.Now;
- if (S.ClientNum > 0)
- {
- var victimPlayer = S.Players.Where(pl => pl != null).ToList()[rand.Next(0, S.ClientNum - 1)];
- var attackerPlayer = S.Players.Where(pl => pl != null).ToList()[rand.Next(0, S.ClientNum - 1)];
-
- await S.ExecuteEvent(new Event(Event.GType.Say, $"test_{attackerPlayer.ClientNumber}", victimPlayer, attackerPlayer, S));
-
- string[] eventLine = null;
-
- for (int i = 0; i < 1; i++)
- {
- if (S.GameName == Server.Game.IW4)
- {
-
- // attackerID ; victimID ; attackerOrigin ; victimOrigin ; Damage ; Weapon ; hitLocation ; meansOfDeath
- var minimapInfo = StatsPlugin.MinimapConfig.IW4Minimaps().MapInfo.FirstOrDefault(m => m.MapName == S.CurrentMap.Name);
- if (minimapInfo == null)
- return;
- eventLine = new string[]
- {
- "ScriptKill",
- attackerPlayer.NetworkId.ToString(),
- victimPlayer.NetworkId.ToString(),
- new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(),
- new Vector3(rand.Next(minimapInfo.MaxRight, minimapInfo.MaxLeft), rand.Next(minimapInfo.MaxBottom, minimapInfo.MaxTop), rand.Next(0, 100)).ToString(),
- rand.Next(50, 105).ToString(),
- ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(),
- ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(),
- ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString()
- };
-
- }
- else
- {
- eventLine = new string[]
- {
- "K",
- victimPlayer.NetworkId.ToString(),
- victimPlayer.ClientNumber.ToString(),
- rand.Next(0, 1) == 0 ? "allies" : "axis",
- victimPlayer.Name,
- attackerPlayer.NetworkId.ToString(),
- attackerPlayer.ClientNumber.ToString(),
- rand.Next(0, 1) == 0 ? "allies" : "axis",
- attackerPlayer.Name.ToString(),
- ((StatsPlugin.IW4Info.WeaponName)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.WeaponName)).Length - 1)).ToString(), // Weapon
- rand.Next(50, 105).ToString(), // Damage
- ((StatsPlugin.IW4Info.MeansOfDeath)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.MeansOfDeath)).Length - 1)).ToString(), // Means of Death
- ((StatsPlugin.IW4Info.HitLocation)rand.Next(0, Enum.GetValues(typeof(StatsPlugin.IW4Info.HitLocation)).Length - 1)).ToString(), // Hit Location
- };
- }
-
- var _event = Event.ParseEventString(eventLine, S);
- await S.ExecuteEvent(_event);
- }
- }
- }
- */
- }
-
- public Task OnUnloadAsync() => Task.CompletedTask;
- }
-}
-#endif
\ No newline at end of file
diff --git a/Plugins/Tests/ServerTests.cs b/Plugins/Tests/ServerTests.cs
new file mode 100644
index 000000000..c62138f90
--- /dev/null
+++ b/Plugins/Tests/ServerTests.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Tests
+{
+ class ServerTests
+ {
+ }
+}
diff --git a/Plugins/Tests/Tests.csproj b/Plugins/Tests/Tests.csproj
index 96a2628c9..254d60977 100644
--- a/Plugins/Tests/Tests.csproj
+++ b/Plugins/Tests/Tests.csproj
@@ -11,16 +11,18 @@
TRACE;DEBUG;NETCOREAPP2_0
-
-
-
+
+
+
+
+
-
+
diff --git a/README.md b/README.md
index ce6daae60..7fe4eab41 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
* `{{TOTALPLAYTIME}}` — displays the cumulative play time (in man-hours) on all monitored servers
* `{{VERSION}}` — displays the version of **IW4MAdmin**
* `{{ADMINS}}` — displays the currently connected and *unmasked* privileged users online
-* `{{NEXTMAP}} &dmash; displays the next map in rotation
+* `{{NEXTMAP}}` — displays the next map and gametype in rotation
`GlobalRules`
* Specifies the list of rules that apply to **all** servers`
@@ -120,7 +120,7 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
`Maps`
* Specifies the list of maps for each supported game
* `Name`
- * Specifies the name of the map as returned by the game
+ * Specifies the name of the map as returned by the game (usually the file name sans the file extension)
* `Alias`
* Specifies the display name of the map (as seen while loading in)
___
@@ -181,7 +181,7 @@ All players are identified 5 separate ways
2. `IP` - The player's IP Address
3. `Client ID` - The internal reference to a player, generated by **IW4MAdmin**
4. `Name` - The visible player name as it appears in game
-5. `Client Number` - The slot the client client occupies on the server. The number ranges between 0 and the max number of clients allowed on the server
+5. `Client Number` - The slot the client occupies on a server. (The number ranges between 0 and the max number of clients allowed on the server)
For most commands players are identified by their `Name`
However, if they are currently offline, or their name contains un-typable characters, their `Client ID` must be used
diff --git a/SharedLibraryCore/Database/DatabaseContext.cs b/SharedLibraryCore/Database/DatabaseContext.cs
index ea6844c44..50c3d461b 100644
--- a/SharedLibraryCore/Database/DatabaseContext.cs
+++ b/SharedLibraryCore/Database/DatabaseContext.cs
@@ -43,6 +43,8 @@ namespace SharedLibraryCore.Database
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
$"{Path.DirectorySeparatorChar}{currentPath}" :
currentPath;
+ // todo: fix later
+
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = $"{currentPath}{Path.DirectorySeparatorChar}Database.db".Substring(6) };
var connectionString = connectionStringBuilder.ToString();
var connection = new SqliteConnection(connectionString);
@@ -98,22 +100,28 @@ namespace SharedLibraryCore.Database
// adapted from
// https://aleemkhan.wordpress.com/2013/02/28/dynamically-adding-dbset-properties-in-dbcontext-for-entity-framework-code-first/
-#if !DEBUG
- foreach (string dllPath in Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins"))
-#else
+//#if DEBUG == TRUE
+// // foreach (string dllPath in Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins"))
+//#else
+//todo: fix the debug thingie for entity scanning
IEnumerable directoryFiles;
- try
+
+ string pluginDir = $@"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Debug{Path.DirectorySeparatorChar}netcoreapp2.0{Path.DirectorySeparatorChar}Plugins";
+
+ if (!Directory.Exists(pluginDir))
{
- directoryFiles = Directory.GetFiles($@"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Debug{Path.DirectorySeparatorChar}netcoreapp2.0{Path.DirectorySeparatorChar}Plugins").Where(f => f.Contains(".dll"));
+ pluginDir = $@"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}Plugins";
+
+ if (!Directory.Exists(pluginDir))
+ {
+ pluginDir = Utilities.OperatingDirectory;
+ }
}
- catch (Exception)
- {
- directoryFiles = Directory.GetFiles($@"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}Plugins").Where(f => f.Contains(".dll"));
- }
+ directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.Contains(".dll"));
foreach (string dllPath in directoryFiles)
-#endif
+//#endif
{
Assembly library;
try
diff --git a/SharedLibraryCore/Event.cs b/SharedLibraryCore/Event.cs
index da9606492..8cb6d5733 100644
--- a/SharedLibraryCore/Event.cs
+++ b/SharedLibraryCore/Event.cs
@@ -93,6 +93,7 @@ namespace SharedLibraryCore
queuedEvent.Type != EventType.Connect &&
queuedEvent.Type != EventType.Join &&
queuedEvent.Type != EventType.Quit &&
+ queuedEvent.Type != EventType.Disconnect &&
// we don't care about unknown events
queuedEvent.Origin.NetworkId != 0;
}
diff --git a/SharedLibraryCore/Interfaces/IGameLogReader.cs b/SharedLibraryCore/Interfaces/IGameLogReader.cs
new file mode 100644
index 000000000..5e95f293e
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/IGameLogReader.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharedLibraryCore.Interfaces
+{
+ ///
+ /// represents the abtraction of game log reading
+ ///
+ public interface IGameLogReader
+ {
+ ///
+ /// get new events that have occured since the last poll
+ ///
+ ///
+ ///
+ ///
+ ///
+ ICollection EventsFromLog(Server server, long fileSizeDiff, long startPosition);
+ ///
+ /// how long the log file is
+ ///
+ long Length { get; }
+ ///
+ /// how often to poll the log file
+ ///
+ int UpdateInterval { get; }
+ }
+}
diff --git a/SharedLibraryCore/Localization/Layout.cs b/SharedLibraryCore/Localization/Layout.cs
index 3cbf48725..5ccfc4b4b 100644
--- a/SharedLibraryCore/Localization/Layout.cs
+++ b/SharedLibraryCore/Localization/Layout.cs
@@ -28,7 +28,10 @@ namespace SharedLibraryCore.Localization
get
{
if (!Set.TryGetValue(key, out string value))
- throw new Exception($"Invalid locale key {key}");
+ {
+ // throw new Exception($"Invalid locale key {key}");
+ return $"unknown locale key {key}";
+ }
return value;
}
}
diff --git a/SharedLibraryCore/PluginImporter.cs b/SharedLibraryCore/PluginImporter.cs
index 397f9b163..cf913e3bc 100644
--- a/SharedLibraryCore/PluginImporter.cs
+++ b/SharedLibraryCore/PluginImporter.cs
@@ -15,8 +15,21 @@ namespace SharedLibraryCore.Plugins
public static bool Load(IManager Manager)
{
- string[] dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll");
- string[] scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
+ string pluginDir = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}";
+ string[] dllFileNames = null;
+ string[] scriptFileNames = null;
+
+ if (Directory.Exists(pluginDir))
+ {
+ dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll");
+ scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
+ }
+
+ else
+ {
+ dllFileNames = new string[0];
+ scriptFileNames = new string[0];
+ }
if (dllFileNames.Length == 0 &&
scriptFileNames.Length == 0)
diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs
index c90170efb..39779f730 100644
--- a/SharedLibraryCore/RCon/Connection.cs
+++ b/SharedLibraryCore/RCon/Connection.cs
@@ -40,7 +40,7 @@ namespace SharedLibraryCore.RCon
ILogger Log;
int FailedSends;
int FailedReceives;
- DateTime LastQuery;
+ static DateTime LastQuery;
string response;
ManualResetEvent OnConnected;
diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs
index 753dffa68..35e1f7372 100644
--- a/SharedLibraryCore/Utilities.cs
+++ b/SharedLibraryCore/Utilities.cs
@@ -18,7 +18,7 @@ namespace SharedLibraryCore
{
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
public static Encoding EncodingType;
- public static Localization.Layout CurrentLocalization;
+ public static Localization.Layout CurrentLocalization = new Localization.Layout(new Dictionary());
public static string HttpRequest(string location, string header, string headerValue)
{