fixed issue with status response erroring when incorrect length

view angle vector parse fail is now a handled exception
change local host check to byte array to make it faster than comparing string
kick command now requires moderator level or higher
tempban now requires administrator level or higher
hopefully fixed negative SPM bug
pipelined the events and consolidated them to run through GameEventHandler
uniform console colors
This commit is contained in:
RaidMax 2018-04-26 01:13:04 -05:00
parent ece519251a
commit 99390f1f35
26 changed files with 526 additions and 355 deletions

View File

@ -19,6 +19,7 @@
<AssemblyName>IW4MAdmin</AssemblyName>
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,46 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
private Queue<GameEvent> EventQueue;
private IManager Manager;
public GameEventHandler(IManager mgr)
{
EventQueue = new Queue<GameEvent>();
Manager = mgr;
}
public void AddEvent(GameEvent gameEvent)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
#endif
EventQueue.Enqueue(gameEvent);
#if DEBUG
Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue");
#endif
Manager.SetHasEvent();
}
public string[] GetEventOutput()
{
throw new NotImplementedException();
}
public GameEvent GetNextEvent()
{
#if DEBUG
Manager.GetLogger().WriteDebug("Getting next event to be processed");
#endif
return EventQueue.Count > 0 ? EventQueue.Dequeue() : null;
}
}
}

View File

@ -0,0 +1,72 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEvent
{
FileSystemWatcher LogPathWatcher;
Server Server;
long PreviousFileSize;
GameLogReader Reader;
Timer RefreshInfoTimer;
string GameLogFile;
FileInfo Info;
public GameLogEvent(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
RefreshInfoTimer = new Timer((sender) =>
{
var newInfo = new FileInfo(GameLogFile);
if (newInfo.Length - Info?.Length > 0)
LogPathWatcher_Changed(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, "", ""));
Info = newInfo;
}, null, 0, 100);
LogPathWatcher = new FileSystemWatcher()
{
Path = gameLogPath.Replace(gameLogName, ""),
Filter = gameLogName,
NotifyFilter = (NotifyFilters)383,
InternalBufferSize = 4096
};
LogPathWatcher.Changed += LogPathWatcher_Changed;
LogPathWatcher.EnableRaisingEvents = true;
}
~GameLogEvent()
{
LogPathWatcher.EnableRaisingEvents = false;
}
private void LogPathWatcher_Changed(object sender, FileSystemEventArgs e)
{
// retrieve the new file size
long newFileSize = new FileInfo(GameLogFile).Length;
if (PreviousFileSize == 0)
PreviousFileSize = newFileSize;
long fileDiff = newFileSize - PreviousFileSize;
if (fileDiff < 1)
return;
var events = Reader.EventsFromLog(Server, fileDiff);
foreach (var ev in events)
Server.Manager.GetEventHandler().AddEvent(ev);
PreviousFileSize = newFileSize;
}
}
}

View File

@ -0,0 +1,49 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace IW4MAdmin.Application.IO
{
class GameLogReader
{
IEventParser Parser;
string LogFile;
public GameLogReader(string logFile, IEventParser parser)
{
LogFile = logFile;
Parser = parser;
}
public ICollection<GameEvent> EventsFromLog(Server server, long fileSizeDiff)
{
// allocate the bytes for the new log lines
byte[] fileBytes = new byte[fileSizeDiff];
// open the file as a stream
using (var rd = new BinaryReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
{
rd.BaseStream.Seek(rd.BaseStream.Length - fileSizeDiff - 1, SeekOrigin.Begin);
// the difference should be in the range of a int :P
rd.Read(fileBytes, 0, (int)fileSizeDiff);
}
// convert to event line list
string[] logLines = Utilities.EncodingType.GetString(fileBytes).Replace("\r", "").Split('\n');
List<GameEvent> events = new List<GameEvent>();
// parse each line
foreach (string eventLine in logLines)
{
if (eventLine.Length > 0)
events.Add(Parser.GetEvent(server, eventLine));
}
return events;
}
}
}

View File

@ -30,17 +30,8 @@ namespace IW4MAdmin.Application
void Write(string msg, LogType type)
{
string stringType;
try
{
stringType = Utilities.CurrentLocalization.LocalizationSet[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch(KeyNotFoundException)
{
if (!Utilities.CurrentLocalization.LocalizationSet.TryGetValue($"GLOBAL_{type.ToString().ToUpper()}", out string stringType))
stringType = type.ToString();
}
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {stringType}: {msg}";
lock (ThreadLock)

View File

@ -24,6 +24,7 @@ namespace IW4MAdmin.Application
Localization.Configure.Initialize();
var loc = Utilities.CurrentLocalization.LocalizationSet;
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
Version = Math.Round(Version, 2);
@ -40,7 +41,7 @@ namespace IW4MAdmin.Application
ServerManager = ApplicationManager.GetInstance();
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration().ConnectionString))
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration().ConnectionString))
new ContextSeed(db).Seed().Wait();
var api = API.Master.Endpoint.Get();
@ -69,7 +70,7 @@ namespace IW4MAdmin.Application
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
@ -78,7 +79,7 @@ namespace IW4MAdmin.Application
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}]");
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
@ -86,14 +87,14 @@ namespace IW4MAdmin.Application
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}-pr]");
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
}
ServerManager.Init().Wait();
@ -112,13 +113,13 @@ namespace IW4MAdmin.Application
if (ServerManager.Servers.Count == 0)
{
Console.WriteLine("No servers are currently being monitored");
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
continue;
}
Origin.CurrentServer = ServerManager.Servers[0];
GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]);
ServerManager.Servers[0].ExecuteEvent(E);
ServerManager.GetEventHandler().AddEvent(E);
Console.Write('>');
} while (ServerManager.Running);
@ -128,10 +129,6 @@ namespace IW4MAdmin.Application
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
}
ServerManager.Start();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
}
catch (Exception e)
@ -145,6 +142,10 @@ namespace IW4MAdmin.Application
Console.WriteLine(loc["MANAGER_EXIT"]);
Console.ReadKey();
}
ServerManager.Start();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
}
static void CheckDirectories()

View File

@ -41,10 +41,13 @@ namespace IW4MAdmin.Application
PenaltyService PenaltySvc;
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
EventApi Api;
GameEventHandler Handler;
ManualResetEventSlim OnEvent;
Timer StatusUpdateTimer;
#if FTP_LOG
const int UPDATE_FREQUENCY = 700;
#else
const int UPDATE_FREQUENCY = 450;
const int UPDATE_FREQUENCY = 2000;
#endif
private ApplicationManager()
@ -63,6 +66,7 @@ namespace IW4MAdmin.Application
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
StartTime = DateTime.UtcNow;
OnEvent = new ManualResetEventSlim();
}
private void OnCancelKey(object sender, ConsoleCancelEventArgs args)
@ -86,8 +90,18 @@ namespace IW4MAdmin.Application
return Instance ?? (Instance = new ApplicationManager());
}
public void UpdateStatus(object state)
{
foreach (var server in Servers)
{
Task.Run(() => server.ProcessUpdatesAsync(new CancellationToken()));
}
}
public async Task Init()
{
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
#region DATABASE
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new
@ -248,13 +262,8 @@ namespace IW4MAdmin.Application
}
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationSet["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
// this way we can keep track of execution time and see if problems arise.
var Status = new AsyncStatus(ServerInstance, UPDATE_FREQUENCY);
lock (TaskStatuses)
{
TaskStatuses.Add(Status);
}
// add the start event for this server
Handler.AddEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, ServerInstance));
}
catch (ServerException e)
@ -273,6 +282,8 @@ namespace IW4MAdmin.Application
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
// start polling servers
StatusUpdateTimer = new Timer(UpdateStatus, null, 0, 10000);
#endregion
@ -344,38 +355,32 @@ namespace IW4MAdmin.Application
public void Start()
{
Task.Run(() => HeartBeatThread());
while (Running || TaskStatuses.Count > 0)
GameEvent newEvent;
while (Running)
{
for (int i = 0; i < TaskStatuses.Count; i++)
// wait for new event to be added
OnEvent.Wait();
// todo: sequencially or parallelize?
while ((newEvent = Handler.GetNextEvent()) != null)
{
var Status = TaskStatuses[i];
// task is read to be rerun
if (Status.RequestedTask == null || Status.RequestedTask.Status == TaskStatus.RanToCompletion)
try
{
// remove the task when we want to quit and last run has finished
if (!Running)
{
TaskStatuses.RemoveAt(i);
continue;
}
// normal operation
else
{
Status.Update(new Task<bool>(() => { return (Status.Dependant as Server).ProcessUpdatesAsync(Status.GetToken()).Result; }));
if (Status.RunAverage > 1000 + UPDATE_FREQUENCY && !(Status.Dependant as Server).Throttled)
Logger.WriteWarning($"Update task average execution is longer than desired for {(Status.Dependant as Server)} [{Status.RunAverage}ms]");
}
Task.WaitAll(newEvent.Owner.ExecuteEvent(newEvent));
}
if (Status.RequestedTask.Status == TaskStatus.Faulted)
catch (Exception E)
{
Logger.WriteWarning($"Update task for {(Status.Dependant as Server)} faulted, restarting");
Status.Abort();
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationSet["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
}
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
}
Thread.Sleep(UPDATE_FREQUENCY);
// signal that all events have been processed
OnEvent.Reset();
}
#if !DEBUG
foreach (var S in Servers)
@ -388,6 +393,9 @@ namespace IW4MAdmin.Application
public void Stop()
{
Running = false;
// trigger the event processing loop to end
SetHasEvent();
}
public ILogger GetLogger()
@ -417,5 +425,11 @@ namespace IW4MAdmin.Application
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
public IEventApi GetEventApi() => Api;
public bool ShutdownRequested() => !Running;
public IEventHandler GetEventHandler() => Handler;
public void SetHasEvent()
{
OnEvent.Set();
}
}
}

View File

@ -85,6 +85,8 @@ namespace Application.RconParsers
if (Regex.Matches(responseLine, @" *^\d+", RegexOptions.IgnoreCase).Count > 0)
{
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (playerInfo.Length < 4)
continue;
int cID = -1;
int Ping = -1;
Int32.TryParse(playerInfo[2], out Ping);

View File

@ -18,6 +18,7 @@ using SharedLibraryCore.Exceptions;
using Application.Misc;
using Application.RconParsers;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.IO;
namespace IW4MAdmin
{
@ -25,6 +26,8 @@ namespace IW4MAdmin
{
private CancellationToken cts;
private static Dictionary<string, string> loc = Utilities.CurrentLocalization.LocalizationSet;
private GameLogEvent LogEvent;
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { }
@ -180,7 +183,8 @@ namespace IW4MAdmin
Logger.WriteInfo($"Client {player} connecting...");
await ExecuteEvent(new GameEvent(GameEvent.EventType.Connect, "", player, null, this));
Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Connect, "", player, null, this));
if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs &&
@ -208,7 +212,7 @@ namespace IW4MAdmin
Player Leaving = Players[cNum];
Logger.WriteInfo($"Client {Leaving} disconnecting...");
await ExecuteEvent(new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this));
Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this));
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
Leaving.LastConnection = DateTime.UtcNow;
@ -255,16 +259,8 @@ namespace IW4MAdmin
if (C.RequiresTarget || Args.Length > 0)
{
int cNum = -1;
try
{
cNum = Convert.ToInt32(Args[0]);
}
catch (FormatException)
{
}
if (!Int32.TryParse(Args[0], out int cNum))
cNum = -1;
if (Args[0][0] == '@') // user specifying target by database ID
{
@ -359,24 +355,27 @@ namespace IW4MAdmin
public override async Task ExecuteEvent(GameEvent E)
{
//if (Throttled)
// return;
bool canExecuteCommand = true;
await ProcessEvent(E);
Manager.GetEventApi().OnServerEvent(this, E);
foreach (IPlugin P in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
#if !DEBUG
try
#endif
{
if (cts.IsCancellationRequested)
break;
await P.OnEventAsync(E, this);
}
#if !DEBUG
// this happens if a plugin (login) wants to stop commands from executing
catch (AuthorizationException e)
{
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
catch (Exception Except)
{
Logger.WriteError(String.Format("The plugin \"{0}\" generated an error. ( see log )", P.Name));
@ -389,10 +388,168 @@ namespace IW4MAdmin
}
continue;
}
#endif
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command &&
E.Extra != null &&
(canExecuteCommand ||
E.Origin?.Level == Player.Permission.Console))
{
await (((Command)E.Extra).ExecuteAsync(E));
}
}
/// <summary>
/// Perform the server specific tasks when an event occurs
/// </summary>
/// <param name="E"></param>
/// <returns></returns>
override protected async Task ProcessEvent(GameEvent E)
{
if (E.Type == GameEvent.EventType.Connect)
{
// special case for IW5 when connect is from the log
if (E.Extra != null)
{
var logClient = (Player)E.Extra;
var client = (await this.GetStatusAsync())
.Single(c => c.ClientNumber == logClient.ClientNumber &&
c.Name == logClient.Name);
client.NetworkId = logClient.NetworkId;
await AddPlayer(client);
// hack: to prevent plugins from registering it as a real connect
E.Type = GameEvent.EventType.Unknown;
}
else
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "CONNECTED",
Time = DateTime.UtcNow
});
if (E.Origin.Level > Player.Permission.Moderator)
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
}
}
else if (E.Type == GameEvent.EventType.Disconnect)
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "DISCONNECTED",
Time = DateTime.UtcNow
});
}
else if (E.Type == GameEvent.EventType.Script)
{
Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Kill, E.Data, E.Origin, E.Target, this));
}
if (E.Type == GameEvent.EventType.Say && E.Data.Length >= 2)
{
if (E.Data.Substring(0, 1) == "!" ||
E.Data.Substring(0, 1) == "@" ||
E.Origin.Level == Player.Permission.Console)
{
Command C = null;
try
{
C = await ValidateCommand(E);
}
catch (CommandException e)
{
Logger.WriteInfo(e.Message);
}
if (C != null)
{
if (C.RequiresTarget && E.Target == null)
{
Logger.WriteWarning("Requested event (command) requiring target does not have a target!");
}
Manager.GetEventHandler().AddEvent(new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = E.Data,
Origin = E.Origin,
Target = E.Target,
Owner = this,
Extra = C,
Remote = E.Remote,
Message = E.Message
});
}
}
else // Not a command
{
E.Data = E.Data.StripColors();
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = E.Data,
Time = DateTime.UtcNow
});
}
}
if (E.Type == GameEvent.EventType.MapChange)
{
Logger.WriteInfo($"New map loaded - {ClientNum} active players");
// iw4 doesn't log the game info
if (E.Extra == null)
{
var dict = await this.GetInfoAsync();
Gametype = dict["gametype"].StripColors();
Hostname = dict["hostname"].StripColors();
string mapname = dict["mapname"].StripColors();
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
}
else
{
var dict = (Dictionary<string, string>)E.Extra;
Gametype = dict["g_gametype"].StripColors();
Hostname = dict["sv_hostname"].StripColors();
string mapname = dict["mapname"].StripColors();
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
}
}
if (E.Type == GameEvent.EventType.MapEnd)
{
Logger.WriteInfo("Game ending...");
}
//todo: move
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
ChatHistory.RemoveAt(0);
// the last client hasn't fully disconnected yet
// so there will still be at least 1 client left
if (ClientNum < 2)
ChatHistory.Clear();
}
async Task<int> PollPlayersAsync()
{
var now = DateTime.Now;
@ -429,30 +586,16 @@ namespace IW4MAdmin
return CurrentPlayers.Count;
}
long l_size = -1;
String[] lines = new String[8];
String[] oldLines = new String[8];
DateTime start = DateTime.Now;
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
DateTime tickTime = DateTime.Now;
bool firstRun = true;
int count = 0;
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{
this.cts = cts;
//#if DEBUG == false
try
//#endif
{
// first start
if (firstRun)
{
await ExecuteEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, this));
firstRun = false;
}
if ((DateTime.Now - LastPoll).TotalMinutes < 2 && ConnectionErrors >= 1)
return true;
@ -517,44 +660,6 @@ namespace IW4MAdmin
start = DateTime.Now;
}
if (LogFile == null)
return true;
if (l_size != LogFile.Length())
{
lines = l_size != -1 ? await LogFile.Tail(12) : lines;
if (lines != oldLines)
{
l_size = LogFile.Length();
int end = (lines.Length == oldLines.Length) ? lines.Length - 1 : Math.Abs((lines.Length - oldLines.Length)) - 1;
for (count = 0; count < lines.Length; count++)
{
if (lines.Length < 1 && oldLines.Length < 1)
continue;
if (lines[count] == oldLines[oldLines.Length - 1])
continue;
if (lines[count].Length < 10) // it's not a needed line
continue;
else
{
GameEvent event_ = EventParser.GetEvent(this, lines[count]);
if (event_ != null)
{
if (event_.Origin == null)
continue;
await ExecuteEvent(event_);
}
}
}
}
}
oldLines = lines;
l_size = LogFile.Length();
if (Manager.ShutdownRequested())
{
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
@ -565,7 +670,6 @@ namespace IW4MAdmin
}
return true;
}
//#if !DEBUG
catch (NetworkException)
{
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
@ -575,7 +679,6 @@ namespace IW4MAdmin
catch (InvalidOperationException)
{
Logger.WriteWarning("Event could not parsed properly");
Logger.WriteDebug($"Log Line: {lines[count]}");
return false;
}
@ -586,7 +689,6 @@ namespace IW4MAdmin
Logger.WriteDebug("Error Trace: " + E.StackTrace);
return false;
}
//#endif
}
public async Task Initialize()
@ -657,7 +759,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)
@ -671,6 +773,7 @@ namespace IW4MAdmin
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Value.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
}
// hopefully fix wine drive name mangling
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -686,183 +789,18 @@ namespace IW4MAdmin
}
else
{
LogFile = new IFile(logPath);
}
// LogFile = new IFile(logPath);
}
LogEvent = new GameLogEvent(this, logPath, logfile.Value);
Logger.WriteInfo($"Log file is {logPath}");
#if DEBUG
// LogFile = new RemoteFile("https://raidmax.org/IW4MAdmin/getlog.php");
// LogFile = new RemoteFile("https://raidmax.org/IW4MAdmin/getlog.php");
#else
await Broadcast(loc["BROADCAST_ONLINE"]);
#endif
}
//Process any server event
override protected async Task ProcessEvent(GameEvent E)
{
if (E.Type == GameEvent.EventType.Connect)
{
// special case for IW5 when connect is from the log
if (E.Extra != null)
{
var logClient = (Player)E.Extra;
var client = (await this.GetStatusAsync())
.Single(c => c.ClientNumber == logClient.ClientNumber &&
c.Name == logClient.Name);
client.NetworkId = logClient.NetworkId;
await AddPlayer(client);
// hack: to prevent plugins from registering it as a real connect
E.Type = GameEvent.EventType.Unknown;
}
else
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "CONNECTED",
Time = DateTime.UtcNow
});
if (E.Origin.Level > Player.Permission.Moderator)
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
}
}
else if (E.Type == GameEvent.EventType.Disconnect)
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "DISCONNECTED",
Time = DateTime.UtcNow
});
}
else if (E.Type == GameEvent.EventType.Script)
{
await ExecuteEvent(new GameEvent(GameEvent.EventType.Kill, E.Data, E.Origin, E.Target, this));
}
if (E.Type == GameEvent.EventType.Say && E.Data.Length >= 2)
{
if (E.Data.Substring(0, 1) == "!" || E.Data.Substring(0, 1) == "@" || E.Origin.Level == Player.Permission.Console)
{
Command C = null;
try
{
C = await ValidateCommand(E);
}
catch (CommandException e)
{
Logger.WriteInfo(e.Message);
}
if (C != null)
{
if (C.RequiresTarget && E.Target == null)
{
Logger.WriteWarning("Requested event (command) requiring target does not have a target!");
}
try
{
if (!E.Remote && E.Origin.Level != Player.Permission.Console)
{
await ExecuteEvent(new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = string.Empty,
Origin = E.Origin,
Target = E.Target,
Owner = this,
Extra = C,
Remote = E.Remote
});
}
await C.ExecuteAsync(E);
}
catch (AuthorizationException e)
{
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
}
catch (Exception Except)
{
Logger.WriteError(String.Format($"\"{0}\" {loc["SERVER_ERROR_COMMAND_LOG"]}", C.Name));
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
await E.Origin.Tell($"^1{loc["SERVER_ERROR_COMMAND_INGAME"]}");
#if DEBUG
await E.Origin.Tell(Except.Message);
#endif
}
}
}
else // Not a command
{
E.Data = E.Data.StripColors();
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = E.Data,
Time = DateTime.UtcNow
});
}
}
if (E.Type == GameEvent.EventType.MapChange)
{
Logger.WriteInfo($"New map loaded - {ClientNum} active players");
// iw4 doesn't log the game info
if (E.Extra == null)
{
var dict = await this.GetInfoAsync();
Gametype = dict["gametype"].StripColors();
Hostname = dict["hostname"].StripColors();
string mapname = dict["mapname"].StripColors();
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
}
else
{
var dict = (Dictionary<string, string>)E.Extra;
Gametype = dict["g_gametype"].StripColors();
Hostname = dict["sv_hostname"].StripColors();
string mapname = dict["mapname"].StripColors();
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
}
}
if (E.Type == GameEvent.EventType.MapEnd)
{
Logger.WriteInfo("Game ending...");
}
//todo: move
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
ChatHistory.RemoveAt(0);
// the last client hasn't fully disconnected yet
// so there will still be at least 1 client left
if (ClientNum < 2)
ChatHistory.Clear();
}
public override async Task Warn(String Reason, Player Target, Player Origin)
{
// ensure player gets warned if command not performed on them in game

View File

@ -70,12 +70,8 @@ namespace IW4MAdmin.Plugins.Login
Config = cfg.Configuration();
}
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public Task OnUnloadAsync()
{
AuthorizedClients.Clear();
return Utilities.CompletedTask;
}
public Task OnUnloadAsync() => Task.CompletedTask;
}
}

View File

@ -20,7 +20,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
BaseConfigurationHandler<Configuration> Settings;
ConcurrentDictionary<int, Tracking> ProfanityCounts;
IManager Manager;
Task CompletedTask = Task.FromResult(false);
public async Task OnEventAsync(GameEvent E, Server S)
{
@ -87,8 +86,8 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
Manager = manager;
}
public Task OnTickAsync(Server S) => CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public Task OnUnloadAsync() => CompletedTask;
public Task OnUnloadAsync() => Task.CompletedTask;
}
}

View File

@ -187,7 +187,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// get individual client's stats
var clientStats = playerStats[pl.ClientId];
// sync their score
clientStats.SessionScore = pl.Score;
clientStats.SessionScore += (pl.Score - clientStats.LastScore);
// remove the client from the stats dictionary as they're leaving
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2);
@ -213,11 +213,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var statsSvc = ContextThreads[serverId];
Vector3 vDeathOrigin = null;
Vector3 vKillOrigin = null;
Vector3 vViewAngles = null;
try
{
vDeathOrigin = Vector3.Parse(deathOrigin);
vKillOrigin = Vector3.Parse(killOrigin);
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
}
catch (FormatException)
@ -241,7 +243,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Damage = Int32.Parse(damage),
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
ViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(),
ViewAngles = vViewAngles,
TimeOffset = Int64.Parse(offset),
When = DateTime.UtcNow,
IsKillstreakKill = isKillstreakKill[0] != '0',
@ -338,11 +340,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// update the total stats
Servers[serverId].ServerStatistics.TotalKills += 1;
attackerStats.SessionScore = attacker.Score;
victimStats.SessionScore = victim.Score;
attackerStats.SessionScore += (attacker.Score - attackerStats.LastScore);
victimStats.SessionScore += (victim.Score - victimStats.LastScore);
// calculate for the clients
CalculateKill(attackerStats, victimStats);
// this should fix the negative SPM
attackerStats.LastScore = attacker.Score;
victimStats.LastScore = victim.Score;
// show encouragement/discouragement
string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ?
@ -457,7 +462,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
clientStats.LastStatCalculation = DateTime.UtcNow;
clientStats.LastScore = clientStats.SessionScore;
//clientStats.LastScore = clientStats.SessionScore;
return clientStats;
}

View File

@ -253,7 +253,7 @@ namespace IW4MAdmin.Plugins.Stats
Manager = new StatManager(manager);
}
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public async Task OnUnloadAsync()
{

View File

@ -51,8 +51,9 @@ namespace IW4MAdmin.Plugins
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
public async Task OnTickAsync(Server S)
public Task OnTickAsync(Server S)
{
return Task.CompletedTask;
/*
if ((DateTime.Now - Interval).TotalSeconds > 1)
{

View File

@ -72,9 +72,9 @@ namespace IW4MAdmin.Plugins.Welcome
}
}
public Task OnUnloadAsync() => Utilities.CompletedTask;
public Task OnUnloadAsync() => Task.CompletedTask;
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public async Task OnEventAsync(GameEvent E, Server S)
{

View File

@ -32,7 +32,7 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
if ((await (E.Owner.Manager.GetClientService() as Services.ClientService).GetOwners()).Count == 0)
if ((await (E.Owner.Manager.GetClientService() as ClientService).GetOwners()).Count == 0)
{
E.Origin.Level = Player.Permission.Owner;
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationSet["COMMANDS_OWNER_SUCCESS"]);
@ -94,7 +94,7 @@ namespace SharedLibraryCore.Commands
public class CKick : Command
{
public CKick() :
base("kick", Utilities.CurrentLocalization.LocalizationSet["COMMANDS_KICK_DESC"], "k", Player.Permission.Trusted, true, new CommandArgument[]
base("kick", Utilities.CurrentLocalization.LocalizationSet["COMMANDS_KICK_DESC"], "k", Player.Permission.Moderator, true, new CommandArgument[]
{
new CommandArgument()
{
@ -113,7 +113,7 @@ namespace SharedLibraryCore.Commands
{
if (E.Origin.Level > E.Target.Level)
{
await E.Owner.ExecuteEvent(new GameEvent(GameEvent.EventType.Kick, E.Data, E.Origin, E.Target, E.Owner));
E.Owner.Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Kick, E.Data, E.Origin, E.Target, E.Owner));
await E.Target.Kick(E.Data, E.Origin);
await E.Origin.Tell($"^5{E.Target} ^7{Utilities.CurrentLocalization.LocalizationSet["COMMANDS_KICK_SUCCESS"]}");
}
@ -144,7 +144,7 @@ namespace SharedLibraryCore.Commands
public class CTempBan : Command
{
public CTempBan() :
base("tempban", Utilities.CurrentLocalization.LocalizationSet["COMMANDS_TEMPBAN_DESC"], "tb", Player.Permission.Moderator, true, new CommandArgument[]
base("tempban", Utilities.CurrentLocalization.LocalizationSet["COMMANDS_TEMPBAN_DESC"], "tb", Player.Permission.Administrator, true, new CommandArgument[]
{
new CommandArgument()
{
@ -716,7 +716,7 @@ namespace SharedLibraryCore.Commands
};
await E.Owner.Manager.GetPenaltyService().Create(newPenalty);
await E.Owner.ExecuteEvent(new GameEvent(GameEvent.EventType.Flag, E.Data, E.Origin, E.Target, E.Owner));
E.Owner.Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Flag, E.Data, E.Origin, E.Target, E.Owner));
await E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationSet["COMMANDS_FLAG_SUCCESS"]} ^5{E.Target.Name}");
}
@ -770,7 +770,7 @@ namespace SharedLibraryCore.Commands
E.Owner.Reports.Add(new Report(E.Target, E.Origin, E.Data));
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationSet["COMMANDS_REPORT_SUCCESS"]);
await E.Owner.ExecuteEvent(new GameEvent(GameEvent.EventType.Report, E.Data, E.Origin, E.Target, E.Owner));
E.Owner.Manager.GetEventHandler().AddEvent(new GameEvent(GameEvent.EventType.Report, E.Data, E.Origin, E.Target, E.Owner));
await E.Owner.ToAdmins(String.Format("^5{0}^7->^1{1}^7: {2}", E.Origin.Name, E.Target.Name, E.Data));
}
}
@ -1023,7 +1023,8 @@ namespace SharedLibraryCore.Commands
E.Origin.PasswordSalt = hashedPassword[1];
// update the password for the client in privileged
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId] = E.Origin;
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId].Password = hashedPassword[0];
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId].PasswordSalt = hashedPassword[1];
await E.Owner.Manager.GetClientService().Update(E.Origin);
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationSet["COMMANDS_PASSWORD_SUCCESS"]);

View File

@ -2,7 +2,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using SharedLibraryCore.Objects;
namespace SharedLibraryCore
@ -47,10 +47,13 @@ namespace SharedLibraryCore
Origin = O;
Target = T;
Owner = S;
OnProcessed = new ManualResetEventSlim();
}
public GameEvent() { }
public GameEvent()
{
OnProcessed = new ManualResetEventSlim();
}
public EventType Type;
public string Data; // Data is usually the message sent by player
@ -60,5 +63,6 @@ namespace SharedLibraryCore
public Server Owner;
public Boolean Remote = false;
public object Extra { get; set; }
public ManualResetEventSlim OnProcessed { get; private set; }
}
}

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Linq;
namespace SharedLibraryCore
{
@ -27,7 +28,12 @@ namespace SharedLibraryCore
public override long Length()
{
Retrieve();
return FileCache[0].Length;
return FileCache.Sum(l => l.Length);
}
public override Task<string[]> Tail(int lineCount)
{
return Task.FromResult(FileCache);
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// This class handle games events (from log, manual events, etc)
/// </summary>
public interface IEventHandler
{
/// <summary>
/// Add a game event event to the queue to be processed
/// </summary>
/// <param name="gameEvent">Game event</param>
void AddEvent(GameEvent gameEvent);
/// <summary>
/// Get the next event to be processed
/// </summary>
/// <returns>Game event that needs to be processed</returns>
GameEvent GetNextEvent();
/// <summary>
/// If an event has output. Like executing a command wait until it's available
/// </summary>
/// <returns>List of output strings</returns>
string[] GetEventOutput();
}
}

View File

@ -12,6 +12,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="server">server the event occurred on</param>
/// <param name="logLine">single log line string</param>
/// <returns></returns>
/// todo: make this integrate without needing the server
GameEvent GetEvent(Server server, string logLine);
/// <summary>
/// Get game specific folder prefix for log files

View File

@ -23,6 +23,15 @@ namespace SharedLibraryCore.Interfaces
PenaltyService GetPenaltyService();
IDictionary<int, Player> GetPrivilegedClients();
IEventApi GetEventApi();
/// <summary>
/// Get the event handlers
/// </summary>
/// <returns>EventHandler for the manager</returns>
IEventHandler GetEventHandler();
/// <summary>
/// Signal to the manager that event(s) needs to be processed
/// </summary>
void SetHasEvent();
bool ShutdownRequested();
}
}

View File

@ -41,6 +41,7 @@ namespace SharedLibraryCore
PlayerHistory = new Queue<PlayerHistory>();
ChatHistory = new List<ChatInfo>();
NextMessage = 0;
OnEvent = new ManualResetEventSlim();
CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName;
CustomSayName = Manager.GetApplicationSettings().Configuration().CustomSayName;
InitializeTokens();
@ -132,7 +133,7 @@ namespace SharedLibraryCore
await this.ExecuteCommandAsync(formattedMessage);
#else
Logger.WriteVerbose(Message.StripColors());
await Utilities.CompletedTask;
await Task.CompletedTask;
#endif
}
@ -150,7 +151,7 @@ namespace SharedLibraryCore
await this.ExecuteCommandAsync(formattedMessage);
#else
Logger.WriteVerbose($"{Target.ClientNumber}->{Message.StripColors()}");
await Utilities.CompletedTask;
await Task.CompletedTask;
#endif
if (Target.Level == Player.Permission.Console)
@ -308,6 +309,7 @@ namespace SharedLibraryCore
public RCon.Connection RemoteConnection { get; protected set; }
public IRConParser RconParser { get; protected set; }
public IEventParser EventParser { get; set; }
public ManualResetEventSlim OnEvent { get; private set; }
// Internal
protected string IP;

View File

@ -16,7 +16,6 @@ namespace SharedLibraryCore
public static class Utilities
{
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
public static readonly Task CompletedTask = Task.FromResult(false);
public static Encoding EncodingType;
public static Localization.Layout CurrentLocalization;

View File

@ -16,24 +16,37 @@ namespace WebfrontCore.Controllers
protected IManager Manager;
protected readonly DatabaseContext Context;
protected bool Authorized { get; private set; }
protected EFClient User { get; private set; }
protected EFClient Client { get; private set; }
private static byte[] LocalHost = { 127, 0, 0, 1 };
private static string DiscordLink;
public BaseController()
{
var Manager = Program.Manager;
if (Manager.GetApplicationSettings().Configuration().EnableDiscordLink)
{
string inviteLink = Manager.GetApplicationSettings().Configuration().DiscordInviteCode;
if (inviteLink != null)
DiscordLink = inviteLink.Contains("https") ? inviteLink : $"https://discordapp.com/invite/{inviteLink}";
else
DiscordLink = "";
}
}
public override void OnActionExecuting(ActionExecutingContext context)
{
Manager = Program.Manager;
User = User ?? new EFClient()
Client = Client ?? new EFClient()
{
ClientId = -1
};
if (HttpContext.Connection.RemoteIpAddress.ToString() != "127.0.0.1")
if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost))
{
try
{
User.ClientId = Convert.ToInt32(base.User.Claims.First(c => c.Type == ClaimTypes.Sid).Value);
User.Level = (Player.Permission)Enum.Parse(typeof(Player.Permission), base.User.Claims.First(c => c.Type == ClaimTypes.Role).Value);
User.CurrentAlias = new EFAlias() { Name = base.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
Client.ClientId = Convert.ToInt32(base.User.Claims.First(c => c.Type == ClaimTypes.Sid).Value);
Client.Level = (Player.Permission)Enum.Parse(typeof(Player.Permission), User.Claims.First(c => c.Type == ClaimTypes.Role).Value);
Client.CurrentAlias = new EFAlias() { Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
}
catch (InvalidOperationException)
@ -44,24 +57,17 @@ namespace WebfrontCore.Controllers
else
{
User.ClientId = 1;
User.Level = Player.Permission.Console;
User.CurrentAlias = new EFAlias() { Name = "IW4MAdmin" };
Client.ClientId = 1;
Client.Level = Player.Permission.Console;
Client.CurrentAlias = new EFAlias() { Name = "IW4MAdmin" };
}
Authorized = User.ClientId >= 0;
Authorized = Client.ClientId >= 0;
ViewBag.Authorized = Authorized;
ViewBag.Url = Startup.Configuration["Web:Address"];
ViewBag.User = User;
ViewBag.User = Client;
ViewBag.DiscordLink = DiscordLink;
if (Manager.GetApplicationSettings().Configuration().EnableDiscordLink)
{
string inviteLink = Manager.GetApplicationSettings().Configuration().DiscordInviteCode;
if (inviteLink != null)
ViewBag.DiscordLink = inviteLink.Contains("https") ? inviteLink : $"https://discordapp.com/invite/{inviteLink}";
else
ViewBag.DiscordLink = "";
}
base.OnActionExecuting(context);
}
}

View File

@ -32,10 +32,10 @@ namespace WebfrontCore.Controllers
var server = Manager.GetServers().First(s => s.GetHashCode() == serverId);
var client = new Player()
{
ClientId = User.ClientId,
Level = User.Level,
ClientId = Client.ClientId,
Level = Client.Level,
CurrentServer = server,
CurrentAlias = new Alias() { Name = User.Name }
CurrentAlias = new Alias() { Name = Client.Name }
};
var remoteEvent = new GameEvent()
{
@ -46,8 +46,9 @@ namespace WebfrontCore.Controllers
Remote = true
};
await server.ExecuteEvent(remoteEvent);
Manager.GetEventHandler().AddEvent(remoteEvent);
// wait for the event to process
remoteEvent.OnProcessed.Wait();
var response = server.CommandResult.Where(c => c.ClientId == client.ClientId).ToList();
// remove the added command response

View File

@ -33,7 +33,6 @@ a.nav-link {
}
.server-history,
.server-header,
.server-activity,
#mobile_seperator,
.border-bottom {