From e2e81cc86c0232c137ed8dc1be570737bdc7b463 Mon Sep 17 00:00:00 2001 From: sven-n Date: Mon, 9 Sep 2024 17:44:46 +0200 Subject: [PATCH] Improved game server management through admin panel --- .../DockerGameServerInstanceManager.cs | 55 ++++ src/Dapr/AdminPanel.Host/ServerRestarter.cs | 36 --- src/Interfaces/IGameServerInstanceManager.cs | 31 +++ src/Interfaces/ISupportServerRestart.cs | 18 -- src/Startup/GameServerContainer.cs | 57 ++++- src/Startup/Program.cs | 2 +- src/Startup/ServerContainerBase.cs | 3 +- .../AdminPanel/Components/ModalQuestion.razor | 32 +++ .../AdminPanel/Components/ServerItem.razor | 153 +++++++---- src/Web/AdminPanel/ModalExtensions.cs | 16 ++ .../Pages/CreateGameServerConfig.razor | 24 ++ .../Pages/CreateGameServerConfig.razor.cs | 241 ++++++++++++++++++ src/Web/AdminPanel/Pages/Servers.razor | 58 +++-- 13 files changed, 590 insertions(+), 136 deletions(-) create mode 100644 src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs delete mode 100644 src/Dapr/AdminPanel.Host/ServerRestarter.cs create mode 100644 src/Interfaces/IGameServerInstanceManager.cs delete mode 100644 src/Interfaces/ISupportServerRestart.cs create mode 100644 src/Web/AdminPanel/Components/ModalQuestion.razor create mode 100644 src/Web/AdminPanel/Pages/CreateGameServerConfig.razor create mode 100644 src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs diff --git a/src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs b/src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs new file mode 100644 index 000000000..f5fb570fd --- /dev/null +++ b/src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs @@ -0,0 +1,55 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.AdminPanel.Host; + +using MUnique.OpenMU.Interfaces; + +/// +/// An implementation of . +/// +public class DockerGameServerInstanceManager : IGameServerInstanceManager +{ + private readonly IServerProvider _serverProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The server provider. + public DockerGameServerInstanceManager(IServerProvider serverProvider) + { + this._serverProvider = serverProvider; + } + + /// + public async ValueTask RestartAllAsync(bool onDatabaseInit) + { + var gameServers = this._serverProvider.Servers.Where(server => server.Type == ServerType.GameServer).ToList(); + foreach (var gameServer in gameServers) + { + await gameServer.ShutdownAsync().ConfigureAwait(false); + // It's started again automatically by the docker host. + } + } + + /// + public async ValueTask InitializeGameServerAsync(byte serverId) + { + // TODO: Implement this... by starting a new docker container + + } + + /// + public async ValueTask RemoveGameServerAsync(byte serverId) + { + var gameServer = this._serverProvider.Servers + .Where(server => server.Type == ServerType.GameServer) + .FirstOrDefault(server => server.Id == serverId); + if (gameServer is not null) + { + await gameServer.ShutdownAsync().ConfigureAwait(false); + // TODO: Remove the docker container + } + } +} \ No newline at end of file diff --git a/src/Dapr/AdminPanel.Host/ServerRestarter.cs b/src/Dapr/AdminPanel.Host/ServerRestarter.cs deleted file mode 100644 index 6152045c1..000000000 --- a/src/Dapr/AdminPanel.Host/ServerRestarter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.AdminPanel.Host; - -using MUnique.OpenMU.Interfaces; - -/// -/// An implementation of . -/// It restarts the server by stopping and starting it. -/// -public class ServerRestarter : ISupportServerRestart -{ - private readonly IServerProvider _serverProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The server provider. - public ServerRestarter(IServerProvider serverProvider) - { - this._serverProvider = serverProvider; - } - - /// - public async ValueTask RestartAllAsync(bool onDatabaseInit) - { - var gameServers = this._serverProvider.Servers.Where(server => server.Type == ServerType.GameServer).ToList(); - foreach (var gameServer in gameServers) - { - await gameServer.ShutdownAsync().ConfigureAwait(false); - // It's started again automatically by the docker host. - } - } -} \ No newline at end of file diff --git a/src/Interfaces/IGameServerInstanceManager.cs b/src/Interfaces/IGameServerInstanceManager.cs new file mode 100644 index 000000000..d7feaf02a --- /dev/null +++ b/src/Interfaces/IGameServerInstanceManager.cs @@ -0,0 +1,31 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Interfaces; + +/// +/// Interface for an instance which manages game servers. +/// +public interface IGameServerInstanceManager +{ + + /// + /// Restarts all servers of this container. + /// + /// If set to true, this method is called during a database initialization. + /// + ValueTask RestartAllAsync(bool onDatabaseInit); + + /// + /// Initializes a game server. + /// + /// The server identifier. + ValueTask InitializeGameServerAsync(byte serverId); + + /// + /// Removes the game server instance. + /// + /// The server identifier. + ValueTask RemoveGameServerAsync(byte serverId); +} \ No newline at end of file diff --git a/src/Interfaces/ISupportServerRestart.cs b/src/Interfaces/ISupportServerRestart.cs deleted file mode 100644 index 4d3bc7e80..000000000 --- a/src/Interfaces/ISupportServerRestart.cs +++ /dev/null @@ -1,18 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// - -namespace MUnique.OpenMU.Interfaces; - -/// -/// Interface for a class which supports to restart a server. -/// -public interface ISupportServerRestart -{ - /// - /// Restarts all servers of this container. - /// - /// If set to true, this method is called during a database initialization. - /// - ValueTask RestartAllAsync(bool onDatabaseInit); -} \ No newline at end of file diff --git a/src/Startup/GameServerContainer.cs b/src/Startup/GameServerContainer.cs index 048fb52b3..f9603eb20 100644 --- a/src/Startup/GameServerContainer.cs +++ b/src/Startup/GameServerContainer.cs @@ -20,7 +20,7 @@ namespace MUnique.OpenMU.Startup; /// /// A container which keeps all s in one . /// -public sealed class GameServerContainer : ServerContainerBase, IDisposable +public sealed class GameServerContainer : ServerContainerBase, IGameServerInstanceManager, IDisposable { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -91,6 +91,34 @@ public void Dispose() } } + /// + public async ValueTask InitializeGameServerAsync(byte serverId) + { + using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext(); + var gameServerDefinitions = await persistenceContext.GetAsync().ConfigureAwait(false); + var gameServerDefinition = gameServerDefinitions.FirstOrDefault(def => def.ServerID == serverId) + ?? throw new InvalidOperationException($"GameServerDefinition of server {serverId} was not found."); + + this.InitializeGameServer(gameServerDefinition); + } + + /// + public async ValueTask RemoveGameServerAsync(byte serverId) + { + using var loggerScope = this._logger.BeginScope("GameServer: {0}", serverId); + if (this._gameServers.TryGetValue(serverId, out var gameServer)) + { + await gameServer.ShutdownAsync().ConfigureAwait(false); + this._gameServers.Remove(serverId); + this._servers.Remove(gameServer); + this._logger.LogInformation($"Game Server {gameServer.Id} - [{gameServer.Description}] removed"); + } + else + { + this._logger.LogInformation($"Game Server {serverId} not found"); + } + } + /// protected override async ValueTask BeforeStartAsync(bool onDatabaseInit, CancellationToken cancellationToken) { @@ -107,19 +135,10 @@ protected override async Task StartInnerAsync(CancellationToken cancellationToke using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext(); await this.LoadGameClientDefinitionsAsync(persistenceContext).ConfigureAwait(false); - var gameServerDefinitions = await persistenceContext.GetAsync().ConfigureAwait(false); + var gameServerDefinitions = await persistenceContext.GetAsync(cancellationToken).ConfigureAwait(false); foreach (var gameServerDefinition in gameServerDefinitions) { - using var loggerScope = this._logger.BeginScope("GameServer: {0}", gameServerDefinition.ServerID); - var gameServer = new GameServer(gameServerDefinition, this._guildServer, this._eventPublisher, this._loginServer, this._persistenceContextProvider, this._friendServer, this._loggerFactory, this._plugInManager, this._changeMediator); - foreach (var endpoint in gameServerDefinition.Endpoints) - { - gameServer.AddListener(new DefaultTcpGameServerListener(endpoint, gameServer.CreateServerInfo(), gameServer.Context, this._connectServerContainer.GetObserver(endpoint.Client!), this._ipResolver, this._loggerFactory)); - } - - this._servers.Add(gameServer); - this._gameServers.Add(gameServer.Id, gameServer); - this._logger.LogInformation($"Game Server {gameServer.Id} - [{gameServer.Description}] initialized"); + this.InitializeGameServer(gameServerDefinition); } } @@ -144,6 +163,20 @@ protected override async Task StopInnerAsync(CancellationToken cancellationToken this._gameServers.Clear(); } + private void InitializeGameServer(GameServerDefinition gameServerDefinition) + { + using var loggerScope = this._logger.BeginScope("GameServer: {0}", gameServerDefinition.ServerID); + var gameServer = new GameServer(gameServerDefinition, this._guildServer, this._eventPublisher, this._loginServer, this._persistenceContextProvider, this._friendServer, this._loggerFactory, this._plugInManager, this._changeMediator); + foreach (var endpoint in gameServerDefinition.Endpoints) + { + gameServer.AddListener(new DefaultTcpGameServerListener(endpoint, gameServer.CreateServerInfo(), gameServer.Context, this._connectServerContainer.GetObserver(endpoint.Client!), this._ipResolver, this._loggerFactory)); + } + + this._servers.Add(gameServer); + this._gameServers.Add(gameServer.Id, gameServer); + this._logger.LogInformation($"Game Server {gameServer.Id} - [{gameServer.Description}] initialized"); + } + private async ValueTask LoadGameClientDefinitionsAsync(IContext persistenceContext) { var versions = (await persistenceContext.GetAsync().ConfigureAwait(false)).ToList(); diff --git a/src/Startup/Program.cs b/src/Startup/Program.cs index 34fd551ab..f2f9f69f7 100644 --- a/src/Startup/Program.cs +++ b/src/Startup/Program.cs @@ -261,7 +261,7 @@ private async Task CreateHostAsync(string[] args) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(provider => provider.GetService()!) + .AddSingleton(provider => provider.GetService()!) .AddScoped() .AddSingleton() .AddSingleton>(provider => provider.GetService() ?? throw new Exception($"{nameof(ConnectServerContainer)} not registered.")) diff --git a/src/Startup/ServerContainerBase.cs b/src/Startup/ServerContainerBase.cs index e5d487598..94d941ce0 100644 --- a/src/Startup/ServerContainerBase.cs +++ b/src/Startup/ServerContainerBase.cs @@ -7,13 +7,12 @@ namespace MUnique.OpenMU.Startup; using System.Threading; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using MUnique.OpenMU.Interfaces; using MUnique.OpenMU.Web.AdminPanel.Services; /// /// Base class for a server container, which reacts on database recreations. /// -public abstract class ServerContainerBase : IHostedService, ISupportServerRestart +public abstract class ServerContainerBase : IHostedService { private readonly SetupService _setupService; private readonly ILogger _logger; diff --git a/src/Web/AdminPanel/Components/ModalQuestion.razor b/src/Web/AdminPanel/Components/ModalQuestion.razor new file mode 100644 index 000000000..188d855ed --- /dev/null +++ b/src/Web/AdminPanel/Components/ModalQuestion.razor @@ -0,0 +1,32 @@ + +
+

@Question

+
+ + + + + + +@code { + + [CascadingParameter] + BlazoredModalInstance BlazoredModal { get; set; } = null!; + + /// + /// Gets or sets the text. + /// + [Parameter] + public string Question { get; set; } = string.Empty; + + private async Task SetResponseAsync(bool yes) { + await this.BlazoredModal.CloseAsync(ModalResult.Ok(yes)); + } +} diff --git a/src/Web/AdminPanel/Components/ServerItem.razor b/src/Web/AdminPanel/Components/ServerItem.razor index fef0c46d4..645d22821 100644 --- a/src/Web/AdminPanel/Components/ServerItem.razor +++ b/src/Web/AdminPanel/Components/ServerItem.razor @@ -1,9 +1,15 @@ @using System.ComponentModel @using MUnique.OpenMU.DataModel.Configuration @using MUnique.OpenMU.Interfaces +@using MUnique.OpenMU.Persistence @implements IDisposable - +@if (this._isDeleted) +{ + return; +} + + @if (this.Server.Type == ServerType.GameServer) { @@ -35,21 +41,60 @@ @this.GetStateCaption() - @if (this.IsActionAvailable()) +
+ @if (this.Server.ServerState == ServerState.Started) + { + + + @if (this.Server.Type == ServerType.GameServer) { - + } - else + } + else if ((this.Server.ServerState == ServerState.Stopped)) + { + + + @if (this.Server.Type == ServerType.GameServer) + { + + } + } + else + { + + + @if (this.Server.Type == ServerType.GameServer) { - + } - + } +
+ @code { - private bool _isExpanded; + private bool _isDeleted; /// /// Gets or sets the server which is shown in this component. @@ -57,6 +102,30 @@ [Parameter] public IManageableServer Server { get; set; } = null!; + /// + /// Gets or sets the . + /// + [Inject] + public IGameServerInstanceManager InstanceManager { get; set; } = null!; + + /// + /// Gets or sets the . + /// + [Inject] + public IPersistenceContextProvider ContextProvider { get; set; } = null!; + + /// + /// Gets or sets the data source for the game configuration. + /// + [Inject] + public IDataSource DataSource { get; set; } = null!; + + /// + /// Gets or sets the modal service. + /// + [Inject] + public IModalService ModalService { get; set; } = null!; + /// public void Dispose() { @@ -70,6 +139,13 @@ this.Server.PropertyChanged += this.OnServerPropertyChanged; } + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + this._isDeleted = false; + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Catching all Exceptions.")] private async void OnServerPropertyChanged(object? sender, PropertyChangedEventArgs eventArgs) { @@ -84,21 +160,6 @@ } - private string GetExpandIconClass() - { - if (this._isExpanded) - { - return "oi oi-minus"; - } - - if (this.Server.Type == ServerType.GameServer) - { - return "oi oi-plus"; - } - - return "oi oi-cog"; - } - private string GetActionClass() { if (this.Server.ServerState == ServerState.Started) @@ -107,35 +168,39 @@ return "btn-warning"; } - private async Task OnExpandClick(MouseEventArgs eventArgs) + private async Task OnPauseClickAsync() { - this._isExpanded = !this._isExpanded; - await this.InvokeAsync(this.StateHasChanged); + await this.Server.StopAsync(default); } - private Task OnActionClick(MouseEventArgs eventArgs) + private async Task OnStartClickAsync() { - if (this.Server.ServerState == ServerState.Started) - { - return this.Server.StopAsync(default); - } - else - { - return this.Server.StartAsync(default); - } + await this.Server.StartAsync(default); } - private bool IsActionAvailable() + private async Task OnDeleteClickAsync() { - return (this.Server.ServerState == ServerState.Started || this.Server.ServerState == ServerState.Stopped); - } - - private string GetActionCaption() - { - if (this.Server.ServerState == ServerState.Started) - return "Shutdown"; - else - return "Start"; + var dialogResult = await this.ModalService.ShowQuestionAsync( + "Remove Game Server", + "The server will be deleted from the database. Are you sure to proceed?"); + if (!dialogResult) + { + return; + } + + await this.Server.StopAsync(default); + await this.InstanceManager.RemoveGameServerAsync((byte)this.Server.Id); + + var gameConfiguration = await this.DataSource.GetOwnerAsync().ConfigureAwait(false); + using var context = this.ContextProvider.CreateNewTypedContext(true, gameConfiguration); + var definitions = await context.GetAsync().ConfigureAwait(false); + var definition = definitions.FirstOrDefault(def => def.ServerID == this.Server.Id); + if (definition is not null) + { + await context.DeleteAsync(definition).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + this._isDeleted = true; + } } private string GetStateCaption() diff --git a/src/Web/AdminPanel/ModalExtensions.cs b/src/Web/AdminPanel/ModalExtensions.cs index b51562385..b074d3e2f 100644 --- a/src/Web/AdminPanel/ModalExtensions.cs +++ b/src/Web/AdminPanel/ModalExtensions.cs @@ -53,4 +53,20 @@ public static Task ShowMessageAsync(this IModalService modalService, string titl var modal = modalService.Show(title, messageParams); return modal.Result; } + + /// + /// Shows a message in a modal dialog. + /// + /// The modal service. + /// The title. + /// The question. + public static async Task ShowQuestionAsync(this IModalService modalService, string title, string question) + { + var messageParams = new ModalParameters(); + messageParams.Add(nameof(ModalQuestion.Question), question); + + var modal = modalService.Show(title, messageParams); + var result = await modal.Result.ConfigureAwait(false); + return result.Data is true; + } } \ No newline at end of file diff --git a/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor new file mode 100644 index 000000000..585008080 --- /dev/null +++ b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor @@ -0,0 +1,24 @@ +@page "/create-game-server" + +@using MUnique.OpenMU.Persistence + +

Create Game Server Definition

+ + +@if (this._viewModel is null) +{ + + Loading... + return; +} + +@if (this._initState is { }) +{ + + @this._initState + return; +} + + + + diff --git a/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs new file mode 100644 index 000000000..058c6daae --- /dev/null +++ b/src/Web/AdminPanel/Pages/CreateGameServerConfig.razor.cs @@ -0,0 +1,241 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +using MUnique.OpenMU.Interfaces; + +namespace MUnique.OpenMU.Web.AdminPanel.Pages; + +using System.ComponentModel.DataAnnotations; +using System.Threading; +using Blazored.Modal.Services; +using Microsoft.AspNetCore.Components; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence; + +/// +/// Razor page which shows objects of the specified type in a grid. +/// +public partial class CreateGameServerConfig : ComponentBase, IAsyncDisposable +{ + private Task? _loadTask; + private CancellationTokenSource? _disposeCts; + + private GameServerViewModel? _viewModel; + private string? _initState; + + //private IList _existingServerDefinitions; + + /// + /// Gets or sets the context provider. + /// + [Inject] + public IPersistenceContextProvider ContextProvider { get; set; } = null!; + + /// + /// Gets or sets the server initializer. + /// + [Inject] + public IGameServerInstanceManager ServerInstanceManager { get; set; } = null!; + + /// + /// Gets or sets the data source. + /// + [Inject] + public IDataSource DataSource { get; set; } = null!; + + /// + /// Gets or sets the modal service. + /// + [Inject] + public IModalService ModalService { get; set; } = null!; + + /// + /// Gets or sets the navigation manager. + /// + [Inject] + public NavigationManager NavigationManager { get; set; } = null!; + + /// + public async ValueTask DisposeAsync() + { + await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false); + this._disposeCts?.Dispose(); + this._disposeCts = null; + + try + { + await (this._loadTask ?? Task.CompletedTask).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // we can ignore that ... + } + catch + { + // and we should not throw exceptions in the dispose method ... + } + } + + /// + protected override async Task OnParametersSetAsync() + { + var cts = new CancellationTokenSource(); + this._disposeCts = cts; + this._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token), cts.Token); + await base.OnParametersSetAsync().ConfigureAwait(true); + } + + private async Task LoadDataAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var gameConfiguration = await this.DataSource.GetOwnerAsync(default, cancellationToken).ConfigureAwait(true); + using var persistenceContext = this.ContextProvider.CreateNewContext(gameConfiguration); + + var serverConfigs = await persistenceContext.GetAsync(cancellationToken).ConfigureAwait(false); + var clients = await persistenceContext.GetAsync(cancellationToken).ConfigureAwait(false); + var existingServerDefinitions = (await persistenceContext.GetAsync(cancellationToken).ConfigureAwait(false)).ToList(); + + + var nextServerId = 0; + var networkPort = 55901; + if (existingServerDefinitions.Count > 0) + { + nextServerId = existingServerDefinitions.Max(s => s.ServerID) + 1; + networkPort = existingServerDefinitions.Max(s => s.Endpoints.FirstOrDefault()?.NetworkPort ?? 55900) + 1; + } + + this._viewModel = new GameServerViewModel + { + ServerConfiguration = serverConfigs.FirstOrDefault(), + ServerId = (byte)nextServerId, + ExperienceRate = 1.0f, + PvpEnabled = true, + NetworkPort = networkPort, + Client = clients.FirstOrDefault(), + }; + + await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(false); + } + + private async ValueTask CreateDefinitionByViewModelAsync(IContext context) + { + if (this._viewModel is null) + { + throw new InvalidOperationException("View model not initialized."); + } + + var result = context.CreateNew(); + result.ServerID = this._viewModel.ServerId; + result.Description = this._viewModel.Description; + result.PvpEnabled = this._viewModel.PvpEnabled; + result.ExperienceRate = this._viewModel.ExperienceRate; + result.GameConfiguration = await this.DataSource.GetOwnerAsync(); + result.ServerConfiguration = this._viewModel.ServerConfiguration!; + + var endpoint = context.CreateNew(); + endpoint.NetworkPort = (ushort)this._viewModel.NetworkPort; + endpoint.Client = this._viewModel.Client!; + result.Endpoints.Add(endpoint); + + return result; + } + + private async Task OnSaveButtonClickAsync() + { + string text; + try + { + var gameConfiguration = await this.DataSource.GetOwnerAsync().ConfigureAwait(false); + + using var saveContext = this.ContextProvider.CreateNewTypedContext(true, gameConfiguration); + + var existingServerDefinitions = (await saveContext.GetAsync().ConfigureAwait(false)).ToList(); + if (existingServerDefinitions.Any(def => def.ServerID == this._viewModel?.ServerId)) + { + await this.ModalService.ShowMessageAsync("Save", $"Server with Id {this._viewModel?.ServerId} already exists. Please use another value.").ConfigureAwait(true); + return; + } + + if (existingServerDefinitions.Any(def => def.Endpoints.Any(endpoint => endpoint.NetworkPort == this._viewModel?.NetworkPort))) + { + await this.ModalService.ShowMessageAsync("Save", $"A server with tcp port {this._viewModel?.NetworkPort} already exists. Please use another tcp port.").ConfigureAwait(true); + return; + } + + this._initState = "Creating Configuration ..."; + await this.InvokeAsync(this.StateHasChanged); + var gameServerDefinition = await this.CreateDefinitionByViewModelAsync(saveContext).ConfigureAwait(false); + this._initState = "Saving Configuration ..."; + await this.InvokeAsync(this.StateHasChanged); + var success = await saveContext.SaveChangesAsync().ConfigureAwait(true); + text = success ? "The changes have been saved." : "There were no changes to save."; + + // if success, init new game server instance + if (success) + { + this._initState = "Initializing Game Server ..."; + await this.InvokeAsync(this.StateHasChanged); + await this.ServerInstanceManager.InitializeGameServerAsync(gameServerDefinition.ServerID); + this.NavigationManager.NavigateTo("servers"); + return; + } + } + catch (Exception ex) + { + text = $"An unexpected error occurred: {ex.Message}."; + } + + await this.ModalService.ShowMessageAsync("Save", text).ConfigureAwait(true); + this._initState = null; + } + + /// + /// The view model for a . + /// + public class GameServerViewModel + { + /// + /// Gets or sets the server identifier. + /// + public byte ServerId { get; set; } + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the experience rate. + /// + /// + /// The experience rate. + /// + [Range(0, float.MaxValue)] + public float ExperienceRate { get; set; } + + /// + /// Gets or sets a value indicating whether PVP is enabled on this server. + /// + public bool PvpEnabled { get; set; } + + /// + /// Gets or sets the server configuration. + /// + [Required] + public GameServerConfiguration? ServerConfiguration { get; set; } + + /// + /// Gets or sets the client which is expected to connect. + /// + [Required] + public GameClientDefinition? Client { get; set; } + + /// + /// Gets or sets the network port on which the server is listening. + /// + [Range(1, ushort.MaxValue)] + public int NetworkPort { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/AdminPanel/Pages/Servers.razor b/src/Web/AdminPanel/Pages/Servers.razor index 4ce857960..f6ba8448f 100644 --- a/src/Web/AdminPanel/Pages/Servers.razor +++ b/src/Web/AdminPanel/Pages/Servers.razor @@ -29,28 +29,40 @@ else @foreach (var server in this._servers.OrderBy(s => s.Type).ThenBy(s => s.Description)) { - + } + + + + + + Game Server + + + + + + @if (this.ServerInstanceManager is not null) + { + @if (this._isRestarting) + { + + } + else + { + + } + } + + -
- @if (this.ServerRestartSupporter is not null) - { - @if (this._isRestarting) - { - - } - else - { - - } - } } @@ -66,10 +78,10 @@ else public IServerProvider ServerProvider { get; set; } = null!; /// - /// Gets or sets the . + /// Gets or sets the . /// - [Inject] // TODO: Allow Default - public ISupportServerRestart? ServerRestartSupporter { get; set; } + [Inject] + public IGameServerInstanceManager? ServerInstanceManager { get; set; } /// public void Dispose() @@ -99,7 +111,7 @@ else this._isRestarting = true; try { - await this.ServerRestartSupporter!.RestartAllAsync(false); + await this.ServerInstanceManager!.RestartAllAsync(false); } finally {