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,17 @@
##############################
# ChatServer Configuration #
##############################
# The following values are defaults and are even applied if this file or single configuration value-pairs are missing or are in the wrong format:
# ChatServerListenerPort=55980
# ExDbHost=127.0.0.1
# ExDbPort=55906
# Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D
ChatServerListenerPort=55980
ExDbHost=127.0.0.1
ExDbPort=55906
Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D

View File

@@ -0,0 +1,55 @@
// <copyright file="ConfigurableNetworkEncryptionPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
using System.ComponentModel.DataAnnotations;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Network.Xor;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.
/// </summary>
[PlugIn]
[Display(Name = "Configurable encryption plugin", Description = "A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.")]
[Guid("890997B2-9334-4E9E-8C82-4492A831BCE3")]
public class ConfigurableNetworkEncryptionPlugIn : INetworkEncryptionFactoryPlugIn
{
private readonly byte[] _xor32Key;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurableNetworkEncryptionPlugIn"/> class.
/// </summary>
public ConfigurableNetworkEncryptionPlugIn()
{
var settings = new Settings("ChatServer.cfg");
this._xor32Key = settings.Xor32Key ?? new byte[32];
}
/// <summary>
/// Gets the version for which this plugin is available.
/// </summary>
public static ClientVersion Version { get; } = new(byte.MaxValue, byte.MaxValue, ClientLanguage.Invariant);
/// <inheritdoc />
public ClientVersion Key => Version;
/// <inheritdoc />
public IPipelinedDecryptor? CreateDecryptor(PipeReader source, DataDirection direction)
{
return new PipelinedXor32Decryptor(source, this._xor32Key);
}
/// <inheritdoc />
public IPipelinedEncryptor? CreateEncryptor(PipeWriter target, DataDirection direction)
{
// At least until season 6, there is no encryption from server to client.
// ex700 may require packet twister here.
return null;
}
}

View File

@@ -0,0 +1,275 @@
// <copyright file="ExDbClient.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
using System.Buffers;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets;
using Pipelines.Sockets.Unofficial;
using static System.Buffers.Binary.BinaryPrimitives;
/// <summary>
/// The connected exDB server. This class includes the communication implementation between chat server and exDB server.
/// It registers clients for the chat server and hands back their authentication details.
/// </summary>
public class ExDbClient
{
private readonly ILogger<ExDbClient> _logger;
private readonly string _host;
private readonly int _port;
private readonly IChatServer _chatServer;
private readonly ILoggerFactory _loggerFactory;
private readonly ushort _chatServerPort;
private readonly byte[] _packetBuffer = new byte[0xFF];
private IConnection? _connection;
/// <summary>
/// Initializes a new instance of the <see cref="ExDbClient" /> class.
/// </summary>
/// <param name="host">The host address of the exDB server.</param>
/// <param name="port">The host port of the exDB server.</param>
/// <param name="chatServer">The chat server.</param>
/// <param name="chatServerPort">The chat server port.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ExDbClient(string host, int port, IChatServer chatServer, int chatServerPort, ILoggerFactory loggerFactory)
{
this._host = host;
this._port = port;
this._chatServer = chatServer;
this._loggerFactory = loggerFactory;
this._chatServerPort = (ushort)chatServerPort;
this._logger = this._loggerFactory.CreateLogger<ExDbClient>();
_ = Task.Run(this.ConnectAsync);
}
/// <summary>
/// Disconnects the exDB server.
/// </summary>
public async ValueTask DisconnectAsync()
{
if (this._connection is { } connection)
{
await connection.DisconnectAsync().ConfigureAwait(false);
}
}
private async ValueTask ConnectAsync()
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
while (!socket.Connected)
{
try
{
await socket.ConnectAsync(this._host, this._port).ConfigureAwait(false);
}
catch
{
this._logger.LogWarning($"Connection to ExDB-Server ({this._host}:{this._port}) failed, trying again in 10 Seconds...");
await Task.Delay(10000).ConfigureAwait(false);
}
}
this._logger.LogInformation("Connection to ExDB-Server established");
this._connection = new Connection(SocketConnection.Create(socket), null, null, this._loggerFactory.CreateLogger<Connection>());
this._connection.PacketReceived += this.ExDbPacketReceivedAsync;
this._connection.Disconnected += this.ConnectAsync;
await this.SendHelloAsync().ConfigureAwait(false);
await this._connection!.BeginReceiveAsync().ConfigureAwait(false);
}
private async ValueTask SendHelloAsync()
{
// C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00 ...
int Write()
{
var length = 0x3A;
var span = this._connection!.Output.GetSpan(length)[..length];
var packet = span;
packet[0] = 0xC1;
packet[1] = 0x3A;
packet[3] = 0x02;
packet[4] = this._chatServerPort.GetLowByte();
packet[5] = this._chatServerPort.GetHighByte();
packet.Slice(6).WriteString("ChatServer", Encoding.UTF8);
return length;
}
await this._connection!.SendAsync(Write).ConfigureAwait(false);
this._logger.LogInformation("Sent registration packet to ExDB-Server");
}
/// <summary>
/// Is called when a packet is received from the exDB-Server.
/// </summary>
/// <param name="sequence">The packet.</param>
private async ValueTask ExDbPacketReceivedAsync(ReadOnlySequence<byte> sequence)
{
try
{
sequence.CopyTo(this._packetBuffer);
var packet = this._packetBuffer.AsMemory(0, (int)sequence.Length);
var type = packet.Span[0];
if (type != 0xC1)
{
this._logger.LogWarning($"Unknown packet received from ExDB-Server, type: {type}");
return;
}
var code = packet.Span[2];
switch (code)
{
case 0xA0:
await this.ReadChatRoomCreationAsync(packet).ConfigureAwait(false);
break;
case 0xA1:
await this.ReadChatRoomInvitationAsync(packet).ConfigureAwait(false);
break;
default:
this._logger.LogWarning($"Unknown packet received from ExDB-Server, code: {code}");
break;
}
}
catch (Exception exception)
{
this._logger.LogError(exception, $"An error occurred while processing an incoming packet from ExDB: {this._packetBuffer.AsString()}");
}
}
/// <summary>
/// Reads the invitation to an existing chat room and registers the invited client.
/// </summary>
/// <param name="packet">The packet.</param>
/// <remarks>
/// Example: C1 15 A1 00 00 00 61 62 63 64 65 66 67 68 69 6F 20 01 00 01 57
/// Index 4 and 5 is the room id, the next 10 bytes is the client name, after that the player id, game server id and a "type".
/// The chat server answers this with the same packets as above(ticket 96862210):
/// C1 2C A0 01 00 00 61 62 63 64 65 66 67 68 69 6F CC CC CC CC CC CC CC CC CC CC 53 54 55 56 CC CC 02 00 C6 05 CC CC CC CC 57 CC CC CC.
/// </remarks>
private async ValueTask ReadChatRoomInvitationAsync(Memory<byte> packet)
{
ushort roomId = 0;
string clientName = string.Empty;
ushort clientPlayerId = 0;
ushort clientServerId = 0;
byte type = 0;
void Extract(Span<byte> packet)
{
roomId = NumberConversionExtensions.MakeWord(packet[4], packet[5]);
clientName = packet.ExtractString(6, 10, Encoding.UTF8);
clientPlayerId = packet.TryMakeWordBigEndian(16);
clientServerId = packet.TryMakeWordBigEndian(18);
type = packet.Length > 20 ? packet[20] : (byte)0x57;
}
Extract(packet.Span);
this._logger.LogDebug($"Received request to invite {clientName} to chat room {roomId}, Client-ID: {clientPlayerId}, Server-ID: {clientServerId}");
if (await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false) is { } authentication)
{
await this.SendAuthenticationAsync(authentication, null, clientPlayerId, clientServerId, type).ConfigureAwait(false);
}
}
/// <summary>
/// Reads the chat room creation message, creates a new chat room and registers the clients.
/// </summary>
/// <param name="packet">The packet.</param>
/// <remarks>
/// For example, we get here the following packet in:
/// C1 20 A0 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 E0 2E 01 00 E1 2E 01 00
/// This packet includes the header and both names of the creator and the invited chat partner (each 10 bytes long).
/// The server should then send the following data back to the exDB-Server:
/// s | rid ||-----client name-----------||---------other client name-||plid| |svid||---| |-ticket--| |--------???----------|
/// C1 2C A0 01 00 00 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 00 00 00 CC CC 00 00 11 04 CC CC CC CC 00 CC CC CC
/// C1 2C A0 01 00 00 50 51 52 53 54 55 56 57 58 59 41 42 43 44 45 46 47 48 49 4A 00 00 00 00 CC CC 01 00 BB 05 CC CC CC CC 01 CC CC CC.
/// </remarks>
private async ValueTask ReadChatRoomCreationAsync(Memory<byte> packet)
{
string clientName = string.Empty;
string friendName = string.Empty;
ushort clientPlayerId = 0;
ushort clientServerId = 0;
ushort friendPlayerId = 0;
ushort friendServerId = 0;
void Extract(Span<byte> packet)
{
clientName = packet.ExtractString(3, 10, Encoding.UTF8);
friendName = packet.ExtractString(13, 10, Encoding.UTF8);
clientPlayerId = packet.TryMakeWordBigEndian(24);
clientServerId = packet.TryMakeWordBigEndian(26);
friendPlayerId = packet.TryMakeWordBigEndian(28);
friendServerId = packet.TryMakeWordBigEndian(30);
}
Extract(packet.Span);
var roomId = await this._chatServer.CreateChatRoomAsync().ConfigureAwait(false);
this._logger.LogDebug($"Received request to create chat room for {clientName} and {friendName}; Room-ID: {roomId}; Client-ID: {clientPlayerId}; Server-ID: {clientServerId}; Friend-ID: {friendPlayerId}; Friend-Server: {friendServerId}");
var requesterAuthentication = await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false);
var friendAuthentication = await this._chatServer.RegisterClientAsync(roomId, friendName).ConfigureAwait(false);
if (requesterAuthentication is not null)
{
await this.SendAuthenticationAsync(requesterAuthentication, friendAuthentication, clientPlayerId, clientServerId, requesterAuthentication.Index).ConfigureAwait(false);
}
if (friendAuthentication is not null)
{
await this.SendAuthenticationAsync(friendAuthentication, requesterAuthentication, friendPlayerId, friendServerId, friendAuthentication.Index).ConfigureAwait(false);
}
}
/// <summary>
/// Sends the authentication information back to the ExDB-Server.
/// </summary>
/// <param name="authenticationInfo">The authentication information.</param>
/// <param name="friendAuthenticationInfo">The friend authentication information.</param>
/// <param name="clientId">The client identifier on the server where the client plays on.</param>
/// <param name="serverId">The server identifier where the client plays on.</param>
/// <param name="type">The type. Usually 0 for the player who requested the chat and 1 for the other player.</param>
private async ValueTask SendAuthenticationAsync(ChatServerAuthenticationInfo authenticationInfo, ChatServerAuthenticationInfo? friendAuthenticationInfo, ushort clientId, ushort serverId, byte type)
{
this._logger.LogDebug($"Registered client {authenticationInfo.ClientName} with index {authenticationInfo.Index} and token {authenticationInfo.AuthenticationToken}");
var token = uint.Parse(authenticationInfo.AuthenticationToken);
uint friendToken = 0;
if (friendAuthenticationInfo != null)
{
friendToken = uint.Parse(friendAuthenticationInfo.AuthenticationToken);
}
var roomId = authenticationInfo.RoomId;
int Write()
{
var length = 0x2C;
var packet = this._connection!.Output.GetSpan(length);
packet[0] = 0xC1;
packet[1] = 0x2C;
packet[2] = 0xA0;
packet[3] = 0x01;
WriteUInt16LittleEndian(packet.Slice(4), roomId);
packet.Slice(6).WriteString(authenticationInfo.ClientName, Encoding.UTF8);
if (friendAuthenticationInfo != null)
{
packet.Slice(16).WriteString(friendAuthenticationInfo.ClientName, Encoding.UTF8);
}
WriteUInt16LittleEndian(packet.Slice(26), clientId);
WriteUInt16LittleEndian(packet.Slice(28), serverId);
WriteUInt32LittleEndian(packet.Slice(32), token);
WriteUInt32LittleEndian(packet.Slice(36), friendToken);
packet[40] = type;
return length;
}
await this._connection!.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AssemblyName>ChatServer</AssemblyName>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>bin\Release\</OutputPath>
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ChatServer.cfg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AssemblyName>ChatServer</AssemblyName>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>bin\Release\</OutputPath>
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ChatServer.cfg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
// <copyright file="Program.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
using System.ComponentModel.Design;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MUnique.OpenMU.ChatServer;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
using Serilog;
using Serilog.Debugging;
/// <summary>
/// The main entry class of the application.
/// </summary>
internal class Program
{
private static ILogger<Program> _logger = NullLogger<Program>.Instance;
/// <summary>
/// The main entry point for the application.
/// </summary>
/// <param name="args">The arguments. </param>
internal static async Task Main(string[] args)
{
SelfLog.Enable(Console.Error);
var logConfiguration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.Build();
var logger = new LoggerConfiguration()
.ReadFrom.Configuration(logConfiguration)
.CreateLogger();
var loggerFactory = new LoggerFactory().AddSerilog(logger);
_logger = loggerFactory.CreateLogger<Program>();
var addressResolver = IpAddressResolverFactory.CreateIpResolver(args, null, loggerFactory);
var settings = new Settings("ChatServer.cfg");
var serviceContainer = new ServiceContainer();
serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
int chatServerListenerPort = settings.ChatServerListenerPort ?? 55980;
int exDbPort = settings.ExDbPort ?? 55906;
string exDbHost = settings.ExDbHost ?? "127.0.0.1";
try
{
// To make the chat server use our configured encryption key, we need to trick a bit. We add an endpoint with a special client version which is defined in the plugin.
var configuration = new ChatServerSettings();
configuration.Endpoints.Add(new ChatServerEndpoint { ClientVersion = ConfigurableNetworkEncryptionPlugIn.Version, NetworkPort = chatServerListenerPort });
var pluginManager = new PlugInManager(null, loggerFactory, serviceContainer, null);
pluginManager.DiscoverAndRegisterPlugInsOf<INetworkEncryptionFactoryPlugIn>();
var chatServer = new ChatServer(addressResolver, loggerFactory, pluginManager);
chatServer.Initialize(configuration);
await chatServer.StartAsync().ConfigureAwait(false);
var exDbClient = new ExDbClient(exDbHost, exDbPort, chatServer, chatServerListenerPort, loggerFactory);
_logger.LogInformation("ChatServer started and ready");
while (Console.ReadLine() != "exit")
{
// keep application running
}
await exDbClient.DisconnectAsync().ConfigureAwait(false);
await chatServer.ShutdownAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Unexpected error occured");
}
}
}

View File

@@ -0,0 +1,12 @@
// <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.ChatServer.ExDbConnector")]
[assembly: InternalsVisibleTo("MUnique.OpenMU.ChatServer.ExDbConnector.Tests")]

View File

@@ -0,0 +1,156 @@
# ChatServer ExDB Connector
This isn't directly a part of the OpenMU project. It's more like a side product
to make the ChatServer available to users of the 'classical' private MU Servers.
They have - or maybe had :) - the problem that they are bound to use the
original closed source ChatServer of Webzen, if they get it working at all.
So to offer an open source alternative to the original ChatServer of Webzen,
you can use this project to connect the OpenMU-ChatServer with your 'classic'
ExDB server.
## Configuration
To make this work correctly with an existing ExDB-Server, some might do some
minor adjustments in the configuration.
It's all configured in the ChatServer.cfg an should be self-explanatory.
### ChatServerListenerPort
It's the port to which the game clients should connect. Default is 55980,
but I'm not sure if it can be changed without modifying the client.
### ExDbHost and Port
The host and tcp port of the ExDB server. Usually it's on the same server, so
127.0.0.1 on port 55906.
### Xor32Key
This one is actually very important to get right. Otherwise, the game clients
will not be able to connect.
It's the same XOR32 key which is used for the 0xC1 packet encryption from game
client to game server.
You can't edit this key at the original ChatServer of Webzen, that's the reason
why it's pretty hard to get the ChatServer working on a private server.
## Communication between ExDB-Server and ChatServer
The ExDB server usually leaves the tcp port 55906 open, so that the ChatServer
(and maybe other kind of subservers?) can connect to it.
### Registration
When the ChatServer connects to the ExDB server, it sends a data packet to
register itself. It has the following struture:
| Length | Data type | Value | Description |
|----------|---------|-------------|---------|
| 1 | byte | 0xC1 | Packet header - type |
| 1 | byte | 0x3A | Packet header - length of the packet |
| 1 | byte | 0x00 | Packet Type "server registration" |
| 1 | byte | 0x02 | Id for "ChatServer" |
| 2 | ushort | 0xDAAC | ChatServer client port (default: 55980) |
| 11 | string | "ChatServer" | ChatServer name |
Example: C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00
From now, the ChatServer will receive chat room creation and invitation
requests from the ExDB Server, which were previously requested by the players.
### Chat Room Creation Request
When a client requests to create a new chat room, the following data packet is
sent from the ExDB Server to the ChatServer.
| Length | Data type | Value | Description |
|----------|---------|-------------|---------|
| 1 | byte | 0xC1 | Packet header - type |
| 1 | byte | 0x25 | Packet header - length of the packet |
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
| 10 | string | | Name of the character who wants to create the room |
| 10 | string | | Name of the character who should be invited to the room |
| 1 | byte | 0x01 | "Type", not relevant? |
| 2 | ushort | | Player id of the character who wants to create the room, big endian |
| 2 | ushort | | Server id of the character who wants to create the room, big endian |
| 2 | ushort | | Player id of the character who should be invited, big endian |
| 2 | ushort | | Server id of the character who should be invited, big endian |
Example:
C1 25 A0 41 42 43 44 45
46 47 48 49 4A 50 51 52
53 54 55 56 57 58 59 01
20 01 00 01 20 02 00 01
### Chat Room Creation Responses
For each of both players, there is one data packet sent back to the ExDB Server:
| Length | Data type | Value | Description |
|----------|---------|-------------|---------|
| 1 | byte | 0xC1 | Packet header - type |
| 1 | byte | 0x2C | Packet header - length of the packet |
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
| 1 | byte | 0x01 | Success flag |
| 2 | ushort | | Chat room id, big endian |
| 10 | string | | Name of the character to which a chat room invitation should be sent |
| 10 | string | | Name of the chat partner character |
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
| 2 | byte | | Padding bytes for the alignment of the following authentication token |
| 4 | uint | | Authentication token of the character to which a chat room invitation should be sent, big endian |
| 4 | uint | | Authentication token of the chat partner, big endian |
| 1 | byte | | 'Type' |
| 3 | byte | | Don't know - padding?|
Example First Player:
C1 2C A0 01 00 00 41 42
43 44 45 46 47 48 49 4A
50 51 52 53 54 55 56 57
58 59 00 00 00 00 CC CC
00 00 11 04 01 00 BB 05
00 CC CC CC
Example Second Player:
C1 2C A0 01 00 00 50 51
52 53 54 55 56 57 58 59
41 42 43 44 45 46 47 48
49 4A 00 00 00 00 CC CC
01 00 BB 05 00 00 11 04
01 CC CC CC
### Chat Room Invitation Request
When a client requests to invite another friend to an existing chat room, the
following data packet is sent from the ExDB Server to the ChatServer.
| Length | Data type | Value | Description |
|----------|---------|-------------|---------|
| 1 | byte | 0xC1 | Packet header - type |
| 1 | byte | 0x16 | Packet header - length of the packet |
| 1 | byte | 0xA1 | Packet Type 'chat room invitation' |
| 1 | byte | 0x00 | Padding |
| 2 | ushort | | Chat room id, big endian |
| 10 | string | | Name of the character who should be invited to the room |
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
| 1 | byte | | 'Type' |
Example:
C1 15 A1 00 00 00 61 62
63 64 65 66 67 68 69 6F
01 20 01 00 57
The ChatServer answers this with the same packet as above, but without filling
the second character name - no wonder, there is more than one player in the
room already.
Example:
C1 2C A0 01 00 00 61 62
63 64 65 66 67 68 69 6F
CC CC CC CC CC CC CC CC
CC CC 01 20 01 00 CC CC
02 00 C6 05 CC CC CC CC
57 CC CC CC

View File

@@ -0,0 +1,121 @@
// <copyright file="Settings.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
using System.Globalization;
using System.IO;
/// <summary>
/// A class which reads settings from a file.
/// Line Format:
/// [Key]=[Value]
/// Line comments can be added by starting with "#".
/// </summary>
internal class Settings
{
private readonly IDictionary<string, string> _settingsDictionary = new Dictionary<string, string>();
/// <summary>
/// Initializes a new instance of the <see cref="Settings"/> class.
/// Reads the file contents in, if the file is available.
/// </summary>
/// <param name="file">The file.</param>
public Settings(string file)
{
if (!File.Exists(file))
{
return;
}
foreach (var line in File.ReadAllLines(file))
{
var elements = line.Split('=');
if (elements.Length > 1
&& !elements[0].StartsWith("#", StringComparison.InvariantCulture)
&& !this._settingsDictionary.ContainsKey(elements[0]))
{
this._settingsDictionary.Add(elements[0], elements[1]);
}
}
}
/// <summary>
/// Gets the configured chat server listener port.
/// </summary>
public int? ChatServerListenerPort
{
get
{
if (this["ChatServerListenerPort"] != null && int.TryParse(this["ChatServerListenerPort"], out var result))
{
return result;
}
return default;
}
}
/// <summary>
/// Gets the configured exDb server port.
/// </summary>
public int? ExDbPort
{
get
{
if (this["ExDbPort"] != null)
{
if (int.TryParse(this["ExDbPort"], out int result))
{
return result;
}
return default;
}
return null;
}
}
/// <summary>
/// Gets the configured exDb server host.
/// </summary>
public string? ExDbHost => this["ExDbHost"];
/// <summary>
/// Gets the configured xor32 key.
/// </summary>
public byte[]? Xor32Key
{
get
{
if (this["Xor32Key"] != null)
{
var customXor32KeyList = new List<byte>();
var keyAsString = this["Xor32Key"];
if (keyAsString is not null)
{
var bytesAsString = keyAsString.Split(' ');
foreach (var byteString in bytesAsString)
{
customXor32KeyList.Add(byte.Parse(byteString, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
}
}
return customXor32KeyList.ToArray();
}
return null;
}
}
private string? this[string key]
{
get
{
this._settingsDictionary.TryGetValue(key, out var value);
return value;
}
}
}

View File

@@ -0,0 +1,38 @@
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Fatal",
"System": "Fatal",
"Npgsql": "Information",
"MUnique.OpenMU.Network.Connection": "Error",
"MUnique": "Information"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/log.txt",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}",
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 4194304,
"retainedFileCountLimit": 48,
"rollingInterval": "Hour"
}
}
],
"Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "MUnique.OpenMU.ChatServer.ExDbConnector"
}
}
}