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,14 @@
// <copyright file="AllianceGuildEntry.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// An entry of the alliance guild list.
/// </summary>
/// <param name="Id">The unique identifier of the guild.</param>
/// <param name="GuildName">The name of the guild.</param>
/// <param name="MemberCount">The number of members in the guild.</param>
/// <param name="Logo">The logo of the guild.</param>
public record AllianceGuildEntry(uint Id, string GuildName, int MemberCount, Memory<byte> Logo);

View File

@@ -0,0 +1,61 @@
// <copyright file="ChatServerAuthenticationInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Authentication info of a chat server client.
/// This is created by the chatserver through <see cref="IChatServer.RegisterClientAsync"/>
/// and the chat client needs to provide all of this information to authenticate itself.
/// </summary>
public class ChatServerAuthenticationInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ChatServerAuthenticationInfo" /> class.
/// </summary>
/// <param name="index">The index of the client in the room.</param>
/// <param name="roomId">The room identifier.</param>
/// <param name="clientName">Name of the client.</param>
/// <param name="hostAddress">The address of the chat server which hosts the chat room.</param>
/// <param name="authenticationToken">The authentication token.</param>
public ChatServerAuthenticationInfo(byte index, ushort roomId, string clientName, string hostAddress, string authenticationToken)
{
this.Index = index;
this.RoomId = roomId;
this.ClientName = clientName;
this.AuthenticationToken = authenticationToken;
this.HostAddress = hostAddress;
this.AuthenticationRequiredUntil = DateTime.Now.AddSeconds(30);
}
/// <summary>
/// Gets the index of the client in the room.
/// </summary>
public byte Index { get; }
/// <summary>
/// Gets the room identifier of the room which is reserved for the client.
/// </summary>
public ushort RoomId { get; }
/// <summary>
/// Gets the name of the client, usually character name.
/// </summary>
public string ClientName { get; }
/// <summary>
/// Gets the authentication token. It's like a random passwort which the client has to provide to enter the chat room.
/// </summary>
public string AuthenticationToken { get; }
/// <summary>
/// Gets the (IP-)address of the chat server which hosts the chat room.
/// </summary>
public string HostAddress { get; }
/// <summary>
/// Gets the datetime until a authentication of at least two clients is required. If this time passed by, the chat room will be closed automatically.
/// </summary>
public DateTime AuthenticationRequiredUntil { get; }
}

36
src/Interfaces/Friend.cs Normal file
View File

@@ -0,0 +1,36 @@
// <copyright file="Friend.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The friend class used by the <see cref="IFriendServer"/>.
/// </summary>
public class Friend
{
/// <summary>
/// Gets or sets the identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the id of the character.
/// </summary>
public Guid CharacterId { get; set; }
/// <summary>
/// Gets or sets the id of the friend character.
/// </summary>
public Guid FriendId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the friend request got accepted.
/// </summary>
public bool Accepted { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this request is open.
/// </summary>
public bool RequestOpen { get; set; }
}

View File

@@ -0,0 +1,32 @@
// <copyright file="FriendViewItem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// A friend view item, which includes the character names.
/// </summary>
public class FriendViewItem : Friend
{
/// <summary>
/// Initializes a new instance of the <see cref="FriendViewItem"/> class.
/// </summary>
/// <param name="characterName">Name of the character.</param>
/// <param name="friendName">Name of the friend.</param>
public FriendViewItem(string characterName, string friendName)
{
this.CharacterName = characterName;
this.FriendName = friendName;
}
/// <summary>
/// Gets or sets the name of the character.
/// </summary>
public string CharacterName { get; set; }
/// <summary>
/// Gets or sets the name of the friend.
/// </summary>
public string FriendName { get; set; }
}

55
src/Interfaces/Guild.cs Normal file
View File

@@ -0,0 +1,55 @@
// <copyright file="Guild.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// A guild is a group of players who like to play together.
/// </summary>
/// <remarks>
/// You may wonder where the Id is. The original server and this <see cref="IGuildServer"/> uses an integer id for guilds, too.
/// I decided to manage these keys in memory and on demand, they are not persistent.
/// I could've used an integer Id as primary key in the persistent Guild class, but don't do it by purpose:
/// We would lose the ability to easily merge two databases (realms), if we do that. Even if we use a separate integer id column in the database,
/// it needs to be re-assigned after a merge.
/// </remarks>
public class Guild
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the logo.
/// </summary>
/// <remarks>
/// It's like a 16 color 8x8 pixel bitmap, therefore has a size of 32 bytes.
/// </remarks>
public byte[]? Logo { get; set; }
/// <summary>
/// Gets or sets the score.
/// </summary>
public int Score { get; set; }
/// <summary>
/// Gets or sets the guild notice which can be set by the guild master.
/// </summary>
/// <remarks>Visible in green color after a character entered the game.</remarks>
public string? Notice { get; set; }
/// <summary>
/// Gets or sets the hostile guild. Members of a hostile guild can be killed without consequences.
/// </summary>
public virtual Guild? Hostility { get; set; }
/// <summary>
/// Gets or sets the parent alliance guild.
/// </summary>
/// <value>
/// The alliance guild.
/// </value>
public virtual Guild? AllianceGuild { get; set; }
}

View File

@@ -0,0 +1,38 @@
// <copyright file="GuildMemberStatus.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The guild member status of a guild member.
/// </summary>
public class GuildMemberStatus
{
/// <summary>
/// Initializes a new instance of the <see cref="GuildMemberStatus"/> class.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="position">The position.</param>
public GuildMemberStatus(uint guildId, GuildPosition position)
{
this.GuildId = guildId;
this.Position = position;
}
/// <summary>
/// Gets the guild identifier.
/// </summary>
/// <value>
/// The guild identifier.
/// </value>
public uint GuildId { get; }
/// <summary>
/// Gets the position.
/// </summary>
/// <value>
/// The position.
/// </value>
public GuildPosition Position { get; }
}

View File

@@ -0,0 +1,37 @@
// <copyright file="GuildPosition.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The position of a character in a guild.
/// Some of them have special skills in the castle siege event.
/// </summary>
public enum GuildPosition : byte
{
/// <summary>
/// Undefined position, not a guild member.
/// </summary>
Undefined,
/// <summary>
/// A normal guild member.
/// </summary>
NormalMember,
/// <summary>
/// The guild master.
/// </summary>
GuildMaster,
/// <summary>
/// The battle master.
/// </summary>
BattleMaster,
/// <summary>
/// The assistant guild master (needed for castle siege NPC management).
/// </summary>
AssistantMaster,
}

View File

@@ -0,0 +1,25 @@
// <copyright file="IChatServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The interface for a chat server.
/// </summary>
public interface IChatServer : IManageableServer
{
/// <summary>
/// Registers the client to the server.
/// </summary>
/// <param name="roomId">The room identifier.</param>
/// <param name="clientName">Name of the client.</param>
/// <returns>The authentication info.</returns>
ValueTask<ChatServerAuthenticationInfo?> RegisterClientAsync(ushort roomId, string clientName);
/// <summary>
/// Creates the chat room.
/// </summary>
/// <returns>The new chat room id.</returns>
ValueTask<ushort> CreateChatRoomAsync();
}

View File

@@ -0,0 +1,63 @@
// <copyright file="IConfigurationChangePublisher.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface for an object which publishes configuration changes to other services.
/// </summary>
public interface IConfigurationChangePublisher
{
/// <summary>
/// Gets a publisher which doesn't publish at all.
/// </summary>
static IConfigurationChangePublisher None { get; } = new NoneConfigurationChangePublisher();
/// <summary>
/// A configuration has changed.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="id">The identifier.</param>
/// <param name="configuration">The changed configuration.</param>
Task ConfigurationChangedAsync(Type type, Guid id, object configuration);
/// <summary>
/// A configuration has been added.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="id">The identifier.</param>
/// <param name="configuration">The added configuration.</param>
Task ConfigurationAddedAsync(Type type, Guid id, object configuration);
/// <summary>
/// A configuration has been removed.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="id">The identifier.</param>
Task ConfigurationRemovedAsync(Type type, Guid id);
/// <summary>
/// A publisher which doesn't publish changes at all.
/// </summary>
private class NoneConfigurationChangePublisher : IConfigurationChangePublisher
{
/// <inheritdoc />
public Task ConfigurationChangedAsync(Type type, Guid id, object configuration)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task ConfigurationAddedAsync(Type type, Guid id, object configuration)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task ConfigurationRemovedAsync(Type type, Guid id)
{
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,12 @@
// <copyright file="IConnectServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The interface for a connect server.
/// </summary>
public interface IConnectServer : IManageableServer, IGameServerStateObserver
{
}

View File

@@ -0,0 +1,23 @@
// <copyright file="IConnectServerInstanceManager.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface for an instance which manages connect servers.
/// </summary>
public interface IConnectServerInstanceManager
{
/// <summary>
/// Initializes a connect server with the specified definition.
/// </summary>
/// <param name="connectServerDefinitionId">The connect server definition identifier.</param>
ValueTask InitializeConnectServerAsync(Guid connectServerDefinitionId);
/// <summary>
/// Removes the connect server instance of the specified definition.
/// </summary>
/// <param name="connectServerDefinitionId">The connect server definition identifier.</param>
ValueTask RemoveConnectServerAsync(Guid connectServerDefinitionId);
}

View File

@@ -0,0 +1,101 @@
// <copyright file="IConnectServerSettings.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The connectServerSettings of the connect server.
/// </summary>
public interface IConnectServerSettings
{
/// <summary>
/// Gets the identifier of the configuration on which this settings are based on.
/// </summary>
Guid ConfigurationId { get; }
/// <summary>
/// Gets the server identifier.
/// </summary>
/// <remarks>Should be unique within all <see cref="IConnectServerSettings"/>.</remarks>
byte ServerId { get; }
/// <summary>
/// Gets the description of the server.
/// </summary>
/// <remarks>
/// Will be displayed in the server list in the admin panel as <see cref="IManageableServer.Description"/>.
/// </remarks>
string Description { get; }
/// <summary>
/// Gets a value indicating whether the client should get disconnected when a unknown packet is getting received.
/// </summary>
bool DisconnectOnUnknownPacket { get; }
/// <summary>
/// Gets the maximum size of the packets which should be received from the client. If this size is exceeded, the client will be disconnected.
/// </summary>
/// <remarks>DOS protection.</remarks>
byte MaximumReceiveSize { get; }
/// <summary>
/// Gets the client listener port.
/// </summary>
int ClientListenerPort { get; }
/// <summary>
/// Gets the client which is expected to connect.
/// </summary>
IGameClientVersion Client { get; }
/// <summary>
/// Gets the timeout after which clients without activity get disconnected.
/// </summary>
TimeSpan Timeout { get; }
/// <summary>
/// Gets the current patch version.
/// </summary>
byte[] CurrentPatchVersion { get; }
/// <summary>
/// Gets the patch address.
/// </summary>
string PatchAddress { get; }
/// <summary>
/// Gets the maximum connections per ip.
/// </summary>
int MaxConnectionsPerAddress { get; }
/// <summary>
/// Gets a value indicating whether the <see cref="MaxConnectionsPerAddress"/> should be checked.
/// </summary>
bool CheckMaxConnectionsPerAddress { get; }
/// <summary>
/// Gets the maximum connections the connect server should handle.
/// </summary>
int MaxConnections { get; }
/// <summary>
/// Gets the listener backlog for the client listener.
/// </summary>
int ListenerBacklog { get; }
/// <summary>
/// Gets the maximum FTP requests per connection.
/// </summary>
int MaxFtpRequests { get; }
/// <summary>
/// Gets the maximum ip requests per connection.
/// </summary>
int MaxIpRequests { get; }
/// <summary>
/// Gets the maximum server list requests per connection.
/// </summary>
int MaxServerListRequests { get; }
}

View File

@@ -0,0 +1,53 @@
// <copyright file="IEventPublisher.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface for a publisher of events which happen on the game server
/// and might have multiple interested subscribers/services.
/// </summary>
public interface IEventPublisher
{
/// <summary>
/// Notifies that a player entered the game with a character.
/// </summary>
/// <param name="serverId">The identifier of the server on which the player entered.</param>
/// <param name="characterId">The character identifier.</param>
/// <param name="characterName">Name of the character.</param>
ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName);
/// <summary>
/// Notifies that a player entered the game with a character.
/// </summary>
/// <param name="serverId">The identifier of the server on which the player entered.</param>
/// <param name="characterId">The character identifier.</param>
/// <param name="characterName">Name of the character.</param>
/// <param name="guildId">If the character is in a guild, a guild id can be passed.</param>
ValueTask PlayerLeftGameAsync(byte serverId, Guid characterId, string characterName, uint guildId = 0);
/// <summary>
/// Notifies the guild server that a guild message was sent and maybe needs to be forwarded to the game servers.
/// </summary>
/// <param name="guildId">The guild id.</param>
/// <param name="sender">The sender.</param>
/// <param name="message">The message.</param>
ValueTask GuildMessageAsync(uint guildId, string sender, string message);
/// <summary>
/// Notifies the guild server that an alliance message was sent and maybe needs to be forwarded to the game servers.
/// </summary>
/// <param name="guildId">The guild id.</param>
/// <param name="sender">The sender.</param>
/// <param name="message">The message.</param>
ValueTask AllianceMessageAsync(uint guildId, string sender, string 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>
ValueTask PlayerAlreadyLoggedInAsync(byte serverId, string loginName);
}

View File

@@ -0,0 +1,109 @@
// -----------------------------------------------------------------------
// <copyright file="IFriendServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Defines some server ids which are used for some specific states.
/// </summary>
public enum SpecialServerId : byte
{
/// <summary>
/// Gets the server id which represents being offline.
/// </summary>
Offline = 0xFF,
/// <summary>
/// Gets the server id which represents being invisible (=offline to other players).
/// </summary>
Invisible = 0xFE,
}
/// <summary>
/// The friend server interface.
/// </summary>
public interface IFriendServer
{
/// <summary>
/// Forwards the letter.
/// </summary>
/// <param name="letter">The letter.</param>
ValueTask ForwardLetterAsync(LetterHeader letter);
/// <summary>
/// Handles the friend request response.
/// </summary>
/// <param name="characterName">The character name of the responder.</param>
/// <param name="friendName">The character name of the requester.</param>
/// <param name="accepted">Indicating whether the request got accepted.</param>
ValueTask FriendResponseAsync(string characterName, string friendName, bool accepted);
/// <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="serverId">The server identifier.</param>
/// <param name="characterId">The character identifier.</param>
/// <param name="characterName">Name of the character.</param>
ValueTask PlayerEnteredGameAsync(byte serverId, Guid characterId, string characterName);
/// <summary>
/// Is called when a player leaves the game.
/// It will cause a state update for friends.
/// </summary>
/// <param name="characterId">The character identifier.</param>
/// <param name="characterName">Name of the character.</param>
ValueTask PlayerLeftGameAsync(Guid characterId, string characterName);
/// <summary>
/// Sets the online visibility state of a character.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="characterId">Id of the character.</param>
/// <param name="characterName">Name of the character.</param>
/// <param name="isVisible">If set to <c>true</c>, the character is visible as online. Otherwise, it appears as offline for other players, but is still online.</param>
ValueTask SetPlayerVisibilityStateAsync(byte serverId, Guid characterId, string characterName, bool isVisible);
/// <summary>
/// Determines whether two players are friends (accepted friend relationship).
/// </summary>
/// <param name="characterName">The character name of the first player.</param>
/// <param name="friendName">The character name of the second player.</param>
/// <returns>True if the two players are friends; otherwise false.</returns>
ValueTask<bool> IsFriendAsync(string characterName, string friendName);
/// <summary>
/// Sends a friend request to the friend, and adds a new friend view item to the players friend list.
/// </summary>
/// <param name="playerName">The name of the requesting player.</param>
/// <param name="friendName">The name of the requested friend.</param>
/// <returns>If a new friend view item got added to the players friend list.</returns>
ValueTask<bool> FriendRequestAsync(string playerName, string friendName);
/// <summary>
/// Deletes the friend.
/// </summary>
/// <param name="name">The player who is deleting a friend from his friend list.</param>
/// <param name="friendName">Name of the friend who should be deleted.</param>
ValueTask DeleteFriendAsync(string name, string friendName);
/// <summary>
/// Creates a new chat room.
/// </summary>
/// <param name="playerName">Name of the player who is creating the chat room.</param>
/// <param name="friendName">Name of the friend who should be invited to the chat room.</param>
ValueTask CreateChatRoomAsync(string playerName, string friendName);
/// <summary>
/// Invites a friend to an existing chat room.
/// </summary>
/// <param name="selectedCharacterName">Name of the selected character.</param>
/// <param name="friendName">Name of the friend.</param>
/// <param name="roomNumber">The room number.</param>
/// <returns>The success of the invitation.</returns>
ValueTask<bool> InviteFriendToChatRoomAsync(string selectedCharacterName, string friendName, ushort roomNumber);
}

View File

@@ -0,0 +1,52 @@
// <copyright file="IFriendSystemSubscriber.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.Collections.Immutable;
/// <summary>
/// Interface for an object which subscribes for changes in the friends system.
/// </summary>
public interface IFriendSystemSubscriber
{
/// <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 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>
ValueTask FriendRequestAsync(string requester, string receiver);
/// <summary>
/// Notifies the game server that a friend online state changed.
/// </summary>
/// <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="serverId">The new server identifier of the <paramref name="friend"/>.</param>
ValueTask FriendOnlineStateChangedAsync(string player, string friend, int 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="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(ChatServerAuthenticationInfo playerAuthenticationInfo, string friendName);
/// <summary>
/// Initializes the messenger of a player.
/// </summary>
/// <param name="initializationData">The initialization data.</param>
ValueTask InitializeMessengerAsync(MessengerInitializationData initializationData);
}
/// <summary>
/// The data of a messenger initialization for <see cref="IFriendSystemSubscriber.InitializeMessengerAsync"/>.
/// </summary>
public record MessengerInitializationData(string PlayerName, IImmutableList<string> Friends, IImmutableList<string> OpenFriendRequests);

View File

@@ -0,0 +1,36 @@
// <copyright file="IGameClientVersion.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Defines the game client (binary) version which is supposed to connect.
/// </summary>
public interface IGameClientVersion
{
/// <summary>
/// Gets the description.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the season.
/// </summary>
byte Season { get; }
/// <summary>
/// Gets the episode.
/// </summary>
byte Episode { get; }
/// <summary>
/// Gets the version which is defined in the client binaries.
/// </summary>
byte[] Version { get; }
/// <summary>
/// Gets the serial which is defined in the client binaries.
/// </summary>
byte[] Serial { get; }
}

View File

@@ -0,0 +1,158 @@
// <copyright file="IGameServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The state of the server.
/// </summary>
public enum ServerState
{
/// <summary>
/// The server has finished stopping.
/// </summary>
Stopped,
/// <summary>
/// The server is currently starting, but has not yet finished initialization.
/// </summary>
Starting,
/// <summary>
/// The server started and is available.
/// </summary>
Started,
/// <summary>
/// The server is not available anymore and is stopping it's services.
/// </summary>
Stopping,
/// <summary>
/// The server state is unknown, because of a timeout.
/// </summary>
Timeout,
}
/// <summary>
/// Types of messages.
/// </summary>
public enum MessageType
{
/// <summary>
/// The message is shown as centered golden message in the client.
/// </summary>
GoldenCenter = 0,
/// <summary>
/// The message is shown as blue entry.
/// </summary>
BlueNormal = 1,
/// <summary>
/// The message is a guild notice (green center).
/// </summary>
GuildNotice = 2,
}
/// <summary>
/// Interface for the inter-server communication.
/// </summary>
public interface IGameServer : IManageableServer, IFriendSystemSubscriber
{
/// <summary>
/// Sends a chat message to all connected guild members.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="sender">The sender character name.</param>
/// <param name="message">The message which should be sent.</param>
ValueTask GuildChatMessageAsync(uint guildId, string sender, string message);
/// <summary>
/// Notifies the game server that a guild got deleted.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
ValueTask GuildDeletedAsync(uint 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>
ValueTask GuildPlayerKickedAsync(string playerName);
/// <summary>
/// Sends a chat message to all connected alliance members.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="sender">The sender character name.</param>
/// <param name="message">The message.</param>
ValueTask AllianceChatMessageAsync(uint guildId, string sender, string message);
/// <summary>
/// Sends a global message to all connected players with the specified message type.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="messageType">Type of the message.</param>
ValueTask SendGlobalMessageAsync(string message, MessageType messageType);
/// <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>
ValueTask<bool> DisconnectPlayerAsync(string playerName);
/// <summary>
/// Disconnects the account from the game.
/// </summary>
/// <param name="accountName">Name of the account.</param>
/// <returns>True, if the account has been disconnected; False, otherwise.</returns>
ValueTask<bool> DisconnectAccountAsync(string 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>
ValueTask<bool> BanPlayerAsync(string playerName);
/// <summary>
/// Assigns the guild to the player.
/// </summary>
/// <param name="characterName">Name of the character.</param>
/// <param name="guildStatus">The guild status of the character.</param>
ValueTask AssignGuildToPlayerAsync(string characterName, GuildMemberStatus guildStatus);
/// <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>
ValueTask PlayerAlreadyLoggedInAsync(byte serverId, string loginName);
/// <summary>
/// Notifies the game server that an alliance between two guilds has been created.
/// </summary>
/// <param name="masterGuildId">The master guild identifier.</param>
/// <param name="memberGuildId">The member guild identifier.</param>
ValueTask AllianceCreatedAsync(uint masterGuildId, uint memberGuildId);
/// <summary>
/// Notifies the game server that an alliance between two guilds has been disbanded.
/// </summary>
/// <param name="masterGuildId">The master guild identifier.</param>
/// <param name="memberGuildId">The member guild identifier.</param>
ValueTask AllianceDisbandedAsync(uint masterGuildId, uint memberGuildId);
/// <summary>
/// Notifies the game server that the hostility between two guilds (or alliances) has changed.
/// </summary>
/// <param name="guildIdA">The identifier of the first guild.</param>
/// <param name="allianceGuildIdsA">All guild IDs in the alliance of guild A (or just [guildIdA] if not in an alliance).</param>
/// <param name="guildIdB">The identifier of the second guild.</param>
/// <param name="allianceGuildIdsB">All guild IDs in the alliance of guild B (or just [guildIdB] if not in an alliance).</param>
/// <param name="created"><c>true</c> if hostility was created; <c>false</c> if it was removed.</param>
ValueTask GuildHostilityChangedAsync(uint guildIdA, IReadOnlyList<uint> allianceGuildIdsA, uint guildIdB, IReadOnlyList<uint> allianceGuildIdsB, bool created);
}

View File

@@ -0,0 +1,29 @@
// <copyright file="IGameServerInstanceManager.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface for an instance which manages game servers.
/// </summary>
public interface IGameServerInstanceManager
{
/// <summary>
/// Restarts all servers of this container.
/// </summary>
/// <param name="onDatabaseInit">If set to <c>true</c>, this method is called during a database initialization.</param>
ValueTask RestartAllAsync(bool onDatabaseInit);
/// <summary>
/// Initializes a game server.
/// </summary>
/// <param name="serverId">The server identifier.</param>
ValueTask InitializeGameServerAsync(byte serverId);
/// <summary>
/// Removes the game server instance.
/// </summary>
/// <param name="serverId">The server identifier.</param>
ValueTask RemoveGameServerAsync(byte serverId);
}

View File

@@ -0,0 +1,33 @@
// <copyright file="IGameServerStateObserver.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.Net;
/// <summary>
/// An interface for an object which observes the state of game servers.
/// </summary>
public interface IGameServerStateObserver
{
/// <summary>
/// Registers the game server, so that it can be accessed through the connect server.
/// </summary>
/// <param name="gameServer">The game server information.</param>
/// <param name="publicEndPoint">The public end point.</param>
void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint);
/// <summary>
/// Un-registers the game server from the observer.
/// </summary>
/// <param name="gameServerId">The game server identifier.</param>
void UnregisterGameServer(ushort gameServerId);
/// <summary>
/// Is called when the number of <see cref="ServerInfo.CurrentConnections"/> changed for a server.
/// </summary>
/// <param name="serverId">The server id.</param>
/// <param name="currentConnections">The number of current connections, <see cref="ServerInfo.CurrentConnections"/>.</param>
void CurrentConnectionsChanged(ushort serverId, int currentConnections);
}

View File

@@ -0,0 +1,246 @@
// <copyright file="IGuildServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.Collections.Immutable;
/// <summary>
/// Describes the relationship between two guilds.
/// </summary>
public enum GuildRelationship
{
/// <summary>
/// No special relationship.
/// </summary>
None = 0,
/// <summary>
/// Both guilds are in the same alliance.
/// </summary>
Union = 1,
/// <summary>
/// The guilds are rivals / hostile to each other.
/// </summary>
Rival = 2,
}
/// <summary>
/// Defines the result of an alliance creation attempt.
/// </summary>
public enum AllianceCreationResult
{
/// <summary>
/// The alliance creation failed for an unspecified reason.
/// </summary>
Failed,
/// <summary>
/// The alliance was created successfully.
/// </summary>
Success,
/// <summary>
/// The master guild could not be found.
/// </summary>
MasterGuildNotFound,
/// <summary>
/// The target guild could not be found.
/// </summary>
TargetGuildNotFound,
/// <summary>
/// The target guild is already a member of an alliance.
/// </summary>
TargetGuildAlreadyInAlliance,
/// <summary>
/// The maximum number of guilds allowed in an alliance has been reached.
/// </summary>
MaximumAllianceSizeReached,
/// <summary>
/// The guild could not be found in the target context.
/// </summary>
GuildNotFoundInTargetContext,
/// <summary>
/// An unexpected error occurred during alliance creation.
/// </summary>
Error,
}
/// <summary>
/// Interface for the guild server.
/// </summary>
/// <remarks>
/// A little note about the guild id:
/// The original GMO server uses an 32-bit integer in all of its messages. However, actually it's only using (or used?) 16 bits of it for created guilds (see struct SDHP_GUILDCREATED).
/// Some people may remember the "guildbug" on GMO - I guess the keys exceeded these 16 bits and somehow caused a crash... but after restart of the servers it started working again.
/// </remarks>
public interface IGuildServer
{
/// <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>
ValueTask<bool> GuildExistsAsync(string guildName);
/// <summary>
/// Gets the guild by the guild identifier.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <returns>The guild.</returns>
ValueTask<Guild?> GetGuildAsync(uint 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>
ValueTask<uint> GetGuildIdByNameAsync(string 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="name">The name.</param>
/// <param name="masterName">Name of the master.</param>
/// <param name="masterId">The master identifier.</param>
/// <param name="logo">The logo.</param>
/// <param name="serverId">The identifier of the server on which the guild is getting created.</param>
/// <returns>A flag, indicating if the guild has been created successfully.</returns>
ValueTask<bool> CreateGuildAsync(string name, string masterName, Guid masterId, byte[] logo, byte 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="guildId">The guild identifier.</param>
/// <param name="characterId">The identifier.</param>
/// <param name="characterName">The name.</param>
/// <param name="role">The role of the member.</param>
/// <param name="serverId">The identifier of the server on which the guild member is getting created.</param>
ValueTask CreateGuildMemberAsync(uint guildId, Guid characterId, string characterName, GuildPosition role, byte serverId);
/// <summary>
/// Updates the guild member position.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="characterId">The id of the character.</param>
/// <param name="role">The role.</param>
ValueTask ChangeGuildMemberPositionAsync(uint guildId, Guid characterId, GuildPosition role);
/// <summary>
/// Notifies the guild server that a player (potential guild member) entered the game.
/// </summary>
/// <param name="characterId">The character identifier.</param>
/// <param name="characterName">Name of the character.</param>
/// <param name="serverId">The identifier of the server on which the guild member entered.</param>
ValueTask PlayerEnteredGameAsync(Guid characterId, string characterName, byte serverId);
/// <summary>
/// Notifies the guild server that a guild member left the game.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="guildMemberId">The identifier of the guild member.</param>
/// <param name="serverId">The identifier of the server from which the guild member left.</param>
ValueTask GuildMemberLeftGameAsync(uint guildId, Guid guildMemberId, byte serverId);
/// <summary>
/// Gets the guild member list.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <returns>The guild member list.</returns>
ValueTask<IImmutableList<GuildListEntry>> GetGuildListAsync(uint guildId);
/// <summary>
/// Kicks a guild member from a guild.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <param name="playerName">Name of the player which is getting kicked.</param>
ValueTask KickMemberAsync(uint guildId, string playerName);
/// <summary>
/// Gets the guild position of a specific character.
/// </summary>
/// <param name="characterId">The character identifier.</param>
/// <returns>The guild position.</returns>
ValueTask<GuildPosition> GetGuildPositionAsync(Guid characterId);
/// <summary>
/// Increases the guild score by one.
/// </summary>
/// <param name="guildId">The identifier of the guild.</param>
ValueTask IncreaseGuildScoreAsync(uint guildId);
/// <summary>
/// Creates an alliance between the master guild and the target guild.
/// The master guild becomes (or remains) the alliance master.
/// </summary>
/// <param name="masterGuildId">The identifier of the master guild that initiates the alliance.</param>
/// <param name="targetGuildId">The identifier of the target guild to add to the alliance.</param>
/// <returns><c>true</c> if the alliance was created successfully; <c>false</c> otherwise.</returns>
ValueTask<AllianceCreationResult> CreateAllianceAsync(uint masterGuildId, uint targetGuildId);
/// <summary>
/// Removes a guild from an alliance.
/// </summary>
/// <param name="targetGuildId">The identifier of the guild to remove from its alliance.</param>
/// <returns><c>true</c> if the guild was removed successfully; <c>false</c> otherwise.</returns>
ValueTask<bool> RemoveAllianceAsync(uint targetGuildId);
/// <summary>
/// Gets the list of guilds in the alliance of the specified guild.
/// </summary>
/// <param name="guildId">The identifier of any guild in the alliance.</param>
/// <returns>The list of alliance guilds.</returns>
ValueTask<IImmutableList<AllianceGuildEntry>> GetAllianceGuildsAsync(uint guildId);
/// <summary>
/// Determines whether the specified guild is the alliance master.
/// </summary>
/// <param name="guildId">The guild identifier.</param>
/// <returns><c>true</c> if the guild is the alliance master; <c>false</c> otherwise.</returns>
ValueTask<bool> IsAllianceMasterAsync(uint guildId);
/// <summary>
/// Sets or clears the hostility between guilds.
/// </summary>
/// <param name="guildIdA">The guild identifier of the requesting guild.</param>
/// <param name="guildIdB">The identifier of the target guild. Only used when <paramref name="create"/> is <c>true</c>.</param>
/// <param name="create"><c>true</c> to set hostility; <c>false</c> to clear any existing hostility.</param>
/// <returns><c>true</c> if the hostility state was changed successfully; <c>false</c> otherwise.</returns>
ValueTask<bool> SetHostilityAsync(uint guildIdA, uint guildIdB, bool create);
/// <summary>
/// Gets the relationship between two guilds.
/// </summary>
/// <param name="guild1">The first guild identifier.</param>
/// <param name="guild2">The second guild identifier.</param>
/// <returns>The relationship between the two guilds.</returns>
ValueTask<GuildRelationship> GetGuildRelationshipAsync(uint guild1, uint guild2);
}
/// <summary>
/// The guild list entry.
/// </summary>
public class GuildListEntry
{
/// <summary>
/// Gets or sets the name of the player.
/// </summary>
public string? PlayerName { get; set; }
/// <summary>
/// Gets or sets the server identifier on which the player is playing.
/// </summary>
public byte ServerId { get; set; }
/// <summary>
/// Gets or sets the players position in the guild.
/// </summary>
public GuildPosition PlayerPosition { get; set; }
}

View File

@@ -0,0 +1,34 @@
// -----------------------------------------------------------------------
// <copyright file="ILoginServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Interface of the login server, which keeps track of all connected accounts.
/// </summary>
public interface ILoginServer
{
/// <summary>
/// Tries to login the account on the specified server.
/// </summary>
/// <param name="accountName">Name of the account.</param>
/// <param name="serverId">The server identifier.</param>
/// <returns>The success.</returns>
Task<bool> TryLoginAsync(string accountName, byte serverId);
/// <summary>
/// Logs the account off from the specified server.
/// </summary>
/// <param name="accountName">Name of the account.</param>
/// <param name="serverId">The server identifier.</param>
ValueTask LogOffAsync(string accountName, byte serverId);
/// <summary>
/// Gets a snapshot of the currently logged in accounts and their corresponding server ids.
/// </summary>
/// <returns>A snapshot of the currently logged in accounts and their corresponding server ids.</returns>
ValueTask<Dictionary<string, byte>> GetSnapshotAsync();
}

View File

@@ -0,0 +1,59 @@
// <copyright file="IManageableServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.ComponentModel;
using Microsoft.Extensions.Hosting;
/// <summary>
/// General interface for a server which provides some information and functions to manage it from outside.
/// </summary>
public interface IManageableServer : INotifyPropertyChanged, IHostedService
{
/// <summary>
/// Gets the identifier of the server.
/// </summary>
int Id { get; }
/// <summary>
/// Gets the identifier of the configuration of the server.
/// </summary>
Guid ConfigurationId { get; }
/// <summary>
/// Gets the description.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the type.
/// </summary>
ServerType Type { get; }
/// <summary>
/// Gets the current state of the server.
/// </summary>
ServerState ServerState { get; }
/// <summary>
/// Gets the maximum number of connections the server can handle.
/// </summary>
int MaximumConnections { get; }
/// <summary>
/// Gets the current connection count.
/// </summary>
int CurrentConnections { get; }
/// <summary>
/// Starts the server.
/// </summary>
ValueTask StartAsync();
/// <summary>
/// Stops the server.
/// </summary>
ValueTask ShutdownAsync();
}

View File

@@ -0,0 +1,18 @@
// <copyright file="IServerProvider.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.ComponentModel;
/// <summary>
/// An interface for an object which provides a list of <see cref="IManageableServer"/>s.
/// </summary>
public interface IServerProvider : INotifyPropertyChanged
{
/// <summary>
/// Gets the servers.
/// </summary>
IList<IManageableServer> Servers { get; }
}

View File

@@ -0,0 +1,55 @@
// <copyright file="LetterHeader.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The header of a letter.
/// </summary>
public class LetterHeader
{
/// <summary>
/// Gets or sets the identifier.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the sender.
/// </summary>
/// <remarks>
/// The persistence implementation can implement to keep an internal id, too.
/// However, it should keep the name, if it should stay available after a sender character got deleted.
/// </remarks>
public string? SenderName { get; set; }
/// <summary>
/// Gets or sets the receiver.
/// </summary>
/// <remarks>
/// The persistence implementation can implement to keep an internal id, too.
/// In this case, it may not be required to save the name itself.
/// </remarks>
public string? ReceiverName { get; set; }
/// <summary>
/// Gets or sets the subject.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Gets or sets the letter date.
/// </summary>
public DateTime LetterDate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the letter has been read.
/// </summary>
public bool ReadFlag { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{this.SenderName} -> {this.ReceiverName} - {this.Subject} ({this.LetterDate})";
}
}

View File

@@ -0,0 +1,354 @@
// <copyright file="LocalizedString.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.Globalization;
/// <summary>
/// Represents a string which can be translated to different languages and serializes into a single string.
/// It's meant for simple usage in database fields and not for complex localization scenarios.
/// To keep compatibility with normal strings, we simply assume the first string to be in neutral (usually english) language.
/// Example: "Some text||de=Etwas Text||fr=Un peu de texte" where the first part is english, second german and third french.
/// </summary>
public readonly struct LocalizedString : IEquatable<LocalizedString>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalizedString"/> struct.
/// </summary>
/// <param name="value">The value.</param>
public LocalizedString(string? value)
{
this.Value = value;
}
/// <summary>
/// Gets the separator string which separates the different languages.
/// </summary>
public static string Separator => "||";
/// <summary>
/// Gets the neutral language code.
/// </summary>
public static string NeutralLanguageCode => "en";
/// <summary>
/// Gets the underlying serialized value of this localized string.
/// </summary>
public string? Value { get; }
/// <summary>
/// Gets the value in the neutral language as a <see cref="string"/>.
/// </summary>
/// <value>
/// A <see cref="string"/> that contains the neutral language text, or an empty string
/// if the underlying value is <see langword="null"/>.
/// </value>
public string ValueInNeutralLanguage
{
get
{
if (this.Value?.IndexOf(Separator, StringComparison.OrdinalIgnoreCase) is not >= 0)
{
return this.Value ?? string.Empty;
}
var span = this.ValueInNeutralLanguageAsSpan;
return new(span);
}
}
/// <summary>
/// Gets the value in the neutral language as a <see cref="ReadOnlySpan{T}"/> of characters.
/// </summary>
/// <value>
/// A <see cref="ReadOnlySpan{T}"/> that contains the neutral language text, or an empty span
/// if the underlying value is <see langword="null"/>.
/// </value>
[System.Text.Json.Serialization.JsonIgnore]
public ReadOnlySpan<char> ValueInNeutralLanguageAsSpan
{
get
{
if (this.Value is null)
{
return [];
}
var separatorIndex = this.Value.IndexOf(Separator, StringComparison.OrdinalIgnoreCase);
if (separatorIndex == -1)
{
return this.Value.AsSpan();
}
return this.Value.AsSpan(0, separatorIndex);
}
}
/// <summary>
/// Performs an implicit conversion from <see cref="LocalizedString"/> to <see cref="string"/>.
/// </summary>
/// <param name="localizedString">The localized string.</param>
/// <returns>The underlying serialized value of the localized string, or <see langword="null"/> if the instance is <see langword="null"/>.</returns>
public static implicit operator string?(LocalizedString? localizedString)
{
return localizedString?.Value;
}
/// <summary>
/// Performs an implicit conversion from <see cref="LocalizedString"/> to <see cref="string"/>.
/// </summary>
/// <param name="localizedString">The localized string.</param>
/// <returns>The underlying serialized value of the localized string, or an empty string if the underlying value is <see langword="null"/>.</returns>
public static implicit operator string(LocalizedString localizedString)
{
return localizedString.Value ?? string.Empty;
}
/// <summary>
/// Performs an implicit conversion from <see cref="string"/> to a nullable <see cref="LocalizedString"/>.
/// </summary>
/// <param name="localizedString">The serialized localized string value.</param>
/// <returns>A new <see cref="LocalizedString"/> instance, or <see langword="null"/> if <paramref name="localizedString"/> is <see langword="null"/>.</returns>
public static implicit operator LocalizedString?(string? localizedString)
{
if (localizedString is null)
{
return null;
}
return new LocalizedString(localizedString);
}
/// <summary>
/// Performs an implicit conversion from <see cref="string"/> to <see cref="LocalizedString"/>.
/// </summary>
/// <param name="localizedString">The serialized localized string value.</param>
/// <returns>A new <see cref="LocalizedString"/> instance.</returns>
public static implicit operator LocalizedString(string localizedString)
{
return new LocalizedString(localizedString);
}
/// <summary>
/// Returns a <see cref="string"/> that represents this instance for the current UI culture.
/// </summary>
/// <returns>
/// A <see cref="string"/> containing the translation for the current UI culture,
/// or the neutral language translation if none is available for the current culture.
/// </returns>
public override string? ToString()
{
return this.Value is null ? null : this.GetTranslation(CultureInfo.CurrentCulture);
}
/// <summary>
/// Gets the translation for the specified culture.
/// </summary>
/// <param name="cultureInfo">The culture for which the translation is requested.</param>
/// <param name="fallbackToNeutral">
/// If set to <see langword="true"/>, falls back to the neutral language if no translation for
/// <paramref name="cultureInfo"/> is available; otherwise returns an empty string.
/// </param>
/// <returns>
/// The translation for the specified culture, the neutral language translation if
/// <paramref name="fallbackToNeutral"/> is <see langword="true"/> and no specific translation exists,
/// or an empty string if neither is available.
/// </returns>
public string? GetTranslation(CultureInfo cultureInfo, bool fallbackToNeutral = true)
{
var span = this.GetTranslationAsSpan(cultureInfo, fallbackToNeutral);
return span.IsEmpty
? null
: new(span);
}
/// <summary>
/// Gets the translation for the specified culture as a <see cref="ReadOnlySpan{T}"/> of characters.
/// </summary>
/// <param name="cultureInfo">The culture for which the translation is requested.</param>
/// <param name="fallbackToNeutral">
/// If set to <see langword="true"/>, falls back to the neutral language if no translation for
/// <paramref name="cultureInfo"/> is available; otherwise returns an empty span.
/// </param>
/// <returns>
/// A <see cref="ReadOnlySpan{T}"/> containing the translation for the specified culture,
/// the neutral language translation if <paramref name="fallbackToNeutral"/> is <see langword="true"/>
/// and no specific translation exists, or an empty span if neither is available.
/// </returns>
public ReadOnlySpan<char> GetTranslationAsSpan(CultureInfo cultureInfo, bool fallbackToNeutral = true)
{
// Implementation for retrieving the localized string based on the cultureInfo
if (this.Value is null)
{
return [];
}
if (cultureInfo.TwoLetterISOLanguageName == NeutralLanguageCode)
{
return this.ValueInNeutralLanguageAsSpan;
}
var searchPattern = Separator + cultureInfo.TwoLetterISOLanguageName + "=";
var startIndex = this.Value.IndexOf(searchPattern, StringComparison.OrdinalIgnoreCase);
if (startIndex == -1)
{
return fallbackToNeutral ? this.ValueInNeutralLanguageAsSpan : [];
}
var part = this.Value.AsSpan(startIndex + searchPattern.Length);
var endIndex = part.IndexOf(Separator);
if (endIndex == -1)
{
return part;
}
return part.Slice(0, endIndex);
}
/// <summary>
/// Returns a new <see cref="LocalizedString"/> with the specified translation added, updated, or removed.
/// </summary>
/// <param name="cultureInfo">The culture for which the translation should be added or updated.</param>
/// <param name="text">
/// The translation text. If <see langword="null"/> or empty, an existing translation for the specified culture
/// is removed.
/// </param>
/// <returns>
/// A new <see cref="LocalizedString"/> instance with the modified translation for the specified culture.
/// </returns>
public LocalizedString WithTranslation(CultureInfo cultureInfo, string? text)
{
// Plan (pseudocode):
// 1. Determine language code from culture (TwoLetterISOLanguageName).
// 2. If language is neutral ("en"):
// a. If Value is null or empty:
// - If text is null or empty: return this (no change).
// - Else: create new base + keep all existing non-neutral parts (if any) and return new instance.
// b. If Value has separator:
// - Replace the part before first separator with the new text (may be null/empty).
// c. If Value has no separator:
// - Replace whole value with new text.
// 3. If language is non-neutral:
// a. If Value is null or empty:
// - If text is null or empty: return this.
// - Else: initialize base as empty string and append "||xx=text".
// b. Search for existing "||xx=" section.
// - If found:
// i. If text is null or empty: remove that section (and possible trailing/leading separators).
// ii. Else: replace its content with text.
// - If not found and text is not null/empty: append "||xx=text".
// 4. Return new LocalizedString with computed value.
var languageCode = cultureInfo.TwoLetterISOLanguageName;
// Work with a mutable string representation
var current = this.Value ?? string.Empty;
if (languageCode == NeutralLanguageCode)
{
// Handle neutral language as the base string (before first separator)
var separatorIndex = current.IndexOf(Separator, StringComparison.OrdinalIgnoreCase);
if (separatorIndex == -1)
{
// Only neutral text present
if (string.IsNullOrEmpty(text))
{
if (string.IsNullOrEmpty(current))
{
return this;
}
return new LocalizedString(string.Empty);
}
return new LocalizedString(text);
}
// There are additional translations after the base text
var suffix = current.Substring(separatorIndex); // includes the separator
var newBase = text ?? string.Empty;
return new LocalizedString(newBase + suffix);
}
// Handle non-neutral languages
var searchPattern = Separator + languageCode + "=";
var startIndex = current.IndexOf(searchPattern, StringComparison.OrdinalIgnoreCase);
if (startIndex == -1)
{
// No existing translation for this language
if (string.IsNullOrEmpty(text))
{
return this;
}
if (string.IsNullOrEmpty(current))
{
// No base text yet, just start with empty base and language entry
return new LocalizedString(string.Empty + Separator + languageCode + "=" + text);
}
return new LocalizedString(current + Separator + languageCode + "=" + text);
}
// Existing translation found
var partStart = startIndex + searchPattern.Length;
var span = current.AsSpan(partStart);
var endIndex = span.IndexOf(Separator);
var removeLength = endIndex == -1 ? current.Length - partStart : endIndex;
if (string.IsNullOrEmpty(text))
{
// Remove this translation entry completely, including "||xx=" prefix
var prefix = current.AsSpan(0, startIndex);
ReadOnlySpan<char> suffixSpan;
if (endIndex == -1)
{
suffixSpan = ReadOnlySpan<char>.Empty;
}
else
{
suffixSpan = current.AsSpan(partStart + removeLength);
}
var result = string.Concat(prefix, suffixSpan);
return new LocalizedString(result);
}
else
{
// Replace the content of this translation
var prefix = current.AsSpan(0, partStart);
ReadOnlySpan<char> suffixSpan;
if (endIndex == -1)
{
suffixSpan = ReadOnlySpan<char>.Empty;
}
else
{
suffixSpan = current.AsSpan(partStart + removeLength);
}
var result = string.Concat(prefix, text, suffixSpan);
return new LocalizedString(result);
}
}
/// <inheritdoc />
public bool Equals(LocalizedString other)
{
return this.Value == other.Value;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is LocalizedString other && this.Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return this.Value != null ? this.Value.GetHashCode() : 0;
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="LocalizedStringJsonConverter.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// A JSON converter for the <see cref="LocalizedString"/> type which serializes it as a simple JSON string.
/// </summary>
public class LocalizedStringJsonConverter : JsonConverter<LocalizedString>
{
/// <inheritdoc />
public override LocalizedString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}
if (reader.TokenType == JsonTokenType.String)
{
return new LocalizedString(reader.GetString());
}
throw new Exception($"Unexpected token parsing binary. Expected String, got {reader.TokenType}.");
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, LocalizedString value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Value);
}
}

View File

@@ -0,0 +1,24 @@
<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>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\bin\Debug\</OutputPath>
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.Interfaces.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Release\</OutputPath>
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.Interfaces.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
// <copyright file="AssemblyInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using System.Reflection;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MUnique.OpenMU.Interfaces")]

View File

@@ -0,0 +1,451 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LetterHeader_TypeCaption" xml:space="preserve">
<value>Letter Header</value>
</data>
<data name="LetterHeader_TypeCaptionPlural" xml:space="preserve">
<value>Letter Headers</value>
</data>
<data name="LetterHeader_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_Id_Caption" xml:space="preserve">
<value>Id</value>
</data>
<data name="LetterHeader_Id_Description" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_SenderName_Caption" xml:space="preserve">
<value>Sender Name</value>
</data>
<data name="LetterHeader_SenderName_Description" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_ReceiverName_Caption" xml:space="preserve">
<value>Receiver Name</value>
</data>
<data name="LetterHeader_ReceiverName_Description" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_Subject_Caption" xml:space="preserve">
<value>Subject</value>
</data>
<data name="LetterHeader_Subject_Description" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_LetterDate_Caption" xml:space="preserve">
<value>Letter Date</value>
</data>
<data name="LetterHeader_LetterDate_Description" xml:space="preserve">
<value></value>
</data>
<data name="LetterHeader_ReadFlag_Caption" xml:space="preserve">
<value>Read Flag</value>
</data>
<data name="LetterHeader_ReadFlag_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildListEntry_TypeCaption" xml:space="preserve">
<value>Guild List Entry</value>
</data>
<data name="GuildListEntry_TypeCaptionPlural" xml:space="preserve">
<value>Guild List Entrys</value>
</data>
<data name="GuildListEntry_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="GuildListEntry_PlayerName_Caption" xml:space="preserve">
<value>Player Name</value>
</data>
<data name="GuildListEntry_PlayerName_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildListEntry_ServerId_Caption" xml:space="preserve">
<value>Server Id</value>
</data>
<data name="GuildListEntry_ServerId_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildListEntry_PlayerPosition_Caption" xml:space="preserve">
<value>Player Position</value>
</data>
<data name="GuildListEntry_PlayerPosition_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_TypeCaption" xml:space="preserve">
<value>Guild</value>
</data>
<data name="Guild_TypeCaptionPlural" xml:space="preserve">
<value>Guilds</value>
</data>
<data name="Guild_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="Guild_Name_Caption" xml:space="preserve">
<value>Name</value>
</data>
<data name="Guild_Name_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_Logo_Caption" xml:space="preserve">
<value>Logo</value>
</data>
<data name="Guild_Logo_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_Score_Caption" xml:space="preserve">
<value>Score</value>
</data>
<data name="Guild_Score_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_Notice_Caption" xml:space="preserve">
<value>Notice</value>
</data>
<data name="Guild_Notice_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_Hostility_Caption" xml:space="preserve">
<value>Hostility</value>
</data>
<data name="Guild_Hostility_Description" xml:space="preserve">
<value></value>
</data>
<data name="Guild_AllianceGuild_Caption" xml:space="preserve">
<value>Alliance Guild</value>
</data>
<data name="Guild_AllianceGuild_Description" xml:space="preserve">
<value></value>
</data>
<data name="Friend_TypeCaption" xml:space="preserve">
<value>Friend</value>
</data>
<data name="Friend_TypeCaptionPlural" xml:space="preserve">
<value>Friends</value>
</data>
<data name="Friend_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="Friend_Id_Caption" xml:space="preserve">
<value>Id</value>
</data>
<data name="Friend_Id_Description" xml:space="preserve">
<value></value>
</data>
<data name="Friend_CharacterId_Caption" xml:space="preserve">
<value>Character Id</value>
</data>
<data name="Friend_CharacterId_Description" xml:space="preserve">
<value></value>
</data>
<data name="Friend_FriendId_Caption" xml:space="preserve">
<value>Friend Id</value>
</data>
<data name="Friend_FriendId_Description" xml:space="preserve">
<value></value>
</data>
<data name="Friend_Accepted_Caption" xml:space="preserve">
<value>Accepted</value>
</data>
<data name="Friend_Accepted_Description" xml:space="preserve">
<value></value>
</data>
<data name="Friend_RequestOpen_Caption" xml:space="preserve">
<value>Request Open</value>
</data>
<data name="Friend_RequestOpen_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildMemberStatus_TypeCaption" xml:space="preserve">
<value>Guild Member Status</value>
</data>
<data name="GuildMemberStatus_TypeCaptionPlural" xml:space="preserve">
<value>Guild Member Status</value>
</data>
<data name="GuildMemberStatus_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="GuildMemberStatus_GuildId_Caption" xml:space="preserve">
<value>Guild Id</value>
</data>
<data name="GuildMemberStatus_GuildId_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildMemberStatus_Position_Caption" xml:space="preserve">
<value>Position</value>
</data>
<data name="GuildMemberStatus_Position_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_TypeCaption" xml:space="preserve">
<value>Chat Server Authentication Info</value>
</data>
<data name="ChatServerAuthenticationInfo_TypeCaptionPlural" xml:space="preserve">
<value>Chat Server Authentication Infos</value>
</data>
<data name="ChatServerAuthenticationInfo_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_Index_Caption" xml:space="preserve">
<value>Index</value>
</data>
<data name="ChatServerAuthenticationInfo_Index_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_RoomId_Caption" xml:space="preserve">
<value>Room Id</value>
</data>
<data name="ChatServerAuthenticationInfo_RoomId_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_ClientName_Caption" xml:space="preserve">
<value>Client Name</value>
</data>
<data name="ChatServerAuthenticationInfo_ClientName_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_AuthenticationToken_Caption" xml:space="preserve">
<value>Authentication Token</value>
</data>
<data name="ChatServerAuthenticationInfo_AuthenticationToken_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_HostAddress_Caption" xml:space="preserve">
<value>Host Address</value>
</data>
<data name="ChatServerAuthenticationInfo_HostAddress_Description" xml:space="preserve">
<value></value>
</data>
<data name="ChatServerAuthenticationInfo_AuthenticationRequiredUntil_Caption" xml:space="preserve">
<value>Authentication Required Until</value>
</data>
<data name="ChatServerAuthenticationInfo_AuthenticationRequiredUntil_Description" xml:space="preserve">
<value></value>
</data>
<data name="FriendViewItem_TypeCaption" xml:space="preserve">
<value>Friend View Item</value>
</data>
<data name="FriendViewItem_TypeCaptionPlural" xml:space="preserve">
<value>Friend View Items</value>
</data>
<data name="FriendViewItem_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="FriendViewItem_CharacterName_Caption" xml:space="preserve">
<value>Character Name</value>
</data>
<data name="FriendViewItem_CharacterName_Description" xml:space="preserve">
<value></value>
</data>
<data name="FriendViewItem_FriendName_Caption" xml:space="preserve">
<value>Friend Name</value>
</data>
<data name="FriendViewItem_FriendName_Description" xml:space="preserve">
<value></value>
</data>
<data name="LocalizableException_TypeCaption" xml:space="preserve">
<value>Localizable Exception</value>
</data>
<data name="LocalizableException_TypeCaptionPlural" xml:space="preserve">
<value>Localizable Exceptions</value>
</data>
<data name="LocalizableException_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="LocalizableException_InnerException_Caption" xml:space="preserve">
<value>Inner Exception</value>
</data>
<data name="LocalizableException_InnerException_Description" xml:space="preserve">
<value></value>
</data>
<data name="SpecialServerId_TypeCaption" xml:space="preserve">
<value>Special Server Id</value>
</data>
<data name="SpecialServerId_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="SpecialServerId_Offline_Caption" xml:space="preserve">
<value>Offline</value>
</data>
<data name="SpecialServerId_Offline_Description" xml:space="preserve">
<value></value>
</data>
<data name="SpecialServerId_Invisible_Caption" xml:space="preserve">
<value>Invisible</value>
</data>
<data name="SpecialServerId_Invisible_Description" xml:space="preserve">
<value></value>
</data>
<data name="NoneConfigurationChangePublisher_TypeCaption" xml:space="preserve">
<value>None Configuration Change Publisher</value>
</data>
<data name="NoneConfigurationChangePublisher_TypeCaptionPlural" xml:space="preserve">
<value>None Configuration Change Publishers</value>
</data>
<data name="NoneConfigurationChangePublisher_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="GuildPosition_TypeCaption" xml:space="preserve">
<value>Guild Position</value>
</data>
<data name="GuildPosition_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="GuildPosition_Undefined_Caption" xml:space="preserve">
<value>Undefined</value>
</data>
<data name="GuildPosition_Undefined_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildPosition_NormalMember_Caption" xml:space="preserve">
<value>Normal Member</value>
</data>
<data name="GuildPosition_NormalMember_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildPosition_GuildMaster_Caption" xml:space="preserve">
<value>Guild Master</value>
</data>
<data name="GuildPosition_GuildMaster_Description" xml:space="preserve">
<value></value>
</data>
<data name="GuildPosition_BattleMaster_Caption" xml:space="preserve">
<value>Battle Master</value>
</data>
<data name="GuildPosition_BattleMaster_Description" xml:space="preserve">
<value></value>
</data>
<data name="LocalizableExceptionBase_TypeCaption" xml:space="preserve">
<value>Localizable Exception Base</value>
</data>
<data name="LocalizableExceptionBase_TypeCaptionPlural" xml:space="preserve">
<value>Localizable Exception Bases</value>
</data>
<data name="LocalizableExceptionBase_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="LocalizableExceptionBase_FormatArguments_Caption" xml:space="preserve">
<value>Format Arguments</value>
</data>
<data name="LocalizableExceptionBase_FormatArguments_Description" xml:space="preserve">
<value></value>
</data>
<data name="LocalizableExceptionBase_ResourceKey_Caption" xml:space="preserve">
<value>Resource Key</value>
</data>
<data name="LocalizableExceptionBase_ResourceKey_Description" xml:space="preserve">
<value></value>
</data>
<data name="SpecialServerIds_TypeCaption" xml:space="preserve">
<value>Special Server Ids</value>
</data>
<data name="SpecialServerIds_TypeCaptionPlural" xml:space="preserve">
<value>Special Server Ids</value>
</data>
<data name="SpecialServerIds_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="ServerType_TypeCaption" xml:space="preserve">
<value>Server Type</value>
</data>
<data name="ServerType_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="ServerType_Undefined_Caption" xml:space="preserve">
<value>Undefined</value>
</data>
<data name="ServerType_Undefined_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerType_GameServer_Caption" xml:space="preserve">
<value>Game Server</value>
</data>
<data name="ServerType_GameServer_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerType_ConnectServer_Caption" xml:space="preserve">
<value>Connect Server</value>
</data>
<data name="ServerType_ConnectServer_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerType_ChatServer_Caption" xml:space="preserve">
<value>Chat Server</value>
</data>
<data name="ServerType_ChatServer_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_TypeCaption" xml:space="preserve">
<value>Server State</value>
</data>
<data name="ServerState_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_Stopped_Caption" xml:space="preserve">
<value>Stopped</value>
</data>
<data name="ServerState_Stopped_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_Starting_Caption" xml:space="preserve">
<value>Starting</value>
</data>
<data name="ServerState_Starting_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_Started_Caption" xml:space="preserve">
<value>Started</value>
</data>
<data name="ServerState_Started_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_Stopping_Caption" xml:space="preserve">
<value>Stopping</value>
</data>
<data name="ServerState_Stopping_Description" xml:space="preserve">
<value></value>
</data>
<data name="ServerState_Timeout_Caption" xml:space="preserve">
<value>Timeout</value>
</data>
<data name="ServerState_Timeout_Description" xml:space="preserve">
<value></value>
</data>
<data name="MessageType_TypeCaption" xml:space="preserve">
<value>Message Type</value>
</data>
<data name="MessageType_TypeDescription" xml:space="preserve">
<value></value>
</data>
<data name="MessageType_GoldenCenter_Caption" xml:space="preserve">
<value>Golden Center</value>
</data>
<data name="MessageType_GoldenCenter_Description" xml:space="preserve">
<value></value>
</data>
<data name="MessageType_BlueNormal_Caption" xml:space="preserve">
<value>Blue Normal</value>
</data>
<data name="MessageType_BlueNormal_Description" xml:space="preserve">
<value></value>
</data>
<data name="MessageType_GuildNotice_Caption" xml:space="preserve">
<value>Guild Notice</value>
</data>
<data name="MessageType_GuildNotice_Description" xml:space="preserve">
<value></value>
</data>
</root>

12
src/Interfaces/Readme.md Normal file
View File

@@ -0,0 +1,12 @@
# Interfaces
This project contains the interfaces which are used to communicate between the
different sub-systems ("servers") of the whole system.
The game logic and game server should only use these interfaces instead of the
actual implementations.
The goal is to make them exchangeable. This can be helpful for tests, and also
if we want to move specific systems to other machines or external processes
(scale-out).
Then there could be an implementation of an interface which forwards the calls
over the network (etc.) to another process.

View File

@@ -0,0 +1,16 @@
// <copyright file="ServerInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The state info about a server.
/// </summary>
public record ServerInfo(ushort Id, string Description, int CurrentConnections, int MaximumConnections)
{
/// <summary>
/// Gets or sets the count of current connections.
/// </summary>
public int CurrentConnections { get; set; } = CurrentConnections;
}

View File

@@ -0,0 +1,31 @@
// <copyright file="ServerType.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// The type of a <see cref="IManageableServer"/>.
/// </summary>
public enum ServerType
{
/// <summary>
/// Undefined type.
/// </summary>
Undefined = 0,
/// <summary>
/// A game server.
/// </summary>
GameServer = 1,
/// <summary>
/// A connect server.
/// </summary>
ConnectServer = 2,
/// <summary>
/// A chat server.
/// </summary>
ChatServer = 3,
}

View File

@@ -0,0 +1,22 @@
// <copyright file="SpecialServerIds.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Interfaces;
/// <summary>
/// Special server ids.
/// </summary>
/// <remarks>Ids from 0 to 0xFFFF are reserved to game servers.</remarks>
public static class SpecialServerIds
{
/// <summary>
/// The connect server special server id.
/// </summary>
public static readonly int ConnectServer = 0x10000;
/// <summary>
/// The chat server special server id.
/// </summary>
public static readonly int ChatServer = 0x20000;
}