baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
69
src/FriendServer/FriendNotifierToGameServer.cs
Normal file
69
src/FriendServer/FriendNotifierToGameServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
329
src/FriendServer/FriendServer.cs
Normal file
329
src/FriendServer/FriendServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/FriendServer/IFriendNotifier.cs
Normal file
51
src/FriendServer/IFriendNotifier.cs
Normal 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);
|
||||
}
|
||||
27
src/FriendServer/MUnique.OpenMU.FriendServer.csproj
Normal file
27
src/FriendServer/MUnique.OpenMU.FriendServer.csproj
Normal 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>
|
||||
223
src/FriendServer/OnlineFriend.cs
Normal file
223
src/FriendServer/OnlineFriend.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/FriendServer/Properties/AssemblyInfo.cs
Normal file
13
src/FriendServer/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
Reference in New Issue
Block a user