8667532d24
more fixes for alias stuff hopefully fix rare bug where client activity cshtml loop goes oob add URLProtocol format to event parsers to allow connecting through webfront
1028 lines
38 KiB
C#
1028 lines
38 KiB
C#
using IW4MAdmin.Application.EventParsers;
|
|
using IW4MAdmin.Application.IO;
|
|
using IW4MAdmin.Application.RconParsers;
|
|
using SharedLibraryCore;
|
|
using SharedLibraryCore.Configuration;
|
|
using SharedLibraryCore.Database.Models;
|
|
using SharedLibraryCore.Dtos;
|
|
using SharedLibraryCore.Exceptions;
|
|
using SharedLibraryCore.Interfaces;
|
|
using SharedLibraryCore.Localization;
|
|
using SharedLibraryCore.Objects;
|
|
using SharedLibraryCore.Services;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
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 Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
|
private GameLogEventDetection LogEvent;
|
|
private DateTime SessionStart;
|
|
public int Id { get; private set; }
|
|
|
|
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg)
|
|
{
|
|
}
|
|
|
|
override public async Task OnClientConnected(EFClient clientFromLog)
|
|
{
|
|
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
|
|
|
|
try
|
|
{
|
|
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.IsBot = clientFromLog.IsBot;
|
|
client.Score = clientFromLog.Score;
|
|
client.Ping = clientFromLog.Ping;
|
|
client.CurrentServer = this;
|
|
|
|
Clients[client.ClientNumber] = client;
|
|
|
|
client.State = ClientState.Connected;
|
|
#if DEBUG == true
|
|
Logger.WriteDebug($"End PreConnect for {client}");
|
|
#endif
|
|
var e = new GameEvent()
|
|
{
|
|
Origin = client,
|
|
Owner = this,
|
|
Type = GameEvent.EventType.Connect
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
await client.OnJoin(client.IPAddress);
|
|
}
|
|
|
|
catch (Exception ex)
|
|
{
|
|
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {clientFromLog}");
|
|
Logger.WriteError(ex.GetExceptionInfo());
|
|
}
|
|
}
|
|
|
|
override public async Task OnClientDisconnected(EFClient client)
|
|
{
|
|
#if DEBUG == true
|
|
if (client.ClientNumber >= 0)
|
|
{
|
|
#endif
|
|
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
|
|
await client.OnDisconnect();
|
|
Clients[client.ClientNumber] = null;
|
|
|
|
var e = new GameEvent()
|
|
{
|
|
Origin = client,
|
|
Owner = this,
|
|
Type = GameEvent.EventType.Disconnect
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
#if DEBUG == true
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public override async Task ExecuteEvent(GameEvent E)
|
|
{
|
|
bool canExecuteCommand = true;
|
|
|
|
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);
|
|
}
|
|
|
|
if (C != null)
|
|
{
|
|
E.Extra = C;
|
|
}
|
|
}
|
|
|
|
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
|
{
|
|
try
|
|
{
|
|
await plugin.OnEventAsync(E, this);
|
|
}
|
|
catch (AuthorizationException e)
|
|
{
|
|
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
|
canExecuteCommand = false;
|
|
}
|
|
catch (Exception Except)
|
|
{
|
|
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
|
|
Logger.WriteDebug(Except.GetExceptionInfo());
|
|
}
|
|
}
|
|
|
|
// hack: this prevents commands from getting executing that 'shouldn't' be
|
|
if (E.Type == GameEvent.EventType.Command &&
|
|
E.Extra != null &&
|
|
(canExecuteCommand ||
|
|
E.Origin?.Level == EFClient.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<bool> ProcessEvent(GameEvent E)
|
|
{
|
|
if (E.Type == GameEvent.EventType.ChangePermission)
|
|
{
|
|
var newPermission = (EFClient.Permission)E.Extra;
|
|
|
|
if (newPermission < Permission.Moderator)
|
|
{
|
|
// remove banned or demoted privileged user
|
|
Manager.GetPrivilegedClients().Remove(E.Target.ClientId);
|
|
}
|
|
|
|
else
|
|
{
|
|
Manager.GetPrivilegedClients()[E.Target.ClientId] = E.Target;
|
|
}
|
|
|
|
await Manager.GetClientService().UpdateLevel((Permission)E.Extra, E.Target, E.Origin);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
|
|
|
|
// they're already connected
|
|
if (existingClient != null)
|
|
{
|
|
Logger.WriteWarning($"detected preconnect for {E.Origin}, but they are already connected");
|
|
return false;
|
|
}
|
|
|
|
CONNECT:
|
|
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;
|
|
await OnClientConnected(E.Origin);
|
|
|
|
ChatHistory.Add(new ChatInfo()
|
|
{
|
|
Name = E.Origin.Name,
|
|
Message = "CONNECTED",
|
|
Time = DateTime.UtcNow
|
|
});
|
|
|
|
if (E.Origin.Level > EFClient.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 connecteding but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
|
|
await OnClientDisconnected(Clients[E.Origin.ClientNumber]);
|
|
goto CONNECT;
|
|
}
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.Flag)
|
|
{
|
|
// todo: maybe move this to a seperate function
|
|
Penalty newPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Flag,
|
|
Expires = DateTime.UtcNow,
|
|
Offender = E.Target,
|
|
Offense = E.Data,
|
|
Punisher = 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 Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Unflag,
|
|
Expires = DateTime.UtcNow,
|
|
Offender = E.Target,
|
|
Offense = E.Data,
|
|
Punisher = E.Origin,
|
|
When = DateTime.UtcNow,
|
|
Link = E.Target.AliasLink
|
|
};
|
|
|
|
await Manager.GetPenaltyService().Create(unflagPenalty);
|
|
E.Target.SetLevel(Permission.User, E.Origin);
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.Report)
|
|
{
|
|
Reports.Add(new Report()
|
|
{
|
|
Origin = E.Origin,
|
|
Target = E.Target,
|
|
Reason = E.Data
|
|
});
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.TempBan)
|
|
{
|
|
await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, 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.Origin, isEvade);
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.Unban)
|
|
{
|
|
await Unban(E.Data, E.Target, E.Origin);
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.Kick)
|
|
{
|
|
await Kick(E.Data, E.Target, E.Origin);
|
|
}
|
|
|
|
else if (E.Type == GameEvent.EventType.Warn)
|
|
{
|
|
await Warn(E.Data, E.Target, 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)
|
|
{
|
|
// 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)
|
|
{
|
|
#if DEBUG == true
|
|
Logger.WriteDebug($"Begin PreDisconnect for {client}");
|
|
#endif
|
|
await OnClientDisconnected(client);
|
|
#if DEBUG == true
|
|
Logger.WriteDebug($"End PreDisconnect for {client}");
|
|
#endif
|
|
}
|
|
|
|
else
|
|
{
|
|
Logger.WriteWarning($"Client {E.Origin} detected as disconnecting, but could not find them in the player list");
|
|
Logger.WriteDebug($"Expected {E.Origin} but found {GetClientsAsList().FirstOrDefault(_client => _client.ClientNumber == E.Origin.ClientNumber)}");
|
|
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)
|
|
{
|
|
E.Data = E.Data.StripColors();
|
|
|
|
if (E.Data.Length > 0)
|
|
{
|
|
// this may be a fix for a hard to reproduce null exception error
|
|
lock (ChatHistory)
|
|
{
|
|
ChatHistory.Add(new ChatInfo()
|
|
{
|
|
Name = E.Origin.Name,
|
|
Message = E.Data ?? "NULL",
|
|
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();
|
|
|
|
if (dict == null)
|
|
{
|
|
Logger.WriteWarning("Map change event response doesn't have any data");
|
|
}
|
|
|
|
else
|
|
{
|
|
Gametype = dict["gametype"].StripColors();
|
|
Hostname = dict["hostname"]?.StripColors();
|
|
|
|
string mapname = dict["mapname"]?.StripColors() ?? CurrentMap.Name;
|
|
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();
|
|
MaxClients = int.Parse(dict["sv_maxclients"]);
|
|
|
|
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...");
|
|
SessionStart = DateTime.UtcNow;
|
|
}
|
|
|
|
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 = Clients.FirstOrDefault(_client => _client.Equals(origin));
|
|
|
|
if (client != null)
|
|
{
|
|
client.Ping = origin.Ping;
|
|
client.Score = origin.Score;
|
|
|
|
// update their IP if it hasn't been set yet
|
|
if (client.IPAddress == null && !client.IsBot)
|
|
{
|
|
await client.OnJoin(origin.IPAddress);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// lists the connecting and disconnecting clients via RCon response
|
|
/// array index 0 = connecting clients
|
|
/// array index 1 = disconnecting clients
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
async Task<IList<EFClient>[]> PollPlayersAsync()
|
|
{
|
|
#if DEBUG
|
|
var now = DateTime.Now;
|
|
#endif
|
|
var currentClients = GetClientsAsList();
|
|
var polledClients = (await this.GetStatusAsync()).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
|
|
Throttled = false;
|
|
|
|
var disconnectingClients = currentClients.Except(polledClients);
|
|
var connectingClients = polledClients.Except(currentClients);
|
|
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
|
|
|
|
return new List<EFClient>[]
|
|
{
|
|
connectingClients.ToList(),
|
|
disconnectingClients.ToList(),
|
|
updatedClients.ToList()
|
|
};
|
|
}
|
|
|
|
DateTime start = DateTime.Now;
|
|
DateTime playerCountStart = DateTime.Now;
|
|
DateTime lastCount = DateTime.Now;
|
|
|
|
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
|
{
|
|
try
|
|
{
|
|
#region SHUTDOWN
|
|
if (Manager.ShutdownRequested())
|
|
{
|
|
foreach (var client in GetClientsAsList())
|
|
{
|
|
var e = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.PreDisconnect,
|
|
Origin = client,
|
|
Owner = this,
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
await e.WaitAsync();
|
|
}
|
|
|
|
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
|
{
|
|
await plugin.OnUnloadAsync();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
try
|
|
{
|
|
var polledClients = await PollPlayersAsync();
|
|
var waiterList = new List<GameEvent>();
|
|
|
|
foreach (var disconnectingClient in polledClients[1])
|
|
{
|
|
if (disconnectingClient.State == ClientState.Disconnecting)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var e = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.PreDisconnect,
|
|
Origin = disconnectingClient,
|
|
Owner = this
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
// wait until the disconnect event is complete
|
|
// because we don't want to try to fill up a slot that's not empty yet
|
|
waiterList.Add(e);
|
|
}
|
|
// wait for all the disconnect tasks to finish
|
|
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
|
|
|
|
waiterList.Clear();
|
|
// 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))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var e = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.PreConnect,
|
|
Origin = client,
|
|
Owner = this
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
waiterList.Add(e);
|
|
}
|
|
|
|
// wait for all the connect tasks to finish
|
|
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
|
|
|
|
waiterList.Clear();
|
|
// these are the clients that have updated
|
|
foreach (var client in polledClients[2])
|
|
{
|
|
var e = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.Update,
|
|
Origin = client,
|
|
Owner = this
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(e);
|
|
waiterList.Add(e);
|
|
}
|
|
|
|
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
|
|
|
|
if (ConnectionErrors > 0)
|
|
{
|
|
Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}");
|
|
|
|
var _event = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.ConnectionRestored,
|
|
Owner = this,
|
|
Origin = Utilities.IW4MAdminClient(this),
|
|
Target = Utilities.IW4MAdminClient(this)
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(_event);
|
|
|
|
Throttled = false;
|
|
}
|
|
|
|
ConnectionErrors = 0;
|
|
LastPoll = DateTime.Now;
|
|
}
|
|
|
|
catch (NetworkException e)
|
|
{
|
|
ConnectionErrors++;
|
|
if (ConnectionErrors == 3)
|
|
{
|
|
Logger.WriteError($"{e.Message} {IP}:{Port}, {loc["SERVER_ERROR_POLLING"]}");
|
|
Logger.WriteDebug($"Internal Exception: {e.Data["internal_exception"]}");
|
|
|
|
var _event = new GameEvent()
|
|
{
|
|
Type = GameEvent.EventType.ConnectionLost,
|
|
Owner = this,
|
|
Origin = Utilities.IW4MAdminClient(this),
|
|
Target = Utilities.IW4MAdminClient(this),
|
|
Extra = e,
|
|
Data = ConnectionErrors.ToString()
|
|
};
|
|
|
|
Manager.GetEventHandler().AddEvent(_event);
|
|
|
|
Throttled = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
LastMessage = DateTime.Now - start;
|
|
lastCount = DateTime.Now;
|
|
|
|
// update the player history
|
|
if ((lastCount - playerCountStart).TotalMinutes >= SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval)
|
|
{
|
|
while (ClientHistory.Count > ((60 / SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
|
|
{
|
|
ClientHistory.Dequeue();
|
|
}
|
|
|
|
ClientHistory.Enqueue(new SharedLibraryCore.Helpers.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;
|
|
}
|
|
|
|
// this one is ok
|
|
catch (ServerException e)
|
|
{
|
|
if (e is NetworkException)
|
|
{
|
|
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
|
|
Logger.WriteDebug(e.GetExceptionInfo());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
catch (Exception E)
|
|
{
|
|
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {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 ?? new BaseRConParser();
|
|
EventParser = EventParser ?? new BaseEventParser();
|
|
|
|
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)
|
|
{
|
|
RconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value) ?? RconParser;
|
|
EventParser = Manager.AdditionalEventParsers.FirstOrDefault(_parser => _parser.Version == version.Value) ?? EventParser;
|
|
Version = RconParser.Version;
|
|
}
|
|
|
|
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<string>("mapname")).Value :
|
|
infoResponse["mapname"];
|
|
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
|
|
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
|
|
infoResponse == null ?
|
|
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
|
|
Convert.ToInt32(infoResponse["sv_maxclients"]);
|
|
var gametype = infoResponse == null ?
|
|
(await this.GetDvarAsync<string>("g_gametype")).Value :
|
|
infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First();
|
|
var basepath = await this.GetDvarAsync<string>("fs_basepath");
|
|
var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ?
|
|
(await this.GetDvarAsync<string>("fs_game")).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");
|
|
|
|
WorkingDirectory = basepath.Value;
|
|
|
|
try
|
|
{
|
|
var website = await this.GetDvarAsync<string>("_website");
|
|
Website = website.Value;
|
|
}
|
|
|
|
catch (DvarException)
|
|
{
|
|
Website = loc["SERVER_WEBSITE_GENERIC"];
|
|
}
|
|
|
|
InitializeMaps();
|
|
|
|
this.Hostname = hostname.StripColors();
|
|
this.CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
|
|
this.MaxClients = maxplayers;
|
|
this.FSGame = game;
|
|
this.Gametype = gametype;
|
|
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
|
|
|
if ((logsync.Value == 0 || logfile.Value == string.Empty) && RconParser.CanGenerateLogPath)
|
|
{
|
|
// this DVAR isn't set until the a map is loaded
|
|
await this.SetDvarAsync("logfile", 2);
|
|
await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4
|
|
//await this.SetDvarAsync("g_log", "games_mp.log");
|
|
Logger.WriteWarning("Game log file not properly initialized, restarting map...");
|
|
await this.ExecuteCommandAsync("map_restart");
|
|
logfile = await this.GetDvarAsync<string>("g_log");
|
|
}
|
|
|
|
CustomCallback = await ScriptLoaded();
|
|
|
|
// they've manually specified the log path
|
|
if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
|
{
|
|
LogPath = ServerConfig.ManualLogPath;
|
|
}
|
|
|
|
else
|
|
{
|
|
string mainPath = EventParser.Configuration.GameDirectory;
|
|
|
|
LogPath = string.IsNullOrEmpty(game) ?
|
|
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile?.Value}" :
|
|
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game?.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile?.Value}";
|
|
|
|
// fix wine drive name mangling
|
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
LogPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", "");
|
|
}
|
|
|
|
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
|
|
{
|
|
Logger.WriteError($"{LogPath} {loc["SERVER_ERROR_DNE"]}");
|
|
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {LogPath}");
|
|
}
|
|
}
|
|
|
|
LogEvent = new GameLogEventDetection(this, LogPath, ServerConfig.GameLogServerUrl);
|
|
Logger.WriteInfo($"Log file is {LogPath}");
|
|
|
|
_ = Task.Run(() => LogEvent.PollForChanges());
|
|
#if !DEBUG
|
|
Broadcast(loc["BROADCAST_ONLINE"]);
|
|
#endif
|
|
}
|
|
|
|
protected override async Task Warn(String Reason, EFClient Target, EFClient Origin)
|
|
{
|
|
// ensure player gets warned if command not performed on them in game
|
|
if (Target.ClientNumber < 0)
|
|
{
|
|
var ingameClient = Manager.GetActiveClients()
|
|
.FirstOrDefault(c => c.ClientId == Target.ClientId);
|
|
|
|
if (ingameClient != null)
|
|
{
|
|
await Warn(Reason, ingameClient, Origin);
|
|
return;
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
if (Target.Warnings >= 4)
|
|
{
|
|
Target.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
|
|
return;
|
|
}
|
|
|
|
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
|
|
Target.CurrentServer.Broadcast(message);
|
|
}
|
|
|
|
Penalty newPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Warning,
|
|
Expires = DateTime.UtcNow,
|
|
Offender = Target,
|
|
Punisher = Origin,
|
|
Offense = Reason,
|
|
Link = Target.AliasLink
|
|
};
|
|
|
|
await Manager.GetPenaltyService().Create(newPenalty);
|
|
}
|
|
|
|
protected override async Task Kick(String Reason, EFClient Target, EFClient Origin)
|
|
{
|
|
// ensure player gets kicked if command not performed on them in game
|
|
if (Target.ClientNumber < 0)
|
|
{
|
|
var ingameClient = Manager.GetActiveClients()
|
|
.FirstOrDefault(c => c.ClientId == Target.ClientId);
|
|
|
|
if (ingameClient != null)
|
|
{
|
|
await Kick(Reason, ingameClient, Origin);
|
|
return;
|
|
}
|
|
}
|
|
#if !DEBUG
|
|
else
|
|
{
|
|
string formattedKick = String.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
|
|
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
|
|
}
|
|
#endif
|
|
|
|
#if DEBUG
|
|
await Target.CurrentServer.OnClientDisconnected(Target);
|
|
#endif
|
|
|
|
var newPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Kick,
|
|
Expires = DateTime.UtcNow,
|
|
Offender = Target,
|
|
Offense = Reason,
|
|
Punisher = Origin,
|
|
Link = Target.AliasLink
|
|
};
|
|
|
|
await Manager.GetPenaltyService().Create(newPenalty);
|
|
}
|
|
|
|
protected override async Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin)
|
|
{
|
|
// ensure player gets banned if command not performed on them in game
|
|
if (Target.ClientNumber < 0)
|
|
{
|
|
var ingameClient = Manager.GetActiveClients()
|
|
.FirstOrDefault(c => c.ClientId == Target.ClientId);
|
|
|
|
if (ingameClient != null)
|
|
{
|
|
await TempBan(Reason, length, ingameClient, Origin);
|
|
return;
|
|
}
|
|
}
|
|
#if !DEBUG
|
|
else
|
|
{
|
|
string formattedKick = String.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
|
|
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
|
|
}
|
|
#else
|
|
await Target.CurrentServer.OnClientDisconnected(Target);
|
|
#endif
|
|
|
|
Penalty newPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.TempBan,
|
|
Expires = DateTime.UtcNow + length,
|
|
Offender = Target,
|
|
Offense = Reason,
|
|
Punisher = Origin,
|
|
Link = Target.AliasLink
|
|
};
|
|
|
|
await Manager.GetPenaltyService().Create(newPenalty);
|
|
}
|
|
|
|
override protected async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
|
|
{
|
|
// ensure player gets banned if command not performed on them in game
|
|
if (targetClient.ClientNumber < 0)
|
|
{
|
|
EFClient ingameClient = null;
|
|
|
|
ingameClient = Manager.GetServers()
|
|
.Select(s => s.GetClientsAsList())
|
|
.FirstOrDefault(l => l.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) != null)
|
|
?.First(c => c.ClientId == targetClient.ClientId);
|
|
|
|
if (ingameClient != null)
|
|
{
|
|
await Ban(reason, ingameClient, originClient, isEvade);
|
|
return;
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
|
|
#if !DEBUG
|
|
string formattedString = String.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7({loc["SERVER_BAN_APPEAL"]} {Website})^7");
|
|
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
|
|
#else
|
|
await targetClient.CurrentServer.OnClientDisconnected(targetClient);
|
|
#endif
|
|
}
|
|
|
|
Penalty newPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Ban,
|
|
Expires = null,
|
|
Offender = targetClient,
|
|
Offense = reason,
|
|
Punisher = originClient,
|
|
Link = targetClient.AliasLink,
|
|
AutomatedOffense = originClient.AdministeredPenalties?.FirstOrDefault()?.AutomatedOffense,
|
|
IsEvadedOffense = isEvade
|
|
};
|
|
|
|
await Manager.GetPenaltyService().Create(newPenalty);
|
|
targetClient.SetLevel(Permission.Banned, originClient);
|
|
}
|
|
|
|
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
|
|
{
|
|
var unbanPenalty = new Penalty()
|
|
{
|
|
Type = Penalty.PenaltyType.Unban,
|
|
Expires = null,
|
|
Offender = Target,
|
|
Offense = reason,
|
|
Punisher = Origin,
|
|
When = DateTime.UtcNow,
|
|
Active = true,
|
|
Link = Target.AliasLink
|
|
};
|
|
|
|
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId, Origin);
|
|
await Manager.GetPenaltyService().Create(unbanPenalty);
|
|
Target.SetLevel(Permission.User, Origin);
|
|
}
|
|
|
|
override public void InitializeTokens()
|
|
{
|
|
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
|
|
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
|
|
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s)));
|
|
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s))));
|
|
}
|
|
}
|
|
}
|