Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Realm #18

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions etc/atlasd.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@
"port": 6112,
"tcp_nodelay": true
},
"realm_listener": {
"interface": "0.0.0.0",
"port": 6113,
"tcp_nodelay": true
},
"realm": {
"host": "Anonymous",
"name": "Atlas"
Expand Down
6 changes: 4 additions & 2 deletions src/Atlasd/Battlenet/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,8 @@ public void SquelchUpdate(GameState client)
{
foreach (var user in Users)
{
new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(client, user), user.Ping, RenderOnlineName(client, user), user.Statstring).WriteTo(client.Client);
//NOTE: statstrings are not sent on EID_USERUPDATE, doing so causes a rendering bug with Diablo II hardcore characters gaining ops
new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(client, user), user.Ping, RenderOnlineName(client, user), "").WriteTo(client.Client);
}
}
}
Expand Down Expand Up @@ -925,7 +926,8 @@ public bool UpdateUser(GameState client, Account.Flags flags, Int32 ping, byte[]
{
foreach (var user in Users)
{
new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(user, client), client.Ping, RenderOnlineName(user, client), client.Statstring).WriteTo(user.Client);
//NOTE: statstrings are not sent on EID_USERUPDATE, doing so causes a rendering bug with Diablo II hardcore characters gaining ops
new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(user, client), client.Ping, RenderOnlineName(user, client), "").WriteTo(user.Client);
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/Atlasd/Battlenet/ClientState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Atlasd.Battlenet.Protocols.Game;
using Atlasd.Battlenet.Protocols.Game.Messages;
using Atlasd.Daemon;
using Atlasd.Helpers;
using Atlasd.Localization;
using System;
using System.Collections.Generic;
Expand All @@ -22,6 +23,7 @@ class ClientState
public bool IsClosing { get; private set; } = false;

public GameState GameState { get; private set; }
public RealmState RealmState { get; set; }
public ProtocolType ProtocolType { get; private set; }
public EndPoint RemoteEndPoint { get; private set; }
public IPAddress RemoteIPAddress { get; private set; }
Expand Down Expand Up @@ -563,5 +565,27 @@ public void SocketIOCompleted_External(object sender, SocketAsyncEventArgs e)

SocketIOCompleted(sender, e);
}

// exists at this level because state split across two component objects
public byte[] GenerateDiabloIIStatstring()
{
var statstring = Product.ToByteArray(GameState.Product);

if (RealmState.ActiveCharacter != null)
{
var partial = new byte[][]
{
"Olympus".ToBytes(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be made into a configurable. Perhaps you want to use this?

var realmName = Settings.GetString(new string[] { "realm", "name" });

new byte[] { 0x2C }, //comma
RealmState.ActiveCharacter.Name.ToBytes(),
new byte[] { 0x2C }, //comma
RealmState.ActiveCharacter.Statstring.ToBytes()
}.SelectMany(x => x).ToArray();

statstring = statstring.Concat(partial).ToArray();
}

return statstring;
}
}
}
38 changes: 37 additions & 1 deletion src/Atlasd/Battlenet/Common.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Atlasd.Battlenet.Protocols.Game;
using Atlasd.Battlenet.Protocols.Game.Messages;
using Atlasd.Battlenet.Protocols.MCP.Models;
using Atlasd.Battlenet.Protocols.Udp;
using Atlasd.Daemon;
using Atlasd.Localization;
Expand Down Expand Up @@ -42,13 +43,17 @@ public ShutdownEvent(string adminMessage, bool cancelled, DateTime eventDate, Ti
public static ConcurrentDictionary<string, Channel> ActiveChannels;
public static ConcurrentDictionary<byte[], Clan> ActiveClans;
public static ConcurrentDictionary<Socket, ClientState> ActiveClientStates;
public static ConcurrentDictionary<UInt32, ClientState> RealmClientStates;
public static List<GameAd> ActiveGameAds;
public static ConcurrentDictionary<string, GameState> ActiveGameStates;
public static Realm Realm;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alphabetize this by property name

public static IPAddress DefaultAddress { get; private set; }
public static int DefaultPort { get; private set; }
public static ServerSocket Listener { get; private set; }
public static RealmSocket RealmListener { get; private set; }
public static IPAddress ListenerAddress { get; private set; }
public static IPEndPoint ListenerEndPoint { get; private set; }
public static IPEndPoint RealmListenerEndPoint { get; private set; }
public static int ListenerPort { get; private set; }
public static Timer NullTimer { get; private set; }
public static Timer PingTimer { get; private set; }
Expand Down Expand Up @@ -102,14 +107,17 @@ public static void Initialize()
ActiveChannels = new ConcurrentDictionary<string, Channel>(StringComparer.OrdinalIgnoreCase);
ActiveClans = new ConcurrentDictionary<byte[], Clan>();
ActiveClientStates = new ConcurrentDictionary<Socket, ClientState>();
RealmClientStates = new ConcurrentDictionary<UInt32, ClientState>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alphabetize this

ActiveGameAds = new List<GameAd>();
ActiveGameStates = new ConcurrentDictionary<string, GameState>(StringComparer.OrdinalIgnoreCase);
Realm = new Realm();

InitializeAds();

DefaultAddress = IPAddress.Any;
DefaultPort = 6112;
InitializeListener();
InitializeRealmListener();

NullTimer = new Timer(ProcessNullTimer, ActiveGameStates, 100, 100);
PingTimer = new Timer(ProcessPingTimer, ActiveGameStates, 100, 100);
Expand Down Expand Up @@ -201,7 +209,35 @@ private static void InitializeListener()
UdpListener = new UdpListener(ListenerEndPoint);
Listener = new ServerSocket(ListenerEndPoint);
}


private static void InitializeRealmListener()
{
Settings.State.RootElement.TryGetProperty("battlenet", out var battlenetJson);
battlenetJson.TryGetProperty("realm_listener", out var listenerJson);
listenerJson.TryGetProperty("interface", out var interfaceJson);
listenerJson.TryGetProperty("port", out var portJson);

var listenerAddressStr = interfaceJson.GetString();
Comment on lines +215 to +220
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be refactored using the settings getter.

string listenerAddressStr = Settings.Get(new string[] { "battlenet", "realm_listener", "interface", "port" }, default: string.Empty);

if (!IPAddress.TryParse(listenerAddressStr, out IPAddress listenerAddress))
{
Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Unable to parse IP address from [battlenet.realm_listener.interface] with value [{listenerAddressStr}]; using any");
listenerAddress = DefaultAddress;
}
ListenerAddress = listenerAddress;

portJson.TryGetInt32(out var port);
ListenerPort = port;

if (!IPEndPoint.TryParse($"{ListenerAddress}:{ListenerPort}", out IPEndPoint listenerEndPoint))
{
Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Unable to parse endpoint with value [{ListenerAddress}:{ListenerPort}]");
return;
}
ListenerEndPoint = listenerEndPoint;

RealmListener = new RealmSocket(ListenerEndPoint);
}

public static uint GetActiveClientCountByProduct(Product.ProductCode productCode)
{
var count = (uint)0;
Expand Down
11 changes: 11 additions & 0 deletions src/Atlasd/Battlenet/Exceptions/RealmProtocolException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Atlasd.Battlenet.Exceptions
{
class RealmProtocolException : ProtocolException
{
public RealmProtocolException(ClientState client) : base(Battlenet.ProtocolType.Types.Game, client) { }
public RealmProtocolException(ClientState client, string message) : base(Battlenet.ProtocolType.Types.Game, client, message) { }
public RealmProtocolException(ClientState client, string message, Exception innerException) : base(Battlenet.ProtocolType.Types.Game, client, message, innerException) { }
}
}
21 changes: 20 additions & 1 deletion src/Atlasd/Battlenet/Protocols/Game/Messages/SID_ENTERCHAT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Text;
using Atlasd.Helpers;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alphabetize the using list


namespace Atlasd.Battlenet.Protocols.Game.Messages
{
Expand All @@ -26,6 +27,7 @@ public SID_ENTERCHAT(byte[] buffer)
public override bool Invoke(MessageContext context)
{
if (context == null || context.Client == null || !context.Client.Connected) return false;
var realmState = context.Client.RealmState;
var gameState = context.Client.GameState;

switch (context.Direction)
Expand Down Expand Up @@ -68,7 +70,24 @@ public override bool Invoke(MessageContext context)

if (Product.IsDiabloII(gameState.Product))
{
statstring = Product.ToByteArray(gameState.Product);
// for some reason, the client sends SID_ENTERCHAT with no MCP_CHARLOGON, right after MCP_CHARCREATE
// i'm not certain this is the correct design, but it works for now
var providedStatstring = statstring.AsString();
var tokens = providedStatstring.Split(",");
var realm = tokens[0];
var name = tokens[1];

if (realm.Length > 0 && realmState != null)
{
var character = Battlenet.Common.Realm.GetCharacter(accountName, name);
if (character != null)
{
realmState.ActiveCharacter = character;
gameState.CharacterName = character.Name.ToBytes();
}
}

statstring = context.Client.GenerateDiabloIIStatstring();
}

// Do not use client-provided statstring if config.battlenet.emulation.statstring_updates is not enabled for this product.
Expand Down
85 changes: 78 additions & 7 deletions src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Atlasd.Helpers;
using Atlasd.Utilities;

namespace Atlasd.Battlenet.Protocols.Game.Messages
{
Expand Down Expand Up @@ -62,8 +66,6 @@ public override bool Invoke(MessageContext context)
}
case MessageDirection.ServerToClient:
{
var clientToken = (UInt32)context.Arguments["clientToken"];

/**
* [Note this format is slightly different from BNETDocs reference as of 2023-02-18]
* (UINT32) MCP Cookie (Client Token)
Expand All @@ -75,15 +77,84 @@ public override bool Invoke(MessageContext context)
* (STRING) Battle.net unique name (* as of D2 1.14d, this is empty)
*/

Buffer = new byte[8]; // MCP Cookie + MCP Status only; Atlas does not have realm/MCP servers implemented yet.
if (((byte[])context.Arguments["realmTitle"]).AsString() == "Olympus")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it configurable

{
Buffer = new byte[18 * 4 + 1];

using var m = new MemoryStream(Buffer);
using var w = new BinaryWriter(m);
using var m = new MemoryStream(Buffer);
using var w = new BinaryWriter(m);

var cookie = (UInt32)(new Random().Next(int.MinValue, int.MaxValue));

w.Write((UInt32)cookie);
w.Write((UInt32)0x00000000); // status

var chunk1 = new UInt32[]
{
0x33316163,
0x65303830
}.GetBytes();

w.Write(chunk1);

#if DEBUG
string ipString = "127.0.0.1";
#else
string ipString = NetworkUtilities.GetPublicAddress();
#endif

IPAddress ipAddress = IPAddress.Parse(ipString);

w.Write(ipAddress.GetBytes());

w.Write((UInt32)clientToken);
w.Write((UInt32)Statuses.RealmUnavailable); // Atlas does not have realm/MCP servers implemented yet.
Settings.State.RootElement.TryGetProperty("battlenet", out var battlenetJson);
battlenetJson.TryGetProperty("realm_listener", out var listenerJson);
listenerJson.TryGetProperty("interface", out var interfaceJson);
listenerJson.TryGetProperty("port", out var portJson);

portJson.TryGetUInt16(out var port);
Comment on lines +110 to +115
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be refactored using the settings getter.

ushort port = Settings.Get(new string[] { "battlenet", "interface", "port" }, default: 0);


// strange protocol design
ushort hostOrderPort = port;
UInt32 networkOrderPort = (UInt32)IPAddress.HostToNetworkOrder((short)hostOrderPort);

w.Write(networkOrderPort);

// this could use some love
var chunk2 = new UInt32[]
{
0x66663162,
0x34613566,
0x64326639,
0x63336330,
0x38326135,
0x39663937,
0x62653134,
0x36313861,
0x36353032,
0x31353066,
0x00000000,
0x00000000
}.GetBytes();

w.Write(chunk2);
w.WriteByteString(Encoding.UTF8.GetBytes(""));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string.Empty is preferred over ""


// to allow associating game and realm connections after MCP_STARTUP
Battlenet.Common.RealmClientStates.TryAdd(cookie, context.Client);
}
else
{
Buffer = new byte[4];

using var m = new MemoryStream(Buffer);
using var w = new BinaryWriter(m);

w.Write((UInt32)Statuses.RealmUnavailable);
}

Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)");

context.Client.Send(ToByteArray(context.Client.ProtocolType));
return true;
}
Expand Down
13 changes: 7 additions & 6 deletions src/Atlasd/Battlenet/Protocols/Game/Messages/SID_QUERYREALMS2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ public override bool Invoke(MessageContext context)
}
case MessageDirection.ServerToClient:
{
Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)");

Dictionary<byte[], byte[]> realms =
context.Arguments == null || !context.Arguments.ContainsKey("realms") ?
new Dictionary<byte[], byte[]>() :
(Dictionary<byte[], byte[]>)context.Arguments["realms"];
Dictionary<byte[], byte[]> realms = new Dictionary<byte[], byte[]>
{
{
Encoding.UTF8.GetBytes("Olympus"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this configurable from the server config json

Encoding.UTF8.GetBytes("Diablo II Realm Server")
}
};

/**
* (UINT32) Unknown (0)
Expand Down
41 changes: 41 additions & 0 deletions src/Atlasd/Battlenet/Protocols/MCP/Frame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Concurrent;

namespace Atlasd.Battlenet.Protocols.MCP
{
class Frame
{
public ConcurrentQueue<Message> Messages { get; protected set; }

public Frame()
{
Messages = new ConcurrentQueue<Message>();
}

public Frame(ConcurrentQueue<Message> messages)
{
Messages = messages;
}

public byte[] ToByteArray(ProtocolType protocolType)
{
var framebuf = new byte[0];
var msgs = new ConcurrentQueue<Message>(Messages); // Clone Messages into local variable

while (msgs.Count > 0)
{
if (!msgs.TryDequeue(out var msg)) break;

var messagebuf = msg.ToByteArray(protocolType);
var buf = new byte[framebuf.Length + messagebuf.Length];

Buffer.BlockCopy(framebuf, 0, buf, 0, framebuf.Length);
Buffer.BlockCopy(messagebuf, 0, buf, framebuf.Length, messagebuf.Length);

framebuf = buf;
}

return framebuf;
}
}
}
Loading