Skip to content
Draft
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
14 changes: 14 additions & 0 deletions DistrictServerCSharp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bin/
obj/
Logs/
_scratch_verify/
_scratch_drive/
_drive_scratch/
_build_scratch_pub/
_ds_cs_publish/
_map_ds.py
_bracecheck.py
_verify_ds.py
# The repo root .gitignore's NuGet `packages/` pattern would otherwise match
# this source folder on case-insensitive filesystems; re-include it.
!Packages/
291 changes: 291 additions & 0 deletions DistrictServerCSharp/Accounts/Account.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
namespace DistrictServerCSharp.Accounts;

/// <summary>
/// Per-account state for one client on the district server, ported from
/// Account.cpp. Every accessor is guarded by a lock because the UDP receive
/// thread and the TCP world-control thread can touch the same account.
/// </summary>
public sealed class Account
{
public enum HandshakeState
{
WaitingForAuth,
ChallengeSent,
Complete
}

private readonly object _gate = new();
private readonly uint _id;
private byte[] _authToken = new byte[20];
private byte[] _encryptionKey = new byte[16];
private bool _characterProfileAvailable;
private uint _characterId;
private byte _characterFaction;
private byte _characterGender;
private byte _appearanceVersion;
private string _characterName = "";
private string _clanName = "";
private byte[] _appearance = Array.Empty<byte>();
private bool _endpointBound;
private uint _endpointAddress;
private ushort _endpointPort;
private bool _authenticated;
private uint _nextServerPacketId;
private ushort _nextServerReliableSequence = 1;
private uint _lastClientPacketId;
private uint _handshakeChallenge;
private HandshakeState _currentHandshakeState = HandshakeState.WaitingForAuth;
private uint _challengeSendCount;
private uint _udpReceiveCount;

public Account(uint id, byte[] authToken, byte[] encryptionKey)
{
_id = id;
if (authToken.Length >= 20)
Array.Copy(authToken, _authToken, 20);
if (encryptionKey.Length >= 16)
Array.Copy(encryptionKey, _encryptionKey, 16);
}

public uint GetId()
{
lock (_gate) return _id;
}

/// <summary>
/// Rebinds the account to a fresh connection: new keys, endpoint unbound,
/// handshake and sequence counters reset. This is the hook that lets a
/// relaunched client reconnect without inheriting the previous session's
/// state.
/// </summary>
public void UpdateKeys(byte[] authToken, byte[] encryptionKey)
{
lock (_gate)
{
if (authToken.Length >= 20)
Array.Copy(authToken, _authToken, 20);
if (encryptionKey.Length >= 16)
Array.Copy(encryptionKey, _encryptionKey, 16);
_endpointBound = false;
_endpointAddress = 0;
_endpointPort = 0;
_authenticated = false;
_nextServerPacketId = 0;
_nextServerReliableSequence = 1;
_lastClientPacketId = 0;
_handshakeChallenge = 0;
_currentHandshakeState = HandshakeState.WaitingForAuth;
_challengeSendCount = 0;
_udpReceiveCount = 0;
}
}

public byte[] GetAuthToken()
{
lock (_gate) return (byte[])_authToken.Clone();
}

public byte[] GetEncryptionKey()
{
lock (_gate) return (byte[])_encryptionKey.Clone();
}

public void SetCharacterProfile(
uint characterId,
byte faction,
byte gender,
byte appearanceVersion,
string characterName,
string clanName,
byte[] appearance)
{
lock (_gate)
{
_characterProfileAvailable = characterId != 0;
_characterId = characterId;
_characterFaction = faction;
_characterGender = gender;
_appearanceVersion = appearanceVersion;
_characterName = characterName;
_clanName = clanName;
_appearance = (byte[])appearance.Clone();
}
}

public void ClearCharacterProfile()
{
lock (_gate)
{
_characterProfileAvailable = false;
_characterId = 0;
_characterFaction = 0;
_characterGender = 0;
_appearanceVersion = 0;
_characterName = "";
_clanName = "";
_appearance = Array.Empty<byte>();
}
}

public bool HasCharacterProfile()
{
lock (_gate) return _characterProfileAvailable;
}

public uint GetCharacterId()
{
lock (_gate) return _characterId;
}

public byte GetCharacterFaction()
{
lock (_gate) return _characterFaction;
}

public byte GetCharacterGender()
{
lock (_gate) return _characterGender;
}

public byte GetAppearanceVersion()
{
lock (_gate) return _appearanceVersion;
}

public string GetCharacterName()
{
lock (_gate) return _characterName;
}

public string GetClanName()
{
lock (_gate) return _clanName;
}

public int GetAppearanceSize()
{
lock (_gate) return _appearance.Length;
}

public byte[] GetAppearance()
{
lock (_gate) return (byte[])_appearance.Clone();
}

public void BindEndpoint(uint addressNetworkOrder, ushort portHostOrder)
{
lock (_gate)
{
_endpointAddress = addressNetworkOrder;
_endpointPort = portHostOrder;
_endpointBound = true;
}
}

public bool MatchesEndpoint(uint addressNetworkOrder, ushort portHostOrder)
{
lock (_gate)
{
return _endpointBound &&
_endpointAddress == addressNetworkOrder &&
_endpointPort == portHostOrder;
}
}

public bool HasEndpoint()
{
lock (_gate) return _endpointBound;
}

public uint GetEndpointAddress()
{
lock (_gate) return _endpointAddress;
}

public ushort GetEndpointPort()
{
lock (_gate) return _endpointPort;
}

public void SetAuthenticated(bool value)
{
lock (_gate) _authenticated = value;
}

public bool IsAuthenticated()
{
lock (_gate) return _authenticated;
}

public uint AllocateServerPacketId()
{
lock (_gate)
{
uint value = _nextServerPacketId;
_nextServerPacketId = (_nextServerPacketId + 1u) & 0x3FFFFFFFu;
return value;
}
}

public ushort AllocateServerReliableSequence()
{
lock (_gate)
{
ushort value = _nextServerReliableSequence;
_nextServerReliableSequence = (ushort)((_nextServerReliableSequence + 1u) % 1024u);
if (_nextServerReliableSequence == 0)
_nextServerReliableSequence = 1;
return value;
}
}

public void SetLastClientPacketId(uint value)
{
lock (_gate) _lastClientPacketId = value;
}

public uint GetLastClientPacketId()
{
lock (_gate) return _lastClientPacketId;
}

public void SetHandshakeChallenge(uint value)
{
lock (_gate) _handshakeChallenge = value;
}

public uint GetHandshakeChallenge()
{
lock (_gate) return _handshakeChallenge;
}

public void SetHandshakeState(HandshakeState value)
{
lock (_gate) _currentHandshakeState = value;
}

public HandshakeState GetHandshakeState()
{
lock (_gate) return _currentHandshakeState;
}

public uint IncrementChallengeSendCount()
{
lock (_gate) return ++_challengeSendCount;
}

public uint GetChallengeSendCount()
{
lock (_gate) return _challengeSendCount;
}

public uint IncrementUdpReceiveCount()
{
lock (_gate) return ++_udpReceiveCount;
}

public uint GetUdpReceiveCount()
{
lock (_gate) return _udpReceiveCount;
}
}
79 changes: 79 additions & 0 deletions DistrictServerCSharp/Accounts/AccountManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Net;
using DistrictServerCSharp.Net;

namespace DistrictServerCSharp.Accounts;

/// <summary>
/// The district's account registry, ported from the g_accounts list in
/// DistrictServer.cpp. Accounts arrive via the world-server character handoff
/// and are looked up by id (AUTH) or by UDP endpoint.
/// </summary>
public static class AccountManager
{
private static readonly object Gate = new();
private static readonly List<Account> Accounts = new();

public static Account? Find(uint id)
{
lock (Gate)
{
foreach (Account account in Accounts)
{
if (account.GetId() == id)
return account;
}
return null;
}
}

public static Account? FindByEndpoint(IPEndPoint endpoint)
{
uint address = EndpointAddress.ToWireValue(endpoint.Address);
ushort port = (ushort)endpoint.Port;

lock (Gate)
{
foreach (Account account in Accounts)
{
if (account.MatchesEndpoint(address, port))
return account;
}
return null;
}
}

/// <summary>All currently registered accounts (for remote-pawn viewers).</summary>
public static List<Account> GetAll()
{
lock (Gate)
{
return new List<Account>(Accounts);
}
}

/// <summary>
/// Finds the account by id, rebinding its keys for a reconnect, or creates
/// a fresh one. <paramref name="replaced"/> reports whether an existing
/// account was rebound.
/// </summary>
public static Account AddOrUpdate(uint id, byte[] authToken, byte[] encryptionKey, out bool replaced)
{
lock (Gate)
{
foreach (Account account in Accounts)
{
if (account.GetId() == id)
{
account.UpdateKeys(authToken, encryptionKey);
replaced = true;
return account;
}
}

var created = new Account(id, authToken, encryptionKey);
Accounts.Add(created);
replaced = false;
return created;
}
}
}
Loading