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