baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// <copyright file="DockerConnectServerInstanceManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AdminPanel.Host;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IConnectServerInstanceManager"/>.
|
||||
/// </summary>
|
||||
public class DockerConnectServerInstanceManager : IConnectServerInstanceManager
|
||||
{
|
||||
private readonly IServerProvider _serverProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DockerConnectServerInstanceManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverProvider">The server provider.</param>
|
||||
public DockerConnectServerInstanceManager(IServerProvider serverProvider)
|
||||
{
|
||||
this._serverProvider = serverProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeConnectServerAsync(Guid connectServerDefinitionId)
|
||||
{
|
||||
// TODO: Implement this... by starting a new docker container
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RemoveConnectServerAsync(Guid connectServerDefinitionId)
|
||||
{
|
||||
var connectServers = this._serverProvider.Servers
|
||||
.Where(server => server.Type == ServerType.ConnectServer)
|
||||
.FirstOrDefault(server => server.ConfigurationId == connectServerDefinitionId);
|
||||
if (connectServers is not null)
|
||||
{
|
||||
await connectServers.ShutdownAsync().ConfigureAwait(false);
|
||||
|
||||
// TODO: Remove the docker container
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs
Normal file
56
src/Dapr/AdminPanel.Host/DockerGameServerInstanceManager.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// <copyright file="DockerGameServerInstanceManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AdminPanel.Host;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IGameServerInstanceManager"/>.
|
||||
/// </summary>
|
||||
public class DockerGameServerInstanceManager : IGameServerInstanceManager
|
||||
{
|
||||
private readonly IServerProvider _serverProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DockerGameServerInstanceManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverProvider">The server provider.</param>
|
||||
public DockerGameServerInstanceManager(IServerProvider serverProvider)
|
||||
{
|
||||
this._serverProvider = serverProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RestartAllAsync(bool onDatabaseInit)
|
||||
{
|
||||
var gameServers = this._serverProvider.Servers.Where(server => server.Type == ServerType.GameServer).ToList();
|
||||
foreach (var gameServer in gameServers)
|
||||
{
|
||||
await gameServer.ShutdownAsync().ConfigureAwait(false);
|
||||
|
||||
// It's started again automatically by the docker host.
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeGameServerAsync(byte serverId)
|
||||
{
|
||||
// TODO: Implement this... by starting a new docker container
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask RemoveGameServerAsync(byte serverId)
|
||||
{
|
||||
var gameServer = this._serverProvider.Servers
|
||||
.Where(server => server.Type == ServerType.GameServer)
|
||||
.FirstOrDefault(server => server.Id == serverId);
|
||||
if (gameServer is not null)
|
||||
{
|
||||
await gameServer.ShutdownAsync().ConfigureAwait(false);
|
||||
|
||||
// TODO: Remove the docker container
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Dapr/AdminPanel.Host/Dockerfile
Normal file
22
src/Dapr/AdminPanel.Host/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/AdminPanel.Host/MUnique.OpenMU.AdminPanel.Host.csproj", "Dapr/AdminPanel.Host/"]
|
||||
RUN dotnet restore "Dapr/AdminPanel.Host/MUnique.OpenMU.AdminPanel.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/AdminPanel.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.AdminPanel.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.AdminPanel.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.AdminPanel.Host.dll"]
|
||||
@@ -0,0 +1,48 @@
|
||||
<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>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
<DockerComposeProjectPath>..\..\..\deploy\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>c2dfacaa-6b66-4d66-9c79-de236b5c05b5</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Web\AdminPanel\MUnique.OpenMU.Web.AdminPanel.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="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>
|
||||
44
src/Dapr/AdminPanel.Host/Program.cs
Normal file
44
src/Dapr/AdminPanel.Host/Program.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.AdminPanel.Host;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
using MUnique.OpenMU.Web.AdminPanel;
|
||||
|
||||
var builder = DaprService.CreateBuilder("AdminPanel", args);
|
||||
|
||||
var plugInConfigurations = new List<PlugInConfiguration>();
|
||||
|
||||
var services = builder.Services;
|
||||
|
||||
services.AddPeristenceProvider(true)
|
||||
.AddPlugInManager(plugInConfigurations)
|
||||
.AddManageableServerRegistry()
|
||||
.AddSingleton<ILoginServer, LoginServer>()
|
||||
.AddSingleton<IGameServerInstanceManager, DockerGameServerInstanceManager>()
|
||||
.AddSingleton<IConnectServerInstanceManager, DockerConnectServerInstanceManager>();
|
||||
|
||||
builder.AddAdminPanel();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
|
||||
// todo: add some meaningful metrics
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure(false);
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
app.MapRazorComponents<MUnique.OpenMU.Web.AdminPanel.Components.App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
await app.Services.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/AdminPanel.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/AdminPanel.Host/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.AdminPanel.Host")]
|
||||
50
src/Dapr/AdminPanel.Host/ServerStateController.cs
Normal file
50
src/Dapr/AdminPanel.Host/ServerStateController.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// <copyright file="ServerStateController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.AdminPanel.Host;
|
||||
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Controller which receives server state updates from the pub/sub component.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class ServerStateController
|
||||
{
|
||||
private readonly ManagableServerRegistry _registry;
|
||||
private readonly ILogger<ServerStateController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerStateController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registry">The registry.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ServerStateController(ManagableServerRegistry registry, ILogger<ServerStateController> logger)
|
||||
{
|
||||
this._registry = registry;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server state update.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost(ManagableServerStatePublisher.TopicName)]
|
||||
[Topic("pubsub", ManagableServerStatePublisher.TopicName)]
|
||||
public void ServerStateUpdate([FromBody] ServerStateData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
this._registry.HandleUpdate(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error updating the ManagableServerRegistry");
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/Dapr/AdminPanel.Host/appsettings.json
Normal file
4
src/Dapr/AdminPanel.Host/appsettings.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"DetailedErrors": true,
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
70
src/Dapr/ChatServer.Host/ChatServerController.cs
Normal file
70
src/Dapr/ChatServer.Host/ChatServerController.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
// <copyright file="ChatServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Host;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The API controller for the <see cref="IChatServer"/>.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class ChatServerController : ControllerBase
|
||||
{
|
||||
private readonly IChatServer _chatServer;
|
||||
|
||||
private readonly ILogger<ChatServerController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatServer">The chat server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ChatServerController(IChatServer chatServer, ILogger<ChatServerController> logger)
|
||||
{
|
||||
this._chatServer = chatServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the client to the server.
|
||||
/// </summary>
|
||||
/// <param name="data">The registration arguments.</param>
|
||||
/// <returns>The authentication info.</returns>
|
||||
[HttpPost(nameof(IChatServer.RegisterClientAsync))]
|
||||
public async ValueTask<ChatServerAuthenticationInfo?> RegisterClientAsync([FromBody] RegisterChatClientArguments data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._chatServer.RegisterClientAsync(data.RoomId, data.ClientName).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, $"Error during registration of {data.ClientName} for room {data.RoomId}.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the chat room.
|
||||
/// </summary>
|
||||
/// <returns>The id of the created chat room.</returns>
|
||||
[HttpPost(nameof(IChatServer.CreateChatRoomAsync))]
|
||||
public async ValueTask<ushort> CreateChatRoomAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._chatServer.CreateChatRoomAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error creating a new chat room.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Dapr/ChatServer.Host/ChatServerDefinitionExtensions.cs
Normal file
46
src/Dapr/ChatServer.Host/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.ChatServer.Host;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
76
src/Dapr/ChatServer.Host/ChatServerHostedServiceWrapper.cs
Normal file
76
src/Dapr/ChatServer.Host/ChatServerHostedServiceWrapper.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
// <copyright file="ChatServerHostedServiceWrapper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.Host;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using ChatServer = MUnique.OpenMU.ChatServer.ChatServer;
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper which takes a <see cref="ChatServer"/> and wraps it as <see cref="IHostedLifecycleService"/>,
|
||||
/// so that additional initialization can be done before actually starting it.
|
||||
/// The actual server start is deferred to <see cref="StartedAsync"/> which is called after the web application
|
||||
/// has started (i.e. the HTTP API is already available), breaking the circular startup dependency with the Dapr sidecar.
|
||||
/// TODO: listen to configuration changes/database reinit.
|
||||
/// See also: ServerContainerBase.
|
||||
/// </summary>
|
||||
public class ChatServerHostedServiceWrapper : IHostedLifecycleService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private ChatServer? _chatServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServerHostedServiceWrapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
public ChatServerHostedServiceWrapper(IServiceProvider serviceProvider)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this._serviceProvider.GetService<ICollection<PlugInConfiguration>>() is { } plugInCollection)
|
||||
{
|
||||
if (plugInCollection is not List<PlugInConfiguration> plugInConfigurations)
|
||||
{
|
||||
throw new InvalidOperationException($"The registered {nameof(ICollection<PlugInConfiguration>)} must be a {nameof(List<PlugInConfiguration>)} to be able to load plugin configurations.");
|
||||
}
|
||||
|
||||
await this._serviceProvider.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var settings = this._serviceProvider.GetRequiredService<ChatServerDefinition>().ConvertToSettings();
|
||||
this._chatServer = this._serviceProvider.GetRequiredService<ChatServer>();
|
||||
this._chatServer.Initialize(settings);
|
||||
await this._chatServer.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._chatServer?.StopAsync(cancellationToken) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
23
src/Dapr/ChatServer.Host/Dockerfile
Normal file
23
src/Dapr/ChatServer.Host/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
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 ["Dapr/ChatServer.Host/MUnique.OpenMU.ChatServer.Host.csproj", "Dapr/ChatServer.Host/"]
|
||||
RUN dotnet restore "Dapr/ChatServer.Host/MUnique.OpenMU.ChatServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/ChatServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.ChatServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.ChatServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.ChatServer.Host.dll"]
|
||||
@@ -0,0 +1,26 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>6c345677-49b4-4872-833e-65acbe67fcae</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ChatServer\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
35
src/Dapr/ChatServer.Host/Program.cs
Normal file
35
src/Dapr/ChatServer.Host/Program.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using MUnique.OpenMU.ChatServer.Host;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using ChatServer = MUnique.OpenMU.ChatServer.ChatServer;
|
||||
|
||||
var plugInConfigurations = new List<PlugInConfiguration>();
|
||||
var builder = DaprService.CreateBuilder("ChatServer", args);
|
||||
|
||||
// Add services to the container.
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<ChatServer>()
|
||||
.AddPeristenceProvider() // todo: Config API instead of using persistence?
|
||||
.AddPlugInManager(plugInConfigurations)
|
||||
.AddIpResolver(args)
|
||||
.AddPersistentSingleton<ChatServerDefinition>();
|
||||
|
||||
services.AddHostedService<ChatServerHostedServiceWrapper>();
|
||||
|
||||
services.PublishManageableServer<ChatServer>();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
metricsRegistry.AddNetworkMeters();
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure();
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/ChatServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/ChatServer.Host/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.ChatServer.Host")]
|
||||
10
src/Dapr/Common/AssemblyInfo.cs
Normal file
10
src/Dapr/Common/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.Dapr.Common")]
|
||||
10
src/Dapr/Common/ConfigurationChangeArguments.cs
Normal file
10
src/Dapr/Common/ConfigurationChangeArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="ConfigurationChangeArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for the change notifications of <see cref="ConfigurationChangePublisher"/>.
|
||||
/// </summary>
|
||||
public record class ConfigurationChangeArguments(Type Type, Guid Id, object? Configuration);
|
||||
67
src/Dapr/Common/ConfigurationChangePublisher.cs
Normal file
67
src/Dapr/Common/ConfigurationChangePublisher.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// <copyright file="ConfigurationChangePublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using global::Dapr.Client;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Publisher for configuration changes.
|
||||
/// Changes are published to the configured pub/sub Dapr component.
|
||||
/// </summary>
|
||||
public class ConfigurationChangePublisher : IConfigurationChangePublisher
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationChangePublisher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
public ConfigurationChangePublisher(DaprClient daprClient)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a changed, previously existing configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the configuration.</param>
|
||||
/// <param name="id">The identifier of the changed configuration.</param>
|
||||
/// <param name="configuration">The changed configuration.</param>
|
||||
public Task ConfigurationChangedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
return this._daprClient.PublishEventAsync(
|
||||
"pubsub",
|
||||
nameof(this.ConfigurationChangedAsync),
|
||||
new ConfigurationChangeArguments(type, id, configuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an added configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the configuration.</param>
|
||||
/// <param name="id">The identifier of the added configuration.</param>
|
||||
/// <param name="configuration">The added configuration.</param>
|
||||
public Task ConfigurationAddedAsync(Type type, Guid id, object configuration)
|
||||
{
|
||||
return this._daprClient.PublishEventAsync(
|
||||
"pubsub",
|
||||
nameof(this.ConfigurationAddedAsync),
|
||||
new ConfigurationChangeArguments(type, id, configuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a removed, previously existing configuration.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the configuration.</param>
|
||||
/// <param name="id">The identifier of the removed configuration.</param>
|
||||
public Task ConfigurationRemovedAsync(Type type, Guid id)
|
||||
{
|
||||
return this._daprClient.PublishEventAsync(
|
||||
"pubsub",
|
||||
nameof(this.ConfigurationRemovedAsync),
|
||||
new ConfigurationChangeArguments(type, id, null));
|
||||
}
|
||||
}
|
||||
70
src/Dapr/Common/DaprService.cs
Normal file
70
src/Dapr/Common/DaprService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
// <copyright file="DaprService.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Json;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using OpenTelemetry.Logs;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to create an <see cref="WebApplicationBuilder"/> which predefined common
|
||||
/// services for all of our service applications.
|
||||
/// </summary>
|
||||
public static class DaprService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebApplicationBuilder" /> class with preconfigured defaults.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">Name of the service, used for the OpenAPI and OpenTelemetry tracing.</param>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="WebApplicationBuilder" />.
|
||||
/// </returns>
|
||||
public static WebApplicationBuilder CreateBuilder(string serviceName, string[] args)
|
||||
{
|
||||
JsonConverterRegistry.RegisterConverter(new LocalizedStringJsonConverter());
|
||||
JsonConverterRegistry.RegisterConverter(new BinaryAsHexJsonConverter());
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.WebHost.UseUrls($"http://*:8080");
|
||||
|
||||
var services = builder.Services;
|
||||
services.AddControllers();
|
||||
services.AddDaprClient();
|
||||
|
||||
services.AddOpenTelemetry()
|
||||
.WithTracing(t =>
|
||||
{
|
||||
t.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddZipkinExporter(o => o.Endpoint = new Uri("http://zipkin:9411/api/v2/spans"));
|
||||
});
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = serviceName, Version = "v1" });
|
||||
});
|
||||
|
||||
services.AddSingleton<IDatabaseConnectionSettingProvider, SecretStoreDatabaseConnectionSettingsProvider>();
|
||||
|
||||
// Logging:
|
||||
builder.UseLoki(serviceName);
|
||||
builder.Logging.AddOpenTelemetry(options => options.AddOtlpExporter());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
352
src/Dapr/Common/Extensions.cs
Normal file
352
src/Dapr/Common/Extensions.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
// <copyright file="Extensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Metrics;
|
||||
using Prometheus;
|
||||
using Serilog;
|
||||
using Serilog.Debugging;
|
||||
using Serilog.Events;
|
||||
using Serilog.Filters;
|
||||
using Serilog.Sinks.Grafana.Loki;
|
||||
|
||||
/// <summary>
|
||||
/// Common extensions for the building of daprized services.
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="PersistenceContextProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="publishConfigChanges">If set to <c>true</c>, configuration changes are published to other Dapr services.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection AddPeristenceProvider(this IServiceCollection services, bool publishConfigChanges = false)
|
||||
{
|
||||
services.AddSingleton<IConfigurationChangeListener, ConfigurationChangeListener>();
|
||||
if (publishConfigChanges)
|
||||
{
|
||||
services.AddSingleton<IConfigurationChangePublisher, ConfigurationChangePublisher>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton(e => IConfigurationChangePublisher.None);
|
||||
}
|
||||
|
||||
return services
|
||||
.AddSingleton<IMigratableDatabaseContextProvider, PersistenceContextProvider>()
|
||||
.AddSingleton(s => (PersistenceContextProvider)s.GetService<IMigratableDatabaseContextProvider>()!)
|
||||
.AddSingleton(s => (IPersistenceContextProvider)s.GetService<IMigratableDatabaseContextProvider>()!)
|
||||
.AddSingleton(s => new Lazy<IPersistenceContextProvider>(s.GetRequiredService<IPersistenceContextProvider>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the plug in manager.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="plugInConfigurations">The plug in configurations.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection AddPlugInManager(this IServiceCollection services, ICollection<PlugInConfiguration> plugInConfigurations)
|
||||
{
|
||||
return services
|
||||
.AddSingleton(plugInConfigurations)
|
||||
.AddSingleton<PlugInManager>()
|
||||
.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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load the plug in configurations.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
/// <param name="plugInConfigurations">The list of plug in configurations, where the loaded configurations will be added.</param>
|
||||
public static async ValueTask TryLoadPlugInConfigurationsAsync(this IServiceProvider serviceProvider, List<PlugInConfiguration> plugInConfigurations)
|
||||
{
|
||||
if (serviceProvider.GetService<IMigratableDatabaseContextProvider>() is not { } persistenceContextProvider)
|
||||
{
|
||||
throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
if (!await persistenceContextProvider.CanConnectToDatabaseAsync(cts.Token).ConfigureAwait(false)
|
||||
|| !await persistenceContextProvider.DatabaseExistsAsync(cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var configs = await persistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false).GetAsync<PlugInConfiguration>().ConfigureAwait(false);
|
||||
plugInConfigurations.AddRange(configs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't load it yet, because the database is not initialized, we just return
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a persistent object as singleton to the services.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The base type of the persistent object.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="predicate">The predicate to select actual object.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection AddPersistentSingleton<T>(this IServiceCollection services, Func<T, bool>? predicate = null)
|
||||
where T : class
|
||||
{
|
||||
return services.AddPersistentSingleton<T, T>(predicate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the persistent object as singleton to the services.
|
||||
/// </summary>
|
||||
/// <typeparam name="TTarget">The target, exposed type of the persistent object, usually an interface.</typeparam>
|
||||
/// <typeparam name="TActual">The actual base type of the persistent object.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="predicate">The predicate to select the actual object.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection AddPersistentSingleton<TTarget, TActual>(this IServiceCollection services, Func<TActual, bool>? predicate = null)
|
||||
where TActual : class, TTarget
|
||||
where TTarget : class
|
||||
{
|
||||
return services.AddSingleton(s =>
|
||||
{
|
||||
if (s.GetService<IPersistenceContextProvider>() is not { } persistenceContextProvider)
|
||||
{
|
||||
throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
|
||||
}
|
||||
|
||||
var objects = persistenceContextProvider.CreateNewConfigurationContext().GetAsync<TActual>().AsTask().WaitAndUnwrapException();
|
||||
return (TTarget)objects.First(predicate ?? (_ => true))!;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <see cref="ManagableServerRegistry"/> to the services.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection AddManageableServerRegistry(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ManagableServerRegistry>()
|
||||
.AddSingleton<IServerProvider>(s => s.GetService<ManagableServerRegistry>()!);
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the server to other daprized services by registering a <see cref="ManagableServerStatePublisher"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TServer">The type of the server.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The modified service collection.</returns>
|
||||
public static IServiceCollection PublishManageableServer<TServer>(this IServiceCollection services)
|
||||
where TServer : IManageableServer
|
||||
{
|
||||
services.AddSingleton<IManageableServer>(s => s.GetService<TServer>()!)
|
||||
.AddHostedService<ManagableServerStatePublisher>()
|
||||
.AddControllers().AddApplicationPart(typeof(ManageableServerController).Assembly);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the usage of logging to loki.
|
||||
/// </summary>
|
||||
/// <param name="builder">The web application builder.</param>
|
||||
/// <param name="serviceName">Name of the service.</param>
|
||||
/// <returns>The configured web application builder.</returns>
|
||||
public static WebApplicationBuilder UseLoki(this WebApplicationBuilder builder, string serviceName)
|
||||
{
|
||||
// We just want to transmit some static labels, as suggested in the best practice in the Loki documentation
|
||||
var includeLabels = new[] { "Account", "Character", "Connection", "ServiceName", "SourceContext" };
|
||||
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.Enrich.WithProperty("ServiceName", serviceName)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo
|
||||
.GrafanaLoki(
|
||||
uri: "http://loki:3100",
|
||||
propertiesAsLabels: includeLabels)
|
||||
.WriteTo
|
||||
.Console(LogEventLevel.Information)
|
||||
.Filter.ByExcluding(Matching.FromSource("Microsoft")) // We don't want all of the ASP.NET logging, because that really keeps loki and the console pretty busy
|
||||
.CreateLogger();
|
||||
|
||||
SelfLog.Enable(Console.Error);
|
||||
|
||||
builder.Host.ConfigureLogging((_, loggingBuilder) => loggingBuilder.ClearProviders());
|
||||
builder.Host.UseSerilog(logger);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the open telemetry metrics.
|
||||
/// </summary>
|
||||
/// <param name="builder">The web application builder.</param>
|
||||
/// <param name="registry">The registry.</param>
|
||||
/// <returns>The configured web application builder.</returns>
|
||||
public static WebApplicationBuilder AddOpenTelemetryMetrics(this WebApplicationBuilder builder, MetricsRegistry registry)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(x =>
|
||||
{
|
||||
x.AddMeter(registry.Meters.ToArray());
|
||||
x.AddPrometheusExporter();
|
||||
x.AddOtlpExporter();
|
||||
});
|
||||
builder.Services.AddHealthChecks().ForwardToPrometheus();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and configures the web application.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
/// <param name="addBlazor">If set to <c>true</c>, it configures the application to provide a blazor server app.</param>
|
||||
/// <returns>The built and configured web application.</returns>
|
||||
public static WebApplication BuildAndConfigure(this WebApplicationBuilder builder, bool addBlazor = false)
|
||||
{
|
||||
var pathBase = Environment.GetEnvironmentVariable("PATH_BASE");
|
||||
var useReverseProxy = !string.IsNullOrWhiteSpace(pathBase);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (useReverseProxy)
|
||||
{
|
||||
app.UsePathBase(pathBase!.TrimEnd('/'));
|
||||
app.UseForwardedHeaders();
|
||||
}
|
||||
|
||||
app.ConfigureDaprService(addBlazor);
|
||||
app.MapPrometheusScrapingEndpoint();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the web application as dapr service.
|
||||
/// </summary>
|
||||
/// <param name="app">The application.</param>
|
||||
/// <param name="addBlazor">If set to <c>true</c>, it configures the application to provide a blazor server app.</param>
|
||||
/// <returns>The configured web application.</returns>
|
||||
public static WebApplication ConfigureDaprService(this WebApplication app, bool addBlazor = false)
|
||||
{
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCloudEvents();
|
||||
app.MapControllers();
|
||||
app.MapSubscribeHandler();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the completion of outstanding database updates.
|
||||
/// </summary>
|
||||
/// <param name="app">The application.</param>
|
||||
public static async Task WaitForUpdatedDatabaseAsync(this WebApplication app)
|
||||
{
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
await app.Services.GetService<PersistenceContextProvider>()!
|
||||
.WaitForUpdatedDatabaseAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for database connection (settings) initialization.
|
||||
/// </summary>
|
||||
/// <param name="app">The application.</param>
|
||||
public static async Task WaitForDatabaseConnectionInitializationAsync(this WebApplication app)
|
||||
{
|
||||
await app.Services.GetService<IDatabaseConnectionSettingProvider>()!
|
||||
.InitializeAsync(default)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the database secrets to be loaded and then for the database to be up-to-date.
|
||||
/// This is intended to be called from <see cref="Microsoft.Extensions.Hosting.IHostedLifecycleService.StartedAsync"/>
|
||||
/// which is called after the web server has already started, breaking the circular dependency where:
|
||||
/// the Dapr sidecar needs the app HTTP API to be up before it initializes its secret store,
|
||||
/// but the app needs Dapr secrets to connect to the database.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static async Task WaitForDatabaseInitializationAsync(this IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dbConnectionProvider = serviceProvider.GetRequiredService<IDatabaseConnectionSettingProvider>();
|
||||
if (dbConnectionProvider.Initialization is { } initTask)
|
||||
{
|
||||
await initTask.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await serviceProvider.GetRequiredService<PersistenceContextProvider>()
|
||||
.WaitForUpdatedDatabaseAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the ip resolver to the collection, depending on the command line arguments
|
||||
/// and the <see cref="SystemConfiguration"/> in the database.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">The service collection.</param>
|
||||
/// <param name="args">The arguments.</param>
|
||||
/// <returns>The <paramref name="serviceCollection"/>.</returns>
|
||||
public static IServiceCollection AddIpResolver(this IServiceCollection serviceCollection, string[] args)
|
||||
{
|
||||
return serviceCollection.AddSingleton(serviceProvider =>
|
||||
{
|
||||
(IpResolverType IpResolver, string? IpResolverParameter)? settings = default;
|
||||
try
|
||||
{
|
||||
var persistenceContextProvider = serviceProvider.GetService<IPersistenceContextProvider>() ?? throw new Exception($"{nameof(IPersistenceContextProvider)} not registered.");
|
||||
using var context = persistenceContextProvider.CreateNewTypedContext(typeof(SystemConfiguration), false);
|
||||
|
||||
// TODO: this may lead to a deadlock?
|
||||
var configuration = context.GetAsync<SystemConfiguration>().AsTask().WaitAndUnwrapException().FirstOrDefault();
|
||||
if (configuration is not null)
|
||||
{
|
||||
settings = (configuration.IpResolver, configuration.IpResolverParameter);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
serviceProvider.GetService<ILogger<IIpAddressResolver>>()?.LogError(ex, "Unexpected error when trying to load the system configuration during ip resolver creation.");
|
||||
}
|
||||
|
||||
return IpAddressResolverFactory.CreateIpResolver(args, settings, serviceProvider.GetService<ILoggerFactory>()!);
|
||||
});
|
||||
}
|
||||
}
|
||||
42
src/Dapr/Common/MUnique.OpenMU.Dapr.Common.csproj
Normal file
42
src/Dapr/Common/MUnique.OpenMU.Dapr.Common.csproj
Normal file
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Properties\**" />
|
||||
<EmbeddedResource Remove="Properties\**" />
|
||||
<None Remove="Properties\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.AspNetCore" />
|
||||
<PackageReference Include="Dapr.Client" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.OpenApi" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Grafana.Loki" />
|
||||
<PackageReference Include="SharpAbp.Abp.OpenTelemetry.Exporter.Prometheus.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\..\Persistence\EntityFramework\MUnique.OpenMU.Persistence.EntityFramework.csproj" />
|
||||
<ProjectReference Include="..\ServerClients\MUnique.OpenMU.ServerClients.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
116
src/Dapr/Common/ManagableServerRegistry.cs
Normal file
116
src/Dapr/Common/ManagableServerRegistry.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
// <copyright file="ManagableServerRegistry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A registry for all <see cref="IManageableServer"/>s in the system.
|
||||
/// </summary>
|
||||
public class ManagableServerRegistry : IServerProvider, IDisposable
|
||||
{
|
||||
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(20);
|
||||
private readonly CancellationTokenSource _disposeCts = new();
|
||||
private readonly ILogger<ManagableServerRegistry> _logger;
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ConcurrentDictionary<int, ManageableServerClient> _serverClients = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagableServerRegistry" /> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ManagableServerRegistry(DaprClient daprClient, ILogger<ManagableServerRegistry> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
|
||||
async Task RunTimeoutLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.TimeoutLoopAsync(this._disposeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error in cleanup loop");
|
||||
}
|
||||
}
|
||||
|
||||
_ = RunTimeoutLoop();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<IManageableServer> Servers => this._serverClients.Values.OfType<IManageableServer>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Handles an update of a server state.
|
||||
/// </summary>
|
||||
/// <param name="serverData">The server data.</param>
|
||||
public void HandleUpdate(ServerStateData serverData)
|
||||
{
|
||||
var isNew = false;
|
||||
this._serverClients.AddOrUpdate(
|
||||
serverData.Id,
|
||||
_ =>
|
||||
{
|
||||
isNew = true;
|
||||
return new ManageableServerClient(this._daprClient, serverData);
|
||||
},
|
||||
(_, client) =>
|
||||
{
|
||||
client.Update(serverData);
|
||||
return client;
|
||||
});
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(this.Servers));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._disposeCts.Cancel();
|
||||
this._disposeCts.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="PropertyChanged"/> event with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private async Task TimeoutLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!this._disposeCts.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var server in this._serverClients.Values)
|
||||
{
|
||||
var diff = DateTime.UtcNow - server.LastUpdate;
|
||||
if (diff > this._timeout)
|
||||
{
|
||||
this._logger.LogInformation("Difference of {0} higher than timeout for server {1}", diff, server.Id);
|
||||
server.ServerState = ServerState.Timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/Dapr/Common/ManagableServerStatePublisher.cs
Normal file
178
src/Dapr/Common/ManagableServerStatePublisher.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
// <copyright file="ManagableServerStatePublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// A state publisher for a <see cref="IManageableServer"/>,
|
||||
/// which can be handled with a corresponding <see cref="ManagableServerRegistry"/>.
|
||||
/// The server registration is deferred to <see cref="StartedAsync"/> which is called after the web application
|
||||
/// has started (i.e. the HTTP API is already available), breaking the circular startup dependency with the Dapr sidecar.
|
||||
/// </summary>
|
||||
public sealed class ManagableServerStatePublisher : IHostedLifecycleService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The topic name for the state updates.
|
||||
/// </summary>
|
||||
public const string TopicName = "ServerState";
|
||||
|
||||
private readonly ILogger<ManagableServerStatePublisher> _logger;
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly AsyncLock _lock = new();
|
||||
|
||||
private IManageableServer? _server;
|
||||
private ServerStateData? _data;
|
||||
|
||||
private Task? _heartbeatTask;
|
||||
private CancellationTokenSource? _heartbeatCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagableServerStatePublisher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="serviceProvider">The service provider used to lazily resolve <see cref="IManageableServer"/>.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ManagableServerStatePublisher(DaprClient daprClient, IServiceProvider serviceProvider, ILogger<ManagableServerStatePublisher> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._serviceProvider = serviceProvider;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.StopAsync(default).WaitAndUnwrapException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this._heartbeatCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
async Task RunHeartbeatTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.HeartbeatLoopAsync(this._heartbeatCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error in heartbeat loop.");
|
||||
}
|
||||
}
|
||||
|
||||
this._heartbeatTask = RunHeartbeatTask();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await (this._heartbeatCancellationTokenSource?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
this._heartbeatCancellationTokenSource?.Dispose();
|
||||
if (this._heartbeatTask is { } heartbeatTask)
|
||||
{
|
||||
this._heartbeatTask = null;
|
||||
await heartbeatTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.InitializeServerAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await this.PublishCurrentStateAsync().ConfigureAwait(false);
|
||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeServerAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && this._server is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var server = this._serviceProvider.GetRequiredService<IManageableServer>();
|
||||
server.PropertyChanged -= this.OnPropertyChanged; // Ensure single subscription in case of retry
|
||||
server.PropertyChanged += this.OnPropertyChanged;
|
||||
this._data = new ServerStateData(server);
|
||||
this._server = server;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogWarning(ex, "Could not resolve IManageableServer yet, retrying...");
|
||||
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishCurrentStateAsync()
|
||||
{
|
||||
if (this._server is null || this._data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var asyncLock = await this._lock.LockAsync(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
if (asyncLock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this._data.UpdateState(this._server);
|
||||
await this._daprClient.PublishEventAsync("pubsub", TopicName, this._data).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error sending server status update");
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "Exceptions are catched.")]
|
||||
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.PropertyName == nameof(IManageableServer.ServerState))
|
||||
{
|
||||
await this.PublishCurrentStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error when publishing current state after property change");
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/Dapr/Common/ManageableServerClient.cs
Normal file
138
src/Dapr/Common/ManageableServerClient.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
// <copyright file="ManageableServerClient.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using global::Dapr.Client;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A client to control a <seealso cref="IManageableServer"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="MUnique.OpenMU.Interfaces.IManageableServer" />
|
||||
internal class ManageableServerClient : IManageableServer
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
|
||||
private readonly string _targetAppId;
|
||||
private ServerState _serverState;
|
||||
private int _currentConnections;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManageableServerClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="serverData">The server data.</param>
|
||||
public ManageableServerClient(DaprClient daprClient, ServerStateData serverData)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._targetAppId = serverData.AppId;
|
||||
this.Id = serverData.Id;
|
||||
this.Description = serverData.Description;
|
||||
this.ConfigurationId = serverData.ConfigurationId;
|
||||
this.CurrentConnections = serverData.CurrentConnections;
|
||||
this.MaximumConnections = serverData.MaximumConnections;
|
||||
this.ServerState = serverData.State;
|
||||
this.Type = serverData.Type;
|
||||
this.LastUpdate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Id { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid ConfigurationId { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerType Type { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaximumConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp of the last update.
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerState ServerState
|
||||
{
|
||||
get => this._serverState;
|
||||
set
|
||||
{
|
||||
if (this._serverState == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._serverState = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CurrentConnections
|
||||
{
|
||||
get => this._currentConnections;
|
||||
set
|
||||
{
|
||||
if (this._currentConnections == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentConnections = value;
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(IManageableServer.StartAsync), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask StartAsync()
|
||||
{
|
||||
await this.StartAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(IManageableServer.ShutdownAsync), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ShutdownAsync()
|
||||
{
|
||||
await this.StopAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the specified server data.
|
||||
/// </summary>
|
||||
/// <param name="serverData">The server data.</param>
|
||||
public void Update(ServerStateData serverData)
|
||||
{
|
||||
this.ServerState = serverData.State;
|
||||
this.CurrentConnections = serverData.CurrentConnections;
|
||||
this.LastUpdate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
45
src/Dapr/Common/ManageableServerController.cs
Normal file
45
src/Dapr/Common/ManageableServerController.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// <copyright file="ManageableServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An api controller to control a <see cref="IManageableServer"/>.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class ManageableServerController
|
||||
{
|
||||
private readonly IManageableServer _manageableServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManageableServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manageableServer">The manageable server.</param>
|
||||
public ManageableServerController(IManageableServer manageableServer)
|
||||
{
|
||||
this._manageableServer = manageableServer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts the manageable server down.
|
||||
/// </summary>
|
||||
[HttpPost(nameof(IManageableServer.ShutdownAsync))]
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
await this._manageableServer.ShutdownAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the manageable server.
|
||||
/// </summary>
|
||||
[HttpPost(nameof(IManageableServer.StartAsync))]
|
||||
public async Task StartAsync()
|
||||
{
|
||||
await this._manageableServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
47
src/Dapr/Common/MetricsRegistry.cs
Normal file
47
src/Dapr/Common/MetricsRegistry.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// <copyright file="MetricsRegistry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Registry for metrics which should be exposed by the application.
|
||||
/// </summary>
|
||||
public class MetricsRegistry
|
||||
{
|
||||
private readonly HashSet<string> _meters = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registered meters.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Meters => this._meters;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the meter with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="meterName">Name of the meter.</param>
|
||||
public void AddMeter(string meterName)
|
||||
{
|
||||
this._meters.Add(meterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the meters with the specified names.
|
||||
/// </summary>
|
||||
/// <param name="meterNames">The names of the meters.</param>
|
||||
public void AddMeters(IEnumerable<string> meterNames)
|
||||
{
|
||||
foreach (var meter in meterNames)
|
||||
{
|
||||
this.AddMeter(meter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the network meters.
|
||||
/// </summary>
|
||||
public void AddNetworkMeters()
|
||||
{
|
||||
this.AddMeters(MUnique.OpenMU.Network.Metrics.Meters);
|
||||
}
|
||||
}
|
||||
107
src/Dapr/Common/SecretStoreDatabaseConnectionSettingsProvider.cs
Normal file
107
src/Dapr/Common/SecretStoreDatabaseConnectionSettingsProvider.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
// <copyright file="SecretStoreDatabaseConnectionSettingsProvider.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System.Threading;
|
||||
using global::Dapr;
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IDatabaseConnectionSettingProvider"/> which retrieves the settings from the
|
||||
/// configured Dapr secret storage.
|
||||
/// </summary>
|
||||
public class SecretStoreDatabaseConnectionSettingsProvider : IDatabaseConnectionSettingProvider
|
||||
{
|
||||
private const string SecretStoreName = "secrets";
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<SecretStoreDatabaseConnectionSettingsProvider> _logger;
|
||||
private readonly Dictionary<string, ConnectionSetting> _connectionSettings = new(StringComparer.InvariantCultureIgnoreCase);
|
||||
private bool _isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretStoreDatabaseConnectionSettingsProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SecretStoreDatabaseConnectionSettingsProvider(DaprClient daprClient, ILogger<SecretStoreDatabaseConnectionSettingsProvider> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task? Initialization { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._isInitialized)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
this.Initialization = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
this._isInitialized = false;
|
||||
while (!this._isInitialized && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("trying to get secrets ...");
|
||||
var secrets = await this._daprClient.GetBulkSecretAsync(SecretStoreName, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
foreach (var secret in secrets.Where(kvp => string.Equals(kvp.Key.Split(':')[0], "connectionStrings", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
var contextTypeName = secret.Value.Keys.First().Split(':').Last();
|
||||
var setting = new ConnectionSetting
|
||||
{
|
||||
ContextTypeName = contextTypeName,
|
||||
ConnectionString = secret.Value.Values.First()!,
|
||||
DatabaseEngine = DatabaseEngine.Npgsql,
|
||||
};
|
||||
|
||||
this._connectionSettings.Add(contextTypeName, setting);
|
||||
}
|
||||
|
||||
Console.WriteLine("secrets retrieved :)");
|
||||
|
||||
this._isInitialized = true;
|
||||
}
|
||||
catch (DaprException ex)
|
||||
{
|
||||
// This should never happen - however, it may happen when we are using a Dapr secret store.
|
||||
// It may not be started yet, and the implementation to get it does retrieve it in the constructor already.
|
||||
this._logger.LogWarning(ex, "Error occurred when retrieving the connection strings from the secrets store. Trying again in 3 seconds...");
|
||||
Console.WriteLine("Error occurred when retrieving the connection strings from the secrets store. Trying again in 3 seconds...");
|
||||
await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
ConnectionConfigurator.Initialize(this);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionSetting GetConnectionSetting<TContextType>()
|
||||
where TContextType : DbContext
|
||||
{
|
||||
return this.GetConnectionSetting(typeof(TContextType));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionSetting GetConnectionSetting(Type contextType)
|
||||
{
|
||||
if (this._connectionSettings.TryGetValue(contextType.FullName ?? contextType.Name, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"No connection string for type '{contextType.FullName}' stored in the secret store.");
|
||||
}
|
||||
}
|
||||
96
src/Dapr/Common/ServerStateData.cs
Normal file
96
src/Dapr/Common/ServerStateData.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// <copyright file="ServerStateData.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
using System.Diagnostics;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Data about the state of a server.
|
||||
/// </summary>
|
||||
public class ServerStateData
|
||||
{
|
||||
private readonly Stopwatch _stopwatch = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerStateData"/> class.
|
||||
/// </summary>
|
||||
public ServerStateData()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerStateData"/> class.
|
||||
/// </summary>
|
||||
/// <param name="server">The server.</param>
|
||||
/// <exception cref="System.InvalidOperationException">Add the environment variable 'APPID' with the app-id of this dapr app.</exception>
|
||||
public ServerStateData(IManageableServer server)
|
||||
{
|
||||
this.AppId = Environment.GetEnvironmentVariable("APPID") ?? throw new InvalidOperationException("Add the environment variable 'APPID' with the app-id of this dapr app.");
|
||||
this.Id = server.Id;
|
||||
this.Description = server.Description;
|
||||
this.ConfigurationId = server.ConfigurationId;
|
||||
this.Type = server.Type;
|
||||
this.MaximumConnections = server.MaximumConnections;
|
||||
this._stopwatch.Start();
|
||||
this.UpdateState(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the (dapr) application identifier.
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the server.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the server.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the configuration of the server.
|
||||
/// </summary>
|
||||
public Guid ConfigurationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the server.
|
||||
/// </summary>
|
||||
public ServerType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state of the server.
|
||||
/// </summary>
|
||||
public ServerState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current connection count of the server.
|
||||
/// </summary>
|
||||
public int CurrentConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum connection count which the server supports.
|
||||
/// </summary>
|
||||
public int MaximumConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the up time of the server.
|
||||
/// </summary>
|
||||
public TimeSpan UpTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the server.
|
||||
/// </summary>
|
||||
/// <param name="server">The server.</param>
|
||||
public void UpdateState(IManageableServer server)
|
||||
{
|
||||
this.State = server.ServerState;
|
||||
this.CurrentConnections = server.CurrentConnections;
|
||||
this.UpTime = this._stopwatch.Elapsed;
|
||||
}
|
||||
}
|
||||
51
src/Dapr/ConnectServer.Host/ConnectServerController.cs
Normal file
51
src/Dapr/ConnectServer.Host/ConnectServerController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// <copyright file="ConnectServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.Host;
|
||||
|
||||
using System.Net;
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// API Controller that receives messages from other services.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class ConnectServerController : ControllerBase
|
||||
{
|
||||
private readonly GameServerRegistry _registry;
|
||||
private readonly ILogger<ConnectServerController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registry">The registry.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ConnectServerController(GameServerRegistry registry, ILogger<ConnectServerController> logger)
|
||||
{
|
||||
this._registry = registry;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the game server heartbeat.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost("GameServerHeartbeat")]
|
||||
[Topic("pubsub", "GameServerHeartbeat")]
|
||||
public async Task GameServerHeartbeatAsync([FromBody] GameServerHeartbeatArguments data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._registry.UpdateRegistrationAsync(data.ServerInfo, IPEndPoint.Parse(data.PublicEndPoint)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error updating the GameServerRegistry");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// <copyright file="ConnectServerHostedServiceWrapper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.Host;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper which takes a <see cref="Interfaces.IConnectServer"/> and wraps it as <see cref="IHostedLifecycleService"/>,
|
||||
/// so that additional initialization can be done before actually starting it.
|
||||
/// The actual server start is deferred to <see cref="StartedAsync"/> which is called after the web application
|
||||
/// has started (i.e. the HTTP API is already available), breaking the circular startup dependency with the Dapr sidecar.
|
||||
/// TODO: listen to configuration changes/database reinit.
|
||||
/// See also: ServerContainerBase.
|
||||
/// </summary>
|
||||
public class ConnectServerHostedServiceWrapper : IHostedLifecycleService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private ConnectServer? _connectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectServerHostedServiceWrapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
public ConnectServerHostedServiceWrapper(IServiceProvider serviceProvider)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false);
|
||||
this._connectServer = this._serviceProvider.GetRequiredService<ConnectServer>();
|
||||
await this._connectServer.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._connectServer?.StopAsync(cancellationToken) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
23
src/Dapr/ConnectServer.Host/Dockerfile
Normal file
23
src/Dapr/ConnectServer.Host/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 44405
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/ConnectServer.Host/MUnique.OpenMU.ConnectServer.Host.csproj", "Dapr/ConnectServer.Host/"]
|
||||
RUN dotnet restore "Dapr/ConnectServer.Host/MUnique.OpenMU.ConnectServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/ConnectServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.ConnectServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.ConnectServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.ConnectServer.Host.dll"]
|
||||
106
src/Dapr/ConnectServer.Host/GameServerRegistry.cs
Normal file
106
src/Dapr/ConnectServer.Host/GameServerRegistry.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// <copyright file="GameServerRegistry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.Host;
|
||||
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// A registry which keeps track of available <see cref="IGameServer"/>s.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
public sealed class GameServerRegistry : IDisposable
|
||||
{
|
||||
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(10);
|
||||
private readonly CancellationTokenSource _disposeCts = new();
|
||||
private readonly IConnectServer _connectServer;
|
||||
private readonly ILogger<GameServerRegistry> _logger;
|
||||
private readonly Dictionary<ushort, DateTime> _entries = new();
|
||||
private readonly AsyncLock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerRegistry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GameServerRegistry(IConnectServer connectServer, ILogger<GameServerRegistry> logger)
|
||||
{
|
||||
this._connectServer = connectServer;
|
||||
this._logger = logger;
|
||||
|
||||
async Task RunCleanupLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.CleanupLoopAsync(this._disposeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error in cleanup loop");
|
||||
}
|
||||
}
|
||||
|
||||
_ = RunCleanupLoop();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._disposeCts.Cancel();
|
||||
this._disposeCts.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the registration.
|
||||
/// </summary>
|
||||
/// <param name="serverInfo">The server information.</param>
|
||||
/// <param name="publicEndPoint">The public end point.</param>
|
||||
public async Task UpdateRegistrationAsync(ServerInfo serverInfo, IPEndPoint publicEndPoint)
|
||||
{
|
||||
using var l = await this._lock.LockAsync().ConfigureAwait(false);
|
||||
var isNew = !this._entries.ContainsKey(serverInfo.Id);
|
||||
this._entries[serverInfo.Id] = DateTime.UtcNow;
|
||||
if (isNew)
|
||||
{
|
||||
this._connectServer.RegisterGameServer(serverInfo, publicEndPoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._connectServer.CurrentConnectionsChanged(serverInfo.Id, serverInfo.CurrentConnections);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tempRemoved = new List<ushort>();
|
||||
while (!this._disposeCts.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
|
||||
using var l = await this._lock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var serverId in this._entries.Keys)
|
||||
{
|
||||
var lastUpdate = this._entries[serverId];
|
||||
var diff = DateTime.UtcNow - lastUpdate;
|
||||
if (diff > this._timeout)
|
||||
{
|
||||
this._logger.LogInformation("Difference of {0} higher than timeout for server {1}", diff, serverId);
|
||||
this._connectServer.UnregisterGameServer(serverId);
|
||||
tempRemoved.Add(serverId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var serverId in tempRemoved)
|
||||
{
|
||||
this._entries.Remove(serverId, out _);
|
||||
}
|
||||
|
||||
tempRemoved.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>50035c44-3745-4419-bbb9-71c2d6592b6e</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ConnectServer\MUnique.OpenMU.ConnectServer.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
src/Dapr/ConnectServer.Host/Program.cs
Normal file
31
src/Dapr/ConnectServer.Host/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.ConnectServer;
|
||||
using MUnique.OpenMU.ConnectServer.Host;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
var builder = DaprService.CreateBuilder("ConnectServer", args);
|
||||
|
||||
// Add services to the container.
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<ConnectServer>()
|
||||
.AddSingleton<GameServerRegistry>()
|
||||
.AddSingleton<IConnectServer>(s => s.GetService<ConnectServer>()!)
|
||||
.AddPeristenceProvider()
|
||||
.AddPersistentSingleton<IConnectServerSettings, ConnectServerDefinition>()
|
||||
.AddHostedService<ConnectServerHostedServiceWrapper>()
|
||||
.PublishManageableServer<IConnectServer>();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
metricsRegistry.AddNetworkMeters();
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure();
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/ConnectServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/ConnectServer.Host/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.ConnectServer.Host")]
|
||||
79
src/Dapr/ConnectServer.Host/ServerInfoController.cs
Normal file
79
src/Dapr/ConnectServer.Host/ServerInfoController.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// <copyright file="ServerInfoController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ConnectServer.Host;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// API Controller which provides information about the connection and game servers.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ServerInfoController : ControllerBase
|
||||
{
|
||||
private readonly ConnectServer _connectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerInfoController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connectServer">The connect server.</param>
|
||||
public ServerInfoController(ConnectServer connectServer)
|
||||
{
|
||||
this._connectServer = connectServer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete information about the connection server and all known online game servers.
|
||||
/// </summary>
|
||||
/// <returns>The complete information about the connection server and all known online game servers.</returns>
|
||||
[HttpGet]
|
||||
public object GetCompleteInfo()
|
||||
{
|
||||
return new
|
||||
{
|
||||
PatchAddress = this._connectServer.Settings.PatchAddress,
|
||||
CurrentPatchVersion = this._connectServer.Settings.CurrentPatchVersion,
|
||||
Version = this._connectServer.Settings.Client.Version,
|
||||
Season = this._connectServer.Settings.Client.Season,
|
||||
Episode = this._connectServer.Settings.Client.Episode,
|
||||
Port = this._connectServer.Settings.ClientListenerPort,
|
||||
State = this._connectServer.ServerState,
|
||||
GameServers = this._connectServer.RegisteredGameServers
|
||||
.OrderBy(gs => gs.ServerId)
|
||||
.Select(gs => new
|
||||
{
|
||||
gs.ServerId,
|
||||
EndPoint = gs.EndPoint.ToString(),
|
||||
gs.ServerLoadPercentage,
|
||||
gs.CurrentConnections,
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection count of all game servers.
|
||||
/// </summary>
|
||||
/// <returns>The overall count of current connections.</returns>
|
||||
[HttpGet("playerCount")]
|
||||
public int GetOverallConnectionCount()
|
||||
{
|
||||
return this._connectServer.CurrentGameServerConnections;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection count of all game servers of a realm.
|
||||
/// </summary>
|
||||
/// <param name="realmIndex">Index of the realm.</param>
|
||||
/// <returns>The connection count of all game servers of a realm.</returns>
|
||||
[HttpGet("{realmIndex}/playerCount")]
|
||||
public int GetRealmConnectionCount(byte realmIndex)
|
||||
{
|
||||
const int realmOffset = 20;
|
||||
|
||||
return this._connectServer.RegisteredGameServers
|
||||
.Where(gs => gs.ServerId >= realmIndex * realmOffset && gs.ServerId < (realmIndex + 1) * realmOffset)
|
||||
.Sum(gs => gs.CurrentConnections);
|
||||
}
|
||||
}
|
||||
22
src/Dapr/FriendServer.Host/Dockerfile
Normal file
22
src/Dapr/FriendServer.Host/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/FriendServer.Host/MUnique.OpenMU.FriendServer.Host.csproj", "Dapr/FriendServer.Host/"]
|
||||
RUN dotnet restore "Dapr/FriendServer.Host/MUnique.OpenMU.FriendServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/FriendServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.FriendServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.FriendServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.FriendServer.Host.dll"]
|
||||
114
src/Dapr/FriendServer.Host/FriendNotifier.cs
Normal file
114
src/Dapr/FriendServer.Host/FriendNotifier.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
// <copyright file="FriendNotifier.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.FriendServer.Host;
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a <see cref="IFriendNotifier"/> which notifies the game server
|
||||
/// about notifications for a player about changes in the friend system.
|
||||
/// </summary>
|
||||
public class FriendNotifier : IFriendNotifier
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<FriendNotifier> _logger;
|
||||
private readonly IReadOnlyDictionary<int, string> _appIds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FriendNotifier" /> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FriendNotifier(DaprClient daprClient, ILogger<FriendNotifier> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
|
||||
var appIds = new Dictionary<int, string>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
appIds.Add(i, $"gameServer{i + 1}");
|
||||
}
|
||||
|
||||
this._appIds = new ReadOnlyDictionary<int, string>(appIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FriendRequestAsync(string requester, string receiver, int serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._appIds[serverId], nameof(IGameServer.FriendRequestAsync), new RequestArguments(requester, receiver)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.FriendRequestAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>It's usually never called here, but at <see cref="FriendServer.ForwardLetterAsync"/>.</remarks>
|
||||
public async ValueTask LetterReceivedAsync(LetterHeader letter)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.PublishEventAsync("pubsub", nameof(IGameServer.LetterReceivedAsync), letter).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.FriendRequestAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FriendOnlineStateChangedAsync(int playerServerId, string player, string friend, int friendServerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// todo: find out if this is correct when logging out
|
||||
if (this._appIds.TryGetValue(playerServerId, out var gameServer))
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(gameServer, nameof(IGameServer.FriendOnlineStateChangedAsync), new FriendOnlineStateChangedArguments(player, friend, friendServerId)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.FriendRequestAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ChatRoomCreatedAsync(int serverId, ChatServerAuthenticationInfo playerAuthenticationInfo, string friendName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._appIds[serverId], nameof(IGameServer.ChatRoomCreatedAsync), new ChatRoomCreationArguments(playerAuthenticationInfo, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.FriendRequestAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeMessengerAsync(int serverId, MessengerInitializationData initializationData)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this._appIds.TryGetValue(serverId, out var gameServer))
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(gameServer, nameof(IGameServer.InitializeMessengerAsync), initializationData).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.FriendRequestAsync));
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/Dapr/FriendServer.Host/FriendServerController.cs
Normal file
132
src/Dapr/FriendServer.Host/FriendServerController.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
// <copyright file="FriendServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.FriendServer.Host;
|
||||
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// API Controller which handles the calls from the <see cref="ServerClients.FriendServer"/>.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class FriendServerController : ControllerBase
|
||||
{
|
||||
private readonly IFriendServer _friendServer;
|
||||
|
||||
private readonly ILogger<FriendServerController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FriendServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="friendServer">The friend server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FriendServerController(IFriendServer friendServer, ILogger<FriendServerController> logger)
|
||||
{
|
||||
this._friendServer = friendServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called when a player entered the game.
|
||||
/// It will cause a response with <see cref="IFriendSystemSubscriber.InitializeMessengerAsync" />
|
||||
/// and a state update for friends.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[Topic("pubsub", nameof(IEventPublisher.PlayerEnteredGameAsync))]
|
||||
[HttpPost(nameof(IEventPublisher.PlayerEnteredGameAsync))]
|
||||
public async Task PlayerEnteredGameAsync([FromBody] PlayerOnlineStateArguments data)
|
||||
{
|
||||
await this._friendServer.PlayerEnteredGameAsync(data.ServerId, data.CharacterId, data.CharacterName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called when a player leaves the game.
|
||||
/// It will cause a state update for friends.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[Topic("pubsub", nameof(IEventPublisher.PlayerLeftGameAsync))]
|
||||
[HttpPost(nameof(IEventPublisher.PlayerLeftGameAsync))]
|
||||
public async Task PlayerLeftGameAsync([FromBody] PlayerOnlineStateArguments data)
|
||||
{
|
||||
await this._friendServer.PlayerLeftGameAsync(data.CharacterId, data.CharacterName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the online visibility state of a character.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost(nameof(IFriendServer.SetPlayerVisibilityStateAsync))]
|
||||
public async Task SetPlayerInvisibilityStateAsync([FromBody] PlayerFriendOnlineStateArguments data)
|
||||
{
|
||||
await this._friendServer.SetPlayerVisibilityStateAsync(data.ServerId, data.CharacterId, data.CharacterName, data.IsVisible).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the friend request response.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost(nameof(IFriendServer.FriendResponseAsync))]
|
||||
public async Task FriendResponseAsync([FromBody] FriendResponseArguments data)
|
||||
{
|
||||
await this._friendServer.FriendResponseAsync(data.CharacterName, data.FriendName, data.Accepted).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two players are friends.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>True if the two players are friends; otherwise false.</returns>
|
||||
[HttpPost(nameof(IFriendServer.IsFriendAsync))]
|
||||
public async Task<bool> IsFriendAsync([FromBody] RequestArguments data)
|
||||
{
|
||||
return await this._friendServer.IsFriendAsync(data.Requester, data.Receiver).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a friend request to the friend, and adds a new friend view item to the players friend list.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>If a new friend view item got added to the players friend list.</returns>
|
||||
[HttpPost(nameof(IFriendServer.FriendRequestAsync))]
|
||||
public async Task<bool> FriendRequestAsync([FromBody] RequestArguments data)
|
||||
{
|
||||
return await this._friendServer.FriendRequestAsync(data.Requester, data.Receiver).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the friend.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost(nameof(IFriendServer.DeleteFriendAsync))]
|
||||
public async Task DeleteFriendAsync([FromBody] RequestArguments data)
|
||||
{
|
||||
await this._friendServer.DeleteFriendAsync(data.Requester, data.Receiver).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new chat room.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
[HttpPost(nameof(IFriendServer.CreateChatRoomAsync))]
|
||||
public async Task CreateChatRoomAsync([FromBody] RequestArguments data)
|
||||
{
|
||||
await this._friendServer.CreateChatRoomAsync(data.Requester, data.Receiver).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invites a friend to an existing chat room.
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>The success of the invitation.</returns>
|
||||
[HttpPost(nameof(IFriendServer.InviteFriendToChatRoomAsync))]
|
||||
public async Task<bool> InviteFriendToChatRoomAsync([FromBody] ChatRoomInvitationArguments data)
|
||||
{
|
||||
return await this._friendServer.InviteFriendToChatRoomAsync(data.CharacterName, data.FriendName, data.RoomNumber).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>7b192ead-65a8-467f-a045-715183400bd9</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\FriendServer\MUnique.OpenMU.FriendServer.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
src/Dapr/FriendServer.Host/Program.cs
Normal file
31
src/Dapr/FriendServer.Host/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.FriendServer;
|
||||
using MUnique.OpenMU.FriendServer.Host;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
using FriendServer = MUnique.OpenMU.FriendServer.FriendServer;
|
||||
|
||||
var builder = DaprService.CreateBuilder("FriendServer", args);
|
||||
|
||||
// Add services to the container.
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<IFriendServer, FriendServer>()
|
||||
.AddSingleton<IChatServer, ChatServer>()
|
||||
.AddSingleton<IFriendNotifier, FriendNotifier>()
|
||||
.AddPeristenceProvider();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
|
||||
// todo: add some meaningful metrics
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure();
|
||||
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/FriendServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/FriendServer.Host/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.FriendServer.Host")]
|
||||
57
src/Dapr/GameServer.Host/App.razor
Normal file
57
src/Dapr/GameServer.Host/App.razor
Normal file
@@ -0,0 +1,57 @@
|
||||
@using MUnique.OpenMU.GameServer.Host.Layout
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenMU Map</title>
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link href="@Assets["MUnique.OpenMU.GameServer.Host.styles.css"]" rel="stylesheet" />
|
||||
@foreach (var stylesheetSrc in Web.Map.Exports.Stylesheets)
|
||||
{
|
||||
<link href="@Assets[stylesheetSrc]" rel="stylesheet" />
|
||||
}
|
||||
<ImportMap />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
@foreach (var scriptSrc in Web.Map.Exports.Scripts)
|
||||
{
|
||||
<script src="@Assets[scriptSrc]"></script>
|
||||
}
|
||||
@if (Web.Map.Exports.ScriptMappings.Any())
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("System.config({").AppendLine(" map: {");
|
||||
bool isFirst = true;
|
||||
foreach (var scriptMapping in Web.Map.Exports.ScriptMappings)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
sb.Append($"'{scriptMapping.Key}': '")
|
||||
.Append(scriptMapping.Path)
|
||||
.Append("'");
|
||||
}
|
||||
sb.AppendLine(" }")
|
||||
.AppendLine("});");
|
||||
|
||||
|
||||
<script>
|
||||
// To be able to resolve three etc. in our TS files and the resulting javascript, SystemJS needs to be configured.
|
||||
// These modules are loaded by SystemJS when they get requested the first time. No need to manually load them.
|
||||
|
||||
@((MarkupString)sb.ToString())
|
||||
</script>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
72
src/Dapr/GameServer.Host/ConfigurationChangeController.cs
Normal file
72
src/Dapr/GameServer.Host/ConfigurationChangeController.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// <copyright file="ConfigurationChangeController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The API controller which handles the calls of the <see cref="ConfigurationChangePublisher"/>
|
||||
/// from other services, such as the AdminPanel.
|
||||
/// It forwards the changes to a <see cref="IConfigurationChangeListener"/>, so
|
||||
/// that the caches are updated and the game logic can react to that.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class ConfigurationChangeController : ControllerBase
|
||||
{
|
||||
private readonly IConfigurationChangeListener _changeListener;
|
||||
private readonly PlugInManager _plugInManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationChangeController" /> class.
|
||||
/// </summary>
|
||||
/// <param name="changeListener">The change listener.</param>
|
||||
/// <param name="plugInManager">The plugin manager.</param>
|
||||
public ConfigurationChangeController(IConfigurationChangeListener changeListener, PlugInManager plugInManager)
|
||||
{
|
||||
this._changeListener = changeListener;
|
||||
this._plugInManager = plugInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a configuration got added on the admin panel.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The message arguments.</param>
|
||||
[HttpPost(nameof(IConfigurationChangePublisher.ConfigurationAddedAsync))]
|
||||
[Topic("pubsub", nameof(IConfigurationChangePublisher.ConfigurationAddedAsync))]
|
||||
public ValueTask ConfigurationAddedAsync([FromBody] ConfigurationChangeArguments arguments)
|
||||
{
|
||||
return this._changeListener.ConfigurationAddedAsync(arguments.Type, arguments.Id, arguments.Configuration!, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a configuration got added on the admin panel.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The message arguments.</param>
|
||||
[HttpPost(nameof(IConfigurationChangePublisher.ConfigurationChangedAsync))]
|
||||
[Topic("pubsub", nameof(IConfigurationChangePublisher.ConfigurationChangedAsync))]
|
||||
public ValueTask ConfigurationChangedAsync([FromBody] ConfigurationChangeArguments arguments)
|
||||
{
|
||||
this._plugInManager.ApplyChangedConfiguration(arguments.Type, arguments.Id, arguments.Configuration);
|
||||
return this._changeListener.ConfigurationChangedAsync(arguments.Type, arguments.Id, arguments.Configuration!, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a configuration got removed on the admin panel.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The message arguments.</param>
|
||||
[HttpPost(nameof(IConfigurationChangePublisher.ConfigurationRemovedAsync))]
|
||||
[Topic("pubsub", nameof(IConfigurationChangePublisher.ConfigurationRemovedAsync))]
|
||||
public ValueTask ConfigurationRemovedAsync([FromBody] ConfigurationChangeArguments arguments)
|
||||
{
|
||||
this._plugInManager.ApplyRemovedConfiguration(arguments.Type, arguments.Id);
|
||||
return this._changeListener.ConfigurationRemovedAsync(arguments.Type, arguments.Id, null, null);
|
||||
}
|
||||
}
|
||||
23
src/Dapr/GameServer.Host/Dockerfile
Normal file
23
src/Dapr/GameServer.Host/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/GameServer.Host/MUnique.OpenMU.GameServer.Host.csproj", "Dapr/GameServer.Host/"]
|
||||
RUN dotnet restore "Dapr/GameServer.Host/MUnique.OpenMU.GameServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/GameServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.GameServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.GameServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.GameServer.Host.dll"]
|
||||
122
src/Dapr/GameServer.Host/EventPublisher.cs
Normal file
122
src/Dapr/GameServer.Host/EventPublisher.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// <copyright file="EventPublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a <see cref="IEventPublisher"/> which publishes the events
|
||||
/// through the Dapr pub/sub component.
|
||||
/// </summary>
|
||||
public class EventPublisher : IEventPublisher
|
||||
{
|
||||
private const string PubSubName = "pubsub";
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<GameServerStatePublisher> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EventPublisher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public EventPublisher(DaprClient daprClient, ILogger<GameServerStatePublisher> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient
|
||||
.PublishEventAsync(
|
||||
PubSubName,
|
||||
nameof(IEventPublisher.PlayerEnteredGameAsync),
|
||||
new PlayerOnlineStateArguments(characterId, characterName, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing a guild message");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerLeftGameAsync(byte serverId, Guid characterId, string characterName, uint guildId = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient
|
||||
.PublishEventAsync(
|
||||
PubSubName,
|
||||
nameof(IEventPublisher.PlayerLeftGameAsync),
|
||||
new PlayerOnlineStateArguments(characterId, characterName, serverId, guildId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing a guild message");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient
|
||||
.PublishEventAsync(
|
||||
PubSubName,
|
||||
nameof(IGameServer.GuildChatMessageAsync),
|
||||
new GuildMessageArguments(guildId, sender, message)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing a guild message");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient
|
||||
.PublishEventAsync(
|
||||
PubSubName,
|
||||
nameof(IGameServer.AllianceChatMessageAsync),
|
||||
new GuildMessageArguments(guildId, sender, message)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing an alliance message");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that a client tried to log into an already logged-in account.
|
||||
/// The connected player can be notified about that.
|
||||
/// </summary>
|
||||
/// <param name="serverId">The identifier of the server on which the client tried to enter.</param>
|
||||
/// <param name="loginName">The login name.</param>
|
||||
public async ValueTask PlayerAlreadyLoggedInAsync(byte serverId, string loginName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient
|
||||
.PublishEventAsync(
|
||||
PubSubName,
|
||||
nameof(IGameServer.PlayerAlreadyLoggedInAsync),
|
||||
new PlayerLoggedInArguments(serverId, loginName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing an alliance message");
|
||||
}
|
||||
}
|
||||
}
|
||||
187
src/Dapr/GameServer.Host/GameServerController.cs
Normal file
187
src/Dapr/GameServer.Host/GameServerController.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// <copyright file="GameServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The API controller for the game server which handles the calls from other services.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class GameServerController : ControllerBase
|
||||
{
|
||||
private readonly IGameServer _gameServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="gameServer">The game server.</param>
|
||||
public GameServerController(GameServer gameServer)
|
||||
{
|
||||
this._gameServer = gameServer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down the server gracefully.
|
||||
/// </summary>
|
||||
[HttpPost(nameof(IGameServer.ShutdownAsync))]
|
||||
public async ValueTask ShutdownAsync()
|
||||
{
|
||||
await this._gameServer.ShutdownAsync().ConfigureAwait(false);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat message to all connected guild members.
|
||||
/// </summary>
|
||||
/// <param name="data">The message arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.GuildChatMessageAsync))]
|
||||
[Topic("pubsub", nameof(IGameServer.GuildChatMessageAsync))]
|
||||
public ValueTask GuildChatMessageAsync([FromBody] GuildMessageArguments data)
|
||||
{
|
||||
return this._gameServer.GuildChatMessageAsync(data.GuildId, data.Sender, data.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat message to all connected alliance members.
|
||||
/// </summary>
|
||||
/// <param name="data">The message arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.AllianceChatMessageAsync))]
|
||||
[Topic("pubsub", nameof(IGameServer.AllianceChatMessageAsync))]
|
||||
public ValueTask AllianceChatMessageAsync([FromBody] GuildMessageArguments data)
|
||||
{
|
||||
return this._gameServer.AllianceChatMessageAsync(data.GuildId, data.Sender, data.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the game server that a guild got deleted.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild identifier.</param>
|
||||
[HttpPost(nameof(IGameServer.GuildDeletedAsync))]
|
||||
[Topic("pubsub", nameof(GuildDeletedAsync))]
|
||||
public ValueTask GuildDeletedAsync([FromBody] uint guildId)
|
||||
{
|
||||
return this._gameServer.GuildDeletedAsync(guildId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the game server that a guild member got removed from a guild.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Name of the player which got removed from a guild.</param>
|
||||
[HttpPost(nameof(IGameServer.GuildPlayerKickedAsync))]
|
||||
[Topic("pubsub", nameof(IGameServer.GuildPlayerKickedAsync))]
|
||||
public ValueTask GuildPlayerKickedAsync([FromBody] string playerName)
|
||||
{
|
||||
return this._gameServer.GuildPlayerKickedAsync(playerName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the game server that a letter got received for an online player.
|
||||
/// </summary>
|
||||
/// <param name="letter">The letter header.</param>
|
||||
[HttpPost(nameof(IGameServer.LetterReceivedAsync))]
|
||||
[Topic("pubsub", nameof(IGameServer.LetterReceivedAsync))]
|
||||
public ValueTask LetterReceivedAsync([FromBody] LetterHeader letter)
|
||||
{
|
||||
return this._gameServer.LetterReceivedAsync(letter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the guild to the player.
|
||||
/// </summary>
|
||||
/// <param name="data">The assignment arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.AssignGuildToPlayerAsync))]
|
||||
public ValueTask AssignGuildToPlayerAsync([FromBody] GuildMemberAssignArguments data)
|
||||
{
|
||||
return this._gameServer.AssignGuildToPlayerAsync(data.CharacterName, data.MemberStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the messenger of a player.
|
||||
/// </summary>
|
||||
/// <param name="initializationData">The initialization data.</param>
|
||||
[HttpPost(nameof(IGameServer.InitializeMessengerAsync))]
|
||||
public ValueTask InitializeMessengerAsync([FromBody] MessengerInitializationData initializationData)
|
||||
{
|
||||
return this._gameServer.InitializeMessengerAsync(initializationData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a global message to all connected players with the specified message type.
|
||||
/// </summary>
|
||||
/// <param name="data">The message arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.SendGlobalMessageAsync))]
|
||||
public ValueTask SendGlobalMessageAsync([FromBody] MessageArguments data)
|
||||
{
|
||||
return this._gameServer.SendGlobalMessageAsync(data.Message, data.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the server that a player made a friend request to another player, which is online on this server.
|
||||
/// </summary>
|
||||
/// <param name="data">The request arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.FriendRequestAsync))]
|
||||
public ValueTask FriendRequestAsync([FromBody] RequestArguments data)
|
||||
{
|
||||
return this._gameServer.FriendRequestAsync(data.Requester, data.Receiver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the game server that a friend online state changed.
|
||||
/// </summary>
|
||||
/// <param name="data">The state change arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.FriendOnlineStateChangedAsync))]
|
||||
public ValueTask FriendOnlineStateChangedAsync([FromBody] FriendOnlineStateChangedArguments data)
|
||||
{
|
||||
return this._gameServer.FriendOnlineStateChangedAsync(data.Player, data.Friend, data.ServerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the game server that a chat room got created on the chat server for a player which is online on this game server.
|
||||
/// </summary>
|
||||
/// <param name="data">The chat room creation arguments.</param>
|
||||
[HttpPost(nameof(IGameServer.ChatRoomCreatedAsync))]
|
||||
public ValueTask ChatRoomCreatedAsync([FromBody] ChatRoomCreationArguments data)
|
||||
{
|
||||
return this._gameServer.ChatRoomCreatedAsync(data.AuthenticationInfo, data.FriendName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the player from the game.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Name of the player.</param>
|
||||
/// <returns>True, if the player has been disconnected; False, otherwise.</returns>
|
||||
[HttpPost(nameof(IGameServer.DisconnectPlayerAsync))]
|
||||
public ValueTask<bool> DisconnectPlayerAsync([FromBody] string playerName)
|
||||
{
|
||||
return this._gameServer.DisconnectPlayerAsync(playerName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the account from the game.
|
||||
/// </summary>
|
||||
/// <param name="accountName">Name of the account.</param>
|
||||
/// <returns>True, if the player has been disconnected; False, otherwise.</returns>
|
||||
[HttpPost(nameof(IGameServer.DisconnectPlayerAsync))]
|
||||
public ValueTask<bool> DisconnectAccountAsync([FromBody] string accountName)
|
||||
{
|
||||
return this._gameServer.DisconnectAccountAsync(accountName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bans the player from the game.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Name of the player.</param>
|
||||
/// <returns>True, if the player has been banned; False, otherwise.</returns>
|
||||
[HttpPost(nameof(IGameServer.BanPlayerAsync))]
|
||||
public ValueTask<bool> BanPlayerAsync([FromBody] string playerName)
|
||||
{
|
||||
return this._gameServer.BanPlayerAsync(playerName);
|
||||
}
|
||||
}
|
||||
80
src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs
Normal file
80
src/Dapr/GameServer.Host/GameServerHostedServiceWrapper.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// <copyright file="GameServerHostedServiceWrapper.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using MUnique.OpenMU.Web.Map;
|
||||
|
||||
/// <summary>
|
||||
/// A wrapper which takes a <see cref="Interfaces.IGameServer"/> and wraps it as <see cref="IHostedLifecycleService"/>,
|
||||
/// so that additional initialization can be done before actually starting it.
|
||||
/// The actual server start is deferred to <see cref="StartedAsync"/> which is called after the web application
|
||||
/// has started (i.e. the HTTP API is already available), breaking the circular startup dependency with the Dapr sidecar.
|
||||
/// TODO: listen to configuration changes/database reinit.
|
||||
/// See also: ServerContainerBase.
|
||||
/// </summary>
|
||||
public class GameServerHostedServiceWrapper : IHostedLifecycleService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private IGameServer? _gameServer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerHostedServiceWrapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
public GameServerHostedServiceWrapper(IServiceProvider serviceProvider)
|
||||
{
|
||||
this._serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task StartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await this._serviceProvider.WaitForDatabaseInitializationAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this._serviceProvider.GetService<ICollection<PlugInConfiguration>>() is { } plugInCollection)
|
||||
{
|
||||
if (plugInCollection is not List<PlugInConfiguration> plugInConfigurations)
|
||||
{
|
||||
throw new InvalidOperationException($"The registered {nameof(ICollection<PlugInConfiguration>)} must be a {nameof(List<PlugInConfiguration>)} to be able to load plugin configurations.");
|
||||
}
|
||||
|
||||
await this._serviceProvider.TryLoadPlugInConfigurationsAsync(plugInConfigurations).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
this._gameServer = this._serviceProvider.GetRequiredService<IGameServer>();
|
||||
var initializer = this._serviceProvider.GetRequiredService<GameServerInitializer>();
|
||||
await initializer.InitializeAsync().ConfigureAwait(false);
|
||||
|
||||
await ((ObservableGameServerAdapter)this._serviceProvider.GetRequiredService<IObservableGameServer>())
|
||||
.InitializeAsync().ConfigureAwait(false);
|
||||
|
||||
await this._gameServer.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._gameServer?.StopAsync(cancellationToken) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
80
src/Dapr/GameServer.Host/GameServerInitializer.cs
Normal file
80
src/Dapr/GameServer.Host/GameServerInitializer.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// <copyright file="GameServerInitializer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Initialization of a <see cref="GameServer"/> which is executed before starting it.
|
||||
/// </summary>
|
||||
public class GameServerInitializer
|
||||
{
|
||||
private readonly GameServer _gameServer;
|
||||
private readonly GameServerDefinition _definition;
|
||||
private readonly IIpAddressResolver _ipResolver;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IGameServerStateObserver _stateObserver;
|
||||
private readonly IPersistenceContextProvider _contextProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerInitializer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="gameServer">The game server.</param>
|
||||
/// <param name="definition">The definition.</param>
|
||||
/// <param name="ipResolver">The ip resolver.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="stateObserver">The state observer.</param>
|
||||
/// <param name="contextProvider">The context provider.</param>
|
||||
public GameServerInitializer(GameServer gameServer, GameServerDefinition definition, IIpAddressResolver ipResolver, ILoggerFactory loggerFactory, IGameServerStateObserver stateObserver, IPersistenceContextProvider contextProvider)
|
||||
{
|
||||
this._gameServer = gameServer;
|
||||
this._definition = definition;
|
||||
this._ipResolver = ipResolver;
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._stateObserver = stateObserver;
|
||||
this._contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the game server.
|
||||
/// </summary>
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
foreach (var endpoint in this._definition.Endpoints)
|
||||
{
|
||||
this._gameServer.AddListener(new DefaultTcpGameServerListener(
|
||||
endpoint,
|
||||
this._gameServer.CreateServerInfo(),
|
||||
this._gameServer.Context,
|
||||
this._stateObserver,
|
||||
this._ipResolver,
|
||||
this._loggerFactory));
|
||||
}
|
||||
|
||||
using var context = this._contextProvider.CreateNewConfigurationContext();
|
||||
await this.LoadGameClientDefinitionsAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/Dapr/GameServer.Host/GameServerStatePublisher.cs
Normal file
129
src/Dapr/GameServer.Host/GameServerStatePublisher.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
// <copyright file="GameServerStatePublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameServer.Host;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IGameServerStateObserver"/> which publishes the state
|
||||
/// by sending a heartbeat to a Dapr pub/sub component.
|
||||
/// </summary>
|
||||
public sealed class GameServerStatePublisher : IGameServerStateObserver, IDisposable
|
||||
{
|
||||
private const string PubSubName = "pubsub";
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<GameServerStatePublisher> _logger;
|
||||
|
||||
private int _currentConnections;
|
||||
private ServerInfo? _serverInfo;
|
||||
private IPEndPoint? _publicEndPoint;
|
||||
|
||||
private CancellationTokenSource? _heartbeatCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerStatePublisher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GameServerStatePublisher(DaprClient daprClient, ILogger<GameServerStatePublisher> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._heartbeatCancellationTokenSource?.Cancel();
|
||||
this._heartbeatCancellationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterGameServer(ServerInfo serverInfo, IPEndPoint publicEndPoint)
|
||||
{
|
||||
this._heartbeatCancellationTokenSource?.Cancel(false);
|
||||
|
||||
this._serverInfo = serverInfo;
|
||||
this._publicEndPoint = publicEndPoint;
|
||||
this._heartbeatCancellationTokenSource = new();
|
||||
try
|
||||
{
|
||||
this._logger.LogInformation("Starting heartbeat thread ...");
|
||||
var heartbeatThread = new Thread(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
this.HeartbeatLoop(this._heartbeatCancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error in heartbeat loop.");
|
||||
}
|
||||
})
|
||||
{
|
||||
Name = "Heartbeat",
|
||||
};
|
||||
heartbeatThread.Start();
|
||||
|
||||
this._logger.LogInformation("...started heartbeat thread.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when publishing the game server registration.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnregisterGameServer(ushort serverId)
|
||||
{
|
||||
this._logger.LogInformation("Stopping heartbeat thread");
|
||||
this._heartbeatCancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
|
||||
{
|
||||
this._currentConnections = currentConnections;
|
||||
}
|
||||
|
||||
private void HeartbeatLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._serverInfo is not { } serverInfo
|
||||
|| this._publicEndPoint is not { } publicEndPoint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
var publicEndPointString = publicEndPoint.ToString();
|
||||
var arguments = new GameServerHeartbeatArguments(serverInfo, publicEndPointString, stopWatch.Elapsed);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
serverInfo.CurrentConnections = this._currentConnections;
|
||||
arguments.UpTime = stopWatch.Elapsed;
|
||||
|
||||
try
|
||||
{
|
||||
this._daprClient.PublishEventAsync(PubSubName, "GameServerHeartbeat", arguments, cancellationToken).WaitAndUnwrapException(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogDebug(ex, "Error when publishing game server heartbeat");
|
||||
}
|
||||
|
||||
Thread.Sleep(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Dapr/GameServer.Host/Layout/MainLayout.razor
Normal file
26
src/Dapr/GameServer.Host/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,26 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<!--
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
<BlazoredToasts />
|
||||
-->
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<BreadcrumbNavigation />
|
||||
<a href="https://munique.net" target="_blank">About</a>
|
||||
</div>
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
44
src/Dapr/GameServer.Host/Layout/NavMenu.razor
Normal file
44
src/Dapr/GameServer.Host/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,44 @@
|
||||
@using MUnique.OpenMU.DataModel.Configuration
|
||||
@using MUnique.OpenMU.Persistence;
|
||||
|
||||
<nav class="rounded-bottom shadow-lg">
|
||||
<div class="top-row ps-4 navbar navbar-dark">
|
||||
<a class="navbar-brand" href="">OpenMU</a>
|
||||
<button class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hiding it programmatically is not a good idea here, because we want to hide it based on the width of the device/window. -->
|
||||
<div class="@NavMenuCssClass">
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="maps">
|
||||
<span class="oi oi-project" aria-hidden="true"></span> Maps
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
private bool _collapseNavMenu = true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the class for the entries of the navigation menu.
|
||||
/// "collapse" is a class of bootstrap which hides it.
|
||||
/// In our css we also define to show it anyway, if the width sufficient.
|
||||
/// </summary>
|
||||
private string NavMenuCssClass => _collapseNavMenu ? "collapse" : string.Empty;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
_collapseNavMenu = !_collapseNavMenu;
|
||||
}
|
||||
}
|
||||
31
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor
Normal file
31
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible">
|
||||
Resume
|
||||
</button>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please reload the page.
|
||||
</p>
|
||||
</div>
|
||||
</dialog>
|
||||
157
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor.css
Normal file
157
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor.css
Normal file
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: var(--omu-bg);
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: var(--omu-link-primary);
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: var(--omu-link-primary-border);
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: var(--omu-link-primary);
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
63
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor.js
Normal file
63
src/Dapr/GameServer.Host/Layout/ReconnectModal.razor.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
8
src/Dapr/GameServer.Host/Layout/Routes.razor
Normal file
8
src/Dapr/GameServer.Host/Layout/Routes.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="@typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<ModalContainer>
|
||||
<RouteView RouteData="routeData" DefaultLayout="@typeof(Layout.MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</ModalContainer>
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,44 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>f08a5e15-a10b-4b4c-b193-2ebf20e8ca71</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Web\Map\MUnique.OpenMU.Web.Map.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
<ProjectReference Include="..\..\GameServer\MUnique.OpenMU.GameServer.csproj" />
|
||||
</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>
|
||||
16
src/Dapr/GameServer.Host/Pages/Error.razor
Normal file
16
src/Dapr/GameServer.Host/Pages/Error.razor
Normal file
@@ -0,0 +1,16 @@
|
||||
@page "/error"
|
||||
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
35
src/Dapr/GameServer.Host/Pages/Index.razor
Normal file
35
src/Dapr/GameServer.Host/Pages/Index.razor
Normal file
@@ -0,0 +1,35 @@
|
||||
@page "/"
|
||||
|
||||
@using MUnique.OpenMU.Web.Map
|
||||
@using MUnique.OpenMU.Web.Map.Components
|
||||
@using MUnique.OpenMU.GameLogic
|
||||
|
||||
@inject IObservableGameServer _gameServer;
|
||||
@inject IGameServerContext _gameServerContext;
|
||||
|
||||
<p>
|
||||
<h3>
|
||||
<span class="badge badge-secondary">
|
||||
GameServer Id: <span class="badge badge-light">@this._gameServer.Id</span>
|
||||
</span>
|
||||
<span class="badge badge-primary">
|
||||
Players: <span class="badge badge-light">@_gameServerContext.PlayerCount / @_gameServerContext.ServerConfiguration.MaximumPlayers</span>
|
||||
</span>
|
||||
<span class="badge badge-primary">
|
||||
Hosted Maps: <span class="badge badge-light">@_gameServer.Maps.Count</span>
|
||||
</span>
|
||||
<span class="badge badge-primary">
|
||||
Experience Rate: <span class="badge badge-light">@_gameServerContext.ExperienceRate</span>
|
||||
</span>
|
||||
<span class="badge badge-primary">
|
||||
PVP Enabled: <span class="badge badge-light">@_gameServerContext.PvpEnabled</span>
|
||||
</span>
|
||||
</h3>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<h2>Hosted Maps</h2>
|
||||
<CascadingValue Name="@nameof(MapCard.LiveMapRoute)" TValue="string" Value="MapPage.LiveMapRoute">
|
||||
<MapCards GameServer="@_gameServer" />
|
||||
</CascadingValue>
|
||||
</p>
|
||||
42
src/Dapr/GameServer.Host/Pages/MapPage.razor
Normal file
42
src/Dapr/GameServer.Host/Pages/MapPage.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@page "/map/{mapId:guid}"
|
||||
|
||||
@using MUnique.OpenMU.Web.Map.Components
|
||||
@using MUnique.OpenMU.Web.Map
|
||||
|
||||
@if (this.GameServer is not null)
|
||||
{
|
||||
<div>
|
||||
<NavLink href="" Match="NavLinkMatch.All">
|
||||
<!--<span class="oi oi-home" aria-hidden="true"></span>-->
|
||||
<span>All</span>
|
||||
</NavLink>
|
||||
<span> / @this._map?.MapName</span>
|
||||
</div>
|
||||
|
||||
<Map Server="@GameServer" MapId="@MapId"></Map>
|
||||
}
|
||||
|
||||
@code {
|
||||
internal const string LiveMapRoute = "map/";
|
||||
|
||||
private IGameMapInfo? _map;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id on which the map is hosted.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public IObservableGameServer? GameServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the map id.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Guid MapId { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
this._map = this.GameServer!.Maps.First(m => m.Id == this.MapId);
|
||||
return base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
6
src/Dapr/GameServer.Host/Pages/NotFound.razor
Normal file
6
src/Dapr/GameServer.Host/Pages/NotFound.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
@page "/not-found"
|
||||
@using MUnique.OpenMU.GameServer.Host.Layout
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
63
src/Dapr/GameServer.Host/Program.cs
Normal file
63
src/Dapr/GameServer.Host/Program.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GameServer.Host;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
using MUnique.OpenMU.Web.Map;
|
||||
using GameServer = MUnique.OpenMU.GameServer.GameServer;
|
||||
|
||||
// Ensure GameLogic and GameServer Assemblies are loaded
|
||||
_ = MUnique.OpenMU.GameLogic.Rand.NextInt(1, 2);
|
||||
_ = MUnique.OpenMU.GameServer.ClientVersionResolver.DefaultVersion;
|
||||
|
||||
var gameServerId = byte.Parse(Environment.GetEnvironmentVariable("GS_ID") ?? "0");
|
||||
var serviceName = $"GameServer{gameServerId + 1}";
|
||||
var builder = DaprService.CreateBuilder(serviceName, args);
|
||||
var plugInConfigurations = new List<PlugInConfiguration>();
|
||||
|
||||
// Add services to the container.
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<GameServer>()
|
||||
.AddSingleton<IGameServer>(s => s.GetService<GameServer>()!)
|
||||
.AddSingleton<IList<IManageableServer>>(s => new List<IManageableServer>() { s.GetService<GameServer>()! })
|
||||
.AddSingleton(s => s.GetService<GameServer>()!.Context)
|
||||
.AddSingleton<IGameServerStateObserver, GameServerStatePublisher>()
|
||||
.AddSingleton<ConfigurationChangeMediator>()
|
||||
.AddSingleton<IConfigurationChangeMediator>(s => s.GetRequiredService<ConfigurationChangeMediator>())
|
||||
.AddSingleton<IConfigurationChangeMediatorListener>(s => s.GetRequiredService<ConfigurationChangeMediator>())
|
||||
.AddSingleton<ILoginServer, LoginServer>()
|
||||
.AddSingleton<IGuildServer, GuildServer>()
|
||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
||||
.AddSingleton<IFriendServer, FriendServer>()
|
||||
.AddSingleton<GameServerInitializer>()
|
||||
.AddSingleton<IObservableGameServer, ObservableGameServerAdapter>()
|
||||
.AddPersistentSingleton<GameServerDefinition>(def => def.ServerID == gameServerId)
|
||||
.AddPeristenceProvider()
|
||||
.AddPlugInManager(plugInConfigurations)
|
||||
.AddIpResolver(args)
|
||||
.AddHostedService<GameServerHostedServiceWrapper>()
|
||||
.PublishManageableServer<IGameServer>();
|
||||
|
||||
builder.AddMapApp();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
metricsRegistry.AddNetworkMeters();
|
||||
metricsRegistry.AddMeters(MUnique.OpenMU.GameLogic.Metrics.Meters);
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure(false);
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
app.MapRazorComponents<MUnique.OpenMU.GameServer.Host.App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/GameServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/GameServer.Host/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.GameServer.Host")]
|
||||
21
src/Dapr/GameServer.Host/_Imports.razor
Normal file
21
src/Dapr/GameServer.Host/_Imports.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@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 static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@using Blazored.Toast
|
||||
@using Blazored.Toast.Services
|
||||
|
||||
@using BlazorInputFile
|
||||
|
||||
@using MUnique.OpenMU.Web.Shared
|
||||
@using MUnique.OpenMU.Web.Shared.Components
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Modal
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Form
|
||||
@using MUnique.OpenMU.Web.Shared.Services
|
||||
22
src/Dapr/GuildServer.Host/Dockerfile
Normal file
22
src/Dapr/GuildServer.Host/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/GuildServer.Host/MUnique.OpenMU.GuildServer.Host.csproj", "Dapr/GuildServer.Host/"]
|
||||
RUN dotnet restore "Dapr/GuildServer.Host/MUnique.OpenMU.GuildServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/GuildServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.GuildServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.GuildServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.GuildServer.Host.dll"]
|
||||
109
src/Dapr/GuildServer.Host/GuildChangePublisher.cs
Normal file
109
src/Dapr/GuildServer.Host/GuildChangePublisher.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
// <copyright file="GuildChangePublisher.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GuildServer.Host;
|
||||
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.EntityFramework.Model;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Publisher for guild changes over Dapr.
|
||||
/// </summary>
|
||||
public class GuildChangePublisher : IGuildChangePublisher
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<GuildChangePublisher> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildChangePublisher" /> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GuildChangePublisher(DaprClient daprClient, ILogger<GuildChangePublisher> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildPlayerKickedAsync(string playerName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.PublishEventAsync("pubsub", nameof(IGameServer.GuildPlayerKickedAsync), playerName).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.GuildPlayerKickedAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildDeletedAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.PublishEventAsync("pubsub", nameof(IGameServer.GuildDeletedAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.GuildDeletedAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AssignGuildToPlayerAsync(byte serverId, string characterName, GuildMemberStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync($"gameServer{serverId + 1}", nameof(IGameServer.AssignGuildToPlayerAsync), new GuildMemberAssignArguments(characterName, status)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.AssignGuildToPlayerAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceCreatedAsync(uint masterGuildId, uint memberGuildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync("pubsub", nameof(IGameServer.AllianceCreatedAsync), (masterGuildId, memberGuildId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.AllianceCreatedAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceDisbandedAsync(uint masterGuildId, uint memberGuildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync("pubsub", nameof(IGameServer.AllianceDisbandedAsync), (masterGuildId, memberGuildId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.AllianceDisbandedAsync));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildHostilityChangedAsync(uint guildIdA, IReadOnlyList<uint> allianceGuildIdsA, uint guildIdB, IReadOnlyList<uint> allianceGuildIdsB, bool created)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.PublishEventAsync("pubsub", nameof(IGameServer.GuildHostilityChangedAsync), (guildIdA, allianceGuildIdsA, guildIdB, allianceGuildIdsB, created)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.GuildHostilityChangedAsync));
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/Dapr/GuildServer.Host/GuildServerController.cs
Normal file
163
src/Dapr/GuildServer.Host/GuildServerController.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
// <copyright file="GuildServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GuildServer.Host;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Controller which handles external requests coming from other dapr applications,
|
||||
/// most probably <see cref="MUnique.OpenMU.ServerClients.GuildServer"/>.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class GuildServerController : ControllerBase
|
||||
{
|
||||
private readonly IGuildServer _guildServer;
|
||||
|
||||
private readonly ILogger<GuildServerController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="guildServer">The guild server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GuildServerController(IGuildServer guildServer, ILogger<GuildServerController> logger)
|
||||
{
|
||||
this._guildServer = guildServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the guild with the specified name exists.
|
||||
/// </summary>
|
||||
/// <param name="guildName">Name of the guild.</param>
|
||||
/// <returns>True, if the guild exists; False, otherwise.</returns>
|
||||
[HttpPost(nameof(IGuildServer.GuildExistsAsync))]
|
||||
public ValueTask<bool> GuildExistsAsync([FromBody] string guildName)
|
||||
{
|
||||
return this._guildServer.GuildExistsAsync(guildName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild by the guild identifier.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild identifier.</param>
|
||||
/// <returns>The guild.</returns>
|
||||
[HttpPost(nameof(IGuildServer.GetGuildAsync))]
|
||||
public ValueTask<Guild?> GetGuildAsync([FromBody] uint guildId)
|
||||
{
|
||||
return this._guildServer.GetGuildAsync(guildId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild id by the guild name.
|
||||
/// </summary>
|
||||
/// <param name="guildName">The guild name.</param>
|
||||
/// <returns>The guild id. <c>0</c>, if not found.</returns>
|
||||
[HttpPost(nameof(IGuildServer.GetGuildIdByNameAsync))]
|
||||
public ValueTask<uint> GetGuildIdByNameAsync([FromBody] string guildName)
|
||||
{
|
||||
return this._guildServer.GetGuildIdByNameAsync(guildName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the guild and sets the guild master online at the guild server. A separate call to <see cref="PlayerEnteredGameAsync" /> is not required.
|
||||
/// </summary>
|
||||
/// <param name="data">The guild creation arguments.</param>
|
||||
[HttpPost(nameof(IGuildServer.CreateGuildAsync))]
|
||||
public ValueTask<bool> CreateGuildAsync([FromBody] GuildCreationArguments data)
|
||||
{
|
||||
return this._guildServer.CreateGuildAsync(data.Name, data.MasterName, data.MasterId, data.Logo, data.ServerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the guild member and sets it online at the guild server. A separate call to <see cref="PlayerEnteredGameAsync" /> is not required.
|
||||
/// </summary>
|
||||
/// <param name="data">The guild member creation arguments.</param>
|
||||
[HttpPost(nameof(IGuildServer.CreateGuildMemberAsync))]
|
||||
public ValueTask CreateGuildMemberAsync([FromBody] GuildMemberCreationArguments data)
|
||||
{
|
||||
return this._guildServer.CreateGuildMemberAsync(data.GuildId, data.CharacterId, data.CharacterName, data.Role, data.ServerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the guild member position.
|
||||
/// </summary>
|
||||
/// <param name="data">The change arguments.</param>
|
||||
[HttpPost(nameof(IGuildServer.ChangeGuildMemberPositionAsync))]
|
||||
public ValueTask ChangeGuildMemberPositionAsync([FromBody] GuildMemberRoleChangeArguments data)
|
||||
{
|
||||
return this._guildServer.ChangeGuildMemberPositionAsync(data.GuildId, data.CharacterId, data.NewRole);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the guild server that a player (potential guild member) entered the game.
|
||||
/// </summary>
|
||||
/// <param name="data">The arguments of the changed player.</param>
|
||||
[Topic("pubsub", nameof(IEventPublisher.PlayerEnteredGameAsync))]
|
||||
[HttpPost(nameof(IEventPublisher.PlayerEnteredGameAsync))]
|
||||
public ValueTask PlayerEnteredGameAsync([FromBody] PlayerOnlineStateArguments data)
|
||||
{
|
||||
return this._guildServer.PlayerEnteredGameAsync(data.CharacterId, data.CharacterName, data.ServerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the guild server that a guild member left the game.
|
||||
/// </summary>
|
||||
/// <param name="data">The arguments of the changed player.</param>
|
||||
[Topic("pubsub", nameof(IEventPublisher.PlayerLeftGameAsync))]
|
||||
[HttpPost(nameof(IEventPublisher.PlayerLeftGameAsync))]
|
||||
public ValueTask PlayerLeftGameAsync([FromBody] PlayerOnlineStateArguments data)
|
||||
{
|
||||
return this._guildServer.GuildMemberLeftGameAsync(data.GuildId, data.CharacterId, data.ServerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild member list.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The guild identifier.</param>
|
||||
/// <returns>The guild member list.</returns>
|
||||
[HttpPost(nameof(IGuildServer.GetGuildListAsync))]
|
||||
public ValueTask<IImmutableList<GuildListEntry>> GetGuildListAsync([FromBody] uint guildId)
|
||||
{
|
||||
return this._guildServer.GetGuildListAsync(guildId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks a guild member from a guild.
|
||||
/// </summary>
|
||||
/// <param name="data">The guild member arguments.</param>
|
||||
[HttpPost(nameof(IGuildServer.KickMemberAsync))]
|
||||
public ValueTask KickMemberAsync([FromBody] GuildMemberArguments data)
|
||||
{
|
||||
return this._guildServer.KickMemberAsync(data.GuildId, data.PlayerName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guild position of a specific character.
|
||||
/// </summary>
|
||||
/// <param name="characterId">The character identifier.</param>
|
||||
/// <returns>The guild position.</returns>
|
||||
[HttpPost(nameof(IGuildServer.GetGuildPositionAsync))]
|
||||
public ValueTask<GuildPosition> GetGuildPositionAsync([FromBody] Guid characterId)
|
||||
{
|
||||
return this._guildServer.GetGuildPositionAsync(characterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increases the guild score by one.
|
||||
/// </summary>
|
||||
/// <param name="guildId">The identifier of the guild.</param>
|
||||
[HttpPost(nameof(IGuildServer.IncreaseGuildScoreAsync))]
|
||||
public ValueTask IncreaseGuildScoreAsync([FromBody] uint guildId)
|
||||
{
|
||||
return this._guildServer.IncreaseGuildScoreAsync(guildId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>b7d410a1-1a65-4459-aab9-843bdcd2ba3e</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\GuildServer\MUnique.OpenMU.GuildServer.csproj" />
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
28
src/Dapr/GuildServer.Host/Program.cs
Normal file
28
src/Dapr/GuildServer.Host/Program.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.GuildServer;
|
||||
using MUnique.OpenMU.GuildServer.Host;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
var builder = DaprService.CreateBuilder("GuildServer", args);
|
||||
|
||||
// Add services to the container.
|
||||
var services = builder.Services;
|
||||
services.AddSingleton<IGuildServer, GuildServer>()
|
||||
.AddSingleton<IGuildChangePublisher, GuildChangePublisher>()
|
||||
.AddPeristenceProvider();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
|
||||
// todo: add some meaningful metrics
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure();
|
||||
await app.WaitForDatabaseConnectionInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/GuildServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/GuildServer.Host/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.GuildServer.Host")]
|
||||
22
src/Dapr/LoginServer.Host/Dockerfile
Normal file
22
src/Dapr/LoginServer.Host/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Dapr/LoginServer.Host/MUnique.OpenMU.LoginServer.Host.csproj", "Dapr/LoginServer.Host/"]
|
||||
RUN dotnet restore "Dapr/LoginServer.Host/MUnique.OpenMU.LoginServer.Host.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Dapr/LoginServer.Host"
|
||||
RUN dotnet build "MUnique.OpenMU.LoginServer.Host.csproj" -c Release -o /app/build -p:ci=true
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MUnique.OpenMU.LoginServer.Host.csproj" -c Release -o /app/publish -p:ci=true
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "MUnique.OpenMU.LoginServer.Host.dll"]
|
||||
121
src/Dapr/LoginServer.Host/GameServerRegistry.cs
Normal file
121
src/Dapr/LoginServer.Host/GameServerRegistry.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// <copyright file="GameServerRegistry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.LoginServer.Host;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// The registry for game servers.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
public sealed class GameServerRegistry : IDisposable
|
||||
{
|
||||
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(20);
|
||||
private readonly TimeSpan _newServerUptimeLimit = TimeSpan.FromSeconds(60);
|
||||
private readonly CancellationTokenSource _disposeCts = new();
|
||||
private readonly ILogger<GameServerRegistry> _logger;
|
||||
private readonly Dictionary<ushort, DateTime> _entries = new();
|
||||
private readonly AsyncLock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServerRegistry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GameServerRegistry(ILogger<GameServerRegistry> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
|
||||
async Task RunCleanupLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.CleanupLoopAsync(this._disposeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error in cleanup loop");
|
||||
}
|
||||
}
|
||||
|
||||
_ = RunCleanupLoop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a game server was added to the registry.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<ushort>? GameServerAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a new (=freshly started) game server was added to the registry.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<ushort>? NewGameServerAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a game server was removed from the registry.
|
||||
/// </summary>
|
||||
public event AsyncEventHandler<ushort>? GameServerRemoved;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this._disposeCts.Cancel();
|
||||
this._disposeCts.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the registration of the game server.
|
||||
/// </summary>
|
||||
/// <param name="gameServerId">The game server identifier.</param>
|
||||
/// <param name="upTime">The up-time of the server.</param>
|
||||
public async Task UpdateRegistrationAsync(ushort gameServerId, TimeSpan upTime)
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
var timestamp = DateTime.UtcNow;
|
||||
if (this._entries.TryAdd(gameServerId, timestamp))
|
||||
{
|
||||
if (upTime <= this._newServerUptimeLimit)
|
||||
{
|
||||
this.NewGameServerAdded?.SafeInvokeAsync(gameServerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.GameServerAdded?.SafeInvokeAsync(gameServerId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._entries[gameServerId] = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tempRemoved = new List<ushort>();
|
||||
while (!this._disposeCts.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
|
||||
using var l = await this._lock.LockAsync();
|
||||
foreach (var serverId in this._entries.Keys)
|
||||
{
|
||||
var lastUpdate = this._entries[serverId];
|
||||
var diff = DateTime.UtcNow - lastUpdate;
|
||||
if (diff > this._timeout)
|
||||
{
|
||||
this._logger.LogInformation("Difference of {0} higher than timeout for server {1}", diff, serverId);
|
||||
this.GameServerRemoved?.SafeInvokeAsync(serverId);
|
||||
tempRemoved.Add(serverId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var serverId in tempRemoved)
|
||||
{
|
||||
this._entries.Remove(serverId, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/Dapr/LoginServer.Host/LoginServerController.cs
Normal file
85
src/Dapr/LoginServer.Host/LoginServerController.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// <copyright file="LoginServerController.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.LoginServer.Host;
|
||||
|
||||
using global::Dapr;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The API controller for the login server.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("")]
|
||||
public class LoginServerController : ControllerBase
|
||||
{
|
||||
private readonly ILoginServer _loginServer;
|
||||
|
||||
private readonly ILogger<LoginServerController> _logger;
|
||||
|
||||
private readonly GameServerRegistry _registry;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loginServer">The login server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="registry">The registry.</param>
|
||||
public LoginServerController(PersistentLoginServer loginServer, ILogger<LoginServerController> logger, GameServerRegistry registry)
|
||||
{
|
||||
this._loginServer = loginServer;
|
||||
this._logger = logger;
|
||||
this._registry = registry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to login the account on the specified server.
|
||||
/// </summary>
|
||||
/// <param name="data">The login data.</param>
|
||||
/// <returns>The success.</returns>
|
||||
[HttpPost(nameof(TryLoginAsync))]
|
||||
public async Task<bool> TryLoginAsync([FromBody] LoginArguments data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._loginServer.TryLoginAsync(data.AccountName, data.ServerId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when calling TryLogin on the login server. Data: {0}", data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the account off from the specified server.
|
||||
/// </summary>
|
||||
/// <param name="data">The login data.</param>
|
||||
[HttpPost(nameof(LogOffAsync))]
|
||||
public async Task LogOffAsync([FromBody] LoginArguments data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._loginServer.LogOffAsync(data.AccountName, data.ServerId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when calling LogOff on the login server. Data: {0}", data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the game server heartbeat by updating the registry.
|
||||
/// </summary>
|
||||
/// <param name="data">The game server heartbeat arguments.</param>
|
||||
[HttpPost("GameServerHeartbeat")]
|
||||
[Topic("pubsub", "GameServerHeartbeat")]
|
||||
public Task GameServerHeartbeatAsync([FromBody] GameServerHeartbeatArguments data)
|
||||
{
|
||||
return this._registry.UpdateRegistrationAsync(data.ServerInfo.Id, data.UpTime);
|
||||
}
|
||||
}
|
||||
84
src/Dapr/LoginServer.Host/LoginStateCleanup.cs
Normal file
84
src/Dapr/LoginServer.Host/LoginStateCleanup.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
// <copyright file="LoginStateCleanup.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.LoginServer.Host;
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IHostedService"/> which cleans up logged in accounts of
|
||||
/// the game servers from the <see cref="PersistentLoginServer"/>, when a game server
|
||||
/// is getting removed from the <see cref="GameServerRegistry"/>.
|
||||
/// </summary>
|
||||
public sealed class LoginStateCleanup : IHostedService
|
||||
{
|
||||
private readonly GameServerRegistry _registry;
|
||||
private readonly PersistentLoginServer _loginServer;
|
||||
private readonly ILogger<LoginStateCleanup> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginStateCleanup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registry">The registry.</param>
|
||||
/// <param name="loginServer">The login server.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public LoginStateCleanup(GameServerRegistry registry, PersistentLoginServer loginServer, ILogger<LoginStateCleanup> logger)
|
||||
{
|
||||
this._registry = registry;
|
||||
this._loginServer = loginServer;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this._registry.NewGameServerAdded += this.OnNewGameServerAddedAsync;
|
||||
this._registry.GameServerRemoved += this.OnGameServerRemovedAsync;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this._registry.NewGameServerAdded -= this.OnNewGameServerAddedAsync;
|
||||
this._registry.GameServerRemoved -= this.OnGameServerRemovedAsync;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It's called when a new server which recently started, is added.
|
||||
/// We clean up the login states for this server as well.
|
||||
/// </summary>
|
||||
/// <param name="serverId">The id of the started server.</param>
|
||||
/// <remarks>
|
||||
/// We handle here the <see cref="GameServerRegistry.NewGameServerAdded"/> instead of the <see cref="GameServerRegistry.GameServerAdded"/>,
|
||||
/// because only then it makes sense to clean the login states of this server.
|
||||
/// Otherwise, it might be possible, that the login server itself just crashed and recognized a longer running game server. In that case, cleaning the states is not wanted.
|
||||
/// </remarks>
|
||||
private async ValueTask OnNewGameServerAddedAsync(ushort serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._loginServer.RemoveServerAsync((byte)serverId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error when adding server {0}", serverId);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnGameServerRemovedAsync(ushort serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._loginServer.RemoveServerAsync((byte)serverId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Error when removing server {0}", serverId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<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\distributed\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>b7d410a1-1a65-4459-aab9-843bdcd2ba3e</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\MUnique.OpenMU.Dapr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
187
src/Dapr/LoginServer.Host/PersistentLoginServer.cs
Normal file
187
src/Dapr/LoginServer.Host/PersistentLoginServer.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// <copyright file="PersistentLoginServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.LoginServer.Host;
|
||||
|
||||
using global::Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of a <see cref="ILoginServer"/> which persists the login state in a dapr state store.
|
||||
/// </summary>
|
||||
public sealed class PersistentLoginServer : ILoginServer
|
||||
{
|
||||
private const string StoreName = "login-state";
|
||||
|
||||
private const int OfflineServerId = -1;
|
||||
|
||||
private readonly ILogger<PersistentLoginServer> _logger;
|
||||
|
||||
private readonly DaprClient _daprClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PersistentLoginServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
public PersistentLoginServer(ILogger<PersistentLoginServer> logger, DaprClient daprClient)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._daprClient = daprClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the server.
|
||||
/// </summary>
|
||||
/// <param name="serverId">The server identifier.</param>
|
||||
public async Task RemoveServerAsync(byte serverId)
|
||||
{
|
||||
var indexName = $"serverindex-{serverId}";
|
||||
var (serverIndex, eTag) = await this._daprClient.GetStateAndETagAsync<HashSet<string>>(StoreName, indexName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (serverIndex is null || serverIndex.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var accountName in serverIndex)
|
||||
{
|
||||
await this.SetAccountOfflineAsync(accountName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
serverIndex.Clear();
|
||||
|
||||
if (!await this._daprClient.TrySaveStateAsync(StoreName, indexName, serverIndex, eTag).ConfigureAwait(false))
|
||||
{
|
||||
// try again, if it failed
|
||||
await this.RemoveServerAsync(serverId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> TryLoginAsync(string accountName, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (currentServerId, eTag) = await this._daprClient.GetStateAndETagAsync<int?>(StoreName, accountName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (currentServerId is >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(eTag))
|
||||
{
|
||||
// Never logged in, so first insert a fresh state and try again.
|
||||
// We never want to have the same account logged in twice, because that may lead to game mechanic exploits.
|
||||
await this._daprClient.SaveStateAsync<int?>(StoreName, accountName, OfflineServerId, new StateOptions { Concurrency = ConcurrencyMode.FirstWrite, Consistency = ConsistencyMode.Strong }).ConfigureAwait(false);
|
||||
return await this.TryLoginAsync(accountName, serverId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var success = await this._daprClient.TrySaveStateAsync(StoreName, accountName, serverId, eTag).ConfigureAwait(false);
|
||||
await this.AddToIndexAsync(accountName, serverId).ConfigureAwait(false);
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Couldn't get/set logged-in state for account {0}", accountName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask LogOffAsync(string accountName, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.SetAccountOfflineAsync(accountName).ConfigureAwait(false);
|
||||
await this.RemoveFromIndexAsync(accountName, serverId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when removing account {0} from server {1}", accountName, serverId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Dictionary<string, byte>> GetSnapshotAsync()
|
||||
{
|
||||
var result = new Dictionary<string, byte>();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var indexName = $"serverindex-{i}";
|
||||
|
||||
var (serverIndex, eTag) = await this._daprClient.GetStateAndETagAsync<HashSet<string>>(StoreName, indexName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (serverIndex is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var accountName in serverIndex)
|
||||
{
|
||||
result[accountName] = (byte)i;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task AddToIndexAsync(string accountName, byte serverId)
|
||||
{
|
||||
var indexName = $"serverindex-{serverId}";
|
||||
var (serverIndex, eTag) = await this._daprClient.GetStateAndETagAsync<HashSet<string>>(StoreName, indexName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (serverIndex is null)
|
||||
{
|
||||
serverIndex = new HashSet<string>();
|
||||
serverIndex.Add(accountName);
|
||||
await this._daprClient.SaveStateAsync(StoreName, indexName, serverIndex).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverIndex.Add(accountName))
|
||||
{
|
||||
if (!await this._daprClient.TrySaveStateAsync(StoreName, indexName, serverIndex, eTag).ConfigureAwait(false))
|
||||
{
|
||||
// try again, if it failed
|
||||
await this.AddToIndexAsync(accountName, serverId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveFromIndexAsync(string accountName, byte serverId)
|
||||
{
|
||||
var indexName = $"serverindex-{serverId}";
|
||||
var (serverIndex, eTag) = await this._daprClient.GetStateAndETagAsync<HashSet<string>>(StoreName, indexName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (serverIndex is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverIndex.Remove(accountName))
|
||||
{
|
||||
if (!await this._daprClient.TrySaveStateAsync(StoreName, indexName, serverIndex, eTag).ConfigureAwait(false))
|
||||
{
|
||||
// try again, if it failed
|
||||
await this.RemoveFromIndexAsync(accountName, serverId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetAccountOfflineAsync(string accountName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (currentServerId, eTag) = await this._daprClient.GetStateAndETagAsync<int?>(StoreName, accountName, ConsistencyMode.Strong).ConfigureAwait(false);
|
||||
if (currentServerId == OfflineServerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this._daprClient.TrySaveStateAsync<int?>(StoreName, accountName, OfflineServerId, eTag).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Couldn't get/set logged-out state for account {0}", accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Dapr/LoginServer.Host/Program.cs
Normal file
24
src/Dapr/LoginServer.Host/Program.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.Dapr.Common;
|
||||
using MUnique.OpenMU.LoginServer.Host;
|
||||
|
||||
var builder = DaprService.CreateBuilder("LoginServer", args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services
|
||||
.AddSingleton<PersistentLoginServer>()
|
||||
.AddHostedService<LoginStateCleanup>()
|
||||
.AddSingleton<GameServerRegistry>();
|
||||
|
||||
var metricsRegistry = new MetricsRegistry();
|
||||
|
||||
// todo: add some meaningful metrics
|
||||
builder.AddOpenTelemetryMetrics(metricsRegistry);
|
||||
|
||||
var app = builder.BuildAndConfigure();
|
||||
|
||||
app.Run();
|
||||
10
src/Dapr/LoginServer.Host/Properties/AssemblyInfo.cs
Normal file
10
src/Dapr/LoginServer.Host/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.LoginServer.Host")]
|
||||
10
src/Dapr/ServerClients/AssemblyInfo.cs
Normal file
10
src/Dapr/ServerClients/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.ServerClients")]
|
||||
16
src/Dapr/ServerClients/ChatRoomCreationArguments.cs
Normal file
16
src/Dapr/ServerClients/ChatRoomCreationArguments.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="ChatRoomCreationArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a chat room creation.
|
||||
/// </summary>
|
||||
/// <param name="AuthenticationInfo">Authentication information of the player who should get notified about the created chat room.</param>
|
||||
/// <param name="FriendName">Name of the friend player which is expected to be in the chat room.</param>
|
||||
public record ChatRoomCreationArguments(
|
||||
ChatServerAuthenticationInfo AuthenticationInfo,
|
||||
string FriendName);
|
||||
13
src/Dapr/ServerClients/ChatRoomInvitationArguments.cs
Normal file
13
src/Dapr/ServerClients/ChatRoomInvitationArguments.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// <copyright file="ChatRoomInvitationArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments for a chat room invitation.
|
||||
/// </summary>
|
||||
public record ChatRoomInvitationArguments(
|
||||
string CharacterName,
|
||||
string FriendName,
|
||||
ushort RoomNumber);
|
||||
114
src/Dapr/ServerClients/ChatServer.cs
Normal file
114
src/Dapr/ServerClients/ChatServer.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
// <copyright file="ChatServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using System.ComponentModel;
|
||||
using Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IChatServer"/> which accesses another chat server remotely over Dapr.
|
||||
/// </summary>
|
||||
public class ChatServer : IChatServer
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<ChatServer> _logger;
|
||||
private readonly string _targetAppId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ChatServer(DaprClient daprClient, ILogger<ChatServer> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
this._targetAppId = "chatServer";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConfigurationId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Chat Server";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerType Type => ServerType.ChatServer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerState ServerState => ServerState.Started;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MaximumConnections { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask StartAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ShutdownAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ChatServerAuthenticationInfo?> RegisterClientAsync(ushort roomId, string clientName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<RegisterChatClientArguments, ChatServerAuthenticationInfo>(
|
||||
this._targetAppId,
|
||||
nameof(IChatServer.RegisterClientAsync),
|
||||
new RegisterChatClientArguments(roomId, clientName))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.RegisterClientAsync));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ushort> CreateChatRoomAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<ushort>(this._targetAppId, nameof(IChatServer.CreateChatRoomAsync)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, nameof(this.CreateChatRoomAsync));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
10
src/Dapr/ServerClients/FriendOnlineStateChangedArguments.cs
Normal file
10
src/Dapr/ServerClients/FriendOnlineStateChangedArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="FriendOnlineStateChangedArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments for a friend online state change.
|
||||
/// </summary>
|
||||
public record FriendOnlineStateChangedArguments(string Player, string Friend, int ServerId);
|
||||
10
src/Dapr/ServerClients/FriendResponseArguments.cs
Normal file
10
src/Dapr/ServerClients/FriendResponseArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="FriendResponseArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments for a friend response.
|
||||
/// </summary>
|
||||
public record FriendResponseArguments(string CharacterName, string FriendName, bool Accepted);
|
||||
152
src/Dapr/ServerClients/FriendServer.cs
Normal file
152
src/Dapr/ServerClients/FriendServer.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
// <copyright file="FriendServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IFriendServer"/> which accesses another friend server remotely over Dapr.
|
||||
/// </summary>
|
||||
public class FriendServer : IFriendServer
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<FriendServer> _logger;
|
||||
private readonly string _targetAppId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FriendServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FriendServer(DaprClient daprClient, ILogger<FriendServer> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
this._targetAppId = "friendServer";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ForwardLetterAsync(LetterHeader letter)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.PublishEventAsync("pubsub", nameof(IGameServer.LetterReceivedAsync), letter).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when forwarding a letter.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FriendResponseAsync(string characterName, string friendName, bool accepted)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.FriendResponseAsync), new FriendResponseArguments(characterName, friendName, accepted)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a friend response.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName)
|
||||
{
|
||||
// no action required - the friend server listens to the common pubsub, published by EventPublisher
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask PlayerLeftGameAsync(Guid characterId, string characterName)
|
||||
{
|
||||
// no action required - the friend server listens to the common pubsub, published by EventPublisher
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask SetPlayerVisibilityStateAsync(byte serverId, Guid characterId, string characterName, bool isVisible)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.SetPlayerVisibilityStateAsync), new PlayerFriendOnlineStateArguments(characterId, characterName, serverId, isVisible)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when setting the friend visibility state.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> IsFriendAsync(string characterName, string friendName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<RequestArguments, bool>(this._targetAppId, nameof(this.IsFriendAsync), new RequestArguments(characterName, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when checking friendship.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> FriendRequestAsync(string playerName, string friendName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<RequestArguments, bool>(this._targetAppId, nameof(this.FriendRequestAsync), new RequestArguments(playerName, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a friend request.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DeleteFriendAsync(string name, string friendName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.DeleteFriendAsync), new RequestArguments(name, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when deleting a friend.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask CreateChatRoomAsync(string playerName, string friendName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.CreateChatRoomAsync), new RequestArguments(playerName, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when creating a chat room.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> InviteFriendToChatRoomAsync(string selectedCharacterName, string friendName, ushort roomNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<ChatRoomInvitationArguments, bool>(this._targetAppId, nameof(this.InviteFriendToChatRoomAsync), new ChatRoomInvitationArguments(selectedCharacterName, friendName, roomNumber)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when inviting a friend to a chat room.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/Dapr/ServerClients/GameServer.cs
Normal file
199
src/Dapr/ServerClients/GameServer.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
// <copyright file="GameServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using System.ComponentModel;
|
||||
using Dapr.Client;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using Nito.AsyncEx.Synchronous;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IGameServer"/> which accesses another game server remotely over Dapr.
|
||||
/// </summary>
|
||||
public class GameServer : IGameServer
|
||||
{
|
||||
private readonly DaprClient _client;
|
||||
private readonly string _targetAppId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="serverId">The server identifier.</param>
|
||||
public GameServer(DaprClient daprClient, int serverId)
|
||||
{
|
||||
this.Id = serverId;
|
||||
this.MaximumConnections = 1000;
|
||||
this.CurrentConnections = 0;
|
||||
|
||||
this._client = daprClient;
|
||||
this._targetAppId = $"gameServer{serverId}";
|
||||
this.Description = $"Game Server {serverId}";
|
||||
|
||||
async Task InitAsync()
|
||||
{
|
||||
this.ConfigurationId = await this._client.InvokeMethodAsync<Guid>(this._targetAppId, $"get{nameof(this.ConfigurationId)}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_ = InitAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ConfigurationId { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerType Type => ServerType.GameServer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ServerState ServerState => this._client.InvokeMethodAsync<ServerState>(this._targetAppId, nameof(this.ServerState)).WaitAndUnwrapException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MaximumConnections { get; } // TODO
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections { get; } // TODO
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask StartAsync()
|
||||
{
|
||||
await this.StartAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._client.InvokeMethodAsync(this._targetAppId, nameof(this.StartAsync), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ShutdownAsync()
|
||||
{
|
||||
await this.StopAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return this._client.InvokeMethodAsync(this._targetAppId, nameof(this.ShutdownAsync), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildChatMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.GuildChatMessageAsync), new GuildMessageArguments(guildId, sender, message)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceChatMessageAsync(uint guildId, string sender, string message)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.AllianceChatMessageAsync), new GuildMessageArguments(guildId, sender, message)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildDeletedAsync(uint guildId)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.GuildDeletedAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildPlayerKickedAsync(string playerName)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.GuildPlayerKickedAsync), playerName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AssignGuildToPlayerAsync(string characterName, GuildMemberStatus guildStatus)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.AssignGuildToPlayerAsync), new GuildMemberAssignArguments(characterName, guildStatus)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask PlayerAlreadyLoggedInAsync(byte serverId, string loginName)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.PlayerAlreadyLoggedInAsync), new PlayerLoggedInArguments(serverId, loginName)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask LetterReceivedAsync(LetterHeader letter)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.LetterReceivedAsync), letter).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeMessengerAsync(MessengerInitializationData initializationData)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.InitializeMessengerAsync), initializationData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask SendGlobalMessageAsync(string message, MessageType messageType)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.SendGlobalMessageAsync), new MessageArguments(message, messageType)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FriendRequestAsync(string requester, string receiver)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.FriendRequestAsync), new RequestArguments(requester, receiver)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FriendOnlineStateChangedAsync(string player, string friend, int serverId)
|
||||
{
|
||||
// TODO: Use PubSub!
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.FriendOnlineStateChangedAsync), new FriendOnlineStateChangedArguments(player, friend, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ChatRoomCreatedAsync(ChatServerAuthenticationInfo playerAuthenticationInfo, string friendName)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.ChatRoomCreatedAsync), new ChatRoomCreationArguments(playerAuthenticationInfo, friendName)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> DisconnectPlayerAsync(string playerName)
|
||||
{
|
||||
return await this._client.InvokeMethodAsync<string, bool>(this._targetAppId, nameof(this.DisconnectPlayerAsync), playerName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> DisconnectAccountAsync(string playerName)
|
||||
{
|
||||
return await this._client.InvokeMethodAsync<string, bool>(this._targetAppId, nameof(this.DisconnectAccountAsync), playerName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> BanPlayerAsync(string playerName)
|
||||
{
|
||||
return await this._client.InvokeMethodAsync<string, bool>(this._targetAppId, nameof(this.BanPlayerAsync), playerName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceCreatedAsync(uint masterGuildId, uint memberGuildId)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.AllianceCreatedAsync), (masterGuildId, memberGuildId)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask AllianceDisbandedAsync(uint masterGuildId, uint memberGuildId)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.AllianceDisbandedAsync), (masterGuildId, memberGuildId)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask GuildHostilityChangedAsync(uint guildIdA, IReadOnlyList<uint> allianceGuildIdsA, uint guildIdB, IReadOnlyList<uint> allianceGuildIdsB, bool created)
|
||||
{
|
||||
await this._client.InvokeMethodAsync(this._targetAppId, nameof(this.GuildHostilityChangedAsync), (guildIdA, allianceGuildIdsA, guildIdB, allianceGuildIdsB, created)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
18
src/Dapr/ServerClients/GameServerHeartbeatArguments.cs
Normal file
18
src/Dapr/ServerClients/GameServerHeartbeatArguments.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// <copyright file="GameServerHeartbeatArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a game server heartbeat.
|
||||
/// </summary>
|
||||
public record GameServerHeartbeatArguments(ServerInfo ServerInfo, string PublicEndPoint, TimeSpan UpTime)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the up-time of the server.
|
||||
/// </summary>
|
||||
public TimeSpan UpTime { get; set; } = UpTime;
|
||||
}
|
||||
10
src/Dapr/ServerClients/GuildCreationArguments.cs
Normal file
10
src/Dapr/ServerClients/GuildCreationArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="GuildCreationArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild creation.
|
||||
/// </summary>
|
||||
public record GuildCreationArguments(string Name, string MasterName, Guid MasterId, byte[] Logo, byte ServerId);
|
||||
10
src/Dapr/ServerClients/GuildMemberArguments.cs
Normal file
10
src/Dapr/ServerClients/GuildMemberArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="GuildMemberArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild member action.
|
||||
/// </summary>
|
||||
public record GuildMemberArguments(uint GuildId, string PlayerName);
|
||||
12
src/Dapr/ServerClients/GuildMemberAssignArguments.cs
Normal file
12
src/Dapr/ServerClients/GuildMemberAssignArguments.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="GuildMemberAssignArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild member assignment.
|
||||
/// </summary>
|
||||
public record GuildMemberAssignArguments(string CharacterName, GuildMemberStatus MemberStatus);
|
||||
12
src/Dapr/ServerClients/GuildMemberCreationArguments.cs
Normal file
12
src/Dapr/ServerClients/GuildMemberCreationArguments.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="GuildMemberCreationArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild member creation.
|
||||
/// </summary>
|
||||
public record GuildMemberCreationArguments(uint GuildId, Guid CharacterId, string CharacterName, GuildPosition Role, byte ServerId);
|
||||
12
src/Dapr/ServerClients/GuildMemberRoleChangeArguments.cs
Normal file
12
src/Dapr/ServerClients/GuildMemberRoleChangeArguments.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="GuildMemberRoleChangeArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild member role change.
|
||||
/// </summary>
|
||||
public record GuildMemberRoleChangeArguments(uint GuildId, Guid CharacterId, GuildPosition NewRole);
|
||||
10
src/Dapr/ServerClients/GuildMessageArguments.cs
Normal file
10
src/Dapr/ServerClients/GuildMessageArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="GuildMessageArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a guild chat message.
|
||||
/// </summary>
|
||||
public record GuildMessageArguments(uint GuildId, string Sender, string Message);
|
||||
10
src/Dapr/ServerClients/GuildPlayerLeftGameArguments.cs
Normal file
10
src/Dapr/ServerClients/GuildPlayerLeftGameArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="GuildPlayerLeftGameArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a notification about a game-leaving guild member.
|
||||
/// </summary>
|
||||
public record GuildPlayerLeftGameArguments(uint GuildId, Guid GuildMemberId, byte ServerId);
|
||||
266
src/Dapr/ServerClients/GuildServer.cs
Normal file
266
src/Dapr/ServerClients/GuildServer.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
// <copyright file="GuildServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IGuildServer"/> which accesses the guild server remotely over Dapr.
|
||||
/// </summary>
|
||||
public class GuildServer : IGuildServer
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<GuildServer> _logger;
|
||||
private readonly string _targetAppId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuildServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public GuildServer(DaprClient daprClient, ILogger<GuildServer> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
this._targetAppId = "guildServer";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> GuildExistsAsync(string guildName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<string, bool>(this._targetAppId, nameof(this.GuildExistsAsync), guildName).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when checking a guild existence.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Guild?> GetGuildAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<uint, Guild?>(this._targetAppId, nameof(this.GetGuildAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a guild retrieval message.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<uint> GetGuildIdByNameAsync(string guildName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<string, uint>(this._targetAppId, nameof(this.GetGuildIdByNameAsync), guildName).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when getting the id by guild name.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> CreateGuildAsync(string name, string masterName, Guid masterId, byte[] logo, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<GuildCreationArguments, bool>(this._targetAppId, nameof(this.CreateGuildAsync), new GuildCreationArguments(name, masterName, masterId, logo, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a guild creation message.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask CreateGuildMemberAsync(uint guildId, Guid characterId, string characterName, GuildPosition role, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.CreateGuildMemberAsync), new GuildMemberCreationArguments(guildId, characterId, characterName, role, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a guild member creation.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ChangeGuildMemberPositionAsync(uint guildId, Guid characterId, GuildPosition role)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.ChangeGuildMemberPositionAsync), new GuildMemberRoleChangeArguments(guildId, characterId, role)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a guild member position change.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask PlayerEnteredGameAsync(Guid characterId, string characterName, byte serverId)
|
||||
{
|
||||
// Handled by EventPublisher, through pub/sub component.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask GuildMemberLeftGameAsync(uint guildId, Guid guildMemberId, byte serverId)
|
||||
{
|
||||
// Handled by EventPublisher, through pub/sub component.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IImmutableList<GuildListEntry>> GetGuildListAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<uint, IImmutableList<GuildListEntry>>(this._targetAppId, nameof(this.GetGuildListAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when getting a guild list.");
|
||||
return ImmutableList<GuildListEntry>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask KickMemberAsync(uint guildId, string playerName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.KickMemberAsync), new GuildMemberArguments(guildId, playerName)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when kicking a guild member.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<GuildPosition> GetGuildPositionAsync(Guid characterId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<Guid, GuildPosition>(this._targetAppId, nameof(this.GetGuildPositionAsync), characterId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when retrieving a guild position.");
|
||||
return GuildPosition.Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask IncreaseGuildScoreAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.IncreaseGuildScoreAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when sending a guild score increase.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AllianceCreationResult> CreateAllianceAsync(uint masterGuildId, uint targetGuildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<(uint, uint), AllianceCreationResult>(this._targetAppId, nameof(this.CreateAllianceAsync), (masterGuildId, targetGuildId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when creating an alliance.");
|
||||
return AllianceCreationResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> RemoveAllianceAsync(uint targetGuildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<uint, bool>(this._targetAppId, nameof(this.RemoveAllianceAsync), targetGuildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when removing a guild from alliance.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IImmutableList<AllianceGuildEntry>> GetAllianceGuildsAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<uint, IImmutableList<AllianceGuildEntry>>(this._targetAppId, nameof(this.GetAllianceGuildsAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when getting alliance guilds.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> IsAllianceMasterAsync(uint guildId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<uint, bool>(this._targetAppId, nameof(this.IsAllianceMasterAsync), guildId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when checking alliance master.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> SetHostilityAsync(uint guildIdA, uint guildIdB, bool create)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<(uint, uint, bool), bool>(this._targetAppId, nameof(this.SetHostilityAsync), (guildIdA, guildIdB, create)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when setting hostility.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<GuildRelationship> GetGuildRelationshipAsync(uint guild1, uint guild2)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<(uint, uint), GuildRelationship>(this._targetAppId, nameof(this.GetGuildRelationshipAsync), (guild1, guild2)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when getting guild relationship.");
|
||||
return GuildRelationship.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Dapr/ServerClients/LoginArguments.cs
Normal file
10
src/Dapr/ServerClients/LoginArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="LoginArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for an account login.
|
||||
/// </summary>
|
||||
public record LoginArguments(string AccountName, byte ServerId);
|
||||
73
src/Dapr/ServerClients/LoginServer.cs
Normal file
73
src/Dapr/ServerClients/LoginServer.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// <copyright file="LoginServer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using Dapr.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="ILoginServer"/> which accesses the login server remotely over Dapr.
|
||||
/// </summary>
|
||||
public class LoginServer : ILoginServer
|
||||
{
|
||||
private readonly DaprClient _daprClient;
|
||||
private readonly ILogger<LoginServer> _logger;
|
||||
private readonly string _targetAppId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginServer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="daprClient">The dapr client.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public LoginServer(DaprClient daprClient, ILogger<LoginServer> logger)
|
||||
{
|
||||
this._daprClient = daprClient;
|
||||
this._logger = logger;
|
||||
this._targetAppId = "loginServer";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> TryLoginAsync(string accountName, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<LoginArguments, bool>(this._targetAppId, nameof(this.TryLoginAsync), new LoginArguments(accountName, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when trying to call TryLogin on the login server.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask LogOffAsync(string accountName, byte serverId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.LogOffAsync), new LoginArguments(accountName, serverId)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when trying to call LogOff on the login server.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Dictionary<string, byte>> GetSnapshotAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._daprClient.InvokeMethodAsync<Dictionary<string, byte>>(this._targetAppId, nameof(this.GetSnapshotAsync)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "Unexpected error when trying to call GetSnapshotAsync on the login server.");
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
24
src/Dapr/ServerClients/MUnique.OpenMU.ServerClients.csproj
Normal file
24
src/Dapr/ServerClients/MUnique.OpenMU.ServerClients.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile></DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Client" />
|
||||
<PackageReference Include="Nito.AsyncEx" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DataModel\MUnique.OpenMU.DataModel.csproj" />
|
||||
<ProjectReference Include="..\..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
src/Dapr/ServerClients/MessageArguments.cs
Normal file
12
src/Dapr/ServerClients/MessageArguments.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="MessageArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a game message.
|
||||
/// </summary>
|
||||
public record MessageArguments(string Message, MessageType Type);
|
||||
10
src/Dapr/ServerClients/PlayerFriendOnlineStateArguments.cs
Normal file
10
src/Dapr/ServerClients/PlayerFriendOnlineStateArguments.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// <copyright file="PlayerFriendOnlineStateArguments.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ServerClients;
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for a friend online state change.
|
||||
/// </summary>
|
||||
public record PlayerFriendOnlineStateArguments(Guid CharacterId, string CharacterName, byte ServerId, bool IsVisible);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user