baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
66
src/Startup/ChatServerContainer.cs
Normal file
66
src/Startup/ChatServerContainer.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// <copyright file="ChatServerContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A container which takes care of the <see cref="Interfaces.IChatServer"/>.
|
||||
/// It initializes and restarts it, as soon as the database is reinitialized.
|
||||
/// </summary>
|
||||
public class ChatServerContainer : ServerContainerBase
|
||||
{
|
||||
private readonly ChatServer.ChatServer _chatServer;
|
||||
private readonly IPersistenceContextProvider _persistenceContextProvider;
|
||||
private readonly ILogger<ChatServerContainer> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServerContainer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatServer">The chat server.</param>
|
||||
/// <param name="persistenceContextProvider">The persistence context provider.</param>
|
||||
/// <param name="setupService">The setup service.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ChatServerContainer(ChatServer.ChatServer chatServer, IPersistenceContextProvider persistenceContextProvider, SetupService setupService, ILogger<ChatServerContainer> logger)
|
||||
: base(setupService, logger)
|
||||
{
|
||||
this._chatServer = chatServer;
|
||||
this._persistenceContextProvider = persistenceContextProvider;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var definitions = await this._persistenceContextProvider.CreateNewConfigurationContext().GetAsync<ChatServerDefinition>().ConfigureAwait(false);
|
||||
if (definitions.FirstOrDefault() is { } definition)
|
||||
{
|
||||
definition.ConvertToSettings();
|
||||
this._chatServer.Initialize(definition.ConvertToSettings());
|
||||
await this._chatServer.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("No chat server configuration found.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StopInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this._chatServer.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartListenersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// listeners are always started...
|
||||
await this._chatServer.StartAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
46
src/Startup/ChatServerDefinitionExtensions.cs
Normal file
46
src/Startup/ChatServerDefinitionExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// <copyright file="ChatServerDefinitionExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using MUnique.OpenMU.ChatServer;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="ChatServerDefinition"/>.
|
||||
/// </summary>
|
||||
public static class ChatServerDefinitionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the <see cref="ChatServerDefinition"/> into corresponding <see cref="ChatServerSettings"/>.
|
||||
/// </summary>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <returns>The settings.</returns>
|
||||
public static ChatServerSettings ConvertToSettings(this ChatServerDefinition definition)
|
||||
{
|
||||
var result = new ChatServerSettings
|
||||
{
|
||||
Id = definition.GetId(),
|
||||
MaximumConnections = definition.MaximumConnections,
|
||||
ClientTimeout = definition.ClientTimeout,
|
||||
ClientCleanUpInterval = definition.ClientCleanUpInterval,
|
||||
RoomCleanUpInterval = definition.RoomCleanUpInterval,
|
||||
Description = definition.Description,
|
||||
ServerId = definition.ServerId + SpecialServerIds.ChatServer,
|
||||
};
|
||||
foreach (var endpoint in definition.Endpoints)
|
||||
{
|
||||
result.Endpoints.Add(new OpenMU.ChatServer.ChatServerEndpoint
|
||||
{
|
||||
ClientVersion = new ClientVersion(endpoint.Client!.Season, endpoint.Client.Episode, endpoint.Client.Language),
|
||||
NetworkPort = endpoint.NetworkPort,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
106
src/Startup/ConfigurationChangeHandler.cs
Normal file
106
src/Startup/ConfigurationChangeHandler.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// <copyright file="ConfigurationChangeHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IConfigurationChangePublisher"/> which directly handles the changes
|
||||
/// by updating some components and forwarding the events to the <see cref="IConfigurationChangeMediator"/>.
|
||||
/// </summary>
|
||||
public class ConfigurationChangeHandler : IConfigurationChangePublisher
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfigurationChangeMediatorListener _changeMediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationChangeHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
/// <param name="changeMediator">The change mediator.</param>
|
||||
public ConfigurationChangeHandler(IServiceProvider serviceProvider, IConfigurationChangeMediatorListener changeMediator)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
this._changeMediator = changeMediator;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConfigurationChangedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
// TODO: subscribe these systems to the change mediator
|
||||
if (this._serviceProvider.GetService<PlugInManager>() is { } plugInManager)
|
||||
{
|
||||
plugInManager.ApplyChangedConfiguration(type, id, configuration);
|
||||
}
|
||||
|
||||
if (configuration is ConnectServerDefinition connectServerDefinition)
|
||||
{
|
||||
await this.OnConnectServerDefinitionChangedAsync(id, connectServerDefinition).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (configuration is SystemConfiguration systemConfiguration)
|
||||
{
|
||||
this.OnSystemConfigurationChanged(id, systemConfiguration);
|
||||
}
|
||||
|
||||
await this._changeMediator.HandleConfigurationChangedAsync(type, id, configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConfigurationAddedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
await this._changeMediator.HandleConfigurationAddedAsync(type, id, configuration).ConfigureAwait(false);
|
||||
|
||||
if (type.IsAssignableTo(typeof(PlugInConfiguration)) && this._serviceProvider.GetService<PlugInManager>() is { } plugInManager)
|
||||
{
|
||||
// todo: find out what to do, because usually, plugin configs are not added during runtime.
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConfigurationRemovedAsync(Type type, Guid id)
|
||||
{
|
||||
if (this._serviceProvider.GetService<PlugInManager>() is { } plugInManager)
|
||||
{
|
||||
plugInManager.ApplyRemovedConfiguration(type, id);
|
||||
}
|
||||
|
||||
await this._changeMediator.HandleConfigurationRemovedAsync(type, id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnSystemConfigurationChanged(Guid id, SystemConfiguration systemConfiguration)
|
||||
{
|
||||
if (this._serviceProvider.GetService<IIpAddressResolver>() is not ConfigurableIpResolver ipAddressResolver)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ipAddressResolver.Configure(systemConfiguration.IpResolver, systemConfiguration.IpResolverParameter);
|
||||
}
|
||||
|
||||
private async ValueTask OnConnectServerDefinitionChangedAsync(Guid id, ConnectServerDefinition connectServerDefinition)
|
||||
{
|
||||
if (this._serviceProvider.GetService<ConnectServerContainer>() is not { } connectServerContainer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var connectServer in connectServerContainer)
|
||||
{
|
||||
if (connectServer.ServerState == ServerState.Started)
|
||||
{
|
||||
await connectServer.ShutdownAsync().ConfigureAwait(false);
|
||||
|
||||
//// todo: is applying new settings required?
|
||||
await connectServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/Startup/ConnectServerContainer.cs
Normal file
145
src/Startup/ConnectServerContainer.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
// <copyright file="ConnectServerContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.Collections;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.ConnectServer;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A container which keeps all <see cref="Interfaces.IConnectServer"/>s in one <see cref="IHostedService"/>.
|
||||
/// </summary>
|
||||
public class ConnectServerContainer : ServerContainerBase, IEnumerable<IConnectServer>, IConnectServerInstanceManager
|
||||
{
|
||||
private readonly IList<IManageableServer> _servers;
|
||||
private readonly IPersistenceContextProvider _persistenceContextProvider;
|
||||
private readonly ConnectServerFactory _connectServerFactory;
|
||||
private readonly IList<IConnectServer> _connectServers = new List<IConnectServer>();
|
||||
private readonly Dictionary<GameClientDefinition, MulticastConnectionServerStateObserver> _observers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectServerContainer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="servers">The servers.</param>
|
||||
/// <param name="persistenceContextProvider">The persistence context provider.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="connectServerFactory">The connect server factory.</param>
|
||||
/// <param name="setupService">The setup service.</param>
|
||||
public ConnectServerContainer(IList<IManageableServer> servers, IPersistenceContextProvider persistenceContextProvider, ILogger<ConnectServerContainer> logger, ConnectServerFactory connectServerFactory, SetupService setupService)
|
||||
: base(setupService, logger)
|
||||
{
|
||||
this._servers = servers;
|
||||
this._persistenceContextProvider = persistenceContextProvider;
|
||||
this._connectServerFactory = connectServerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeConnectServerAsync(Guid connectServerDefinitionId)
|
||||
{
|
||||
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
|
||||
var definition = await persistenceContext
|
||||
.GetByIdAsync<ConnectServerDefinition>(connectServerDefinitionId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var newConnectServer = this.InitializeConnectServer(definition ?? throw new InvalidOperationException($"ConnectServerDefinition with id {connectServerDefinitionId} was not found."));
|
||||
if (this._observers.TryGetValue(definition.Client!, out var observer))
|
||||
{
|
||||
observer.PullRegistrations(newConnectServer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RemoveConnectServerAsync(Guid connectServerDefinitionId)
|
||||
{
|
||||
var connectServer = this._connectServers
|
||||
.FirstOrDefault(server => server.ConfigurationId == connectServerDefinitionId)
|
||||
?? throw new InvalidOperationException($"ConnectServer with Definition with id {connectServerDefinitionId} was not found.");
|
||||
await connectServer.StopAsync(default).ConfigureAwait(false);
|
||||
this._servers.Remove(connectServer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the observer.
|
||||
/// </summary>
|
||||
/// <param name="gameClient">The game client.</param>
|
||||
/// <returns>The observer for the client.</returns>
|
||||
public IGameServerStateObserver GetObserver(GameClientDefinition gameClient)
|
||||
{
|
||||
if (!this._observers.TryGetValue(gameClient, out var observer))
|
||||
{
|
||||
// In this case, most probably the game server gets started before the connection server.
|
||||
observer = new MulticastConnectionServerStateObserver();
|
||||
this._observers[gameClient] = observer;
|
||||
}
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<IConnectServer> GetEnumerator()
|
||||
{
|
||||
return this._connectServers.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartListenersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var server in this._connectServers)
|
||||
{
|
||||
await server.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
|
||||
foreach (var connectServerDefinition in await persistenceContext.GetAsync<ConnectServerDefinition>(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
this.InitializeConnectServer(connectServerDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StopInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var connectServer in this._connectServers)
|
||||
{
|
||||
await connectServer.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._servers.Remove(connectServer);
|
||||
}
|
||||
|
||||
this._connectServers.Clear();
|
||||
}
|
||||
|
||||
private IConnectServer InitializeConnectServer(ConnectServerDefinition connectServerDefinition)
|
||||
{
|
||||
var connectServer = this._connectServerFactory.CreateConnectServer(connectServerDefinition);
|
||||
this._servers.Add(connectServer);
|
||||
this._connectServers.Add(connectServer);
|
||||
var client = connectServerDefinition.Client!;
|
||||
if (!this._observers.TryGetValue(client, out var observer))
|
||||
{
|
||||
// we're now always creating a multicast observer, because we want to support
|
||||
// creation of connect servers during runtime.
|
||||
observer = new MulticastConnectionServerStateObserver();
|
||||
this._observers[client] = observer;
|
||||
}
|
||||
|
||||
observer.AddObserver(connectServer);
|
||||
return connectServer;
|
||||
}
|
||||
}
|
||||
53
src/Startup/Dockerfile
Normal file
53
src/Startup/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 55901
|
||||
EXPOSE 55902
|
||||
EXPOSE 55903
|
||||
EXPOSE 55904
|
||||
EXPOSE 55905
|
||||
EXPOSE 55906
|
||||
EXPOSE 44405
|
||||
EXPOSE 55980
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY Directory.Packages.props .
|
||||
COPY Directory.Build.props .
|
||||
|
||||
COPY Startup/*.csproj Startup/
|
||||
COPY Persistence/**/*.csproj Persistence/
|
||||
|
||||
RUN dotnet restore "Startup/MUnique.OpenMU.Startup.csproj"
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /src/Startup
|
||||
|
||||
RUN dotnet build "MUnique.OpenMU.Startup.csproj" \
|
||||
-c Release \
|
||||
-o /app/build \
|
||||
-p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.Startup.csproj" \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
-p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
RUN mkdir -p /app/logs && chmod 777 /app/logs
|
||||
|
||||
ARG APP_UID=1000
|
||||
USER ${APP_UID}
|
||||
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.Startup.dll", "-autostart"]
|
||||
195
src/Startup/GameServerContainer.cs
Normal file
195
src/Startup/GameServerContainer.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
// <copyright file="GameServerContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GameServer;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A container which keeps all <see cref="IGameServer"/>s in one <see cref="IHostedService"/>.
|
||||
/// </summary>
|
||||
public sealed class GameServerContainer : ServerContainerBase, IGameServerInstanceManager, IDisposable
|
||||
{
|
||||
private readonly ILogger<GameServerContainer> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IList<IManageableServer> _servers;
|
||||
private readonly IPersistenceContextProvider _persistenceContextProvider;
|
||||
private readonly ConnectServerContainer _connectServerContainer;
|
||||
private readonly IGuildServer _guildServer;
|
||||
private readonly ILoginServer _loginServer;
|
||||
private readonly IFriendServer _friendServer;
|
||||
private readonly IIpAddressResolver _ipResolver;
|
||||
private readonly PlugInManager _plugInManager;
|
||||
private readonly IConfigurationChangeMediator _changeMediator;
|
||||
private readonly IDictionary<int, IGameServer> _gameServers;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerContainer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="servers">The servers.</param>
|
||||
/// <param name="gameServers">The game servers.</param>
|
||||
/// <param name="persistenceContextProvider">The persistence context provider.</param>
|
||||
/// <param name="connectServerContainer">The connect server container.</param>
|
||||
/// <param name="guildServer">The guild server.</param>
|
||||
/// <param name="loginServer">The login server.</param>
|
||||
/// <param name="friendServer">The friend server.</param>
|
||||
/// <param name="ipResolver">The ip resolver.</param>
|
||||
/// <param name="plugInManager">The plug in manager.</param>
|
||||
/// <param name="setupService">The setup service.</param>
|
||||
/// <param name="changeMediator">The change mediator.</param>
|
||||
public GameServerContainer(
|
||||
ILoggerFactory loggerFactory,
|
||||
IList<IManageableServer> servers,
|
||||
IDictionary<int, IGameServer> gameServers,
|
||||
IPersistenceContextProvider persistenceContextProvider,
|
||||
ConnectServerContainer connectServerContainer,
|
||||
IGuildServer guildServer,
|
||||
ILoginServer loginServer,
|
||||
IFriendServer friendServer,
|
||||
IIpAddressResolver ipResolver,
|
||||
PlugInManager plugInManager,
|
||||
SetupService setupService,
|
||||
IConfigurationChangeMediator changeMediator)
|
||||
: base(setupService, loggerFactory.CreateLogger<GameServerContainer>())
|
||||
{
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._servers = servers;
|
||||
this._gameServers = gameServers;
|
||||
this._persistenceContextProvider = persistenceContextProvider;
|
||||
this._connectServerContainer = connectServerContainer;
|
||||
this._guildServer = guildServer;
|
||||
this._loginServer = loginServer;
|
||||
this._friendServer = friendServer;
|
||||
this._ipResolver = ipResolver;
|
||||
this._plugInManager = plugInManager;
|
||||
this._changeMediator = changeMediator;
|
||||
|
||||
this._logger = this._loggerFactory.CreateLogger<GameServerContainer>();
|
||||
this._eventPublisher = new InMemoryEventPublisher(this._gameServers, this._friendServer, this._guildServer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var gameServer in this._gameServers.Values)
|
||||
{
|
||||
(gameServer as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeGameServerAsync(byte serverId)
|
||||
{
|
||||
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
|
||||
var gameServerDefinitions = await persistenceContext.GetAsync<GameServerDefinition>().ConfigureAwait(false);
|
||||
var gameServerDefinition = gameServerDefinitions.FirstOrDefault(def => def.ServerID == serverId)
|
||||
?? throw new InvalidOperationException($"GameServerDefinition of server {serverId} was not found.");
|
||||
|
||||
this.InitializeGameServer(gameServerDefinition);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask BeforeStartAsync(bool onDatabaseInit, CancellationToken cancellationToken)
|
||||
{
|
||||
await base.BeforeStartAsync(onDatabaseInit, cancellationToken);
|
||||
if (!onDatabaseInit)
|
||||
{
|
||||
(this._persistenceContextProvider as IMigratableDatabaseContextProvider)?.ResetCache();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
|
||||
await this.LoadGameClientDefinitionsAsync(persistenceContext).ConfigureAwait(false);
|
||||
|
||||
var gameServerDefinitions = await persistenceContext.GetAsync<GameServerDefinition>(cancellationToken).ConfigureAwait(false);
|
||||
foreach (var gameServerDefinition in gameServerDefinitions)
|
||||
{
|
||||
this.InitializeGameServer(gameServerDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StartListenersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var gameServer in this._gameServers.Values)
|
||||
{
|
||||
await gameServer.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task StopInnerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var gameServer in this._gameServers.Values)
|
||||
{
|
||||
await gameServer.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._servers.Remove(gameServer);
|
||||
}
|
||||
|
||||
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<GameClientDefinition>().ConfigureAwait(false)).ToList();
|
||||
foreach (var gameClientDefinition in versions)
|
||||
{
|
||||
ClientVersionResolver.Register(
|
||||
gameClientDefinition.Version,
|
||||
new ClientVersion(gameClientDefinition.Season, gameClientDefinition.Episode, gameClientDefinition.Language));
|
||||
}
|
||||
|
||||
if (versions.FirstOrDefault() is { } firstVersion)
|
||||
{
|
||||
ClientVersionResolver.DefaultVersion = ClientVersionResolver.Resolve(firstVersion.Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/Startup/InMemoryEventPublisher.cs
Normal file
76
src/Startup/InMemoryEventPublisher.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="InMemoryEventPublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEventPublisher"/> which publishes directly to the available started servers in this application.
|
||||
/// </summary>
|
||||
public class InMemoryEventPublisher : IEventPublisher
|
||||
{
|
||||
private readonly IDictionary<int, IGameServer> _gameServers;
|
||||
private readonly IFriendServer _friendServer;
|
||||
private readonly IGuildServer _guildServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InMemoryEventPublisher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="gameServers">The game servers.</param>
|
||||
/// <param name="friendServer">The friend server.</param>
|
||||
/// <param name="guildServer">The guild server.</param>
|
||||
public InMemoryEventPublisher(IDictionary<int, IGameServer> gameServers, IFriendServer friendServer, IGuildServer guildServer)
|
||||
{
|
||||
this._gameServers = gameServers;
|
||||
this._friendServer = friendServer;
|
||||
this._guildServer = guildServer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName)
|
||||
{
|
||||
await this._guildServer.PlayerEnteredGameAsync(characterId, characterName, serverId).ConfigureAwait(false);
|
||||
|
||||
await this._friendServer.PlayerEnteredGameAsync(serverId, characterId, characterName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerLeftGameAsync(byte serverId, Guid characterId, string characterName, uint guildId = 0)
|
||||
{
|
||||
if (guildId > 0)
|
||||
{
|
||||
await this._guildServer.GuildMemberLeftGameAsync(guildId, characterId, serverId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this._friendServer.PlayerLeftGameAsync(characterId, characterName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
foreach (var gameServer in this._gameServers)
|
||||
{
|
||||
await gameServer.Value.GuildChatMessageAsync(guildId, sender, message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
foreach (var gameServer in this._gameServers)
|
||||
{
|
||||
await gameServer.Value.AllianceChatMessageAsync(guildId, sender, message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerAlreadyLoggedInAsync(byte serverId, string loginName)
|
||||
{
|
||||
foreach (var gameServer in this._gameServers)
|
||||
{
|
||||
await gameServer.Value.PlayerAlreadyLoggedInAsync(serverId, loginName).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Startup/MUnique.OpenMU.Startup.csproj
Normal file
69
src/Startup/MUnique.OpenMU.Startup.csproj
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<DockerComposeProjectPath>..\..\deploy\all-in-one\docker-compose-all-in-one.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>4D77CA72-8356-43AA-8689-00F2367561B2</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Web\AdminPanel\MUnique.OpenMU.Web.AdminPanel.csproj" />
|
||||
<ProjectReference Include="..\ChatServer\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\ConnectServer\MUnique.OpenMU.ConnectServer.csproj" />
|
||||
<ProjectReference Include="..\DataModel\MUnique.OpenMU.DataModel.csproj" />
|
||||
<ProjectReference Include="..\FriendServer\MUnique.OpenMU.FriendServer.csproj" />
|
||||
<ProjectReference Include="..\GameLogic\MUnique.OpenMU.GameLogic.csproj" />
|
||||
<ProjectReference Include="..\GameServer\MUnique.OpenMU.GameServer.csproj" />
|
||||
<ProjectReference Include="..\GuildServer\MUnique.OpenMU.GuildServer.csproj" />
|
||||
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\LoginServer\MUnique.OpenMU.LoginServer.csproj" />
|
||||
<ProjectReference Include="..\Persistence\InMemory\MUnique.OpenMU.Persistence.InMemory.csproj" />
|
||||
<ProjectReference Include="..\Persistence\EntityFramework\MUnique.OpenMU.Persistence.EntityFramework.csproj" />
|
||||
<ProjectReference Include="..\Persistence\Initialization\MUnique.OpenMU.Persistence.Initialization.csproj" />
|
||||
<ProjectReference Include="..\Persistence\MUnique.OpenMU.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Target Name="FixNetSdkDiscoverAssetsBug" BeforeTargets="ResolveBuildCompressedStaticWebAssetsConfiguration">
|
||||
<!-- Workaround for:
|
||||
The "DiscoverPrecompressedAssets" task failed unexpectedly.
|
||||
System.ArgumentException: An item with the same key has already been added. Key: C:\Users\[user]\.nuget\packages\microsoft.aspnetcore.app.internal.assets\10.0.0\_framework\blazor.web.js
|
||||
|
||||
https://github.com/dotnet/sdk/issues/52089
|
||||
|
||||
This is caused by a Microsoft.NET.Sdk.Web project (like this) referencing another project that also uses Microsoft.NET.Sdk.Web.
|
||||
It appears to be a .NET SDK bug, and we're not the only one hitting it.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<!-- Remove any duplicate StaticWebAsset and StaticWebAssetEndpoint items -->
|
||||
<_StaticWebAsset Include="@(StaticWebAsset)" />
|
||||
<StaticWebAsset Remove="@(StaticWebAsset)" />
|
||||
<StaticWebAsset Include="@(_StaticWebAsset->Distinct())" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
96
src/Startup/MulticastConnectionServerStateObserver.cs
Normal file
96
src/Startup/MulticastConnectionServerStateObserver.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// <copyright file="MulticastConnectionServerStateObserver.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of a <see cref="IGameServerStateObserver"/> which forwards registrations to multiple state observers,
|
||||
/// e.g when there are multiple connect servers defined for the same game client.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.Interfaces.IGameServerStateObserver" />
|
||||
internal class MulticastConnectionServerStateObserver : IGameServerStateObserver
|
||||
{
|
||||
private readonly MemorizingObserver _memorizingObserver = new();
|
||||
private readonly List<IGameServerStateObserver> _observers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MulticastConnectionServerStateObserver"/> class.
|
||||
/// </summary>
|
||||
public MulticastConnectionServerStateObserver()
|
||||
{
|
||||
this._observers.Add(this._memorizingObserver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the observer which wants to get notified about changes.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer.</param>
|
||||
public void AddObserver(IGameServerStateObserver observer) => this._observers.Add(observer);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint)
|
||||
{
|
||||
for (int i = 0; i < this._observers.Count; i++)
|
||||
{
|
||||
this._observers[i].RegisterGameServer(gameServer, publicEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnregisterGameServer(ushort gameServerId)
|
||||
{
|
||||
for (int i = 0; i < this._observers.Count; i++)
|
||||
{
|
||||
this._observers[i].UnregisterGameServer(gameServerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
|
||||
{
|
||||
for (int i = 0; i < this._observers.Count; i++)
|
||||
{
|
||||
this._observers[i].CurrentConnectionsChanged(serverId, currentConnections);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls the registrations.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer.</param>
|
||||
public void PullRegistrations(IGameServerStateObserver observer)
|
||||
{
|
||||
foreach (var (serverInfo, endpoint) in this._memorizingObserver.ServerInfos.Values)
|
||||
{
|
||||
observer.RegisterGameServer(serverInfo, endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
private class MemorizingObserver : IGameServerStateObserver
|
||||
{
|
||||
public ConcurrentDictionary<ushort, (ServerInfo, IPEndPoint)> ServerInfos { get; } = new();
|
||||
|
||||
public void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint)
|
||||
{
|
||||
this.ServerInfos.TryAdd(gameServer.Id, (gameServer, publicEndPoint));
|
||||
}
|
||||
|
||||
public void UnregisterGameServer(ushort gameServerId)
|
||||
{
|
||||
this.ServerInfos.TryRemove(gameServerId, out _);
|
||||
}
|
||||
|
||||
public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
|
||||
{
|
||||
if (this.ServerInfos.TryGetValue(serverId, out var tuple))
|
||||
{
|
||||
tuple.Item1.CurrentConnections = currentConnections;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
539
src/Startup/Program.cs
Normal file
539
src/Startup/Program.cs
Normal file
@@ -0,0 +1,539 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.ChatServer;
|
||||
using MUnique.OpenMU.ConnectServer;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.FriendServer;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GuildServer;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.LoginServer;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.Persistence.Initialization;
|
||||
using MUnique.OpenMU.Persistence.Initialization.Version075;
|
||||
using MUnique.OpenMU.Persistence.InMemory;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using MUnique.OpenMU.Web.AdminPanel;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Services;
|
||||
using MUnique.OpenMU.Web.API;
|
||||
using MUnique.OpenMU.Web.Map.Map;
|
||||
using MUnique.OpenMU.Web.Shared;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
using Serilog;
|
||||
using Serilog.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// The startup class for an all-in-one game server.
|
||||
/// </summary>
|
||||
internal sealed class Program : IDisposable
|
||||
{
|
||||
private static bool _confirmExit;
|
||||
private static SystemConfiguration? _systemConfiguration;
|
||||
|
||||
private readonly IDictionary<int, IGameServer> _gameServers = new Dictionary<int, IGameServer>();
|
||||
private readonly IList<IManageableServer> _servers = new List<IManageableServer>();
|
||||
private readonly Serilog.ILogger _logger;
|
||||
|
||||
private IHost? _serverHost;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Program"/> class.
|
||||
/// </summary>
|
||||
public Program()
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
|
||||
SelfLog.Enable(Console.Error);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", false, true)
|
||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", true, true)
|
||||
.Build();
|
||||
|
||||
this._logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The main method.
|
||||
/// </summary>
|
||||
/// <param name="args">The command line args.</param>
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
using var exitCts = new CancellationTokenSource();
|
||||
var exitToken = exitCts.Token;
|
||||
|
||||
void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
if (_confirmExit)
|
||||
{
|
||||
#pragma warning disable VSTHRD103 // Call async methods when in an async method
|
||||
exitCts.Cancel();
|
||||
#pragma warning restore VSTHRD103 // Call async methods when in an async method
|
||||
Console.CancelKeyPress -= OnCancelKeyPress;
|
||||
Console.WriteLine("\nBye! Press enter to finish");
|
||||
}
|
||||
else
|
||||
{
|
||||
_confirmExit = true;
|
||||
Console.Write("\nConfirm shutdown? (y/N) ");
|
||||
}
|
||||
}
|
||||
|
||||
Console.CancelKeyPress += OnCancelKeyPress;
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) =>
|
||||
{
|
||||
if (!exitToken.IsCancellationRequested)
|
||||
{
|
||||
#pragma warning disable VSTHRD103 // Call async methods when in an async method
|
||||
exitCts.Cancel();
|
||||
#pragma warning restore VSTHRD103 // Call async methods when in an async method
|
||||
Debug.WriteLine("KILL");
|
||||
}
|
||||
};
|
||||
|
||||
using var program = new Program();
|
||||
await program.InitializeAsync(args).ConfigureAwait(false);
|
||||
while (!exitToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
if (_systemConfiguration?.ReadConsoleInput is true)
|
||||
{
|
||||
await HandleConsoleInputAsync(exitCts, exitToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Program"/> class.
|
||||
/// </summary>
|
||||
/// <param name="args">The command line args.</param>
|
||||
public async Task InitializeAsync(string[] args)
|
||||
{
|
||||
JsonConverterRegistry.RegisterConverter(new LocalizedStringJsonConverter());
|
||||
JsonConverterRegistry.RegisterConverter(new BinaryAsHexJsonConverter());
|
||||
|
||||
this._logger.Information("Creating host...");
|
||||
this._serverHost = await this.CreateHostAsync(args).ConfigureAwait(false);
|
||||
|
||||
var autoStart = _systemConfiguration?.AutoStart is true
|
||||
|| args.Contains("-autostart")
|
||||
|| !this.IsAdminPanelEnabled(args);
|
||||
|
||||
if (_systemConfiguration is { }
|
||||
&& this._serverHost.Services.GetService<IIpAddressResolver>() is ConfigurableIpResolver resolver)
|
||||
{
|
||||
resolver.Configure(_systemConfiguration.IpResolver, _systemConfiguration.IpResolverParameter);
|
||||
}
|
||||
|
||||
if (autoStart)
|
||||
{
|
||||
foreach (var chatServer in this._servers.OfType<ChatServer>())
|
||||
{
|
||||
await chatServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var gameServer in this._gameServers.Values)
|
||||
{
|
||||
await gameServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var connectServer in this._servers.OfType<IConnectServer>())
|
||||
{
|
||||
await connectServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this._serverHost?.StopAsync().WaitAndUnwrapException();
|
||||
this._serverHost?.Dispose();
|
||||
}
|
||||
|
||||
private static void DisplayCommands()
|
||||
{
|
||||
var commandList = "help, exit, gc, pid";
|
||||
Console.WriteLine($"Commands available: {commandList}");
|
||||
}
|
||||
|
||||
private static async Task HandleConsoleInputAsync(CancellationTokenSource exitCts, CancellationToken exitToken)
|
||||
{
|
||||
Console.Write("> ");
|
||||
var input = (await Console.In.ReadLineAsync(exitToken).ConfigureAwait(false))?.ToLower();
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case "y" when _confirmExit:
|
||||
case "exit":
|
||||
await exitCts.CancelAsync().ConfigureAwait(false);
|
||||
break;
|
||||
case "gc":
|
||||
GC.Collect();
|
||||
Console.WriteLine("Garbage Collected!");
|
||||
break;
|
||||
case "pid":
|
||||
var process = Process.GetCurrentProcess();
|
||||
var pid = process.Id.ToString();
|
||||
Console.WriteLine($"PID: {pid}");
|
||||
break;
|
||||
case "?":
|
||||
case "help":
|
||||
DisplayCommands();
|
||||
break;
|
||||
case "":
|
||||
case null:
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("Unknown command");
|
||||
DisplayCommands();
|
||||
break;
|
||||
}
|
||||
|
||||
if (_confirmExit && !string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
_confirmExit = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (e.IsTerminating)
|
||||
{
|
||||
this._logger.Fatal(e.ExceptionObject as Exception, "Unhandled exception leading to terminating application: {0}", e.ExceptionObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.Error(e.ExceptionObject as Exception, "Unhandled exception: {0}", e.ExceptionObject);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IHost> CreateHostAsync(string[] args)
|
||||
{
|
||||
// Ensure GameLogic and GameServer Assemblies are loaded
|
||||
_ = GameLogic.Rand.NextInt(1, 2);
|
||||
_ = DataInitialization.Id;
|
||||
_ = OpenMU.GameServer.ClientVersionResolver.DefaultVersion;
|
||||
|
||||
var addAdminPanel = this.IsAdminPanelEnabled(args);
|
||||
await new ConfigFileDatabaseConnectionStringProvider().InitializeAsync(default).ConfigureAwait(false);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseSerilog(this._logger);
|
||||
if (addAdminPanel)
|
||||
{
|
||||
builder.AddAdminPanel(includeMapApp: true);
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton(this._servers)
|
||||
.AddSingleton<IConfigurationChangePublisher, ConfigurationChangeHandler>()
|
||||
.AddSingleton<IConfigurationChangeListener, ConfigurationChangeListener>()
|
||||
.AddSingleton<ConfigurationChangeMediator>()
|
||||
.AddSingleton<IConfigurationChangeMediator>(s => s.GetRequiredService<ConfigurationChangeMediator>())
|
||||
.AddSingleton<IConfigurationChangeMediatorListener>(s => s.GetRequiredService<ConfigurationChangeMediator>())
|
||||
.AddSingleton(s => this.CreateIpResolver(s, args))
|
||||
.AddSingleton(this._gameServers)
|
||||
.AddSingleton(this._gameServers.Values)
|
||||
.AddSingleton(s =>
|
||||
this.DeterminePersistenceContextProviderAsync(
|
||||
args,
|
||||
s.GetService<ILoggerFactory>() ?? throw new InvalidOperationException($"{nameof(ILoggerFactory)} not registered."),
|
||||
s.GetService<IConfigurationChangeListener>() ?? throw new InvalidOperationException($"{nameof(IConfigurationChangeListener)} not registered."),
|
||||
s.GetService<IConfigurationChangePublisher>() ?? throw new InvalidOperationException($"{nameof(IConfigurationChangePublisher)} not registered."))
|
||||
.WaitAndUnwrapException())
|
||||
.AddSingleton<IPersistenceContextProvider>(s => s.GetService<IMigratableDatabaseContextProvider>()!)
|
||||
.AddSingleton<Lazy<IPersistenceContextProvider>>(s => new(() => s.GetService<IMigratableDatabaseContextProvider>()!))
|
||||
.AddSingleton<ILoginServer, LoginServer>()
|
||||
.AddSingleton<IGuildServer, GuildServer>()
|
||||
.AddSingleton<IFriendServer, FriendServer>()
|
||||
.AddSingleton<ChatServer>()
|
||||
.AddSingleton<IChatServer>(s => s.GetService<ChatServer>()!)
|
||||
.AddSingleton<ConnectServerFactory>()
|
||||
.AddSingleton<ConnectServerContainer>()
|
||||
.AddSingleton<IConnectServerInstanceManager>(provider => provider.GetService<ConnectServerContainer>()!)
|
||||
.AddSingleton<GameServerContainer>()
|
||||
.AddSingleton<IGameServerInstanceManager>(provider => provider.GetService<GameServerContainer>()!)
|
||||
.AddScoped<IMapFactory, JavascriptMapFactory>()
|
||||
.AddSingleton<SetupService>()
|
||||
.AddSingleton<IEnumerable<IConnectServer>>(provider => provider.GetService<ConnectServerContainer>() ?? throw new Exception($"{nameof(ConnectServerContainer)} not registered."))
|
||||
.AddSingleton<IGuildChangePublisher, GuildChangeToGameServerPublisher>()
|
||||
.AddSingleton<IFriendNotifier, FriendNotifierToGameServer>()
|
||||
.AddSingleton<PlugInManager>()
|
||||
.AddSingleton<IServerProvider, LocalServerProvider>()
|
||||
.AddSingleton<ICollection<PlugInConfiguration>>(this.PlugInConfigurationsFactory)
|
||||
.AddTransient<ReferenceHandler, ByDataSourceReferenceHandler>(provider =>
|
||||
{
|
||||
var persistenceContextProvider = provider.GetService<IPersistenceContextProvider>();
|
||||
var dataSource = new GameConfigurationDataSource(
|
||||
provider.GetService<ILogger<GameConfigurationDataSource>>()!,
|
||||
persistenceContextProvider!);
|
||||
var configId = persistenceContextProvider!.CreateNewConfigurationContext().GetDefaultGameConfigurationIdAsync(default).AsTask().WaitAndUnwrapException();
|
||||
dataSource.GetOwnerAsync(configId!.Value).AsTask().WaitAndUnwrapException();
|
||||
var referenceHandler = new ByDataSourceReferenceHandler(dataSource);
|
||||
return referenceHandler;
|
||||
})
|
||||
.AddSingleton<IDataSource<GameConfiguration>, GameConfigurationDataSource>()
|
||||
.AddHostedService<ChatServerContainer>()
|
||||
.AddHostedService<GameServerContainer>()
|
||||
.AddHostedService(provider => provider.GetService<GameServerContainer>()!)
|
||||
.AddHostedService(provider => provider.GetService<ConnectServerContainer>()!)
|
||||
.AddControllers().AddApplicationPart(typeof(ServerController).Assembly);
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// NpgsqlLoggingConfiguration.InitializeLogging(host.Services.GetRequiredService<ILoggerFactory>())
|
||||
this._logger.Information("Host created");
|
||||
|
||||
if (addAdminPanel)
|
||||
{
|
||||
host.ConfigureAdminPanel();
|
||||
}
|
||||
|
||||
this._logger.Information("Starting host...");
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
await host.StartAsync().ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
this._logger.Information("Host started, elapsed time: {elapsed}", stopwatch.Elapsed);
|
||||
this._logger.Information("Admin Panel bound to urls: {urls}", string.Join("; ", host.Urls));
|
||||
return host;
|
||||
}
|
||||
|
||||
private IIpAddressResolver CreateIpResolver(IServiceProvider serviceProvider, string[] args)
|
||||
{
|
||||
(IpResolverType IpResolver, string? IpResolverParameter)? settings = default;
|
||||
if (_systemConfiguration is not null)
|
||||
{
|
||||
settings = (_systemConfiguration.IpResolver, _systemConfiguration.IpResolverParameter);
|
||||
}
|
||||
|
||||
return IpAddressResolverFactory.CreateIpResolver(args, settings, serviceProvider.GetService<ILoggerFactory>()!);
|
||||
}
|
||||
|
||||
private ICollection<PlugInConfiguration> PlugInConfigurationsFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
var persistenceContextProvider = serviceProvider.GetService<IPersistenceContextProvider>() ?? throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
|
||||
using var context = persistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false);
|
||||
|
||||
var configs = context.GetAsync<PlugInConfiguration>().AsTask().WaitAndUnwrapException().ToList();
|
||||
|
||||
var referenceHandler = new ByDataSourceReferenceHandler(
|
||||
new GameConfigurationDataSource(serviceProvider.GetService<ILogger<GameConfigurationDataSource>>()!, persistenceContextProvider));
|
||||
|
||||
// We check if we miss any plugin configurations in the database. If we do, we try to add them.
|
||||
var pluginManager = new PlugInManager(null, serviceProvider.GetService<ILoggerFactory>()!, serviceProvider, referenceHandler);
|
||||
pluginManager.DiscoverAndRegisterPlugIns();
|
||||
|
||||
var typesWithCustomConfig = pluginManager.KnownPlugInTypes.Where(t => t.GetInterfaces().Contains(typeof(ISupportDefaultCustomConfiguration))).ToDictionary(t => t.GUID, t => t);
|
||||
|
||||
using var notificationSuspension = context.SuspendChangeNotifications();
|
||||
var typesWithMissingCustomConfigs = configs.Where(c => string.IsNullOrWhiteSpace(c.CustomConfiguration) && typesWithCustomConfig.ContainsKey(c.TypeId)).ToList();
|
||||
if (typesWithMissingCustomConfigs.Any())
|
||||
{
|
||||
typesWithMissingCustomConfigs.ForEach(c => this.CreateDefaultPlugInConfiguration(typesWithCustomConfig[c.TypeId]!, c, referenceHandler));
|
||||
_ = context.SaveChangesAsync().AsTask().WaitAndUnwrapException();
|
||||
}
|
||||
|
||||
var typesWithMissingConfigs = pluginManager.KnownPlugInTypes.Where(t => configs.All(c => c.TypeId != t.GUID)).ToList();
|
||||
if (!typesWithMissingConfigs.Any())
|
||||
{
|
||||
return configs;
|
||||
}
|
||||
|
||||
configs.AddRange(this.CreateMissingPlugInConfigurations(typesWithMissingConfigs, persistenceContextProvider, referenceHandler));
|
||||
_ = context.SaveChangesAsync().AsTask().WaitAndUnwrapException();
|
||||
return configs;
|
||||
}
|
||||
|
||||
private IEnumerable<PlugInConfiguration> CreateMissingPlugInConfigurations(IEnumerable<Type> plugInTypes, IPersistenceContextProvider persistenceContextProvider, ReferenceHandler referenceHandler)
|
||||
{
|
||||
GameConfiguration gameConfiguration;
|
||||
|
||||
using (var context = persistenceContextProvider.CreateNewContext())
|
||||
{
|
||||
gameConfiguration = context.GetAsync<GameConfiguration>().AsTask().WaitAndUnwrapException().First();
|
||||
}
|
||||
|
||||
using var saveContext = persistenceContextProvider.CreateNewContext(gameConfiguration);
|
||||
saveContext.Attach(gameConfiguration);
|
||||
foreach (var plugInType in plugInTypes)
|
||||
{
|
||||
var plugInConfiguration = saveContext.CreateNew<PlugInConfiguration>();
|
||||
plugInConfiguration.TypeId = plugInType.GUID;
|
||||
plugInConfiguration.IsActive = !plugInType.IsAssignableTo(typeof(IDisabledByDefault));
|
||||
gameConfiguration.PlugInConfigurations.Add(plugInConfiguration);
|
||||
if (plugInType.GetInterfaces().Contains(typeof(ISupportDefaultCustomConfiguration)))
|
||||
{
|
||||
this.CreateDefaultPlugInConfiguration(plugInType, plugInConfiguration, referenceHandler);
|
||||
}
|
||||
|
||||
yield return plugInConfiguration;
|
||||
}
|
||||
|
||||
using var notificationSuspension = saveContext.SuspendChangeNotifications();
|
||||
_ = saveContext.SaveChangesAsync().AsTask().WaitAndUnwrapException();
|
||||
}
|
||||
|
||||
private void CreateDefaultPlugInConfiguration(Type plugInType, PlugInConfiguration plugInConfiguration, ReferenceHandler referenceHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = (ISupportDefaultCustomConfiguration)Activator.CreateInstance(plugInType)!;
|
||||
var defaultConfig = plugin.CreateDefaultConfig();
|
||||
plugInConfiguration.SetConfiguration(defaultConfig, referenceHandler);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.Warning(ex, "Could not create custom default configuration for plugin type {plugInType}", plugInType);
|
||||
}
|
||||
}
|
||||
|
||||
private ushort DetermineUshort(string parameterName, string[] args, ushort defaultValue)
|
||||
{
|
||||
var parameter = args.FirstOrDefault(a => a.StartsWith($"-{parameterName}:", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (parameter != null
|
||||
&& int.TryParse(parameter.Substring(parameter.IndexOf(':') + 1), out int value)
|
||||
&& value is >= 0 and <= ushort.MaxValue)
|
||||
{
|
||||
return (ushort)value;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private bool IsAdminPanelEnabled(string[] args) => this.IsFeatureEnabled("adminpanel", args);
|
||||
|
||||
private bool IsFeatureEnabled(string featureName, string[] args)
|
||||
{
|
||||
var parameter = args.FirstOrDefault(a => a.StartsWith($"-{featureName}:", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (parameter is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return parameter.Substring(parameter.IndexOf(':') + 1).StartsWith("enabled", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private string GetVersionParameter(string[] args)
|
||||
{
|
||||
var parameter = args.FirstOrDefault(a => a.StartsWith("-version:", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (parameter is null)
|
||||
{
|
||||
return "season6"; // default
|
||||
}
|
||||
|
||||
return parameter.Substring(parameter.IndexOf(':') + 1);
|
||||
}
|
||||
|
||||
private async Task<IMigratableDatabaseContextProvider> DeterminePersistenceContextProviderAsync(string[] args, ILoggerFactory loggerFactory, IConfigurationChangeListener changeListener, IConfigurationChangePublisher changePublisher)
|
||||
{
|
||||
var version = this.GetVersionParameter(args);
|
||||
|
||||
IMigratableDatabaseContextProvider contextProvider;
|
||||
if (args.Contains("-demo"))
|
||||
{
|
||||
var inMemoryProvider = new InMemoryPersistenceContextProvider();
|
||||
contextProvider = inMemoryProvider;
|
||||
await this.InitializeDataAsync(version, loggerFactory, contextProvider).ConfigureAwait(false);
|
||||
inMemoryProvider.ChangePublisher = changePublisher;
|
||||
}
|
||||
else
|
||||
{
|
||||
contextProvider = await this.PrepareRepositoryProviderAsync(args.Contains("-reinit"), version, loggerFactory, changeListener).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await this.ReadSystemConfigurationAsync(contextProvider).ConfigureAwait(false);
|
||||
|
||||
return contextProvider;
|
||||
}
|
||||
|
||||
private async Task<IMigratableDatabaseContextProvider> PrepareRepositoryProviderAsync(bool reinit, string version, ILoggerFactory loggerFactory, IConfigurationChangeListener changeListener)
|
||||
{
|
||||
var contextProvider = new PersistenceContextProvider(loggerFactory, changeListener);
|
||||
if (reinit || !await contextProvider.DatabaseExistsAsync().ConfigureAwait(false))
|
||||
{
|
||||
this._logger.Information("The database is getting (re-)initialized...");
|
||||
using var update = await contextProvider.ReCreateDatabaseAsync().ConfigureAwait(false);
|
||||
await this.InitializeDataAsync(version, loggerFactory, contextProvider).ConfigureAwait(false);
|
||||
this._logger.Information("...initialization finished.");
|
||||
}
|
||||
else if (!await contextProvider.IsDatabaseUpToDateAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (_systemConfiguration?.AutoUpdateSchema is true || await contextProvider.ShouldDoAutoSchemaUpdateAsync())
|
||||
{
|
||||
Console.WriteLine("The database schema needs to be updated before the server can be started. Updating...");
|
||||
await contextProvider.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);
|
||||
Console.WriteLine("The database schema has been successfully updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("The database schema needs to be updated before the server can be started. Apply update? (y/n)");
|
||||
var key = Console.ReadLine()?.ToLowerInvariant();
|
||||
if (key == "y")
|
||||
{
|
||||
await contextProvider.ApplyAllPendingUpdatesAsync().ConfigureAwait(false);
|
||||
Console.WriteLine("The database schema has been successfully updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Cancelled the schema update process, can't start the server.");
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// everything is fine and ready
|
||||
}
|
||||
|
||||
return contextProvider;
|
||||
}
|
||||
|
||||
private async Task ReadSystemConfigurationAsync(IPersistenceContextProvider persistenceContextProvider)
|
||||
{
|
||||
using var context = persistenceContextProvider.CreateNewTypedContext(typeof(SystemConfiguration), false);
|
||||
var config = (await context.GetAsync<SystemConfiguration>().ConfigureAwait(false)).FirstOrDefault();
|
||||
if (config != null)
|
||||
{
|
||||
_systemConfiguration = config;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeDataAsync(string version, ILoggerFactory loggerFactory, IPersistenceContextProvider contextProvider)
|
||||
{
|
||||
var serviceContainer = new ServiceContainer();
|
||||
serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
|
||||
serviceContainer.AddService(typeof(IPersistenceContextProvider), contextProvider);
|
||||
|
||||
var referenceHandler = new ByDataSourceReferenceHandler(
|
||||
new GameConfigurationDataSource(serviceContainer.GetService<ILogger<GameConfigurationDataSource>>()!, contextProvider));
|
||||
|
||||
var plugInManager = new PlugInManager(null, loggerFactory, serviceContainer, referenceHandler);
|
||||
plugInManager.DiscoverAndRegisterPlugInsOf<IDataInitializationPlugIn>();
|
||||
var initialization = plugInManager.GetStrategy<IDataInitializationPlugIn>(version) ?? throw new Exception("Data initialization plugin not found");
|
||||
await initialization.CreateInitialDataAsync(3, true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
10
src/Startup/Properties/AssemblyInfo.cs
Normal file
10
src/Startup/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.Startup")]
|
||||
77
src/Startup/Readme.md
Normal file
77
src/Startup/Readme.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Startup
|
||||
|
||||
The startup console program is basically what glues all components together and
|
||||
starts the server as a single process.
|
||||
|
||||
## Logging
|
||||
|
||||
Logging can be configured by the *appsettings.json* file.
|
||||
By default, not a lot is configured. If you want to extend the configuration,
|
||||
have a look a the [serilog documentation](https://github.com/serilog/serilog-settings-configuration).
|
||||
The server makes good use of scopes, so you can configure it to log
|
||||
only actions of certain players, for example.
|
||||
|
||||
In the future, it might be possible to change logging settings over the admin
|
||||
panel, too.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Please note, that the most of these parameters (except ```-demo``` and ```-adminpanel```)
|
||||
are not necessary anymore, because these settings/actions can be done more conveniently
|
||||
over the admin panel, too.**
|
||||
|
||||
You can start the server with the following parameters:
|
||||
|
||||
| Parameter | Description |
|
||||
|-------------|-------------------|
|
||||
| -autostart | It automatically initializes the game servers and starts the tcp listeners of all (sub-)servers |
|
||||
| -reinit | It recreates and reinitializes the database. It doesn't have any effect when *-demo* is used. |
|
||||
| -version:[season6\|0.75\|0.95d] | Defines the version of the game client. Has only effect with *-reinit* or *-demo* and affects the initial data creation. Default: season6|
|
||||
| -demo | Instead of using an external database, it uses in-memory repositories and data is initialized at each start. Only for testing, not for production usage, as player progress is **not saved** to a database or file. |
|
||||
| -deamon | Deactivates handling of console inputs |
|
||||
| -adminpanel:[enabled\|disabled] | Defines if the admin panel is available. If disabled, *-autostart* is applied automatically. Default: enabled |
|
||||
|
||||
### -resolveIP
|
||||
|
||||
Defines how the own ip address is determined which is reported back to the game
|
||||
client in case it requests to connect to a selected game server (server selection
|
||||
screen) or the chat server (when starting a chat with the in-game messenger).
|
||||
This may be helpful, if the server is started in an environment where the public
|
||||
IP is not reachable from the outside (e.g. because you share your IPv4-Address
|
||||
or behind a firewall) and you want to use it within your computer or private network.
|
||||
|
||||
It supports the following values:
|
||||
|
||||
| Value | Description | Example |
|
||||
|--------|--------------|---------|
|
||||
| public | Default value, if nothing is specified. The public ip is automatically determined by an [external API](https://www.ipify.org/). | -resolveIP:public |
|
||||
| local | Determines a local ip. If none is found, a loopback IP is used (127.127.127.127). | -resolveIP:local |
|
||||
| loopback | For testing on the same machine, a loopback IP is used (127.127.127.127). | -resolveIP:loopback |
|
||||
| [An IPv4-Address] | Defines a custom and constant IP address or a host name. | -resolveIP:140.82.118.4 |
|
||||
|
||||
## Environment variables
|
||||
|
||||
Additionally (and optionally), there are some settings which can be controlled with environment variables.
|
||||
They may be helpful when running the server in a container or under linux.
|
||||
|
||||
| Variable | Description |
|
||||
|-------------|-------------------|
|
||||
| RESOLVE_IP | See *-resolveIP* parameter. Same description and values applies here. Is only considered, when there is no *-resolveIP* parameter. |
|
||||
| ASPNETCORE_ENVIRONMENT | If no *-resolveIP* parameter and no *RESOLVE_IP* variable is defined, the variable *ASPNETCORE_ENVIRONMENT* is considered to find the optimal ip resolver. If the value is "Development", it uses 'loopback'; Otherwise, it uses 'public'. |
|
||||
| ASPNETCORE_URLS | Defines the address of the admin panel. Example: 'http://+:80' |
|
||||
| DB_HOST | Host name/address of the postgres database |
|
||||
| DB_ADMIN_USER | User name of the admin user of the postgres database |
|
||||
| DB_ADMIN_PW | Password of the admin user of the postgres database |
|
||||
|
||||
## Settings priority
|
||||
|
||||
As you noticed, you can set some options in different ways. Therefore, a clear
|
||||
priority has been worked out to make the most sense:
|
||||
|
||||
1. Start parameters
|
||||
2. Environment variables
|
||||
3. Settings over the admin panel (Configuration -> System)
|
||||
|
||||
Start parameters have the highest priority, then environment variables and then
|
||||
the settings over the admin panel. The idea is, that start parameters and
|
||||
environment variables should only be used in special cases by experienced users.
|
||||
96
src/Startup/ServerContainerBase.cs
Normal file
96
src/Startup/ServerContainerBase.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// <copyright file="ServerContainerBase.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for a server container, which reacts on database recreations.
|
||||
/// </summary>
|
||||
public abstract class ServerContainerBase : IHostedService
|
||||
{
|
||||
private readonly SetupService _setupService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerContainerBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="setupService">The setup service.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
protected ServerContainerBase(SetupService setupService, ILogger logger)
|
||||
{
|
||||
this._setupService = setupService;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.StartInnerAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._setupService.DatabaseInitialized += this.OnDatabaseInitializedAsync;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.StopInnerAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._setupService.DatabaseInitialized -= this.OnDatabaseInitializedAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts all servers of this container.
|
||||
/// </summary>
|
||||
/// <param name="onDatabaseInit">If set to <c>true</c>, this method is called during a database initialization.</param>
|
||||
public virtual async ValueTask RestartAllAsync(bool onDatabaseInit)
|
||||
{
|
||||
await this.StopAsync(default).ConfigureAwait(false);
|
||||
await this.BeforeStartAsync(onDatabaseInit, default).ConfigureAwait(false);
|
||||
await this.StartAsync(default).ConfigureAwait(false);
|
||||
await this.StartListenersAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the hosted service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected abstract Task StartInnerAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the hosted service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected abstract Task StopInnerAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the listeners of the hosted service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected abstract Task StartListenersAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Befores the start asynchronous.
|
||||
/// </summary>
|
||||
/// <param name="onDatabaseInit">If set to <c>true</c>, this method is called during a database initialization.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected virtual async ValueTask BeforeStartAsync(bool onDatabaseInit, CancellationToken cancellationToken)
|
||||
{
|
||||
// can be overwritten
|
||||
}
|
||||
|
||||
private async ValueTask OnDatabaseInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.RestartAllAsync(true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this._logger.LogError(exception, "Unexpected error when handling database creation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Startup/TextReaderExtensions.cs
Normal file
28
src/Startup/TextReaderExtensions.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="TextReaderExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Startup;
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for a <see cref="TextReader"/>.
|
||||
/// </summary>
|
||||
public static class TextReaderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the line asynchronously and considers the <see cref="CancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The read line or <c>null</c>.</returns>
|
||||
public static async Task<string?> ReadLineAsync(this TextReader reader, CancellationToken cancellationToken)
|
||||
{
|
||||
using var taskSource = new CancellationTokenTaskSource<string?>(cancellationToken);
|
||||
var result = await (await Task.WhenAny(taskSource.Task, reader.ReadLineAsync()).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
9
src/Startup/_Imports.razor
Normal file
9
src/Startup/_Imports.razor
Normal file
@@ -0,0 +1,9 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@using MUnique.OpenMU.Startup
|
||||
40
src/Startup/appsettings.json
Normal file
40
src/Startup/appsettings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"DetailedErrors": true,
|
||||
"AllowedHosts": "*",
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Fatal",
|
||||
"System": "Fatal",
|
||||
"Npgsql": "Information",
|
||||
"MUnique.OpenMU.Network.Connection": "Error",
|
||||
"MUnique": "Information"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/log.txt",
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}",
|
||||
"rollOnFileSizeLimit": true,
|
||||
"fileSizeLimitBytes": 4194304,
|
||||
"retainedFileCountLimit": 48,
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ],
|
||||
"Properties": {
|
||||
"Application": "MUnique.OpenMU"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user