IW4M-Admin/Application/IW4MServer.cs
RaidMax 0cabf6f8a3 only fix double forward slash characters (instead of single) when sending messages
retry kicks on banned players if they're banned from webfront, but don't actually get kicked because the game doesn't process the command (looking at you T6)
allow capturing chat messages for names spoofed to an empty string
make sure mostkills uses days not month for cutoff
2020-05-16 20:55:18 -05:00

1303 lines
50 KiB
C#

using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin
{
public class IW4MServer : Server
{
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup;
private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0;
public int Id { get; private set; }
public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
{
_translationLookup = lookup;
}
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog)
{
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
// first time client is connecting to server
if (client == null)
{
Logger.WriteDebug($"Client {clientFromLog} first time connecting");
clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog);
}
/// this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
{
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
Logger.WriteInfo($"Client {client} connected...");
// Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping;
client.CurrentServer = this;
client.State = ClientState.Connecting;
Clients[client.ClientNumber] = client;
#if DEBUG == true
Logger.WriteDebug($"End PreConnect for {client}");
#endif
var e = new GameEvent()
{
Origin = client,
Owner = this,
Type = GameEvent.EventType.Connect
};
Manager.AddEvent(e);
return client;
}
override public async Task OnClientDisconnected(EFClient client)
{
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
{
Logger.WriteInfo($"{client} disconnecting, but they are not connected");
return;
}
#if DEBUG == true
if (client.ClientNumber >= 0)
{
#endif
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
Clients[client.ClientNumber] = null;
await client.OnDisconnect();
var e = new GameEvent()
{
Origin = client,
Owner = this,
Type = GameEvent.EventType.Disconnect
};
Manager.AddEvent(e);
#if DEBUG == true
}
#endif
}
public override async Task ExecuteEvent(GameEvent E)
{
if (E == null)
{
Logger.WriteError("Received NULL event");
return;
}
if (E.IsBlocking)
{
await E.Origin?.Lock();
}
bool canExecuteCommand = true;
Exception lastException = null;
try
{
if (!await ProcessEvent(E))
{
return;
}
Command C = null;
if (E.Type == GameEvent.EventType.Command)
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E);
}
catch (CommandException e)
{
Logger.WriteInfo(e.Message);
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (C != null)
{
E.Extra = C;
}
}
try
{
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
if (loginPlugin != null)
{
await loginPlugin.OnEventAsync(E, this);
}
}
catch (AuthorizationException e)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
await command.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins.Where(_plugin => _plugin.Name != "Login").Select(async _plugin =>
{
try
{
// we don't want to run the events on parser plugins
if (_plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
{
return;
}
using (var tokenSource = new CancellationTokenSource())
{
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
await (_plugin.OnEventAsync(E, this)).WithWaitCancellation(tokenSource.Token);
}
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{_plugin.Name}]");
Logger.WriteDebug(Except.GetExceptionInfo());
}
});
if (pluginTasks.Count() > 0)
{
await Task.WhenAny(pluginTasks);
}
}
catch (Exception e)
{
lastException = e;
if (E.Origin != null && E.Type == GameEvent.EventType.Command)
{
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
}
}
finally
{
if (E.IsBlocking)
{
E.Origin?.Unlock();
}
if (lastException != null)
{
bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
if (notifyDisconnects || (!notifyDisconnects && lastException as NetworkException == null))
{
throw lastException;
}
}
}
}
/// <summary>
/// Perform the server specific tasks when an event occurs
/// </summary>
/// <param name="E"></param>
/// <returns></returns>
override protected async Task<bool> ProcessEvent(GameEvent E)
{
#if DEBUG
Logger.WriteDebug($"processing event of type {E.Type}");
#endif
if (E.Type == GameEvent.EventType.ConnectionLost)
{
var exception = E.Extra as Exception;
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Logger.WriteError(exception.Message);
if (exception.Data["internal_exception"] != null)
{
Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}");
}
}
Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate");
Throttled = true;
}
if (E.Type == GameEvent.EventType.ConnectionRestored)
{
if (Throttled && !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
}
Logger.WriteInfo("Connection restored to server, so we are no longer throttling the poll rate");
Throttled = false;
}
if (E.Type == GameEvent.EventType.ChangePermission)
{
var newPermission = (Permission)E.Extra;
Logger.WriteInfo($"{E.Origin} is setting {E.Target} to permission level {newPermission}");
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
}
else if (E.Type == GameEvent.EventType.Connect)
{
if (E.Origin.State != ClientState.Connected)
{
E.Origin.State = ClientState.Connected;
E.Origin.LastConnection = DateTime.UtcNow;
E.Origin.Connections += 1;
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "CONNECTED",
Time = DateTime.UtcNow
});
await E.Origin.OnJoin(E.Origin.IPAddress);
}
}
else if (E.Type == GameEvent.EventType.PreConnect)
{
// we don't want to track bots in the database at all if ignore bots is requested
if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots)
{
return false;
}
if (E.Origin.CurrentServer == null)
{
Logger.WriteWarning($"preconnecting client {E.Origin} did not have a current server specified");
E.Origin.CurrentServer = this;
}
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
// they're already connected
if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber && !E.Origin.IsBot)
{
Logger.WriteWarning($"detected preconnect for {E.Origin}, but they are already connected");
return false;
}
// this happens for some reason rarely where the client spots get out of order
// possible a connect/reconnect game event before we get to process it here
// it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting)
// bots can have duplicate names which causes conflicting GUIDs
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber && !E.Origin.IsBot)
{
Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping...");
// we need to remove them so the client spots can swap
await OnClientDisconnected(Clients[existingClient.ClientNumber]);
}
if (Clients[E.Origin.ClientNumber] == null)
{
#if DEBUG == true
Logger.WriteDebug($"Begin PreConnect for {E.Origin}");
#endif
// we can go ahead and put them in so that they don't get re added
Clients[E.Origin.ClientNumber] = E.Origin;
try
{
E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin;
}
catch (Exception ex)
{
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}");
Logger.WriteDebug(ex.GetExceptionInfo());
Clients[E.Origin.ClientNumber] = null;
return false;
}
if (E.Origin.Level > Permission.Moderator)
{
E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
}
}
// for some reason there's still a client in the spot
else
{
Logger.WriteWarning($"{E.Origin} is connecting but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
}
}
else if (E.Type == GameEvent.EventType.Flag)
{
DateTime? expires = null;
if (E.Extra is TimeSpan ts)
{
expires = DateTime.UtcNow + ts;
}
// todo: maybe move this to a seperate function
var newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Flag,
Expires = expires,
Offender = E.Target,
Offense = E.Data,
Punisher = E.ImpersonationOrigin ?? E.Origin,
When = DateTime.UtcNow,
Link = E.Target.AliasLink
};
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin);
}
else if (E.Type == GameEvent.EventType.Unflag)
{
var unflagPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Unflag,
Expires = DateTime.UtcNow,
Offender = E.Target,
Offense = E.Data,
Punisher = E.ImpersonationOrigin ?? E.Origin,
When = DateTime.UtcNow,
Link = E.Target.AliasLink
};
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId);
await Manager.GetPenaltyService().Create(unflagPenalty);
}
else if (E.Type == GameEvent.EventType.Report)
{
Reports.Add(new Report()
{
Origin = E.Origin,
Target = E.Target,
Reason = E.Data
});
var newReport = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Report,
Expires = DateTime.UtcNow,
Offender = E.Target,
Offense = E.Message,
Punisher = E.ImpersonationOrigin ?? E.Origin,
Active = true,
When = DateTime.UtcNow,
Link = E.Target.AliasLink
};
await Manager.GetPenaltyService().Create(newReport);
int reportNum = await Manager.GetClientService().GetClientReportCount(E.Target.ClientId);
bool isAutoFlagged = await Manager.GetClientService().IsAutoFlagged(E.Target.ClientId);
if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && !isAutoFlagged)
{
E.Target.Flag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"].FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
}
}
else if (E.Type == GameEvent.EventType.TempBan)
{
await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); ;
}
else if (E.Type == GameEvent.EventType.Ban)
{
bool isEvade = E.Extra != null ? (bool)E.Extra : false;
await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade);
}
else if (E.Type == GameEvent.EventType.Unban)
{
await Unban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
}
else if (E.Type == GameEvent.EventType.Kick)
{
await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
}
else if (E.Type == GameEvent.EventType.Warn)
{
await Warn(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
}
else if (E.Type == GameEvent.EventType.Disconnect)
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "DISCONNECTED",
Time = DateTime.UtcNow
});
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
}
else if (E.Type == GameEvent.EventType.PreDisconnect)
{
bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
if (isPotentialFalseQuit)
{
Logger.WriteInfo($"Receive predisconnect event for {E.Origin}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring");
return false;
}
// predisconnect comes from minimal rcon polled players and minimal log players
// so we need to disconnect the "full" version of the client
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
if (client == null)
{
Logger.WriteWarning($"Client {E.Origin} detected as disconnecting, but could not find them in the player list");
return false;
}
else if (client.State != ClientState.Unknown)
{
#if DEBUG == true
Logger.WriteDebug($"Begin PreDisconnect for {client}");
#endif
await OnClientDisconnected(client);
#if DEBUG == true
Logger.WriteDebug($"End PreDisconnect for {client}");
#endif
return true;
}
else
{
Logger.WriteWarning($"Expected disconnecting client {client} to be in state {ClientState.Connected.ToString()}, but is in state {client.State}");
return false;
}
}
else if (E.Type == GameEvent.EventType.Update)
{
#if DEBUG == true
Logger.WriteDebug($"Begin Update for {E.Origin}");
#endif
await OnClientUpdate(E.Origin);
}
if (E.Type == GameEvent.EventType.Say)
{
if (E.Data?.Length > 0)
{
string message = E.Data;
if (E.Data.IsQuickMessage())
{
try
{
message = Manager.GetApplicationSettings().Configuration()
.QuickMessages
.First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)];
}
catch { }
}
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = message,
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(new TimeSpan(0, 0, 20));
if (dict == null)
{
Logger.WriteWarning("Map change event response doesn't have any data");
}
else
{
Gametype = dict["gametype"];
Hostname = dict["hostname"];
string mapname = dict["mapname"] ?? CurrentMap.Name;
UpdateMap(mapname);
}
}
else
{
var dict = (Dictionary<string, string>)E.Extra;
Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]);
string mapname = dict["mapname"];
UpdateMap(mapname);
}
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
}
if (E.Type == GameEvent.EventType.MapEnd)
{
Logger.WriteInfo("Game ending...");
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
}
if (E.Type == GameEvent.EventType.Tell)
{
await Tell(E.Message, E.Target);
}
if (E.Type == GameEvent.EventType.Broadcast)
{
#if DEBUG == false
// this is a little ugly but I don't want to change the abstract class
if (E.Data != null)
{
await E.Owner.ExecuteCommandAsync(E.Data);
}
#endif
}
lock (ChatHistory)
{
while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
{
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();
}
return true;
}
private async Task OnClientUpdate(EFClient origin)
{
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(origin));
if (client == null)
{
Logger.WriteWarning($"{origin} expected to exist in client list for update, but they do not");
return;
}
client.Ping = origin.Ping;
client.Score = origin.Score;
// update their IP if it hasn't been set yet
if (client.IPAddress == null &&
!client.IsBot &&
client.State == ClientState.Connected)
{
try
{
await client.OnJoin(origin.IPAddress);
}
catch (Exception e)
{
Logger.WriteWarning($"Could not execute on join for {origin}");
Logger.WriteDebug(e.GetExceptionInfo());
}
}
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
client.Level == Permission.Banned)
{
Logger.WriteWarning($"{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...");
await client.CanConnect(client.IPAddress);
}
}
/// <summary>
/// lists the connecting and disconnecting clients via RCon response
/// array index 0 = connecting clients
/// array index 1 = disconnecting clients
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<IList<EFClient>[]> PollPlayersAsync()
{
#if DEBUG
var now = DateTime.Now;
#endif
var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync());
var polledClients = statusResponse.Item1.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
{
polledClients = polledClients.Where(c => !c.IsBot);
}
#if DEBUG
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
#endif
var disconnectingClients = currentClients.Except(polledClients);
var connectingClients = polledClients.Except(currentClients);
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2);
return new List<EFClient>[]
{
connectingClients.ToList(),
disconnectingClients.ToList(),
updatedClients.ToList()
};
}
private void UpdateMap(string mapname)
{
if (!string.IsNullOrEmpty(mapname))
{
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
{
Alias = mapname,
Name = mapname
};
}
}
private async Task ShutdownInternal()
{
foreach (var client in GetClientsAsList())
{
await client.OnDisconnect();
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Owner = this,
Origin = client
};
Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
}
foreach (var plugin in Manager.Plugins)
{
await plugin.OnUnloadAsync();
}
}
DateTime start = DateTime.Now;
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{
bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
try
{
if (cts.IsCancellationRequested)
{
await ShutdownInternal();
return true;
}
try
{
#if DEBUG
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue)
{
return true;
}
#endif
var polledClients = await PollPlayersAsync();
foreach (var disconnectingClient in polledClients[1])
{
disconnectingClient.CurrentServer = this;
var e = new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient,
Owner = this,
Source = GameEvent.EventSource.Status
};
Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken);
}
// this are our new connecting clients
foreach (var client in polledClients[0])
{
// note: this prevents players in ZMBI state from being registered with no name
if (string.IsNullOrEmpty(client.Name) || (client.Ping == 999 && !client.IsBot))
{
continue;
}
client.CurrentServer = this;
var e = new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Origin = client,
Owner = this,
IsBlocking = true,
Source = GameEvent.EventSource.Status
};
Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken);
}
// these are the clients that have updated
foreach (var client in polledClients[2])
{
client.CurrentServer = this;
var e = new GameEvent()
{
Type = GameEvent.EventType.Update,
Origin = client,
Owner = this
};
Manager.AddEvent(e);
}
if (ConnectionErrors > 0)
{
var _event = new GameEvent()
{
Type = GameEvent.EventType.ConnectionRestored,
Owner = this,
Origin = Utilities.IW4MAdminClient(this),
Target = Utilities.IW4MAdminClient(this)
};
Manager.AddEvent(_event);
}
ConnectionErrors = 0;
LastPoll = DateTime.Now;
}
catch (NetworkException e)
{
ConnectionErrors++;
if (ConnectionErrors == 3)
{
var _event = new GameEvent()
{
Type = GameEvent.EventType.ConnectionLost,
Owner = this,
Origin = Utilities.IW4MAdminClient(this),
Target = Utilities.IW4MAdminClient(this),
Extra = e,
Data = ConnectionErrors.ToString()
};
Manager.AddEvent(_event);
}
return true;
}
LastMessage = DateTime.Now - start;
lastCount = DateTime.Now;
// update the player history
if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval)
{
while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
{
ClientHistory.Dequeue();
}
ClientHistory.Enqueue(new PlayerHistory(ClientNum));
playerCountStart = DateTime.Now;
}
// send out broadcast messages
if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod
&& BroadcastMessages.Count > 0
&& ClientNum > 0)
{
string[] messages = (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(Environment.NewLine);
foreach (string message in messages)
{
Broadcast(message);
}
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
start = DateTime.Now;
}
return true;
}
catch (TaskCanceledException)
{
await ShutdownInternal();
return true;
}
// this one is ok
catch (ServerException e)
{
if (e is NetworkException && !Throttled && notifyDisconnects)
{
Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
Logger.WriteDebug(e.GetExceptionInfo());
}
else
{
Logger.WriteError(e.Message);
}
return false;
}
catch (Exception E)
{
Logger.WriteError(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
Logger.WriteDebug(E.GetExceptionInfo());
return false;
}
}
public async Task Initialize()
{
RconParser = Manager.AdditionalRConParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion);
EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RemoteConnection.SetConfiguration(RconParser.Configuration);
var version = await this.GetDvarAsync<string>("version");
Version = version.Value;
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
if (GameName == Game.UKN)
{
GameName = RconParser.GameName;
}
if (version?.Value?.Length != 0)
{
var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value);
RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration;
EventParser = Manager.AdditionalEventParsers.FirstOrDefault(_parser => _parser.Version == version.Value) ?? EventParser;
Version = RconParser.Version;
}
// these T7 specific things aren't ideal , but it's a quick fix
var svRunning = await this.GetDvarAsync("sv_running", GameName == Game.T7 ? "1" : null);
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{
throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]);
}
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
// this is normally slow, but I'm only doing it because different games have different prefixes
var hostname = infoResponse == null ?
(await this.GetDvarAsync<string>("sv_hostname")).Value :
infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First();
var mapname = infoResponse == null ?
(await this.GetDvarAsync("mapname", "Unknown")).Value :
infoResponse["mapname"];
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
infoResponse == null || !infoResponse.ContainsKey("sv_maxclients") ?
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
Convert.ToInt32(infoResponse["sv_maxclients"]);
var gametype = infoResponse == null ?
(await this.GetDvarAsync("g_gametype", GameName == Game.T7 ? "" : null)).Value :
infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First();
var basepath = await this.GetDvarAsync("fs_basepath", GameName == Game.T7 ? "" : null);
var basegame = await this.GetDvarAsync("fs_basegame", GameName == Game.T7 ? "" : null);
var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ?
(await this.GetDvarAsync("fs_game", GameName == Game.T7 ? "" : null)).Value :
infoResponse["fs_game"];
var logfile = await this.GetDvarAsync<string>("g_log");
var logsync = await this.GetDvarAsync<int>("g_logsync");
var ip = await this.GetDvarAsync<string>("net_ip");
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
{
await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName);
}
try
{
var website = await this.GetDvarAsync<string>("_website");
// this occurs for games that don't give us anything back when
// the dvar is not set
if (string.IsNullOrWhiteSpace(website.Value))
{
throw new DvarException("value is empty");
}
Website = website.Value;
}
catch (DvarException)
{
Website = loc["SERVER_WEBSITE_GENERIC"];
}
InitializeMaps();
WorkingDirectory = basepath.Value;
this.Hostname = hostname;
this.MaxClients = maxplayers;
this.FSGame = game;
this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath)
{
bool needsRestart = false;
if (logsync.Value == 0)
{
await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
}
if (string.IsNullOrWhiteSpace(logfile.Value))
{
logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value);
needsRestart = true;
}
if (needsRestart)
{
Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");
}
// this DVAR isn't set until the a map is loaded
await this.SetDvarAsync("logfile", 2);
}
CustomCallback = await ScriptLoaded();
// they've manually specified the log path
if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath) || !RconParser.CanGenerateLogPath)
{
LogPath = ServerConfig.ManualLogPath;
if (string.IsNullOrEmpty(LogPath) && !RconParser.CanGenerateLogPath)
{
throw new ServerException(loc["SERVER_ERROR_REQUIRES_PATH"].FormatExt(GameName.ToString()));
}
}
else
{
var logInfo = new LogPathGeneratorInfo()
{
BaseGameDirectory = basegame.Value,
BasePathDirectory = basepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game ?? "",
LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
};
LogPath = GenerateLogPath(logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{
Logger.WriteError(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
throw new ServerException(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
}
}
LogEvent = new GameLogEventDetection(this, GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
Logger.WriteInfo($"Log file is {LogPath}");
_ = Task.Run(() => LogEvent.PollForChanges());
#if !DEBUG
Broadcast(loc["BROADCAST_ONLINE"]);
#endif
}
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
{
var logUri = new Uri(logPath);
if (string.IsNullOrEmpty(gameLogServerUrl))
{
return new[] { logUri };
}
else
{
return new[] { new Uri(gameLogServerUrl), logUri };
}
}
public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{
string logPath;
string workingDirectory = logInfo.BasePathDirectory;
bool baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
// we want to see if base game is provided and it 'looks' like a directory
if (baseGameIsDirectory && !baseGameIsRelative)
{
workingDirectory = logInfo.BaseGameDirectory;
}
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory))
{
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
}
else
{
logPath = Path.Combine(workingDirectory, logInfo.ModDirectory, logInfo.LogFile);
}
// fix wine drive name mangling
if (!logInfo.IsWindows)
{
logPath = $"{Path.DirectorySeparatorChar}{Regex.Replace(logPath, @"[A-Z]:(\/|\\)", "")}";
}
return logPath.FixDirectoryCharacters();
}
public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin)
{
// ensure player gets warned if command not performed on them in game
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Warning,
Expires = DateTime.UtcNow,
Offender = targetClient,
Punisher = targetOrigin,
Offense = reason,
Link = targetClient.AliasLink
};
Logger.WriteDebug($"Creating warn penalty for {targetClient}");
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
if (targetClient.Warnings >= 4)
{
targetClient.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
return;
}
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}";
targetClient.CurrentServer.Broadcast(message);
}
}
public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient)
{
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow,
Offender = targetClient,
Offense = Reason,
Punisher = originClient,
Link = targetClient.AliasLink
};
Logger.WriteDebug($"Creating kick penalty for {targetClient}");
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Origin = targetClient,
Owner = this
};
Manager.AddEvent(e);
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
public override async Task TempBan(string Reason, TimeSpan length, EFClient targetClient, EFClient originClient)
{
// ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.TempBan,
Expires = DateTime.UtcNow + length,
Offender = targetClient,
Offense = Reason,
Punisher = originClient,
Link = targetClient.AliasLink
};
Logger.WriteDebug($"Creating tempban penalty for {targetClient}");
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{
// ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
EFPenalty newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Ban,
Expires = null,
Offender = targetClient,
Offense = reason,
Punisher = originClient,
Link = targetClient.AliasLink,
IsEvadedOffense = isEvade
};
Logger.WriteDebug($"Creating ban penalty for {targetClient}");
targetClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
Logger.WriteDebug($"Attempting to kicking newly banned client {targetClient}");
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
}
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
{
var unbanPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Unban,
Expires = DateTime.Now,
Offender = Target,
Offense = reason,
Punisher = Origin,
When = DateTime.UtcNow,
Active = true,
Link = Target.AliasLink
};
Target.SetLevel(Permission.User, Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty);
}
override public void InitializeTokens()
{
Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
Manager.GetMessageTokens().Add(new MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
}
}
}