baseline: OpenMU upstream b5a0961 (fresh source)

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

View File

@@ -0,0 +1,69 @@
// <copyright file="FriendNotifierToGameServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.FriendServer;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// An implementation of a <see cref="IFriendNotifier"/> which forwards the calls to the available game server instances.
/// </summary>
public class FriendNotifierToGameServer : IFriendNotifier
{
private readonly IDictionary<int, IGameServer> _gameServers;
/// <summary>
/// Initializes a new instance of the <see cref="FriendNotifierToGameServer"/> class.
/// </summary>
/// <param name="gameServers">The game servers.</param>
public FriendNotifierToGameServer(IDictionary<int, IGameServer> gameServers)
{
this._gameServers = gameServers;
}
/// <inheritdoc />
public async ValueTask FriendRequestAsync(string requester, string receiver, int serverId)
{
if (this._gameServers.TryGetValue(serverId, out var gameServer))
{
await gameServer.FriendRequestAsync(requester, receiver).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async ValueTask LetterReceivedAsync(LetterHeader letter)
{
foreach (var gameServer in this._gameServers.Values)
{
await gameServer.LetterReceivedAsync(letter).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async ValueTask FriendOnlineStateChangedAsync(int playerServerId, string player, string friend, int friendServerId)
{
if (this._gameServers.TryGetValue(playerServerId, out var gameServer))
{
await gameServer.FriendOnlineStateChangedAsync(player, friend, friendServerId).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async ValueTask ChatRoomCreatedAsync(int serverId, ChatServerAuthenticationInfo playerAuthenticationInfo, string friendName)
{
if (this._gameServers.TryGetValue(serverId, out var gameServer))
{
await gameServer.ChatRoomCreatedAsync(playerAuthenticationInfo, friendName).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async ValueTask InitializeMessengerAsync(int serverId, MessengerInitializationData initializationData)
{
if (this._gameServers.TryGetValue(serverId, out var gameServer))
{
await gameServer.InitializeMessengerAsync(initializationData).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,329 @@
// <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.FriendServer;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence;
/// <summary>
/// The friend server which manages the friend list with chat and letter system.
/// </summary>
public class FriendServer : IFriendServer
{
private readonly IPersistenceContextProvider _persistenceContextProvider;
private readonly ILogger<FriendServer> _logger;
private readonly IFriendNotifier _friendNotifier;
private readonly IChatServer _chatServer;
/// <summary>
/// Initializes a new instance of the <see cref="FriendServer" /> class.
/// </summary>
/// <param name="friendNotifier">The friend notifier.</param>
/// <param name="chatServer">The chat server.</param>
/// <param name="persistenceContextProvider">The persistence context provider.</param>
/// <param name="logger">The logger.</param>
public FriendServer(IFriendNotifier friendNotifier, IChatServer chatServer, IPersistenceContextProvider persistenceContextProvider, ILogger<FriendServer> logger)
{
this._friendNotifier = friendNotifier;
this._chatServer = chatServer;
this._persistenceContextProvider = persistenceContextProvider;
this._logger = logger;
this.OnlineFriends = new Dictionary<string, OnlineFriend>();
}
/// <summary>
/// Gets the server id which represents being offline.
/// </summary>
public static int OfflineServerId { get; } = (int)SpecialServerId.Offline;
/// <summary>
/// Gets the server id which represents being invisible (=offline to other players).
/// </summary>
public static int InvisibleServerId { get; } = (int)SpecialServerId.Invisible;
/// <summary>
/// Gets the online friends dictionary. The key is the name of the character of the corresponding OnlineFriend object.
/// </summary>
protected IDictionary<string, OnlineFriend> OnlineFriends { get; }
/// <inheritdoc/>
public ValueTask ForwardLetterAsync(LetterHeader letter)
{
return this._friendNotifier.LetterReceivedAsync(letter);
}
/// <inheritdoc/>
public async ValueTask<bool> FriendRequestAsync(string playerName, string friendName)
{
var saveSuccess = true;
bool friendIsNew;
using (var context = this._persistenceContextProvider.CreateNewFriendServerContext())
{
var friend = await context.GetFriendByNamesAsync(playerName, friendName).ConfigureAwait(false);
friendIsNew = friend is null;
if (friendIsNew)
{
friend = await context.CreateNewFriendAsync(playerName, friendName).ConfigureAwait(false);
friend.Accepted = false;
friend.RequestOpen = true;
saveSuccess = await context.SaveChangesAsync().ConfigureAwait(false);
}
}
if (saveSuccess && this.OnlineFriends.TryGetValue(friendName, out var onlineFriend))
{
// Friend is online, so we directly send him a request.
await this._friendNotifier.FriendRequestAsync(playerName, friendName, onlineFriend.ServerId).ConfigureAwait(false);
}
return friendIsNew && saveSuccess;
}
/// <inheritdoc/>
public async ValueTask<bool> IsFriendAsync(string characterName, string friendName)
{
if (this.OnlineFriends.TryGetValue(characterName, out var player)
&& this.OnlineFriends.TryGetValue(friendName, out var friend))
{
return player.HasSubscriber(friend);
}
using var context = this._persistenceContextProvider.CreateNewFriendServerContext();
var friendEntry = await context.GetFriendByNamesAsync(characterName, friendName).ConfigureAwait(false);
return friendEntry?.Accepted == true;
}
/// <inheritdoc/>
public async ValueTask DeleteFriendAsync(string playerName, string friendName)
{
if (this.OnlineFriends.TryGetValue(playerName, out var player) && this.OnlineFriends.TryGetValue(friendName, out var friend))
{
player.RemoveSubscriber(friend);
friend.RemoveSubscriber(player);
}
using var context = this._persistenceContextProvider.CreateNewFriendServerContext();
await context.DeleteAsync(playerName, friendName).ConfigureAwait(false);
await context.DeleteAsync(friendName, playerName).ConfigureAwait(false);
await context.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask FriendResponseAsync(string characterName, string friendName, bool accepted)
{
using var context = this._persistenceContextProvider.CreateNewFriendServerContext();
#pragma warning disable S2234 // The parameters are passed correctly
var requester = await context.GetFriendByNamesAsync(friendName, characterName).ConfigureAwait(false);
#pragma warning restore S2234
if (requester is null)
{
return;
}
requester.RequestOpen = false;
requester.Accepted = accepted;
if (accepted)
{
var responder = await context.GetFriendByNamesAsync(characterName, friendName).ConfigureAwait(false) ?? await context.CreateNewFriendAsync(characterName, friendName).ConfigureAwait(false);
responder.RequestOpen = false;
responder.Accepted = true;
await context.SaveChangesAsync().ConfigureAwait(false);
this.AddSubscriptions(friendName, characterName);
}
else
{
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async ValueTask CreateChatRoomAsync(string playerName, string friendName)
{
if (!this.OnlineFriends.TryGetValue(playerName, out var player))
{
return;
}
if (!this.OnlineFriends.TryGetValue(friendName, out var friend))
{
return;
}
if (!friend.HasSubscriber(player))
{
return;
}
// TODO: Remove direct dependency to the chat server.
// Instead of calling the chat server directly here, we could publish a request
// to create the chat room to an pub/sub-system. An available chat server could then
// process the request and notify the corresponding game servers.
var roomId = await this._chatServer.CreateChatRoomAsync().ConfigureAwait(false);
if (await this._chatServer.RegisterClientAsync(roomId, playerName).ConfigureAwait(false) is { } authenticationInfoPlayer)
{
await this._friendNotifier.ChatRoomCreatedAsync(player.ServerId, authenticationInfoPlayer, friendName).ConfigureAwait(false);
}
if (await this._chatServer.RegisterClientAsync(roomId, friendName).ConfigureAwait(false) is { } authenticationInfoFriend)
{
await this._friendNotifier.ChatRoomCreatedAsync(friend.ServerId, authenticationInfoFriend, playerName).ConfigureAwait(false);
}
}
/// <inheritdoc />
public async ValueTask<bool> InviteFriendToChatRoomAsync(string playerName, string friendName, ushort roomId)
{
if (!this.OnlineFriends.TryGetValue(playerName, out var player))
{
return false;
}
if (!this.OnlineFriends.TryGetValue(friendName, out var friend))
{
return false;
}
if (!friend.HasSubscriber(player))
{
return false;
}
if (friend.IsInvisibleOrOffline)
{
return false;
}
var authenticationInfoFriend = await this._chatServer.RegisterClientAsync(roomId, friendName).ConfigureAwait(false);
if (authenticationInfoFriend is not null)
{
await this._friendNotifier.ChatRoomCreatedAsync(friend.ServerId, authenticationInfoFriend, playerName).ConfigureAwait(false);
return true;
}
return false;
}
/// <remarks>Note, that the ServerId is not filled by this implementation. The player will receive it separately when the subscription is created.</remarks>
/// <inheritdoc/>
public async ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName)
{
using var context = this._persistenceContextProvider.CreateNewFriendServerContext();
var friends = await context.GetFriendNamesAsync(characterId).ConfigureAwait(false);
var requesters = await context.GetOpenFriendRequesterNamesAsync(characterId).ConfigureAwait(false);
var initializationData = new MessengerInitializationData(
characterName,
friends.ToImmutableList(),
requesters.ToImmutableList());
try
{
await this._friendNotifier.InitializeMessengerAsync(serverId, initializationData).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Unexpected error when notifying about messenger initialization. Server id {serverId}, CharacterId {characterId}, CharacterName '{characterName}'.", serverId, characterId, characterName);
}
try
{
await this.SetOnlineStateAsync(characterId, characterName, serverId, context).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Unexpected error when changing the online state. Server id {serverId}, CharacterId {characterId}, CharacterName '{characterName}'.", serverId, characterId, characterName);
}
}
/// <inheritdoc/>
public async ValueTask PlayerLeftGameAsync(Guid characterId, string characterName)
{
await this.SetOnlineStateAsync(characterId, characterName, OfflineServerId, null).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask SetPlayerVisibilityStateAsync(byte serverId, Guid characterId, string characterName, bool isVisible)
{
await this.SetOnlineStateAsync(characterId, characterName, isVisible ? serverId : InvisibleServerId, null).ConfigureAwait(false);
}
private async ValueTask SetOnlineStateAsync(Guid characterId, string characterName, int serverId, IFriendServerContext? usedContext)
{
if (!this.OnlineFriends.TryGetValue(characterName, out var observer))
{
if (serverId == InvisibleServerId || serverId == OfflineServerId)
{
return;
}
observer = new OnlineFriend(this._friendNotifier, characterName)
{
ServerId = serverId,
};
this.OnlineFriends.Add(characterName, observer);
IFriendServerContext? newContext = null;
var context = usedContext ?? (newContext = this._persistenceContextProvider.CreateNewFriendServerContext());
try
{
var friends = await context.GetFriendsAsync(characterId).ConfigureAwait(false);
this.AddSubscriptions(friends);
}
finally
{
newContext?.Dispose();
}
}
observer.ChangeServer(serverId == InvisibleServerId ? OfflineServerId : serverId);
if (serverId == OfflineServerId)
{
this.OnlineFriends.Remove(observer.PlayerName);
observer.OnCompleted();
}
}
private void AddSubscriptions(IEnumerable<FriendViewItem> friends)
{
foreach (var friendConnection in friends)
{
if (!friendConnection.Accepted || friendConnection.RequestOpen)
{
continue;
}
if (this.OnlineFriends.TryGetValue(friendConnection.FriendName, out var onlineFriend)
&& this.OnlineFriends.TryGetValue(friendConnection.CharacterName, out var characterFriend))
{
characterFriend.AddSubscription(onlineFriend.Subscribe(characterFriend));
characterFriend.OnNext(onlineFriend);
onlineFriend.AddSubscription(characterFriend.Subscribe(onlineFriend));
onlineFriend.OnNext(characterFriend);
}
}
}
private void AddSubscriptions(string requester, string responder)
{
if (!this.OnlineFriends.TryGetValue(responder, out var responderFriend))
{
return;
}
if (this.OnlineFriends.TryGetValue(requester, out var requesterFriend))
{
responderFriend.AddSubscription(requesterFriend.Subscribe(responderFriend));
requesterFriend.AddSubscription(responderFriend.Subscribe(requesterFriend));
// inform both about their online server ids:
responderFriend.OnNext(requesterFriend);
requesterFriend.OnNext(responderFriend);
}
}
}

View File

@@ -0,0 +1,51 @@
// <copyright file="IFriendNotifier.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.FriendServer;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface for a class which notifies a player about changes in the friend system.
/// </summary>
public interface IFriendNotifier
{
/// <summary>
/// Notifies the server that a player made a friend request to another player, which is online on this server.
/// </summary>
/// <param name="requester">The requester.</param>
/// <param name="receiver">The receiver.</param>
/// <param name="serverId">The server identifier of the receiver.</param>
ValueTask FriendRequestAsync(string requester, string receiver, int serverId);
/// <summary>
/// Notifies the game server that a letter got received for an online player.
/// </summary>
/// <param name="letter">The letter header.</param>
ValueTask LetterReceivedAsync(LetterHeader letter);
/// <summary>
/// Notifies the game server that a friend online state changed.
/// </summary>
/// <param name="playerServerId">The player server identifier.</param>
/// <param name="player">The player who is playing on the server, and needs to get notified.</param>
/// <param name="friend">The friend whose state changed.</param>
/// <param name="friendServerId">The friend server identifier.</param>
ValueTask FriendOnlineStateChangedAsync(int playerServerId, string player, string friend, int friendServerId);
/// <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="serverId">The server identifier.</param>
/// <param name="playerAuthenticationInfo">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>
ValueTask ChatRoomCreatedAsync(int serverId, ChatServerAuthenticationInfo playerAuthenticationInfo, string friendName);
/// <summary>
/// Initializes the messenger for a connected player.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="initializationData">The initialization data.</param>
ValueTask InitializeMessengerAsync(int serverId, MessengerInitializationData initializationData);
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\bin\Debug\</OutputPath>
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.FriendServer.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Release\</OutputPath>
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.FriendServer.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ChatServer\MUnique.OpenMU.ChatServer.csproj" />
<ProjectReference Include="..\DataModel\MUnique.OpenMU.DataModel.csproj" />
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
<ProjectReference Include="..\Persistence\MUnique.OpenMU.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,223 @@
// <copyright file="OnlineFriend.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.FriendServer;
using System.Threading;
using Nito.AsyncEx.Synchronous;
/// <summary>
/// Represents an online friend who can observe his other friends, and be subscribed by other friends.
/// </summary>
public sealed class OnlineFriend : IObservable<OnlineFriend>, IObserver<OnlineFriend>, IDisposable
{
private readonly IFriendNotifier _gameServer;
/// <summary>
/// The subscribers which want to know state changes of this instance.
/// </summary>
private readonly ISet<IObserver<OnlineFriend>> _subscribers;
/// <summary>
/// The synchronize object which is used for locks.
/// </summary>
private readonly ReaderWriterLockSlim _readerWriterLock = new();
/// <summary>
/// This are all subscriptions, to which this player subscribed.
/// </summary>
private readonly List<Unsubscriber> _subscriptions;
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="OnlineFriend" /> class.
/// </summary>
/// <param name="gameServer">The game server.</param>
/// <param name="playerName">Name of the player.</param>
public OnlineFriend(IFriendNotifier gameServer, string playerName)
{
this._gameServer = gameServer;
this.PlayerName = playerName;
this._subscribers = new HashSet<IObserver<OnlineFriend>>();
this._subscriptions = new List<Unsubscriber>();
}
/// <summary>
/// Gets the name of the player.
/// </summary>
public string PlayerName { get; }
/// <summary>
/// Gets or sets the server identifier.
/// </summary>
public int ServerId { get; set; }
/// <summary>
/// Gets a value indicating whether this friend is invisible or offline.
/// </summary>
public bool IsInvisibleOrOffline => this.ServerId == FriendServer.InvisibleServerId || this.ServerId == FriendServer.OfflineServerId;
/// <summary>
/// Gets the subscribers which want to know when the online state of the friend changes.
/// </summary>
public ISet<IObserver<OnlineFriend>> Subscribers => this._subscribers;
/// <summary>
/// Subscribes the specified observer who want to know when the online state of the friend changes.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>The <see cref="IDisposable"/> to unsubscribe.</returns>
public IDisposable Subscribe(IObserver<OnlineFriend> observer)
{
this._readerWriterLock.EnterWriteLock();
try
{
this._subscribers.Add(observer);
}
finally
{
this._readerWriterLock.ExitWriteLock();
}
return new Unsubscriber(() => this.Unsubscribe(observer));
}
/// <summary>
/// Remembers the subscription of an observation, to be able to unsubscribe later.
/// </summary>
/// <param name="subscription">The subscription.</param>
public void AddSubscription(IDisposable subscription)
{
if (subscription is Unsubscriber item)
{
this._subscriptions.Add(item);
}
}
/// <summary>
/// Will be called, when this player is getting removed from the player list.
/// </summary>
public void OnCompleted()
{
this._subscriptions.ForEach(subscription => subscription.Dispose());
this.Dispose();
}
/// <inheritdoc/>
public void OnError(Exception error)
{
// Method intentionally left empty.
}
/// <summary>
/// Will be called when a subscribed onlinefriend changes his state.
/// </summary>
/// <param name="value">online friend with changed state.</param>
public void OnNext(OnlineFriend value)
{
// Send update to this player
this._gameServer
.FriendOnlineStateChangedAsync(this.ServerId, this.PlayerName, value.PlayerName, value.ServerId == FriendServer.InvisibleServerId ? FriendServer.OfflineServerId : value.ServerId)
.AsTask()
.WaitWithoutException();
}
/// <summary>
/// This player is changing its server. All subscribers will be informed.
/// </summary>
/// <param name="serverId">The new server id of this instance.</param>
public void ChangeServer(int serverId)
{
this.ServerId = serverId;
// Notify every subscriber
this._readerWriterLock.EnterReadLock();
try
{
foreach (var friend in this._subscribers)
{
friend.OnNext(this);
}
}
finally
{
this._readerWriterLock.ExitReadLock();
}
}
/// <summary>
/// Determines whether the specified player is a subscriber of this player.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>True, if the specified player is a subscriber of this player.</returns>
public bool HasSubscriber(OnlineFriend player)
{
this._readerWriterLock.EnterReadLock();
try
{
return this._subscribers.Contains(player);
}
finally
{
this._readerWriterLock.ExitReadLock();
}
}
/// <summary>
/// Removes the subscriber (friendship ended) and sends him the new state that this instance is offline now.
/// </summary>
/// <param name="friend">The player who should no longer get state updates.</param>
public void RemoveSubscriber(OnlineFriend friend)
{
this.Unsubscribe(friend);
this._gameServer
.FriendOnlineStateChangedAsync(friend.ServerId, friend.PlayerName, this.PlayerName, FriendServer.OfflineServerId)
.AsTask()
.WaitWithoutException();
}
/// <inheritdoc/>
public void Dispose()
{
if (!this._isDisposed)
{
this._readerWriterLock.Dispose();
this._isDisposed = true;
}
}
private void Unsubscribe(IObserver<OnlineFriend> observer)
{
if (this._isDisposed)
{
return;
}
this._readerWriterLock.EnterWriteLock();
try
{
this._subscribers.Remove(observer);
}
finally
{
this._readerWriterLock.ExitWriteLock();
}
}
private sealed class Unsubscriber : IDisposable
{
private readonly Action _unsubscribeAction;
public Unsubscriber(Action unsubscribeAction)
{
this._unsubscribeAction = unsubscribeAction;
}
public void Dispose()
{
this._unsubscribeAction();
}
}
}

View File

@@ -0,0 +1,13 @@
// <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;
using System.Runtime.CompilerServices;
// 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")]
[assembly: InternalsVisibleTo("OpenMU.Tests")]