mirror of
https://github.com/tribufu/ServerManagers
synced 2026-05-06 15:17:34 +00:00
Rcon Changes
Rcon Window - minor changes to the player list, online player count and server status. Online Player Count - fixed the online player count to display the correct value.
This commit is contained in:
parent
7c43821691
commit
74bad02b94
13 changed files with 454 additions and 341 deletions
|
|
@ -2607,8 +2607,8 @@
|
|||
<sys:String x:Key="ConsoleStatus_Disconnected">Disconnected</sys:String>
|
||||
<sys:String x:Key="ConsoleStatus_Connected">Connected</sys:String>
|
||||
|
||||
<sys:String x:Key="Player_Join">Player '{0}' joined the game.</sys:String>
|
||||
<sys:String x:Key="Player_Leave">Player '{0}' left the game.</sys:String>
|
||||
<sys:String x:Key="Player_Join">'{0}' joined the game.</sys:String>
|
||||
<sys:String x:Key="Player_Leave">'{0}' left the game.</sys:String>
|
||||
<!--#endregion-->
|
||||
|
||||
<!--#region Open RCON window -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using ArkData;
|
||||
using NLog;
|
||||
using ServerManagerTool.Common.Enums;
|
||||
using ServerManagerTool.Common.Extensions;
|
||||
using ServerManagerTool.Common.Interfaces;
|
||||
using ServerManagerTool.Common.Lib;
|
||||
|
|
@ -24,8 +25,6 @@ namespace ServerManagerTool.Lib
|
|||
{
|
||||
public class ServerRCON : DependencyObject, IAsyncDisposable
|
||||
{
|
||||
public event EventHandler PlayersCollectionUpdated;
|
||||
|
||||
private const int STEAM_UPDATE_INTERVAL = 60;
|
||||
private const int PLAYER_LIST_INTERVAL = 5000;
|
||||
private const int GET_CHAT_INTERVAL = 1000;
|
||||
|
|
@ -38,36 +37,7 @@ namespace ServerManagerTool.Lib
|
|||
public const string RCON_COMMAND_SERVERCHAT = "serverchat";
|
||||
public const string RCON_COMMAND_WILDDINOWIPE = "DestroyWildDinos";
|
||||
|
||||
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
|
||||
public enum ConsoleStatus
|
||||
{
|
||||
Disconnected,
|
||||
Connected,
|
||||
};
|
||||
|
||||
public class ConsoleCommand
|
||||
{
|
||||
public ConsoleStatus status;
|
||||
public string rawCommand;
|
||||
|
||||
public string command;
|
||||
public string args;
|
||||
|
||||
public bool suppressCommand;
|
||||
public bool suppressOutput;
|
||||
public IEnumerable<string> lines = new string[0];
|
||||
};
|
||||
|
||||
private class CommandListener : IDisposable
|
||||
{
|
||||
public Action<ConsoleCommand> Callback { get; set; }
|
||||
public Action<CommandListener> DisposeAction { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAction(this);
|
||||
}
|
||||
}
|
||||
public event EventHandler PlayersCollectionUpdated;
|
||||
|
||||
public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(nameof(Status), typeof(ConsoleStatus), typeof(ServerRCON), new PropertyMetadata(ConsoleStatus.Disconnected));
|
||||
public static readonly DependencyProperty PlayersProperty = DependencyProperty.Register(nameof(Players), typeof(SortableObservableCollection<PlayerInfo>), typeof(ServerRCON), new PropertyMetadata(null));
|
||||
|
|
@ -77,16 +47,18 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
private static readonly char[] lineSplitChars = new char[] { '\n' };
|
||||
private static readonly char[] argsSplitChars = new char[] { ' ' };
|
||||
private readonly ActionQueue commandProcessor = new ActionQueue(TaskScheduler.Default);
|
||||
private readonly ActionQueue outputProcessor = new ActionQueue(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
private readonly List<CommandListener> commandListeners = new List<CommandListener>();
|
||||
private readonly RCONParameters rconParams;
|
||||
private QueryMaster.Rcon console;
|
||||
private int maxCommandRetries = 3;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlayerInfo> players = new ConcurrentDictionary<string, PlayerInfo>();
|
||||
private readonly object updatePlayerCollectionLock = new object();
|
||||
private CancellationTokenSource cancellationTokenSource = null;
|
||||
private readonly ActionQueue _commandProcessor = new ActionQueue(TaskScheduler.Default);
|
||||
private readonly ActionQueue _outputProcessor = new ActionQueue(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
private readonly List<CommandListener> _commandListeners = new List<CommandListener>();
|
||||
|
||||
private readonly RCONParameters _rconParameters;
|
||||
private QueryMaster.Rcon _console;
|
||||
private int _maxCommandRetries = 3;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlayerInfo> _players = new ConcurrentDictionary<string, PlayerInfo>();
|
||||
private readonly object _updatePlayerCollectionLock = new object();
|
||||
private CancellationTokenSource _cancellationTokenSource = null;
|
||||
|
||||
private readonly Logger _chatLogger;
|
||||
private readonly Logger _allLogger;
|
||||
|
|
@ -97,28 +69,30 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
public ServerRCON(RCONParameters parameters)
|
||||
{
|
||||
this.rconParams = parameters;
|
||||
this.Players = new SortableObservableCollection<PlayerInfo>();
|
||||
_rconParameters = parameters;
|
||||
|
||||
_allLogger = App.GetProfileLogger(this.rconParams.ProfileId, "RCON_All", LogLevel.Info, LogLevel.Info);
|
||||
_chatLogger = App.GetProfileLogger(this.rconParams.ProfileId, "RCON_Chat", LogLevel.Info, LogLevel.Info);
|
||||
_eventLogger = App.GetProfileLogger(this.rconParams.ProfileId, "RCON_Event", LogLevel.Info, LogLevel.Info);
|
||||
_debugLogger = App.GetProfileLogger(this.rconParams.ProfileId, "RCON_Debug", LogLevel.Trace, LogLevel.Debug);
|
||||
_errorLogger = App.GetProfileLogger(this.rconParams.ProfileId, "RCON_Error", LogLevel.Error, LogLevel.Fatal);
|
||||
_allLogger = App.GetProfileLogger(_rconParameters.ProfileId, "RCON_All", LogLevel.Info, LogLevel.Info);
|
||||
_chatLogger = App.GetProfileLogger(_rconParameters.ProfileId, "RCON_Chat", LogLevel.Info, LogLevel.Info);
|
||||
_eventLogger = App.GetProfileLogger(_rconParameters.ProfileId, "RCON_Event", LogLevel.Info, LogLevel.Info);
|
||||
_debugLogger = App.GetProfileLogger(_rconParameters.ProfileId, "RCON_Debug", LogLevel.Trace, LogLevel.Debug);
|
||||
_errorLogger = App.GetProfileLogger(_rconParameters.ProfileId, "RCON_Error", LogLevel.Error, LogLevel.Fatal);
|
||||
|
||||
this.Players = new SortableObservableCollection<PlayerInfo>();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (cancellationTokenSource != null)
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
await this.commandProcessor.DisposeAsync();
|
||||
await this.outputProcessor.DisposeAsync();
|
||||
|
||||
for (int index = this.commandListeners.Count - 1; index >= 0; index--)
|
||||
await _commandProcessor.DisposeAsync();
|
||||
await _outputProcessor.DisposeAsync();
|
||||
|
||||
for (int index = _commandListeners.Count - 1; index >= 0; index--)
|
||||
{
|
||||
this.commandListeners[index].Dispose();
|
||||
_commandListeners[index].Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
|
@ -159,8 +133,8 @@ namespace ServerManagerTool.Lib
|
|||
#region Methods
|
||||
public void Initialize()
|
||||
{
|
||||
commandProcessor.PostAction(AutoPlayerList);
|
||||
commandProcessor.PostAction(AutoGetChat);
|
||||
_commandProcessor.PostAction(AutoPlayerList);
|
||||
_commandProcessor.PostAction(AutoGetChat);
|
||||
UpdatePlayersAsync().DoNotWait();
|
||||
}
|
||||
|
||||
|
|
@ -189,50 +163,55 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
private bool Reconnect()
|
||||
{
|
||||
if (this.console != null)
|
||||
if (_console != null)
|
||||
{
|
||||
this.console.Dispose();
|
||||
this.console = null;
|
||||
_console.Dispose();
|
||||
_console = null;
|
||||
}
|
||||
|
||||
var endpoint = new IPEndPoint(this.rconParams.RCONHostIP, this.rconParams.RCONPort);
|
||||
var endpoint = new IPEndPoint(_rconParameters.RCONHostIP, _rconParameters.RCONPort);
|
||||
var server = QueryMaster.ServerQuery.GetServerInstance(QueryMaster.EngineType.Source, endpoint);
|
||||
this.console = server.GetControl(this.rconParams.RCONPassword);
|
||||
return this.console != null;
|
||||
_console = server.GetControl(_rconParameters.RCONPassword);
|
||||
return _console != null;
|
||||
}
|
||||
|
||||
public IDisposable RegisterCommandListener(Action<ConsoleCommand> callback)
|
||||
{
|
||||
var listener = new CommandListener { Callback = callback, DisposeAction = UnregisterCommandListener };
|
||||
this.commandListeners.Add(listener);
|
||||
_commandListeners.Add(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
private void UnregisterCommandListener(CommandListener listener)
|
||||
{
|
||||
this.commandListeners.Remove(listener);
|
||||
_commandListeners.Remove(listener);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Process Methods
|
||||
private Task AutoPlayerList()
|
||||
{
|
||||
return commandProcessor.PostAction(() =>
|
||||
return _commandProcessor.PostAction(() =>
|
||||
{
|
||||
ProcessInput(new ConsoleCommand() { rawCommand = RCON_COMMAND_LISTPLAYERS, suppressCommand = true, suppressOutput = true });
|
||||
Task.Delay(PLAYER_LIST_INTERVAL).ContinueWith(t => commandProcessor.PostAction(AutoPlayerList)).DoNotWait();
|
||||
Task.Delay(PLAYER_LIST_INTERVAL).ContinueWith(t => _commandProcessor.PostAction(AutoPlayerList)).DoNotWait();
|
||||
});
|
||||
}
|
||||
|
||||
private Task AutoGetChat()
|
||||
{
|
||||
return commandProcessor.PostAction(() =>
|
||||
return _commandProcessor.PostAction(() =>
|
||||
{
|
||||
ProcessInput(new ConsoleCommand() { rawCommand = RCON_COMMAND_GETCHAT, suppressCommand = true, suppressOutput = true });
|
||||
Task.Delay(GET_CHAT_INTERVAL).ContinueWith(t => commandProcessor.PostAction(AutoGetChat)).DoNotWait();
|
||||
Task.Delay(GET_CHAT_INTERVAL).ContinueWith(t => _commandProcessor.PostAction(AutoGetChat)).DoNotWait();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> IssueCommand(string userCommand)
|
||||
{
|
||||
return _commandProcessor.PostAction(() => ProcessInput(new ConsoleCommand() { rawCommand = userCommand }));
|
||||
}
|
||||
|
||||
private bool ProcessInput(ConsoleCommand command)
|
||||
{
|
||||
try
|
||||
|
|
@ -277,14 +256,14 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
command.status = ConsoleStatus.Connected;
|
||||
|
||||
this.outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
_outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger.Error($"Failed to send command '{command.rawCommand}'. {ex.Message}");
|
||||
command.status = ConsoleStatus.Disconnected;
|
||||
this.outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
_outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -299,11 +278,6 @@ namespace ServerManagerTool.Lib
|
|||
NotifyCommand(command);
|
||||
}
|
||||
|
||||
public Task<bool> IssueCommand(string userCommand)
|
||||
{
|
||||
return this.commandProcessor.PostAction(() => ProcessInput(new ConsoleCommand() { rawCommand = userCommand }));
|
||||
}
|
||||
|
||||
// This is bound to the UI thread
|
||||
private void HandleCommand(ConsoleCommand command)
|
||||
{
|
||||
|
|
@ -320,8 +294,8 @@ namespace ServerManagerTool.Lib
|
|||
//
|
||||
// Update the visible player list
|
||||
//
|
||||
command.suppressOutput = false;
|
||||
command.lines = HandleListPlayersCommand(command.lines);
|
||||
command.suppressOutput = command.lines.Count() == 0;
|
||||
}
|
||||
|
||||
if (command.command.Equals(RCON_COMMAND_GETCHAT, StringComparison.OrdinalIgnoreCase))
|
||||
|
|
@ -361,11 +335,13 @@ namespace ServerManagerTool.Lib
|
|||
private List<string> HandleListPlayersCommand(IEnumerable<string> commandLines)
|
||||
{
|
||||
var output = new List<string>();
|
||||
var onlinePlayers = new List<PlayerInfo>();
|
||||
|
||||
if (commandLines != null)
|
||||
var playerLines = commandLines?.ToList() ?? new List<string>();
|
||||
if (playerLines.Count > 0)
|
||||
{
|
||||
var onlinePlayers = new List<PlayerInfo>();
|
||||
foreach (var line in commandLines)
|
||||
|
||||
foreach (var line in playerLines)
|
||||
{
|
||||
var elements = line.Split(',');
|
||||
if (elements.Length != 2)
|
||||
|
|
@ -373,9 +349,6 @@ namespace ServerManagerTool.Lib
|
|||
continue;
|
||||
|
||||
var id = elements[1]?.Trim();
|
||||
if (id.Any(c => !char.IsDigit(c)))
|
||||
// Invalid data. Ignore it.
|
||||
continue;
|
||||
|
||||
if (onlinePlayers.FirstOrDefault(p => p.PlayerId.Equals(id, StringComparison.OrdinalIgnoreCase)) != null)
|
||||
// Duplicate data. Ignore it.
|
||||
|
|
@ -390,43 +363,56 @@ namespace ServerManagerTool.Lib
|
|||
onlinePlayers.Add(newPlayer);
|
||||
|
||||
var playerJoined = false;
|
||||
this.players.AddOrUpdate(newPlayer.PlayerId, (k) => { playerJoined = true; return newPlayer; }, (k, v) => { playerJoined = !v.IsOnline; v.IsOnline = true; return v; });
|
||||
_players.AddOrUpdate(newPlayer.PlayerId,
|
||||
(k) =>
|
||||
{
|
||||
playerJoined = true;
|
||||
return newPlayer;
|
||||
},
|
||||
(k, v) =>
|
||||
{
|
||||
playerJoined = !v.IsOnline;
|
||||
v.PlayerName = newPlayer.PlayerName;
|
||||
v.IsOnline = newPlayer.IsOnline;
|
||||
return v;
|
||||
}
|
||||
);
|
||||
|
||||
if (playerJoined)
|
||||
{
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Join") ?? "Player '{0}' joined the game.";
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Join") ?? "'{0}' joined the game.";
|
||||
var message = string.Format(messageFormat, newPlayer.PlayerName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
|
||||
var droppedPlayers = this.players.Values.Where(p => onlinePlayers.FirstOrDefault(np => np.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null);
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
if (droppedPlayer.IsOnline)
|
||||
{
|
||||
droppedPlayer.IsOnline = false;
|
||||
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Leave") ?? "Player '{0}' left the game.";
|
||||
var message = string.Format(messageFormat, droppedPlayer.PlayerName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
|
||||
UpdatePlayerCollection();
|
||||
}
|
||||
|
||||
var droppedPlayers = _players.Values.Where(p => onlinePlayers.FirstOrDefault(np => np.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null);
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
if (droppedPlayer.IsOnline)
|
||||
{
|
||||
droppedPlayer.IsOnline = false;
|
||||
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Leave") ?? "'{0}' left the game.";
|
||||
var message = string.Format(messageFormat, droppedPlayer.PlayerName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
|
||||
UpdatePlayerCollection();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// This is bound to the UI thread
|
||||
private void NotifyCommand(ConsoleCommand command)
|
||||
{
|
||||
foreach (var listener in commandListeners)
|
||||
foreach (var listener in _commandListeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -446,13 +432,13 @@ namespace ServerManagerTool.Lib
|
|||
Exception lastException = null;
|
||||
int retries = 0;
|
||||
|
||||
while (retries < maxCommandRetries)
|
||||
while (retries < _maxCommandRetries)
|
||||
{
|
||||
if (this.console != null)
|
||||
if (_console != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return this.console.SendCommand(command);
|
||||
return _console.SendCommand(command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -475,152 +461,158 @@ namespace ServerManagerTool.Lib
|
|||
retries++;
|
||||
}
|
||||
|
||||
this.maxCommandRetries = 10;
|
||||
_errorLogger.Error($"Failed to connect to RCON at {this.rconParams.RCONHostIP}:{this.rconParams.RCONPort} with {this.rconParams.RCONPassword}. {lastException.Message}");
|
||||
throw new Exception($"Command failed to send after {maxCommandRetries} attempts. Last exception: {lastException.Message}", lastException);
|
||||
_maxCommandRetries = 10;
|
||||
_errorLogger.Error($"Failed to connect to RCON at {_rconParameters.RCONHostIP}:{_rconParameters.RCONPort} with {_rconParameters.RCONPassword}. {lastException.Message}");
|
||||
throw new Exception($"Command failed to send after {_maxCommandRetries} attempts. Last exception: {lastException.Message}", lastException);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private async Task UpdatePlayersAsync()
|
||||
{
|
||||
if (this._disposed)
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
var token = cancellationTokenSource.Token;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
await UpdatePlayerDetailsAsync(cancellationTokenSource.Token)
|
||||
.ContinueWith(async t1 =>
|
||||
{
|
||||
await TaskUtils.RunOnUIThreadAsync(() =>
|
||||
{
|
||||
UpdatePlayerCollection();
|
||||
});
|
||||
}, TaskContinuationOptions.NotOnCanceled)
|
||||
.ContinueWith(t2 =>
|
||||
{
|
||||
var cancelled = cancellationTokenSource.IsCancellationRequested;
|
||||
cancellationTokenSource.Dispose();
|
||||
cancellationTokenSource = null;
|
||||
await UpdatePlayerDetailsAsync(_cancellationTokenSource.Token);
|
||||
var cancelled = _cancellationTokenSource.IsCancellationRequested;
|
||||
|
||||
if (!cancelled)
|
||||
Task.Delay(PLAYER_LIST_INTERVAL).ContinueWith(t3 => UpdatePlayersAsync());
|
||||
});
|
||||
if (!cancelled)
|
||||
{
|
||||
await TaskUtils.RunOnUIThreadAsync(() => UpdatePlayerCollection());
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
if (!cancelled)
|
||||
{
|
||||
await Task.Delay(PLAYER_LIST_INTERVAL)
|
||||
.ContinueWith(t => UpdatePlayersAsync());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdatePlayerDetailsAsync(CancellationToken token)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(rconParams.InstallDirectory))
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_rconParameters.InstallDirectory))
|
||||
return;
|
||||
|
||||
DataContainer dataContainer = null;
|
||||
|
||||
try
|
||||
{
|
||||
var savedPath = ServerProfile.GetProfileSavePath(rconParams.InstallDirectory, rconParams.AltSaveDirectoryName, rconParams.PGM_Enabled, rconParams.PGM_Name);
|
||||
DataContainer dataContainer = null;
|
||||
DateTime lastSteamUpdateUtc = DateTime.MinValue;
|
||||
var savedPath = ServerProfile.GetProfileSavePath(_rconParameters.InstallDirectory, _rconParameters.AltSaveDirectoryName, _rconParameters.PGM_Enabled, _rconParameters.PGM_Name);
|
||||
|
||||
try
|
||||
{
|
||||
// load the player data from the files.
|
||||
dataContainer = await DataContainer.CreateAsync(savedPath, savedPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: CreateAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
return;
|
||||
}
|
||||
// load the player data from the files.
|
||||
dataContainer = await DataContainer.CreateAsync(savedPath, savedPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: CreateAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
return;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// update the player data with the latest update value from the players collection
|
||||
foreach (var playerData in dataContainer.Players)
|
||||
{
|
||||
var id = playerData.PlayerId;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
continue;
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
this.players.TryGetValue(id, out PlayerInfo player);
|
||||
player?.UpdatePlatformData(playerData);
|
||||
}
|
||||
}, token);
|
||||
|
||||
try
|
||||
{
|
||||
// load the player data from steam
|
||||
lastSteamUpdateUtc = await dataContainer.LoadSteamAsync(SteamUtils.SteamWebApiKey, STEAM_UPDATE_INTERVAL);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: LoadSteamAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var totalPlayers = dataContainer.Players.Count;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// update the player data with the latest update value from the players collection
|
||||
foreach (var playerData in dataContainer.Players)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var id = playerData.PlayerId;
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
var validPlayer = new PlayerInfo()
|
||||
{
|
||||
PlayerId = playerData.PlayerId,
|
||||
PlayerName = playerData.PlayerName,
|
||||
IsValid = true,
|
||||
};
|
||||
var id = playerData.PlayerId;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
continue;
|
||||
|
||||
this.players.AddOrUpdate(id, validPlayer, (k, v) => { v.PlayerName = playerData.PlayerName; v.IsValid = true; return v; });
|
||||
_players.TryGetValue(id, out PlayerInfo player);
|
||||
player?.UpdatePlatformData(playerData);
|
||||
}
|
||||
}, token);
|
||||
|
||||
try
|
||||
{
|
||||
// load the player data from steam
|
||||
await dataContainer.LoadSteamAsync(SteamUtils.SteamWebApiKey, STEAM_UPDATE_INTERVAL);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: LoadSteamAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
foreach (var playerData in dataContainer.Players)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var id = playerData.PlayerId;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = Path.GetFileNameWithoutExtension(playerData.Filename);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
_debugLogger.Debug($"{nameof(UpdatePlayerDetailsAsync)} - Error: corrupted profile.\r\n{playerData.Filename}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
id = Path.GetFileNameWithoutExtension(playerData.Filename);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
var invalidPlayer = new PlayerInfo()
|
||||
{
|
||||
var invalidPlayer = new PlayerInfo()
|
||||
{
|
||||
PlayerId = id,
|
||||
PlayerName = "< corrupted profile >",
|
||||
IsValid = false,
|
||||
};
|
||||
PlayerId = id,
|
||||
PlayerName = "< corrupted profile >",
|
||||
IsValid = false,
|
||||
};
|
||||
|
||||
this.players.AddOrUpdate(id, invalidPlayer, (k, v) => { v.PlayerName = "< corrupted profile >"; v.IsValid = false; return v; });
|
||||
}
|
||||
else
|
||||
{
|
||||
_debugLogger.Debug($"{nameof(UpdatePlayerDetailsAsync)} - Error: corrupted profile.\r\n{playerData.Filename}.");
|
||||
}
|
||||
_players.AddOrUpdate(id, invalidPlayer, (k, v) => { v.PlayerName = "< corrupted profile >"; v.IsValid = false; return v; });
|
||||
}
|
||||
|
||||
if (this.players.TryGetValue(id, out PlayerInfo player) && player != null)
|
||||
}
|
||||
else
|
||||
{
|
||||
var validPlayer = new PlayerInfo()
|
||||
{
|
||||
player.UpdateData(playerData);
|
||||
PlayerId = id,
|
||||
PlayerName = playerData.PlayerName,
|
||||
IsValid = true,
|
||||
};
|
||||
|
||||
await TaskUtils.RunOnUIThreadAsync(() =>
|
||||
{
|
||||
player.IsAdmin = rconParams?.Server?.Profile?.ServerFilesAdmins?.Any(u => u.PlayerId.Equals(player.PlayerId, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
player.IsWhitelisted = rconParams?.Server?.Profile?.ServerFilesWhitelisted?.Any(u => u.PlayerId.Equals(player.PlayerId, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
});
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
_players.AddOrUpdate(id, validPlayer, (k, v) => { v.PlayerName = playerData.PlayerName; v.IsValid = true; return v; });
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (_players.TryGetValue(id, out PlayerInfo player) && player != null)
|
||||
{
|
||||
player.UpdateData(playerData);
|
||||
|
||||
// remove any players that do not have a player file.
|
||||
var droppedPlayers = this.players.Values.Where(p => dataContainer.Players.FirstOrDefault(pd => pd.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null).ToArray();
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
players.TryRemove(droppedPlayer.PlayerId, out PlayerInfo player);
|
||||
}
|
||||
await TaskUtils.RunOnUIThreadAsync(() =>
|
||||
{
|
||||
player.IsAdmin = _rconParameters?.Server?.Profile?.ServerFilesAdmins?.Any(u => u.PlayerId.Equals(player.PlayerId, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
player.IsWhitelisted = _rconParameters?.Server?.Profile?.ServerFilesWhitelisted?.Any(u => u.PlayerId.Equals(player.PlayerId, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
});
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// remove any players that do not have a player file.
|
||||
var droppedPlayers = _players.Values.Where(p => dataContainer.Players.FirstOrDefault(pd => pd.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null).ToArray();
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
_players.TryRemove(droppedPlayer.PlayerId, out PlayerInfo player);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlayerCollection()
|
||||
{
|
||||
lock (updatePlayerCollectionLock)
|
||||
lock (_updatePlayerCollectionLock)
|
||||
{
|
||||
this.Players = new SortableObservableCollection<PlayerInfo>(players.Values);
|
||||
this.Players = new SortableObservableCollection<PlayerInfo>(_players.Values);
|
||||
this.CountPlayers = this.Players.Count;
|
||||
this.CountInvalidPlayers = this.Players.Count(p => !p.IsValid);
|
||||
this.CountOnlinePlayers = this.Players.Count(p => p.IsOnline);
|
||||
|
|
|
|||
|
|
@ -310,8 +310,7 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
try
|
||||
{
|
||||
var players = server?.GetPlayers()?.Where(p => !string.IsNullOrWhiteSpace(p.Name?.Trim()));
|
||||
onlinePlayerCount = players?.Count() ?? 0;
|
||||
onlinePlayerCount = serverInfo?.Players ?? 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
<entry>
|
||||
<id>urn:uuid:2C48A585-72D2-43FB-8987-6B5F0B3E460F</id>
|
||||
<title>1.1.425 (1.1.425.4)</title>
|
||||
<summary>1.1.425.4</summary>
|
||||
<title>1.1.425 (1.1.425.5)</title>
|
||||
<summary>1.1.425.5</summary>
|
||||
<link href="" />
|
||||
<updated>2022-05-07T00:00:00Z</updated>
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
|
||||
<p>
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
<li>Auto Backup Settings - added RCON broadcast mode droplist, so backup processes can send messages via RCON using this mode.</li>
|
||||
<li>Global Backup Settings - added option to include/exclude the SaveGames folder in the worldsave backup (default exclude).</li>
|
||||
<li>Global Alert Settings - added new textbox allowing the formatting of the ipaddress and port in the server startup message (default {ipaddress}:{port}).</li>
|
||||
<li>Rcon Window - minor changes to the player list, online player count and server status.</li>
|
||||
<li>Security Protocol Changes - updated the security protocols to use TLS12 and TLS13.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,30 @@
|
|||
<title>Ark Server Manager Version Feed</title>
|
||||
<subtitle>This is the Ark Server Manager beta version feed.</subtitle>
|
||||
<link href="http://arkservermanager.freeforums.net/" />
|
||||
<updated>2022-05-07T00:00:00Z</updated>
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
|
||||
<entry>
|
||||
<id>urn:uuid:CEA21F86-5943-46F9-8807-604695E42A25</id>
|
||||
<title>1.1.425 (1.1.425.5)</title>
|
||||
<summary>1.1.425.5</summary>
|
||||
<link href="" />
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
|
||||
<p>
|
||||
<u style="font-size: .9em;">CHANGE</u>
|
||||
<br/>
|
||||
<ul>
|
||||
<li>Rcon Window - minor changes to the player list, online player count and server status.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</content>
|
||||
<author>
|
||||
<name>bletch</name>
|
||||
<email>bletch1971@hotmail.com</email>
|
||||
</author>
|
||||
</entry>
|
||||
|
||||
<entry>
|
||||
<id>urn:uuid:358E0063-27AE-4D5F-BDA5-BD9723EE353E</id>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using ServerManagerTool.Common.Extensions;
|
||||
using ServerManagerTool.Common.Enums;
|
||||
using ServerManagerTool.Common.Extensions;
|
||||
using ServerManagerTool.Common.Lib;
|
||||
using ServerManagerTool.Common.Utils;
|
||||
using ServerManagerTool.Enums;
|
||||
|
|
@ -911,7 +912,7 @@ namespace ServerManagerTool
|
|||
ConsoleContent.Blocks.Add(b);
|
||||
}
|
||||
|
||||
private IEnumerable<Inline> FormatCommandInput(ServerRCON.ConsoleCommand command)
|
||||
private IEnumerable<Inline> FormatCommandInput(ConsoleCommand command)
|
||||
{
|
||||
if (command.command.Equals(ServerRCON.RCON_COMMAND_BROADCAST, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
|
@ -928,7 +929,7 @@ namespace ServerManagerTool
|
|||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Inline> FormatCommandOutput(ServerRCON.ConsoleCommand command)
|
||||
private IEnumerable<Inline> FormatCommandOutput(ConsoleCommand command)
|
||||
{
|
||||
bool firstLine = true;
|
||||
|
||||
|
|
@ -1009,13 +1010,13 @@ namespace ServerManagerTool
|
|||
|
||||
private void RenderConnectionStateChange(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var oldStatus = (ServerRCON.ConsoleStatus)e.OldValue;
|
||||
var newStatus = (ServerRCON.ConsoleStatus)e.NewValue;
|
||||
var oldStatus = (ConsoleStatus)e.OldValue;
|
||||
var newStatus = (ConsoleStatus)e.NewValue;
|
||||
|
||||
if(oldStatus != newStatus)
|
||||
{
|
||||
var p = new Paragraph();
|
||||
if (newStatus == ServerRCON.ConsoleStatus.Connected)
|
||||
if (newStatus == ConsoleStatus.Connected)
|
||||
{
|
||||
p.Inlines.Add(new RCONOutput_ConnectionChanged(true));
|
||||
}
|
||||
|
|
@ -1028,7 +1029,7 @@ namespace ServerManagerTool
|
|||
}
|
||||
}
|
||||
|
||||
private void RenderRCONCommandOutput(ServerRCON.ConsoleCommand command)
|
||||
private void RenderRCONCommandOutput(ConsoleCommand command)
|
||||
{
|
||||
//
|
||||
// Format output
|
||||
|
|
|
|||
|
|
@ -529,8 +529,8 @@
|
|||
<sys:String x:Key="ConsoleStatus_Disconnected">Disconnected</sys:String>
|
||||
<sys:String x:Key="ConsoleStatus_Connected">Connected</sys:String>
|
||||
|
||||
<sys:String x:Key="Player_Join">Player '{0}' joined the game.</sys:String>
|
||||
<sys:String x:Key="Player_Leave">Player '{0}' left the game.</sys:String>
|
||||
<sys:String x:Key="Player_Join">'{0}' joined the game.</sys:String>
|
||||
<sys:String x:Key="Player_Leave">'{0}' left the game.</sys:String>
|
||||
<!--#endregion-->
|
||||
|
||||
<!--#region PlayerListWindow Window -->
|
||||
|
|
|
|||
|
|
@ -29,16 +29,16 @@ namespace ServerManagerTool.Lib
|
|||
public const string RCON_COMMAND_BROADCAST = "broadcast";
|
||||
public const string RCON_COMMAND_ALERT = "alert";
|
||||
public const string RCON_COMMAND_SERVER = "server";
|
||||
public const string RCON_COMMAND_LISTPLAYERS = "#managerplayerlist#";
|
||||
public const string RCON_COMMAND_LISTPLAYERS = "listplayers";
|
||||
|
||||
public event EventHandler PlayersCollectionUpdated;
|
||||
|
||||
public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(nameof(Status), typeof(ConsoleStatus), typeof(ServerRcon), new PropertyMetadata(ConsoleStatus.Disconnected));
|
||||
public static readonly DependencyProperty PlayersProperty = DependencyProperty.Register(nameof(Players), typeof(SortableObservableCollection<PlayerInfo>), typeof(ServerRcon), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty CountPlayersProperty = DependencyProperty.Register(nameof(CountPlayers), typeof(int), typeof(ServerRcon), new PropertyMetadata(0));
|
||||
public static readonly DependencyProperty CountInvalidPlayersProperty = DependencyProperty.Register(nameof(CountInvalidPlayers), typeof(int), typeof(ServerRcon), new PropertyMetadata(0));
|
||||
public static readonly DependencyProperty CountOnlinePlayersProperty = DependencyProperty.Register(nameof(CountOnlinePlayers), typeof(int), typeof(ServerRcon), new PropertyMetadata(0));
|
||||
|
||||
private static readonly ConcurrentDictionary<string, bool> locks = new ConcurrentDictionary<string, bool>();
|
||||
private static readonly char[] lineSplitChars = new char[] { '\n' };
|
||||
private static readonly char[] argsSplitChars = new char[] { ' ' };
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
private RconParameters _rconParameters;
|
||||
private QueryMaster.Rcon _console;
|
||||
private int maxCommandRetries = 3;
|
||||
private int _maxCommandRetries = 3;
|
||||
|
||||
private readonly ConcurrentDictionary<string, PlayerInfo> _players = new ConcurrentDictionary<string, PlayerInfo>();
|
||||
private readonly object _updatePlayerCollectionLock = new object();
|
||||
|
|
@ -63,16 +63,15 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
public ServerRcon(RconParameters parameters)
|
||||
{
|
||||
this._rconParameters = parameters;
|
||||
_rconParameters = parameters;
|
||||
|
||||
_allLogger = App.GetProfileLogger(_rconParameters.ProfileId, "Rcon_All", LogLevel.Info, LogLevel.Info);
|
||||
_chatLogger = App.GetProfileLogger(_rconParameters.ProfileId, "Rcon_Chat", LogLevel.Info, LogLevel.Info);
|
||||
_eventLogger = App.GetProfileLogger(_rconParameters.ProfileId, "Rcon_Event", LogLevel.Info, LogLevel.Info);
|
||||
_debugLogger = App.GetProfileLogger(_rconParameters.ProfileId, "Rcon_Debug", LogLevel.Trace, LogLevel.Debug);
|
||||
_errorLogger = App.GetProfileLogger(_rconParameters.ProfileId, "Rcon_Error", LogLevel.Error, LogLevel.Fatal);
|
||||
|
||||
this.Players = new SortableObservableCollection<PlayerInfo>();
|
||||
|
||||
_allLogger = App.GetProfileLogger(this._rconParameters.ProfileId, "Rcon_All", LogLevel.Info, LogLevel.Info);
|
||||
_chatLogger = App.GetProfileLogger(this._rconParameters.ProfileId, "Rcon_Chat", LogLevel.Info, LogLevel.Info);
|
||||
_eventLogger = App.GetProfileLogger(this._rconParameters.ProfileId, "Rcon_Event", LogLevel.Info, LogLevel.Info);
|
||||
_debugLogger = App.GetProfileLogger(this._rconParameters.ProfileId, "Rcon_Debug", LogLevel.Trace, LogLevel.Debug);
|
||||
_errorLogger = App.GetProfileLogger(this._rconParameters.ProfileId, "Rcon_Error", LogLevel.Error, LogLevel.Fatal);
|
||||
|
||||
UpdatePlayersAsync().DoNotWait();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
|
|
@ -81,18 +80,25 @@ namespace ServerManagerTool.Lib
|
|||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
await this._commandProcessor.DisposeAsync();
|
||||
await this._outputProcessor.DisposeAsync();
|
||||
|
||||
for (int index = this._commandListeners.Count - 1; index >= 0; index--)
|
||||
await _commandProcessor.DisposeAsync();
|
||||
await _outputProcessor.DisposeAsync();
|
||||
|
||||
for (int index = _commandListeners.Count - 1; index >= 0; index--)
|
||||
{
|
||||
this._commandListeners[index].Dispose();
|
||||
_commandListeners[index].Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#region Properties
|
||||
public ConsoleStatus Status
|
||||
{
|
||||
get { return (ConsoleStatus)GetValue(StatusProperty); }
|
||||
set { SetValue(StatusProperty, value); }
|
||||
}
|
||||
|
||||
public SortableObservableCollection<PlayerInfo> Players
|
||||
{
|
||||
get { return (SortableObservableCollection<PlayerInfo>)GetValue(PlayersProperty); }
|
||||
|
|
@ -119,6 +125,12 @@ namespace ServerManagerTool.Lib
|
|||
#endregion
|
||||
|
||||
#region Methods
|
||||
public void Initialize()
|
||||
{
|
||||
_commandProcessor.PostAction(AutoPlayerList);
|
||||
UpdatePlayersAsync().DoNotWait();
|
||||
}
|
||||
|
||||
private void LogEvent(LogEventType eventType, string message)
|
||||
{
|
||||
switch (eventType)
|
||||
|
|
@ -144,32 +156,46 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
private bool Reconnect()
|
||||
{
|
||||
if (this._console != null)
|
||||
if (_console != null)
|
||||
{
|
||||
this._console.Dispose();
|
||||
this._console = null;
|
||||
_console.Dispose();
|
||||
_console = null;
|
||||
}
|
||||
|
||||
var endpoint = new IPEndPoint(this._rconParameters.RconHostIP, this._rconParameters.RconPort);
|
||||
var endpoint = new IPEndPoint(_rconParameters.RconHostIP, _rconParameters.RconPort);
|
||||
var server = QueryMaster.ServerQuery.GetServerInstance(QueryMaster.EngineType.Source, endpoint, sendTimeOut: 10000, receiveTimeOut: 10000);
|
||||
this._console = server.GetControl(this._rconParameters.RconPassword);
|
||||
return this._console != null;
|
||||
_console = server.GetControl(_rconParameters.RconPassword);
|
||||
return _console != null;
|
||||
}
|
||||
|
||||
public IDisposable RegisterCommandListener(Action<ConsoleCommand> callback)
|
||||
{
|
||||
var listener = new CommandListener { Callback = callback, DisposeAction = UnregisterCommandListener };
|
||||
this._commandListeners.Add(listener);
|
||||
_commandListeners.Add(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
private void UnregisterCommandListener(CommandListener listener)
|
||||
{
|
||||
this._commandListeners.Remove(listener);
|
||||
_commandListeners.Remove(listener);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Process Methods
|
||||
private Task AutoPlayerList()
|
||||
{
|
||||
return _commandProcessor.PostAction(() =>
|
||||
{
|
||||
ProcessInput(new ConsoleCommand() { rawCommand = RCON_COMMAND_LISTPLAYERS, suppressCommand = true, suppressOutput = true });
|
||||
Task.Delay(PLAYER_LIST_INTERVAL).ContinueWith(t => _commandProcessor.PostAction(AutoPlayerList)).DoNotWait();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> IssueCommand(string userCommand)
|
||||
{
|
||||
return _commandProcessor.PostAction(() => ProcessInput(new ConsoleCommand() { rawCommand = userCommand }));
|
||||
}
|
||||
|
||||
private bool ProcessInput(ConsoleCommand command)
|
||||
{
|
||||
try
|
||||
|
|
@ -214,14 +240,14 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
command.status = ConsoleStatus.Connected;
|
||||
|
||||
this._outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
_outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger?.Error($"Failed to send command '{command.rawCommand}'. {ex.Message}");
|
||||
command.status = ConsoleStatus.Disconnected;
|
||||
this._outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
_outputProcessor.PostAction(() => ProcessOutput(command));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -236,11 +262,6 @@ namespace ServerManagerTool.Lib
|
|||
NotifyCommand(command);
|
||||
}
|
||||
|
||||
public Task<bool> IssueCommand(string userCommand)
|
||||
{
|
||||
return this._commandProcessor.PostAction(() => ProcessInput(new ConsoleCommand() { rawCommand = userCommand }));
|
||||
}
|
||||
|
||||
// This is bound to the UI thread
|
||||
private void HandleCommand(ConsoleCommand command)
|
||||
{
|
||||
|
|
@ -249,8 +270,8 @@ namespace ServerManagerTool.Lib
|
|||
//
|
||||
if (command?.command?.Equals(RCON_COMMAND_LISTPLAYERS, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
{
|
||||
command.suppressCommand = true;
|
||||
command.suppressOutput = false;
|
||||
command.lines = HandleListPlayersCommand(command.lines);
|
||||
command.suppressOutput = command.lines.Count() == 0;
|
||||
}
|
||||
|
||||
if (command?.command?.Equals(RCON_COMMAND_BROADCAST, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
|
|
@ -272,6 +293,87 @@ namespace ServerManagerTool.Lib
|
|||
}
|
||||
}
|
||||
|
||||
// This is bound to the UI thread
|
||||
private List<string> HandleListPlayersCommand(IEnumerable<string> commandLines)
|
||||
{
|
||||
var output = new List<string>();
|
||||
var onlinePlayers = new List<PlayerInfo>();
|
||||
|
||||
var playerLines = commandLines?.ToList() ?? new List<string>();
|
||||
if (playerLines.Count > 1)
|
||||
{
|
||||
// remove the first line, as it is a header row
|
||||
playerLines.RemoveAt(0);
|
||||
|
||||
foreach (var line in playerLines)
|
||||
{
|
||||
var elements = line.Split('|');
|
||||
if (elements.Length != 6)
|
||||
// Invalid data. Ignore it.
|
||||
continue;
|
||||
|
||||
var id = elements[3]?.Trim();
|
||||
|
||||
if (onlinePlayers.FirstOrDefault(p => p.PlayerId.Equals(id, StringComparison.OrdinalIgnoreCase)) != null)
|
||||
// Duplicate data. Ignore it.
|
||||
continue;
|
||||
|
||||
var newPlayer = new PlayerInfo()
|
||||
{
|
||||
PlayerId = id,
|
||||
PlayerName = elements[2].Substring(0, elements[2].IndexOf('#')).Trim(),
|
||||
CharacterName = elements[1].Trim(),
|
||||
IsOnline = true,
|
||||
};
|
||||
onlinePlayers.Add(newPlayer);
|
||||
|
||||
var playerJoined = false;
|
||||
_players.AddOrUpdate(newPlayer.PlayerId,
|
||||
(k) =>
|
||||
{
|
||||
playerJoined = true;
|
||||
return newPlayer;
|
||||
},
|
||||
(k, v) =>
|
||||
{
|
||||
playerJoined = !v.IsOnline;
|
||||
v.PlayerName = newPlayer.PlayerName;
|
||||
v.IsOnline = newPlayer.IsOnline;
|
||||
return v;
|
||||
}
|
||||
);
|
||||
|
||||
if (playerJoined)
|
||||
{
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Join") ?? "'{0}' joined the game.";
|
||||
var message = string.Format(messageFormat, newPlayer.CharacterName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var droppedPlayers = _players.Values.Where(p => onlinePlayers.FirstOrDefault(np => np.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null);
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
if (droppedPlayer.IsOnline)
|
||||
{
|
||||
droppedPlayer.IsOnline = false;
|
||||
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Leave") ?? "'{0}' left the game.";
|
||||
var message = string.Format(messageFormat, droppedPlayer.CharacterName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
|
||||
UpdatePlayerCollection();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// This is bound to the UI thread
|
||||
private void NotifyCommand(ConsoleCommand command)
|
||||
{
|
||||
|
|
@ -295,13 +397,13 @@ namespace ServerManagerTool.Lib
|
|||
Exception lastException = null;
|
||||
int retries = 0;
|
||||
|
||||
while (retries < maxCommandRetries)
|
||||
while (retries < _maxCommandRetries)
|
||||
{
|
||||
if (this._console != null)
|
||||
if (_console != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return this._console.SendCommand(command);
|
||||
return _console.SendCommand(command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -324,32 +426,25 @@ namespace ServerManagerTool.Lib
|
|||
retries++;
|
||||
}
|
||||
|
||||
this.maxCommandRetries = 10;
|
||||
_errorLogger?.Error($"Failed to connect to Rcon at {this._rconParameters.RconHostIP}:{this._rconParameters.RconPort} with {this._rconParameters.RconPassword}. {lastException.Message}");
|
||||
throw new Exception($"Command failed to send after {maxCommandRetries} attempts. Last exception: {lastException.Message}", lastException);
|
||||
_maxCommandRetries = 10;
|
||||
_errorLogger?.Error($"Failed to connect to Rcon at {_rconParameters.RconHostIP}:{_rconParameters.RconPort} with {_rconParameters.RconPassword}. {lastException.Message}");
|
||||
throw new Exception($"Command failed to send after {_maxCommandRetries} attempts. Last exception: {lastException.Message}", lastException);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private async Task UpdatePlayersAsync()
|
||||
{
|
||||
if (this._disposed)
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
var token = _cancellationTokenSource.Token;
|
||||
|
||||
var output = await UpdatePlayerDetailsAsync(_cancellationTokenSource.Token);
|
||||
await UpdatePlayerDetailsAsync(_cancellationTokenSource.Token);
|
||||
var cancelled = _cancellationTokenSource.IsCancellationRequested;
|
||||
|
||||
if (!cancelled)
|
||||
{
|
||||
await TaskUtils.RunOnUIThreadAsync(() =>
|
||||
{
|
||||
UpdatePlayerCollection();
|
||||
|
||||
var command = new ConsoleCommand() { rawCommand = RCON_COMMAND_LISTPLAYERS, command = RCON_COMMAND_LISTPLAYERS, lines = output };
|
||||
ProcessOutput(command);
|
||||
});
|
||||
await TaskUtils.RunOnUIThreadAsync(() => UpdatePlayerCollection());
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
|
@ -357,19 +452,19 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
if (!cancelled)
|
||||
{
|
||||
await Task.Delay(PLAYER_LIST_INTERVAL).ContinueWith(t => UpdatePlayersAsync());
|
||||
await Task.Delay(PLAYER_LIST_INTERVAL)
|
||||
.ContinueWith(t => UpdatePlayersAsync());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> UpdatePlayerDetailsAsync(CancellationToken token)
|
||||
private async Task UpdatePlayerDetailsAsync(CancellationToken token)
|
||||
{
|
||||
if (this._disposed)
|
||||
return new List<string>();
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_rconParameters.GameFile) || !File.Exists(_rconParameters.GameFile))
|
||||
return new List<string>();
|
||||
return;
|
||||
|
||||
var savedPath = ServerProfile.GetProfileSavePath(_rconParameters.InstallDirectory);
|
||||
DataContainer dataContainer = null;
|
||||
|
||||
try
|
||||
|
|
@ -379,12 +474,12 @@ namespace ServerManagerTool.Lib
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorLogger?.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: CreateAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
return new List<string>();
|
||||
_errorLogger.Error($"{nameof(UpdatePlayerDetailsAsync)} - Error: CreateAsync. {ex.Message}\r\n{ex.StackTrace}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return new List<string>();
|
||||
return;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
|
|
@ -395,61 +490,41 @@ namespace ServerManagerTool.Lib
|
|||
if (string.IsNullOrWhiteSpace(id))
|
||||
continue;
|
||||
|
||||
this._players.TryGetValue(id, out PlayerInfo player);
|
||||
_players.TryGetValue(id, out PlayerInfo player);
|
||||
player?.UpdatePlatformData(playerData);
|
||||
}
|
||||
}, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return new List<string>();
|
||||
|
||||
var totalPlayers = dataContainer.Players.Count;
|
||||
var output = new List<string>();
|
||||
return;
|
||||
|
||||
foreach (var playerData in dataContainer.Players)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var id = playerData.PlayerId;
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
_debugLogger?.Debug($"{nameof(UpdatePlayerDetailsAsync)} - Error: corrupted profile.\r\n{playerData.CharacterId}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var validPlayer = new PlayerInfo()
|
||||
{
|
||||
PlayerId = id,
|
||||
PlayerName = playerData.PlayerName,
|
||||
CharacterName = playerData.CharacterName,
|
||||
IsValid = true,
|
||||
};
|
||||
|
||||
this._players.AddOrUpdate(id, validPlayer, (k, v) => { v.PlayerName = playerData.PlayerName; v.IsValid = true; return v; });
|
||||
}
|
||||
else
|
||||
{
|
||||
_debugLogger?.Debug($"{nameof(UpdatePlayerDetailsAsync)} - Error: corrupted profile.\r\n{playerData.CharacterId}.");
|
||||
_players.AddOrUpdate(id, validPlayer, (k, v) => { v.PlayerName = playerData.PlayerName; v.CharacterName = playerData.CharacterName; v.IsValid = true; return v; });
|
||||
}
|
||||
|
||||
if (this._players.TryGetValue(id, out PlayerInfo player) && player != null)
|
||||
if (_players.TryGetValue(id, out PlayerInfo player) && player != null)
|
||||
{
|
||||
if (player.IsOnline != playerData.Online)
|
||||
{
|
||||
if (playerData.Online)
|
||||
{
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Join") ?? "Player '{0}' joined the game.";
|
||||
var message = string.Format(messageFormat, playerData.CharacterName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var messageFormat = GlobalizedApplication.Instance.GetResourceString("Player_Leave") ?? "Player '{0}' left the game.";
|
||||
var message = string.Format(messageFormat, playerData.CharacterName);
|
||||
output.Add(message);
|
||||
LogEvent(LogEventType.Event, message);
|
||||
LogEvent(LogEventType.All, message);
|
||||
}
|
||||
}
|
||||
|
||||
player.UpdateData(playerData);
|
||||
|
||||
await TaskUtils.RunOnUIThreadAsync(() =>
|
||||
|
|
@ -461,16 +536,14 @@ namespace ServerManagerTool.Lib
|
|||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return new List<string>();
|
||||
return;
|
||||
|
||||
// remove any players that do not have a player record.
|
||||
var droppedPlayers = this._players.Values.Where(p => dataContainer.Players.FirstOrDefault(pd => pd.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null).ToArray();
|
||||
// remove any players that do not have a player file.
|
||||
var droppedPlayers = _players.Values.Where(p => dataContainer.Players.FirstOrDefault(pd => pd.PlayerId.Equals(p.PlayerId, StringComparison.OrdinalIgnoreCase)) == null).ToArray();
|
||||
foreach (var droppedPlayer in droppedPlayers)
|
||||
{
|
||||
_players.TryRemove(droppedPlayer.PlayerId, out PlayerInfo player);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private void UpdatePlayerCollection()
|
||||
|
|
|
|||
|
|
@ -311,8 +311,7 @@ namespace ServerManagerTool.Lib
|
|||
|
||||
try
|
||||
{
|
||||
// load the player data from the files.
|
||||
onlinePlayerCount = DataContainer.GetOnlinePlayerCount(gameFile);
|
||||
onlinePlayerCount = serverInfo?.Players ?? 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ namespace ServerManagerTool.Lib.ViewModel
|
|||
this.CharacterId = playerData?.CharacterId ?? 0L;
|
||||
this.CharacterName = playerData?.CharacterName;
|
||||
this.GuildName = playerData?.Guild?.GuildName;
|
||||
this.IsOnline = playerData?.Online ?? false;
|
||||
//this.IsOnline = playerData?.Online ?? false;
|
||||
this.LastOnline = playerData?.LastOnline;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
<title>Conan Server Manager Version Feed</title>
|
||||
<subtitle>This is the Conan Server Manager release version feed.</subtitle>
|
||||
<link href="http://servermanagers.freeforums.net/" />
|
||||
<updated>2022-05-06T00:00:00Z</updated>
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
|
||||
<entry>
|
||||
<id>urn:uuid:AD8ABBB5-093A-4FDB-B473-FCED2DB46781</id>
|
||||
<title>1.1.69 (1.1.69.4)</title>
|
||||
<summary>1.1.69.4</summary>
|
||||
<title>1.1.69 (1.1.69.5)</title>
|
||||
<summary>1.1.69.5</summary>
|
||||
<link href="" />
|
||||
<updated>2022-05-07T00:00:00Z</updated>
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
|
||||
<p>
|
||||
|
|
@ -27,6 +27,8 @@
|
|||
<li>Auto Backup Settings - added RCON broadcast mode droplist, so backup processes can send messages via RCON using this mode.</li>
|
||||
<li>Global Backup Settings - added option to include/exclude the SaveGames folder in the worldsave backup (default exclude).</li>
|
||||
<li>Global Alert Settings - added new textbox allowing the formatting of the ipaddress and port in the server startup message (default {ipaddress}:{port}).</li>
|
||||
<li>Rcon Window - minor changes to the player list, online player count and server status.</li>
|
||||
<li>Online Player Count - fixed the online player count to display the correct value.</li>
|
||||
<li>Security Protocol Changes - updated the security protocols to use TLS12 and TLS13.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,31 @@
|
|||
<title>Conan Server Manager Version Feed</title>
|
||||
<subtitle>This is the Conan Server Manager beta version feed.</subtitle>
|
||||
<link href="http://servermanagers.freeforums.net/" />
|
||||
<updated>2022-05-07T00:00:00Z</updated>
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
|
||||
<entry>
|
||||
<id>urn:uuid:68ECEA73-1C9C-4DCB-807B-0D812E986993</id>
|
||||
<title>1.1.69 (1.1.69.5)</title>
|
||||
<summary>1.1.69.5</summary>
|
||||
<link href="" />
|
||||
<updated>2022-05-08T00:00:00Z</updated>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: Arial, Verdana, Helvetica, Sans-Serif;font-size: .8em;">
|
||||
<p>
|
||||
<u style="font-size: .9em;">CHANGE</u>
|
||||
<br/>
|
||||
<ul>
|
||||
<li>Rcon Window - minor changes to the player list, online player count and server status.</li>
|
||||
<li>Online Player Count - fixed the online player count to display the correct value.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</content>
|
||||
<author>
|
||||
<name>bletch</name>
|
||||
<email>bletch1971@hotmail.com</email>
|
||||
</author>
|
||||
</entry>
|
||||
|
||||
<entry>
|
||||
<id>urn:uuid:D886601C-8260-4E06-8B2A-D8F77F1F1B58</id>
|
||||
|
|
|
|||
|
|
@ -206,6 +206,8 @@ namespace ServerManagerTool
|
|||
this.MaxPlayerLabel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
this.ServerRcon.Initialize();
|
||||
|
||||
this.DataContext = this;
|
||||
|
||||
RenderCommentsBlock(
|
||||
|
|
@ -867,12 +869,9 @@ namespace ServerManagerTool
|
|||
}
|
||||
}
|
||||
|
||||
if (!(command.suppressCommand && command.suppressOutput))
|
||||
if (p.Inlines.Count > 0)
|
||||
{
|
||||
if (p.Inlines.Count > 0)
|
||||
{
|
||||
AddBlockContent(p);
|
||||
}
|
||||
AddBlockContent(p);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue