baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View 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);
}
}

View 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;
}
}

View 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);
}
}
}
}

View 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
View 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"]

View 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);
}
}
}

View 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);
}
}
}

View 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>

View 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
View 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);
}
}

View 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
View 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.

View 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.");
}
}
}

View 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;
}
}

View 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

View 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"
}
}
}