diff --git a/DistrictServerCSharp/.gitignore b/DistrictServerCSharp/.gitignore
new file mode 100644
index 0000000..fd75041
--- /dev/null
+++ b/DistrictServerCSharp/.gitignore
@@ -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/
diff --git a/DistrictServerCSharp/Accounts/Account.cs b/DistrictServerCSharp/Accounts/Account.cs
new file mode 100644
index 0000000..691ef32
--- /dev/null
+++ b/DistrictServerCSharp/Accounts/Account.cs
@@ -0,0 +1,291 @@
+namespace DistrictServerCSharp.Accounts;
+
+///
+/// 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.
+///
+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();
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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();
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/DistrictServerCSharp/Accounts/AccountManager.cs b/DistrictServerCSharp/Accounts/AccountManager.cs
new file mode 100644
index 0000000..b64b116
--- /dev/null
+++ b/DistrictServerCSharp/Accounts/AccountManager.cs
@@ -0,0 +1,79 @@
+using System.Net;
+using DistrictServerCSharp.Net;
+
+namespace DistrictServerCSharp.Accounts;
+
+///
+/// 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.
+///
+public static class AccountManager
+{
+ private static readonly object Gate = new();
+ private static readonly List 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;
+ }
+ }
+
+ /// All currently registered accounts (for remote-pawn viewers).
+ public static List GetAll()
+ {
+ lock (Gate)
+ {
+ return new List(Accounts);
+ }
+ }
+
+ ///
+ /// Finds the account by id, rebinding its keys for a reconnect, or creates
+ /// a fresh one. reports whether an existing
+ /// account was rebound.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/DistrictServerCSharp/Bits/BitReader.cs b/DistrictServerCSharp/Bits/BitReader.cs
new file mode 100644
index 0000000..adadf24
--- /dev/null
+++ b/DistrictServerCSharp/Bits/BitReader.cs
@@ -0,0 +1,170 @@
+namespace DistrictServerCSharp.Bits;
+
+///
+/// LSB-first bit reader, ported from the C++ BitReader in ApbUdp.cpp. Reads
+/// over a byte buffer between a begin and end bit position.
+///
+public sealed class BitReader
+{
+ private readonly byte[] _data;
+ private int _position;
+ private readonly int _end;
+
+ public BitReader(byte[] data, int beginBit, int endBit)
+ {
+ _data = data;
+ _position = beginBit;
+ _end = endBit;
+ }
+
+ public int Tell => _position;
+
+ public int Remaining => _position <= _end ? _end - _position : 0;
+
+ public bool ReadBit(out bool value)
+ {
+ if (!ReadBits(1, out uint temporary))
+ {
+ value = false;
+ return false;
+ }
+
+ value = temporary != 0;
+ return true;
+ }
+
+ public bool ReadBits(int count, out uint value)
+ {
+ value = 0;
+
+ if (count > 32 || count > Remaining)
+ return false;
+
+ for (int index = 0; index < count; ++index)
+ {
+ int absolute = _position + index;
+ uint bit = (uint)((_data[absolute / 8] >> (absolute % 8)) & 1);
+
+ value |= bit << index;
+ }
+
+ _position += count;
+ return true;
+ }
+
+ public bool ReadByte(out byte value)
+ {
+ if (!ReadBits(8, out uint temporary))
+ {
+ value = 0;
+ return false;
+ }
+
+ value = (byte)temporary;
+ return true;
+ }
+
+ ///
+ /// UE3's FBitReader::ReadInt(ValueMax): a variable-length encoding that
+ /// reads bits least-significant first and stops as soon as the accumulated
+ /// value plus the next mask would reach ValueMax.
+ ///
+ public bool ReadBoundedInt(uint valueMax, out uint value)
+ {
+ value = 0;
+
+ if (valueMax <= 1)
+ return true;
+
+ for (uint mask = 1;
+ value + mask < valueMax;
+ mask <<= 1)
+ {
+ if (!ReadBit(out bool bit))
+ return false;
+
+ if (bit)
+ value |= mask;
+
+ if ((mask & 0x80000000u) != 0)
+ break;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Reads a UE3 FString: a 32-bit length (positive = ANSI byte count
+ /// including terminator, negative = UTF-16 code-unit count) followed by the
+ /// characters and a trailing null.
+ ///
+ public bool ReadFString(out string value)
+ {
+ value = string.Empty;
+
+ if (!ReadBits(32, out uint rawLength))
+ return false;
+
+ int length = (int)rawLength;
+
+ if (length == 0)
+ return true;
+
+ if (length > 0)
+ {
+ if (length > 65536 ||
+ (long)length * 8 > Remaining)
+ {
+ return false;
+ }
+
+ var builder = new System.Text.StringBuilder(length);
+
+ for (int index = 0; index < length; ++index)
+ {
+ if (!ReadByte(out byte character))
+ return false;
+
+ if (character != 0)
+ builder.Append((char)character);
+ }
+
+ value = builder.ToString();
+ return true;
+ }
+
+ long wideCount = -(long)length;
+
+ if (wideCount <= 0 ||
+ wideCount > 32768 ||
+ wideCount * 16 > Remaining)
+ {
+ return false;
+ }
+
+ var wideBuilder = new System.Text.StringBuilder((int)wideCount);
+
+ for (long index = 0; index < wideCount; ++index)
+ {
+ if (!ReadBits(16, out uint character))
+ return false;
+
+ if (character == 0)
+ continue;
+
+ wideBuilder.Append(character <= 0x7Fu ? (char)character : '?');
+ }
+
+ value = wideBuilder.ToString();
+ return true;
+ }
+
+ public bool Skip(int count)
+ {
+ if (count > Remaining)
+ return false;
+
+ _position += count;
+ return true;
+ }
+}
diff --git a/DistrictServerCSharp/Bits/BitWriter.cs b/DistrictServerCSharp/Bits/BitWriter.cs
new file mode 100644
index 0000000..ab1c456
--- /dev/null
+++ b/DistrictServerCSharp/Bits/BitWriter.cs
@@ -0,0 +1,226 @@
+namespace DistrictServerCSharp.Bits;
+
+///
+/// LSB-first bit writer, ported from the C++ BitWriter in ApbUdp.cpp. Builds a
+/// growable byte buffer; call to append the
+/// UE3 packet trailer bit and get the final bytes.
+///
+public sealed class BitWriter
+{
+ private readonly List _data = new();
+ private int _writtenBits;
+
+ public void WriteBit(bool value)
+ {
+ int byteIndex = _writtenBits / 8;
+ int bitIndex = _writtenBits % 8;
+
+ if (byteIndex >= _data.Count)
+ _data.Add(0);
+
+ if (value)
+ _data[byteIndex] |= (byte)(1u << bitIndex);
+
+ ++_writtenBits;
+ }
+
+ public void WriteBits(uint value, int count)
+ {
+ for (int index = 0; index < count; ++index)
+ WriteBit(((value >> index) & 1u) != 0);
+ }
+
+ public void WriteBytes(ReadOnlySpan value)
+ {
+ foreach (byte b in value)
+ WriteBits(b, 8);
+ }
+
+ ///
+ /// UE3's FBitWriter::SerializeInt(Value, ValueMax): a variable-length
+ /// encoding that emits bits least-significant first and stops as soon as
+ /// the accumulated value plus the next mask would reach ValueMax. This is
+ /// the exact counterpart of , and it
+ /// is how channel indices (max 0x3FF), packet ids (max 0x40000000) and
+ /// package-map net indices (max 0x80000000) go on the wire.
+ ///
+ public void WriteBoundedInt(uint value, uint valueMax)
+ {
+ uint accumulated = 0;
+
+ for (uint mask = 1;
+ accumulated + mask < valueMax;
+ mask <<= 1)
+ {
+ bool bit = (value & mask) != 0;
+ if (bit)
+ accumulated |= mask;
+ WriteBit(bit);
+
+ if ((mask & 0x80000000u) != 0)
+ break;
+ }
+ }
+
+ ///
+ /// FVector::SerializeCompressed, recovered from FUN_10DCC5E0:
+ /// bits = bitlength(max(|X|,|Y|,|Z|)) clamped to [1,20]
+ /// SerializeInt(bits - 1, 20)
+ /// Bias = 1 << bits; Max = 1 << (bits + 1)
+ /// SerializeInt(X + Bias, Max), then Y, then Z
+ /// On load the client computes X = DX - Bias, which is why a bunch that
+ /// ends before this data decodes to (-2,-2,-2): it reads bits-1 = 0,
+ /// giving Bias = 2 and DX = 0.
+ ///
+ public void WriteCompressedVector(float x, float y, float z)
+ {
+ int ix = (int)(x < 0 ? x - 0.5f : x + 0.5f);
+ int iy = (int)(y < 0 ? y - 0.5f : y + 0.5f);
+ int iz = (int)(z < 0 ? z - 0.5f : z + 0.5f);
+
+ uint largest = (uint)Math.Abs(ix);
+ largest = Math.Max(largest, (uint)Math.Abs(iy));
+ largest = Math.Max(largest, (uint)Math.Abs(iz));
+
+ uint bits = 0;
+ while (largest >> (int)bits != 0)
+ ++bits;
+ if (bits < 1) bits = 1;
+ if (bits > 20) bits = 20;
+
+ WriteBoundedInt(bits - 1, 20);
+
+ uint bias = 1u << (int)bits;
+ uint maximum = 1u << (int)(bits + 1);
+
+ WriteBoundedInt((uint)(ix + (int)bias), maximum);
+ WriteBoundedInt((uint)(iy + (int)bias), maximum);
+ WriteBoundedInt((uint)(iz + (int)bias), maximum);
+ }
+
+ ///
+ /// FRotator::SerializeCompressed, recovered from the client's rotation
+ /// reader. Each axis is reduced to its high byte (value >> 8) and written
+ /// as a presence bit plus 8 bits only when non-zero. This is NOT the
+ /// width-field format FVector uses; a pure-yaw rotation writes only the
+ /// yaw byte (pitch/roll each write a single 0 bit).
+ ///
+ /// PORT-ORIGINAL: the C++ oracle has no rotator writer at all (only
+ /// WriteCompressedVector, ApbUdp.cpp:262; its sole rotation touch is
+ /// an AimRotation int READ at ApbUdp.cpp:2410). So the encoding is
+ /// client-RE-derived, hand-checked only (yaw 16384 -> high byte 0x40 ->
+ /// 16384), and has neither a C++ counterpart to diff against nor a reader
+ /// on either side to round-trip through. Not yet exercised against a live
+ /// client.
+ ///
+ public void WriteCompressedRotator(int pitch, int yaw, int roll)
+ {
+ byte bytePitch = (byte)(((uint)pitch & 0xFFFFu) >> 8);
+ byte byteYaw = (byte)(((uint)yaw & 0xFFFFu) >> 8);
+ byte byteRoll = (byte)(((uint)roll & 0xFFFFu) >> 8);
+
+ WriteBit(bytePitch != 0);
+ if (bytePitch != 0)
+ WriteBits(bytePitch, 8);
+
+ WriteBit(byteYaw != 0);
+ if (byteYaw != 0)
+ WriteBits(byteYaw, 8);
+
+ WriteBit(byteRoll != 0);
+ if (byteRoll != 0)
+ WriteBits(byteRoll, 8);
+ }
+
+ ///
+ /// FString as UE3 serialises it: a 32-bit length (including the
+ /// terminator) followed by the characters and a trailing null. Same
+ /// encoding the text control messages already use.
+ ///
+ public void WriteFString(string text, bool nullTerminated = true)
+ {
+ uint length = (uint)(text.Length + (nullTerminated ? 1 : 0));
+ WriteBits(length, 32);
+ foreach (char character in text)
+ WriteBits(character, 8);
+ if (nullTerminated)
+ WriteBits(0, 8);
+ }
+
+ ///
+ /// UPackageMap::SerializeName first serializes a one-bit selector. For
+ /// non-hardcoded names the selector is zero and the name follows as an
+ /// FString. The previous implementation omitted this bit, so the low bit
+ /// of the FString length became the selector. For the terrain name the
+ /// serialized length is 55 (odd), which shifted the remaining payload and
+ /// decoded as:
+ /// FName(Index=76533, Number=405772312), bools=(0,0,0)
+ /// instead of the existing terrain FName and (1,1,0).
+ ///
+ /// APB's network reader derives the FName Number from the string when
+ /// using this path; do not append a raw 32-bit number unless explicitly
+ /// testing an alternate build.
+ ///
+ public void WriteName(
+ string text,
+ uint number = 0,
+ bool includeNumber = false,
+ bool nullTerminated = true)
+ {
+ // v0.5 proved false decodes as NAME_None and consumes no FString.
+ // True selects the FString representation.
+ WriteBit(true); // FString name follows
+ WriteFString(text, nullTerminated);
+ if (includeNumber)
+ WriteBits(number, 32);
+ }
+
+ ///
+ /// UPackageMapLevel::SerializeObject. An object reference is one flag bit
+ /// followed by a bounded int:
+ /// flag = 0 -> package map reference, SerializeInt(NetIndex, 0x80000000)
+ /// flag = 1 -> channel reference, SerializeInt(ChIndex, 0x3FF)
+ /// Classes and other loaded assets use the package-map form; actors that
+ /// already have an open channel use the channel form.
+ ///
+ public void WriteObjectByNetIndex(uint netIndex)
+ {
+ WriteBit(false);
+ WriteBoundedInt(netIndex, 0x80000000u);
+ }
+
+ public void WriteObjectByChannel(uint channelIndex)
+ {
+ WriteBit(true);
+ WriteBoundedInt(channelIndex, 0x3FFu);
+ }
+
+ public byte[] FinishWithTrailer()
+ {
+ WriteBit(true);
+ return _data.ToArray();
+ }
+
+ /// Number of bits written so far (no trailer).
+ public int BitCount => _writtenBits;
+
+ /// Raw bytes written so far, without adding a terminator.
+ public byte[] Snapshot() => _data.ToArray();
+
+ ///
+ /// Appends bits taken from ,
+ /// least-significant bit of each byte first, matching how
+ /// lays bits out.
+ ///
+ public void WriteBitsFrom(ReadOnlySpan source, int count)
+ {
+ for (int index = 0; index < count; ++index)
+ {
+ int byteIndex = index / 8;
+ int bitIndex = index % 8;
+ if (byteIndex >= source.Length)
+ break;
+ WriteBit(((source[byteIndex] >> bitIndex) & 1u) != 0);
+ }
+ }
+}
diff --git a/DistrictServerCSharp/Bits/RWBits.cs b/DistrictServerCSharp/Bits/RWBits.cs
new file mode 100644
index 0000000..53ca2fb
--- /dev/null
+++ b/DistrictServerCSharp/Bits/RWBits.cs
@@ -0,0 +1,67 @@
+namespace DistrictServerCSharp.Bits;
+
+///
+/// Classic UE3 bit helpers carried over from the C++ DistrictServer
+/// (RWBits.h). extracts a value from a byte buffer at a
+/// bit offset; stores a value and returns the new bit
+/// position. Both are LSB-first.
+///
+public static class RWBits
+{
+ /// Reads a value from at the given bit position.
+ /// How many bits to read (1..32).
+ /// Buffer to read from.
+ /// Position in the buffer, in bits.
+ /// The decoded value, or 0 if is out of range.
+ public static uint ReadBits(uint bits, byte[] input, uint inputBits)
+ {
+ uint seekBits, rem, seek = 0, ret = 0, mask = 0xFFFFFFFF;
+
+ if (bits > 32) return 0;
+ if (bits < 32) mask = (1u << (int)bits) - 1;
+
+ for (;;)
+ {
+ seekBits = inputBits & 7;
+ ret |= ((uint)(input[inputBits >> 3] >> (int)seekBits) & mask) << (int)seek;
+ rem = 8 - seekBits;
+ if (rem >= bits) break;
+ bits -= rem;
+ inputBits += rem;
+ seek += rem;
+ mask = (1u << (int)bits) - 1;
+ }
+
+ return ret;
+ }
+
+ /// Stores a value into at the given bit position.
+ /// Number to store.
+ /// How many bits it occupies (1..32).
+ /// Buffer to write into.
+ /// Position in the buffer, in bits.
+ /// The bit position where the stored number finishes.
+ public static uint WriteBits(uint data, uint bits, byte[] output, uint outputBits)
+ {
+ uint seekBits, rem, mask;
+
+ if (bits > 32) return outputBits;
+ if (bits < 32) data &= (1u << (int)bits) - 1;
+
+ for (;;)
+ {
+ seekBits = outputBits & 7;
+ mask = (1u << (int)seekBits) - 1;
+ if ((bits + seekBits) < 8) mask |= ~(((1u << (int)bits) << (int)seekBits) - 1);
+ output[outputBits >> 3] &= (byte)mask; // zero
+ output[outputBits >> 3] |= (byte)(data << (int)seekBits);
+ rem = 8 - seekBits;
+ if (rem >= bits) break;
+ outputBits += rem;
+ bits -= rem;
+ data >>= (int)rem;
+ }
+
+ return outputBits + bits;
+ }
+}
diff --git a/DistrictServerCSharp/Config/DistrictConfig.cs b/DistrictServerCSharp/Config/DistrictConfig.cs
new file mode 100644
index 0000000..67b67dc
--- /dev/null
+++ b/DistrictServerCSharp/Config/DistrictConfig.cs
@@ -0,0 +1,835 @@
+using System.Globalization;
+using System.Text;
+
+namespace DistrictServerCSharp.Config;
+
+public enum AckMode
+{
+ None,
+ Plain,
+ XteaLittle,
+ XteaBig,
+ XteaCbcLittle,
+ XteaCbcBig
+}
+
+public enum ChallengeMode
+{
+ AckOnly,
+ BinaryCombined,
+ BinarySeparate,
+ TextCombined,
+ // Matches the retail district server: RTW replaced the whole UE3 login
+ // handshake with a single round trip. Its AUTH handler does no validation
+ // and no USES negotiation -- it copies the master package map onto the
+ // connection, marks every package as already present and immediately
+ // replies "WELCOME LEVEL=