Skip to content

Instantly share code, notes, and snippets.

@timspurgeon
Created October 9, 2025 03:22
Show Gist options
  • Save timspurgeon/7fcd13774f2ceb8b0f6f28d6544c6b08 to your computer and use it in GitHub Desktop.
Save timspurgeon/7fcd13774f2ceb8b0f6f28d6544c6b08 to your computer and use it in GitHub Desktop.
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Server.Common;
using Server.Net;
using System.IO;
using Server.Game;
using LEWriter = Server.Game.LEWriter;
namespace Server.Game
{
public class GameServer
{
private readonly NetServer net;
private readonly string bindIp;
private readonly int port;
private uint _nextEntityId = 10;
private static int NextConnId = 1;
private readonly ConcurrentDictionary<int, RRConnection> _connections = new();
private readonly ConcurrentDictionary<int, string> _users = new();
private readonly ConcurrentDictionary<int, uint> _peerId24 = new();
private readonly ConcurrentDictionary<int, List<Server.Game.GCObject>> _playerCharacters = new();
private readonly ConcurrentDictionary<int, bool> _charListSent = new();
private readonly ConcurrentDictionary<string, List<Server.Game.GCObject>> _persistentCharacters = new();
private readonly Dictionary<string, MessageQueue> _messageQueues = new Dictionary<string, MessageQueue>();
private readonly HashSet<string> _spawnedPlayers = new HashSet<string>();
// Cache selected character per user (set on 4/5 Play)
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, Server.Game.GCObject> _selectedCharacter = new();
private bool _gameLoopRunning = false;
private readonly object _gameLoopLock = new object();
// Helper: write a null-terminated ASCII string using the existing LEWriter
private static void WriteCString(Server.Game.LEWriter w, string s)
{
var bytes = Encoding.ASCII.GetBytes(s ?? string.Empty);
w.WriteBytes(bytes);
w.WriteByte(0);
}
// MUST be false for the retail client
private const bool DUPLICATE_AVATAR_RECORD = false;
// === Python gateway constants (mirror gatewayserver.py) ===
// In python: msgDest = b'\x01' + b'\x003'[:: -1] => 01 32 00 (LE u24 = 0x003201)
// msgSource = b'\xdd' + b'\x00{'[::-1] => dd 7b 00 (LE u24 = 0x007BDD)
private const uint MSG_DEST = 0x000F01; // LE bytes => 01 0F 00 (ZoneServer 1.15)
private const uint MSG_SOURCE = 0x000F01; // bytes LE => 01 0F 00 (ZoneServer 1.15)
// ===== Dump helper =====
static class DumpUtil
{
// ===== CRC =====
static readonly uint[] _crcTable = InitCrc();
static uint[] InitCrc()
{
const uint poly = 0xEDB88320u;
var t = new uint[256];
for (uint i = 0; i < 256; i++)
{
uint c = i;
for (int k = 0; k < 8; k++) c = ((c & 1) != 0) ? (poly ^ (c >> 1)) : (c >> 1);
t[i] = c;
}
return t;
}
public static uint Crc32(ReadOnlySpan<byte> data)
{
uint crc = 0xFFFFFFFFu;
foreach (var b in data) crc = _crcTable[(crc ^ b) & 0xFF] ^ (crc >> 8);
return ~crc;
}
// ===== Paths =====
static string _root;
static string _dumpDir;
static string _logDir;
public static string DumpRoot
{
get
{
if (!string.IsNullOrEmpty(_root)) return _root;
// 1) Env override
try
{
var env = Environment.GetEnvironmentVariable("DR_DUMP_DIR");
if (!string.IsNullOrWhiteSpace(env))
{
Directory.CreateDirectory(env);
_root = env;
return _root;
}
}
catch { }
#if UNITY_EDITOR
// 2) Unity Editor: ProjectRoot/Build/ServerOutput
try
{
var assets = UnityEngine.Application.dataPath; // /Project/Assets
if (!string.IsNullOrEmpty(assets))
{
var projRoot = Directory.GetParent(assets)!.FullName;
var candidate = Path.Combine(projRoot, "Build", "ServerOutput");
Directory.CreateDirectory(candidate);
_root = candidate;
return _root;
}
}
catch { }
#endif
// 3) Standalone: next to exe
try
{
var exeDir = AppContext.BaseDirectory;
var candidate = Path.Combine(exeDir, "ServerOutput");
Directory.CreateDirectory(candidate);
_root = candidate;
return _root;
}
catch { }
// 4) Fallback: LocalAppData
var local = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DR", "ServerOutput");
Directory.CreateDirectory(local);
_root = local;
return _root;
}
}
public static string DumpDir
{
get
{
if (string.IsNullOrEmpty(_dumpDir))
{
_dumpDir = Path.Combine(DumpRoot, "dumps");
Directory.CreateDirectory(_dumpDir);
}
return _dumpDir;
}
}
public static string LogDir
{
get
{
if (string.IsNullOrEmpty(_logDir))
{
_logDir = Path.Combine(DumpRoot, "logs");
Directory.CreateDirectory(_logDir);
}
return _logDir;
}
}
public static void WriteBytes(string path, byte[] bytes) => File.WriteAllBytes(path, bytes);
public static void WriteText(string path, string text) => File.WriteAllText(path, text, new UTF8Encoding(false));
public static void DumpBlob(string tag, string suffix, byte[] bytes)
{
string safeTag = Sanitize(tag);
string baseName = $"{DateTime.UtcNow:yyyyMMdd_HHmmssfff}_{safeTag}.{suffix}";
string full = Path.Combine(DumpDir, baseName);
WriteBytes(full, bytes);
Debug.Log($"[DUMP] Wrote {suffix} -> {full} ({bytes?.Length ?? 0} bytes)");
}
public static void DumpCrc(string tag, string label, byte[] bytes)
{
string safeTag = Sanitize(tag);
uint crc = Crc32(bytes);
string name = $"{DateTime.UtcNow:yyyyMMdd_HHmmssfff}_{safeTag}.{label}.crc32.txt";
string path = Path.Combine(LogDir, name);
WriteText(path, $"0x{crc:X8}\nlen={bytes?.Length ?? 0}\n");
Debug.Log($"[DUMP] CRC {label} 0x{crc:X8} (len={bytes?.Length ?? 0}) -> {path}");
}
public static void DumpFullFrame(string tag, byte[] payload)
{
try
{
DumpBlob(tag, "unity.fullframe.bin", payload);
DumpCrc(tag, "fullframe", payload);
int head = Math.Min(32, payload?.Length ?? 0);
if (payload != null && head > 0)
{
var sb = new StringBuilder(head * 3);
for (int i = 0; i < head; i++) sb.Append(payload[i].ToString("X2")).Append(' ');
Debug.Log($"[DUMP] {tag} fullframe head({head}): {sb.ToString().TrimEnd()}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"[DUMP] DumpFullFrame failed for '{tag}': {ex.Message}");
}
}
static string Sanitize(string tag)
{
if (string.IsNullOrWhiteSpace(tag)) return "untagged";
foreach (var c in Path.GetInvalidFileNameChars()) tag = tag.Replace(c, '_');
return tag;
}
}
// ============================================================================
public GameServer(string ip, int port)
{
bindIp = ip;
this.port = port;
net = new NetServer(ip, port, HandleClient);
Debug.Log($"[INIT] DFC Active Version set to 0x{GCObject.DFC_VERSION:X2} ({GCObject.DFC_VERSION})");
}
public Task RunAsync()
{
Debug.Log($"<color=#9f9>[Game]</color> Listening on {bindIp}:{port}");
StartGameLoop();
return net.RunAsync();
}
private void StartGameLoop()
{
lock (_gameLoopLock)
{
if (_gameLoopRunning) return;
_gameLoopRunning = true;
}
Task.Run(async () =>
{
Debug.Log("[Game] Game loop started");
while (_gameLoopRunning)
{
try
{
await Task.Delay(16);
}
catch (Exception ex)
{
Debug.LogError($"[Game] Game loop error: {ex}");
}
}
Debug.Log("[Game] Game loop stopped");
});
}
private async Task HandleClient(TcpClient c)
{
var ep = c.Client.RemoteEndPoint?.ToString() ?? "unknown";
int connId = Interlocked.Increment(ref NextConnId);
Debug.Log($"<color=#9f9>[Game]</color> Connection from {ep} (ID={connId})");
c.NoDelay = true;
using var s = c.GetStream();
var rrConn = new RRConnection(connId, c, s);
_connections[connId] = rrConn;
try
{
Debug.Log($"[Game] Client {connId} connected to gameserver");
Debug.Log($"[Game] Client {connId} - Using improved stream protocol");
byte[] buffer = new byte[10240];
while (rrConn.IsConnected)
{
Debug.Log($"[Game] Client {connId} - Reading data");
int bytesRead = await s.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
Debug.LogWarning($"[Game] Client {connId} closed connection.");
break;
}
Debug.Log($"[Game] Client {connId} - Read {bytesRead} bytes");
Debug.Log($"[Game] Client {connId} - Data: {BitConverter.ToString(buffer, 0, bytesRead)}");
int probe = Math.Min(8, bytesRead);
if (probe > 0)
Debug.Log($"[Game] Client {connId} - First {probe} bytes: {BitConverter.ToString(buffer, 0, probe)}");
byte[] receivedData = new byte[bytesRead];
Buffer.BlockCopy(buffer, 0, receivedData, 0, bytesRead);
await ProcessReceivedData(rrConn, receivedData);
}
}
catch (Exception ex)
{
Debug.LogError($"[Game] Exception from {ep} (ID={connId}): {ex.Message}");
}
finally
{
rrConn.IsConnected = false;
_connections.TryRemove(connId, out _);
_users.TryRemove(connId, out _);
_peerId24.TryRemove(connId, out _);
_playerCharacters.TryRemove(connId, out _);
Debug.Log($"[Game] Client {connId} disconnected");
}
}
private async Task ProcessReceivedData(RRConnection conn, byte[] data)
{
Debug.Log($"[Game] ProcessReceivedData: Processing {data.Length} bytes for client {conn.ConnId}");
try
{
await ReadPacket(conn, data);
}
catch (Exception ex)
{
Debug.LogError($"[Game] ProcessReceivedData: Error processing data for client {conn.ConnId}: {ex.Message}");
if (!string.IsNullOrEmpty(conn.LoginName))
{
Debug.Log($"[Game] ProcessReceivedData: Sending keep-alive for authenticated client {conn.ConnId}");
try
{
await SendKeepAlive(conn);
}
catch (Exception keepAliveEx)
{
Debug.LogError($"[Game] ProcessReceivedData: Keep-alive failed for client {conn.ConnId}: {keepAliveEx.Message}");
}
}
}
}
// Add these enhanced debugging methods after your existing helper methods (around line 300)
private void LogIncomingMessage(string context, byte[] data, int maxBytes = 50)
{
int preview = Math.Min(maxBytes, data.Length);
string hex = BitConverter.ToString(data, 0, preview);
Debug.Log($"[INCOMING][{context}] Length={data.Length}, Preview: {hex}");
}
private void LogOutgoingMessage(string context, byte[] data, int maxBytes = 50)
{
int preview = Math.Min(maxBytes, data.Length);
string hex = BitConverter.ToString(data, 0, preview);
Debug.Log($"[OUTGOING][{context}] Length={data.Length}, Preview: {hex}");
}
private async Task SendKeepAlive(RRConnection conn)
{
Debug.Log($"[Game] SendKeepAlive: Sending keep-alive to client {conn.ConnId}");
var keepAlive = new LEWriter();
keepAlive.WriteByte(0);
try
{
await SendMessage0x10(conn, 0xFF, keepAlive.ToArray(), "keepalive");
Debug.Log($"[Game] SendKeepAlive: Keep-alive sent to client {conn.ConnId}");
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendKeepAlive: Failed to send keep-alive to client {conn.ConnId}: {ex.Message}");
throw;
}
}
private async Task ReadPacket(RRConnection conn, byte[] data)
{
Debug.Log($"[Game] ReadPacket: Processing {data.Length} bytes for client {conn.ConnId}");
LogIncomingMessage($"ReadPacket-Client{conn.ConnId}", data); // ADD THIS
if (data.Length == 0)
{
Debug.LogWarning($"[Game] ReadPacket: Empty data for client {conn.ConnId}");
return;
}
var reader = new LEReader(data);
byte msgType = reader.ReadByte();
Debug.Log($"[Game] ReadPacket: Message type 0x{msgType:X2} for client {conn.ConnId}");
Debug.Log($"[Game] ReadPacket: Login name = '{conn.LoginName}' (authenticated: {!string.IsNullOrEmpty(conn.LoginName)})");
if (msgType != 0x0A && msgType != 0x0E && string.IsNullOrEmpty(conn.LoginName))
{
Debug.LogError($"[Game] ReadPacket: Received invalid message type 0x{msgType:X2} before login for client {conn.ConnId}");
Debug.LogError($"[Game] ReadPacket: Only 0x0A/0x0E messages allowed before authentication!");
return;
}
switch (msgType)
{
case 0x0A:
Debug.Log($"[Game] ReadPacket: Handling Compressed A (zlib3) message for client {conn.ConnId}");
await HandleCompressedA(conn, reader);
break;
case 0x0E:
Debug.Log($"[Game] ReadPacket: Handling Compressed E (zlib1) message for client {conn.ConnId}");
await HandleCompressedE(conn, reader);
break;
case 0x06:
Debug.Log($"[Game] ReadPacket: Handling Type 06 message for client {conn.ConnId}");
await HandleType06(conn, reader);
break;
case 0x31:
Debug.Log($"[Game] ReadPacket: Handling Type 31 message for client {conn.ConnId}");
await HandleType31(conn, reader);
break;
default:
Debug.LogWarning($"[Game] ReadPacket: Unhandled message type 0x{msgType:X2} for client {conn.ConnId}");
Debug.LogWarning($"[Game] ReadPacket: Full message hex: {BitConverter.ToString(data)}");
Debug.LogWarning($"[Game] ReadPacket: First 32 bytes: {BitConverter.ToString(data, 0, Math.Min(32, data.Length))}");
break;
}
}
private async Task HandleCompressedA(RRConnection conn, Server.Game.LEReader reader)
{
Debug.Log($"[Game] HandleCompressedA: Starting for client {conn.ConnId}");
Debug.Log($"[Game] HandleCompressedA: Remaining bytes: {reader.Remaining}");
// Python zlib3 format:
// [0x0A][msgDest:u24][compLen:u32][(if 0x0A) 00 03 00 else msgSource:u24][unclen:u32][zlib]
const int MIN_HDR = 3 + 4 + 3 + 4; // rough min once we know branch
if (reader.Remaining < MIN_HDR)
{
Debug.LogError($"[Game] HandleCompressedA: Insufficient data, have {reader.Remaining}");
}
// We still keep peer24 for downstream since client will send it on 0x0E too
// but for A(0x0A) we don't strictly need to parse all subfields here for routing;
// just decompress and forward to the inner dispatcher (same as before).
// For brevity we reuse previous parsing path that expected:
// [peer:u24][packetLen:u32][dest:u8][sub:u8][zero:u8][unclen:u32][zlib]
// but we only support the branch we generate (00 03 00).
// If your client actually sends A-frames, keep existing inflate path:
if (reader.Remaining < (3 + 4)) return;
uint peer = reader.ReadUInt24();
_peerId24[conn.ConnId] = peer;
uint compPlus7 = reader.ReadUInt32();
int compLen = (int)compPlus7 - 7;
if (compLen < 0) { Debug.LogError("[Game] HandleCompressedA: bad compLen"); return; }
if (reader.Remaining < 3 + 4 + compLen) { /* minimal check */ }
byte dest = reader.ReadByte(); // expected 0x00
byte sub = reader.ReadByte(); // expected 0x03
byte zero = reader.ReadByte(); // expected 0x00
uint unclen = reader.ReadUInt32();
byte[] comp = reader.ReadBytes(compLen);
byte[] inner;
try
{
inner = (compLen == 0 || unclen == 0) ? Array.Empty<byte>() : ZlibUtil.Inflate(comp, unclen);
}
catch (Exception ex)
{
Debug.LogError($"[Game] HandleCompressedA: Decompression failed: {ex.Message}");
return;
}
// In our use, A/0x03 is just small advancement signals; route to same handler:
await ProcessUncompressedMessage(conn, dest, sub, inner);
}
// ===================== zlib1 E-lane (the IMPORTANT fix) ====================
// Python send_zlib1 format:
// [0x0E]
// [msgDest:u24]
// [compressedLen:u24]
// [0x00]
// [msgSource:u24]
// [0x01 0x00 0x01 0x00 0x00]
// [uncompressedLen:u32]
// [zlib(inner)]
private (byte[] payload, byte[] compressed) BuildCompressedEPayload_Zlib1(byte[] innerData)
{
byte[] z = ZlibUtil.Deflate(innerData);
int compressedLen = z.Length + 12; // python: len(zlibMsg) + 12
var w = new LEWriter();
w.WriteByte(0x0E);
w.WriteUInt24((int)MSG_DEST); // msgDest
w.WriteUInt24(compressedLen); // 3-byte comp len
w.WriteByte(0x00);
w.WriteUInt24((int)MSG_SOURCE); // msgSource
// python literal: b'\x01\x00\x01\x00\x00'
w.WriteByte(0x01);
w.WriteByte(0x00);
w.WriteByte(0x01);
w.WriteByte(0x00);
w.WriteByte(0x00);
w.WriteUInt32((uint)innerData.Length); // uncompressed size
w.WriteBytes(z);
return (w.ToArray(), z);
}
// We keep an A-lane helper too, but match python's zlib3 packing when WE send:
// Python send_zlib3 (for pktType==0x0A):
// [0x0A][msgDest:u24][compLen:u32][00 03 00][unclen:u32][zlib]
// A-lane writer aligned to Go (internal/connections/writers.go :: WriteCompressedA)
// Structure:
// 0x0A
// [connId:u24]
// [len:u32 = zlen + 7]
// [dest:1][messageType:1][0x00]
// [uncompressedLen:u32]
// [zlib(innerData)]
private (byte[] payload, byte[] compressed) BuildCompressedAPayload_Zlib3(RRConnection conn, byte dest, byte messageType, byte[] innerData)
{
byte[] z = ZlibUtil.Deflate(innerData);
var w = new LEWriter();
w.WriteByte(0x0A);
// connId (u24) first, per Go
uint peer = GetClientId24(conn.ConnId);
w.WriteUInt24((int)(peer & 0xFFFFFFu));
// length = zlib length + 7
w.WriteUInt32((uint)(z.Length + 7));
// A-lane header
w.WriteByte(dest); // usually 0x01 for gameplay
w.WriteByte(messageType); // usually 0x0F for gameplay
w.WriteByte(0x00);
// inner uncompressed length, then zlib payload
w.WriteUInt32((uint)innerData.Length);
w.WriteBytes(z);
return (w.ToArray(), z);
}
// --------------- SEND helpers (now split: A=zlib3, E=zlib1) ----------------
private async Task SendCompressedEResponse(RRConnection conn, byte[] innerData)
{
try
{
var (payload, z) = BuildCompressedEPayload_Zlib1(innerData);
Debug.Log($"[SEND][E/zlib1] comp={z.Length} unclen={innerData.Length}");
await conn.Stream.WriteAsync(payload, 0, payload.Length);
}
catch (Exception ex)
{
Debug.LogError($"[Wire][E] send failed: {ex.Message}");
}
}
private async Task SendCompressedEResponseWithDump(RRConnection conn, byte[] innerData, string tag)
{
try
{
DumpUtil.DumpBlob(tag, "unity.uncompressed.bin", innerData);
DumpUtil.DumpCrc(tag, "uncompressed", innerData);
var (payload, z) = BuildCompressedEPayload_Zlib1(innerData);
DumpUtil.DumpBlob(tag, "unity.compressed.bin", z);
DumpUtil.DumpCrc(tag, "compressed", z);
DumpUtil.DumpBlob(tag, "unity.fullframe.bin", payload);
DumpUtil.DumpCrc(tag, "fullframe", payload);
await conn.Stream.WriteAsync(payload, 0, payload.Length);
}
catch (Exception ex)
{
Debug.LogError($"[Wire][E] dump/send failed: {ex.Message}");
}
}
private void ReassignEntityIDs(Server.Game.GCObject entity)
{
entity.ID = _nextEntityId++;
foreach (var child in entity.Children)
{
ReassignEntityIDs(child);
}
}
private async Task SendCompressedAResponse(RRConnection conn, byte dest, byte msgType, byte[] innerData)
{
try
{
var (payload, z) = BuildCompressedAPayload_Zlib3(conn, dest, msgType, innerData);
Debug.Log($"[SEND][A/zlib3] comp={z.Length} unclen={innerData.Length} dest=0x{dest:X2} type=0x{msgType:X2}");
await conn.Stream.WriteAsync(payload, 0, payload.Length);
}
catch (Exception ex)
{
Debug.LogError($"[Wire][A] send failed: {ex.Message}");
}
}
// Convenience wrapper for gameplay (Zone/ClientEntity/User): dest=0x01, type=0x0F
private Task SendCompressedAResponse(RRConnection conn, byte[] innerData) =>
SendCompressedAResponse(conn, 0x01, 0x0F, innerData);
private async Task SendCompressedAResponseWithDump(RRConnection conn, byte dest, byte msgType, byte[] innerData, string tag)
{
try
{
DumpUtil.DumpBlob(tag, "unity.uncompressed.bin", innerData);
DumpUtil.DumpCrc(tag, "uncompressed", innerData);
var (payload, z) = BuildCompressedAPayload_Zlib3(conn, dest, msgType, innerData);
DumpUtil.DumpBlob(tag, "unity.compressed.bin", z);
DumpUtil.DumpCrc(tag, "compressed", z);
DumpUtil.DumpFullFrame(tag, payload);
await conn.Stream.WriteAsync(payload, 0, payload.Length);
}
catch (Exception ex)
{
Debug.LogError($"[Wire][A] dump/send failed: {ex.Message}");
}
}
// Convenience wrapper for gameplay (Zone/ClientEntity/User): dest=0x01, type=0x0F
private Task SendCompressedAResponseWithDump(RRConnection conn, byte[] innerData, string tag) =>
SendCompressedAResponseWithDump(conn, 0x01, 0x0F, innerData, tag);
private async Task ProcessUncompressedMessage(RRConnection conn, byte dest, byte msgTypeA, byte[] uncompressed)
{
Debug.Log($"[Game] ProcessUncompressedMessage: A-lane dest=0x{dest:X2} sub=0x{msgTypeA:X2}");
LogIncomingMessage($"A-Lane-{msgTypeA:X2}", uncompressed); // ADD THIS
Debug.Log($"[Game] ProcessUncompressedMessage: A-lane dest=0x{dest:X2} sub=0x{msgTypeA:X2}");
switch (msgTypeA)
{
case 0x00: // initial login blob
await HandleInitialLogin(conn, uncompressed);
break;
case 0x02: // ticks
// echo empty 0x02 on A using zlib3 python layout
await SendCompressedAResponseWithDump(conn, 0x00, 0x02, Array.Empty<byte>(), "a02_empty");
break;
case 0x03: // session token style
if (uncompressed.Length >= 4)
{
var reader = new LEReader(uncompressed);
uint sessionToken = reader.ReadUInt32();
if (GlobalSessions.TryConsume(sessionToken, out var user) && !string.IsNullOrEmpty(user))
{
conn.LoginName = user;
_users[conn.ConnId] = user;
var ack = new LEWriter();
ack.WriteByte(0x03);
await SendMessage0x10(conn, 0x0A, ack.ToArray(), "msg10_auth_ack");
// Immediately tick E so client advances like python does
// await SendCompressedEResponseWithDump(conn, Array.Empty<byte>(), "e_hello_tick");
await Task.Delay(50);
await StartCharacterFlow(conn);
}
else
{
Debug.LogError($"[Game] A/0x03 invalid session token 0x{sessionToken:X8}");
}
}
break;
case 0x0F:
await HandleChannelMessage(conn, uncompressed);
break;
default:
Debug.LogWarning($"[Game] Unhandled A sub=0x{msgTypeA:X2}");
break;
}
}
private async Task HandleInitialLogin(RRConnection conn, byte[] data)
{
Debug.Log($"[Game] HandleInitialLogin: ENTRY client {conn.ConnId}");
if (data.Length < 5)
{
Debug.LogError($"[Game] HandleInitialLogin: need 5 bytes, have {data.Length}");
return;
}
var reader = new LEReader(data);
byte subtype = reader.ReadByte();
uint oneTimeKey = reader.ReadUInt32();
if (!GlobalSessions.TryConsume(oneTimeKey, out var user) || string.IsNullOrEmpty(user))
{
Debug.LogError($"[Game] HandleInitialLogin: Invalid OneTimeKey 0x{oneTimeKey:X8}");
return;
}
conn.LoginName = user;
_users[conn.ConnId] = user;
Debug.Log($"[Game] HandleInitialLogin: SUCCESS user '{user}'");
var ack = new LEWriter();
ack.WriteByte(0x03);
await SendMessage0x10(conn, 0x0A, ack.ToArray(), "msg10_auth_ack_initial");
// prime E-lane per gateway
// await SendCompressedEResponseWithDump(conn, Array.Empty<byte>(), "e_hello_tick");
// small A/0x03 advance (compatible with our zlib3 builder)
var advance = new LEWriter();
advance.WriteUInt24(0x00B2B3B4);
advance.WriteByte(0x00);
await SendCompressedAResponseWithDump(conn, 0x00, 0x03, advance.ToArray(), "advance_a03");
// A/0x02 nudge
await SendCompressedAResponseWithDump(conn, 0x00, 0x02, Array.Empty<byte>(), "nudge_a02");
await Task.Delay(75);
await StartCharacterFlow(conn);
}
private async Task HandleChannelMessage(RRConnection conn, byte[] data)
{
if (data.Length < 2) return;
byte channel = data[0];
byte messageType = data[1];
// ADD THESE LOGS
Debug.Log($"[Game] HandleChannelMessage: *** RECEIVED Channel={channel} Type=0x{messageType:X2} from Client {conn.ConnId} ***");
LogIncomingMessage($"Channel{channel}-Type{messageType:X2}", data);
switch (channel)
{
case 4:
switch (messageType)
{
case 0: // CharacterConnected request from client
await SendCharacterConnectedResponse(conn);
break;
case 1: // UI nudge 4/1 -> send tiny ack on E
{
var ack = new LEWriter();
ack.WriteByte(4);
ack.WriteByte(1);
ack.WriteUInt32(0);
await SendCompressedEResponseWithDump(conn, ack.ToArray(), "char_ui_nudge_4_1_ack");
break;
}
case 3: // Get list
await SendCharacterList(conn);
break;
case 5: // Play
await HandleCharacterPlay(conn, data);
break;
case 2: // Create
await HandleCharacterCreate(conn, data);
break;
default:
Debug.LogWarning($"[Game] Unhandled char msg 0x{messageType:X2}");
break;
}
break;
case 7: // ClientEntity channel
Debug.Log($"[Game] Received ClientEntity message type: 0x{messageType:X2}");
if (messageType == 0x04) // ClientRequestRespawn
{
Debug.Log($"[Game] Client requested respawn - sending spawn sequence");
// Send the spawn data
// await SendPlayerEntitySpawnGO(conn);
// CRITICAL: Send the "Now Connected" message with proper structure
await Task.Delay(100);
var connected = new LEWriter();
connected.WriteByte(0x07); // Channel 7
connected.WriteByte(0x46); // Message type 70 (0x46 hex)
connected.WriteByte(0x06); // END OF STREAM MARKER - MUST BE HERE!
await SendCompressedAResponseWithDump(conn, 0x01, 0x0F, connected.ToArray(), "now_connected_7_46");
Debug.Log($"[Game] Sent 385 connected message after spawn");
}
else
{
Debug.LogWarning($"[Game] Unhandled ClientEntity message: 0x{messageType:X2}");
}
break;
case 9:
await HandleGroupChannelMessages(conn, messageType);
break;
case 13:
await HandleZoneChannelMessages(conn, messageType, data);
break;
default:
Debug.LogWarning($"[Game] Unhandled channel {channel}");
Debug.LogWarning($"[Game] Data: {BitConverter.ToString(data)}");
break;
}
}
// Character flow now **sends on E-lane/zlib1**
private async Task StartCharacterFlow(RRConnection conn)
{
Debug.Log($"[Game] StartCharacterFlow: client {conn.ConnId} ({conn.LoginName})");
await Task.Delay(50);
var sent = await EnsurePeerThenSendCharConnected(conn);
if (!sent)
{
Debug.LogWarning("[Game] StartCharacterFlow: 4/0 deferred; nudging");
}
// keep one gentle tick on A per python gateway behavior
_ = Task.Run(async () =>
{
try
{
await Task.Delay(500);
await SendCompressedAResponseWithDump(conn, 0x00, 0x02, Array.Empty<byte>(), "tick_a02_500ms");
await Task.Delay(500);
if (!_charListSent.TryGetValue(conn.ConnId, out var flag) || !flag)
await SendCompressedAResponseWithDump(conn, 0x00, 0x02, Array.Empty<byte>(), "tick_a02_1000ms");
}
catch (Exception ex) { Debug.LogWarning($"[Game] A/0x02 tick failed: {ex.Message}"); }
});
}
private async Task SendCharacterConnectedResponse(RRConnection conn)
{
Debug.Log($"[Game] SendCharacterConnectedResponse: *** ENTRY (DFC-style) *** For client {conn.ConnId}");
try
{
const int count = 2;
if (!_persistentCharacters.ContainsKey(conn.LoginName))
{
_persistentCharacters[conn.LoginName] = new List<Server.Game.GCObject>(count);
Debug.Log($"[Game] SendCharacterConnectedResponse: Created character list for {conn.LoginName}");
}
var list = _persistentCharacters[conn.LoginName];
while (list.Count < count)
{
try
{
var p = Server.Game.Objects.NewPlayer(conn.LoginName);
p.ID = (uint)Server.Game.Objects.NewID();
list.Add(p);
Debug.Log($"[Game] SendCharacterConnectedResponse: Added DFC player stub ID=0x{p.ID:X8}");
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendCharacterConnectedResponse: ERROR creating player stub: {ex.Message}");
Debug.LogError(ex.StackTrace);
break;
}
}
var body = new LEWriter();
body.WriteByte(4);
body.WriteByte(0);
var inner = body.ToArray();
Debug.Log($"[SEND][inner][4/0] {BitConverter.ToString(inner)} (len={inner.Length})");
Debug.Log($"[SEND][E][prep] 4/0 using peer=0x{GetClientId24(conn.ConnId):X6} dest=0x01 sub=0x0F innerLen={inner.Length}");
await SendCompressedEResponseWithDump(conn, inner, "char_connected");
Debug.Log("[Game] SendCharacterConnectedResponse: *** SUCCESS *** Sent DFC-compatible 4/0 (E-lane)");
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendCharacterConnectedResponse: *** CRITICAL EXCEPTION *** {ex.Message}");
Debug.LogError(ex.StackTrace);
}
}
private void WriteGoSendPlayer(Server.Game.LEWriter body, Server.Game.GCObject character)
{
try
{
long startPos = body.ToArray().Length;
// CRITICAL FIX: First create the Avatar
var avatar = Server.Game.Objects.LoadAvatar();
// CRITICAL FIX: Add the Avatar to the Player first
character.AddChild(avatar);
// CRITICAL FIX: Then add the ProcModifier to the Player
var procMod = Server.Game.Objects.NewProcModifier();
character.AddChild(procMod);
// Log the UnitContainer children before serialization
var unitContainer = avatar.Children?.FirstOrDefault(c => c.NativeClass == "UnitContainer");
if (unitContainer != null)
{
Debug.Log($"[DFC][UnitContainer] ChildCount(before)={unitContainer.Children?.Count ?? 0}");
if (unitContainer.Children != null)
{
for (int i = 0; i < unitContainer.Children.Count; i++)
{
var child = unitContainer.Children[i];
Debug.Log($"[DFC][UnitContainer] Child[{i}] native='{child.NativeClass}' gc='{child.GCClass}'");
}
}
}
Debug.Log($"[Game] WriteGoSendPlayer: Writing character ID={character.ID} with DFC format");
character.WriteFullGCObject(body);
long afterPlayer = body.ToArray().Length;
Debug.Log($"[Game] WriteGoSendPlayer: Player DFC write bytes={afterPlayer - startPos}");
if (DUPLICATE_AVATAR_RECORD)
{
Debug.Log("[Game] WriteGoSendPlayer: DUPLICATE_AVATAR_RECORD=true, adding standalone avatar");
long startAv = body.ToArray().Length;
avatar.WriteFullGCObject(body);
long afterAv = body.ToArray().Length;
Debug.Log($"[Game] WriteGoSendPlayer: Standalone Avatar DFC write bytes={afterAv - startAv}");
body.WriteByte(0x01);
body.WriteByte(0x01);
body.WriteBytes(Encoding.UTF8.GetBytes("Normal"));
body.WriteByte(0x00);
body.WriteByte(0x01);
body.WriteByte(0x01);
body.WriteUInt32(0x01);
}
else
{
Debug.Log("[Game] WriteGoSendPlayer: Sending only Player with Avatar child (DFC format), no tail");
}
}
catch (Exception ex)
{
Debug.LogError($"[Game] WriteGoSendPlayer: EXCEPTION {ex.Message}");
Debug.LogError($"[Game] WriteGoSendPlayer: Stack trace: {ex.StackTrace}");
}
}
private async Task SendCharacterList(RRConnection conn)
{
Debug.Log($"[Game] SendCharacterList: *** ENTRY *** DFC format with djb2 hashes");
try
{
if (!_persistentCharacters.TryGetValue(conn.LoginName, out var characters))
{
Debug.LogError($"[Game] SendCharacterList: *** ERROR *** No characters found for {conn.LoginName}");
return;
}
Debug.Log($"[Game] SendCharacterList: *** FOUND CHARACTERS *** Count: {characters.Count} for {conn.LoginName}");
int count = characters.Count;
if (count > 255)
{
Debug.LogWarning($"[Game] SendCharacterList: Character count {count} exceeds 255; clamping to 255 for wire format");
count = 255;
}
var body = new LEWriter();
body.WriteByte(4);
body.WriteByte(3);
body.WriteByte((byte)count);
Debug.Log($"[Game] SendCharacterList: *** WRITING DFC CHARACTERS *** Processing {count} characters");
for (int i = 0; i < count; i++)
{
var character = characters[i];
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** ID: {character.ID}, Writing DFC character data");
try
{
body.WriteUInt32(character.ID);
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** wrote ID={character.ID}");
WriteGoSendPlayer(body, character);
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** DFC WriteGoSendPlayer complete; current bodyLen={body.ToArray().Length}");
}
catch (Exception charEx)
{
Debug.LogError($"[Game] SendCharacterList: *** ERROR CHARACTER {i + 1} *** {charEx.Message}");
Debug.LogError($"[Game] SendCharacterList: *** CHARACTER {i + 1} STACK TRACE *** {charEx.StackTrace}");
}
}
var inner = body.ToArray();
Debug.Log($"[Game] SendCharacterList: *** SENDING DFC MESSAGE *** Total body length: {inner.Length} bytes");
Debug.Log($"[SEND][inner] CH=4,TYPE=3 DFC: {BitConverter.ToString(inner)} (len={inner.Length})");
if (!(inner.Length >= 3 && inner[0] == 0x04 && inner[1] == 0x03))
{
Debug.LogError($"[Game][FATAL] SendCharacterList header wrong: {BitConverter.ToString(inner, 0, Math.Min(inner.Length, 8))}");
}
else
{
Debug.Log($"[Game] SendCharacterList: Header OK -> 04-03 count={inner[2]} (DFC format)");
}
int head = Math.Min(32, inner.Length);
Debug.Log($"[Game] SendCharacterList: First {head} bytes: {BitConverter.ToString(inner, 0, head)}");
Debug.Log($"[SEND][E][prep] 4/3 DFC using peer=0x{GetClientId24(conn.ConnId):X6} dest=0x01 sub=0x0F innerLen={inner.Length}");
await SendCompressedEResponseWithDump(conn, inner, "charlist");
Debug.Log($"[Game] SendCharacterList: *** SUCCESS *** Sent DFC format with djb2 hashes, {count} characters");
_charListSent[conn.ConnId] = true;
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendCharacterList: *** CRITICAL EXCEPTION *** {ex.Message}");
Debug.LogError($"[Game] SendCharacterList: *** STACK TRACE *** {ex.StackTrace}");
}
}
/* private async Task SendCharacterList(RRConnection conn)
{
Debug.Log($"[Game] SendCharacterList: *** ENTRY *** DFC format with djb2 hashes");
try
{
if (!_persistentCharacters.TryGetValue(conn.LoginName, out var characters))
{
Debug.LogError($"[Game] SendCharacterList: *** ERROR *** No characters found for {conn.LoginName}");
return;
}
Debug.Log($"[Game] SendCharacterList: *** FOUND CHARACTERS *** Count: {characters.Count} for {conn.LoginName}");
int count = characters.Count;
if (count > 255)
{
Debug.LogWarning($"[Game] SendCharacterList: Character count {count} exceeds 255; clamping to 255 for wire format");
count = 255;
}
var body = new LEWriter();
body.WriteByte(4);
body.WriteByte(3);
body.WriteByte((byte)count);
Debug.Log($"[Game] SendCharacterList: *** WRITING DFC CHARACTERS *** Processing {count} characters");
for (int i = 0; i < count; i++)
{
var character = characters[i];
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** ID: {character.ID}, Writing DFC character data");
try
{
body.WriteUInt32(character.ID);
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** wrote ID={character.ID}");
WriteGoSendPlayer(body, character);
Debug.Log($"[Game] SendCharacterList: *** CHARACTER {i + 1} *** DFC WriteGoSendPlayer complete; current bodyLen={body.ToArray().Length}");
}
catch (Exception charEx)
{
Debug.LogError($"[Game] SendCharacterList: *** ERROR CHARACTER {i + 1} *** {charEx.Message}");
Debug.LogError($"[Game] SendCharacterList: *** CHARACTER {i + 1} STACK TRACE *** {charEx.StackTrace}");
}
}
var inner = body.ToArray();
Debug.Log($"[Game] SendCharacterList: *** SENDING DFC MESSAGE *** Total body length: {inner.Length} bytes");
Debug.Log($"[SEND][inner] CH=4,TYPE=3 DFC: {BitConverter.ToString(inner)} (len={inner.Length})");
if (!(inner.Length >= 3 && inner[0] == 0x04 && inner[1] == 0x03))
{
Debug.LogError($"[Game][FATAL] SendCharacterList header wrong: {BitConverter.ToString(inner, 0, Math.Min(inner.Length, 8))}");
}
else
{
Debug.Log($"[Game] SendCharacterList: Header OK -> 04-03 count={inner[2]} (DFC format)");
}
int head = Math.Min(32, inner.Length);
Debug.Log($"[Game] SendCharacterList: First {head} bytes: {BitConverter.ToString(inner, 0, head)}");
Debug.Log($"[SEND][E][prep] 4/3 DFC using peer=0x{GetClientId24(conn.ConnId):X6} dest=0x01 sub=0x0F innerLen={inner.Length}");
await SendCompressedEResponseWithDump(conn, inner, "charlist");
Debug.Log($"[Game] SendCharacterList: *** SUCCESS *** Sent DFC format with djb2 hashes, {count} characters");
_charListSent[conn.ConnId] = true;
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendCharacterList: *** CRITICAL EXCEPTION *** {ex.Message}");
Debug.LogError($"[Game] SendCharacterList: *** STACK TRACE *** {ex.StackTrace}");
}
}*/
private async Task SendToCharacterCreation(RRConnection conn)
{
var create = new LEWriter();
create.WriteByte(4);
create.WriteByte(4);
await SendCompressedEResponseWithDump(conn, create.ToArray(), "char_creation_4_4");
}
private async Task HandleCharacterPlay(RRConnection conn, byte[] data)
{
Debug.Log($"[Play] HandleCharacterPlay ENTRY: LoginName={conn.LoginName}, DataLen={data.Length}");
Debug.Log($"[Play] Data bytes: {BitConverter.ToString(data)}");
var r = new LEReader(data);
if (r.Remaining < 3)
{
Debug.LogError($"[Play] FAIL: Not enough data (remaining={r.Remaining}, need 3)");
await SendPlayFallback();
return;
}
byte ch = r.ReadByte();
byte mt = r.ReadByte();
Debug.Log($"[Play] Read channel={ch}, msgType={mt}");
if (ch != 0x04 || mt != 0x05)
{
Debug.LogError($"[Play] FAIL: Wrong channel/type (expected 4/5, got {ch}/{mt})");
await SendPlayFallback();
return;
}
if (r.Remaining < 1)
{
Debug.LogError($"[Play] FAIL: No slot byte (remaining={r.Remaining})");
await SendPlayFallback();
return;
}
byte slot = r.ReadByte();
Debug.Log($"[Play] Client requesting slot={slot}");
// Check if we have characters for this user
bool hasChars = _persistentCharacters.TryGetValue(conn.LoginName, out var chars);
Debug.Log($"[Play] _persistentCharacters has entry for '{conn.LoginName}': {hasChars}");
if (!hasChars || chars.Count == 0)
{
Debug.LogError($"[Play] FAIL: No characters found for '{conn.LoginName}'");
await SendPlayFallback();
return;
}
Debug.Log($"[Play] Character count for '{conn.LoginName}': {chars.Count}");
// If slot is out of bounds, default to slot 0
if (slot >= chars.Count)
{
Debug.LogWarning($"[Play] Slot {slot} out of bounds (count={chars.Count}), defaulting to slot 0");
slot = 0;
}
Debug.Log($"[Play] Using slot={slot}");
for (int i = 0; i < chars.Count; i++)
{
Debug.Log($"[Play] Slot {i}: ID={chars[i].ID}, Name={chars[i].Name}");
}
var selectedChar = chars[(int)slot];
_selectedCharacter[conn.LoginName] = selectedChar;
Debug.Log($"[Play] ✅ SUCCESS: Selected slot={slot} id={selectedChar.ID} name={selectedChar.Name} for {conn.LoginName}");
Debug.Log($"[Play] Stored in _selectedCharacter['{conn.LoginName}']");
var w = new LEWriter();
w.WriteByte(4);
w.WriteByte(5);
await SendCompressedEResponseWithDump(conn, w.ToArray(), "char_play_ack_4_5");
await Task.Delay(100);
await SendGroupConnectedResponse(conn);
return;
async Task SendPlayFallback()
{
Debug.LogWarning($"[Play] Sending fallback response to {conn.LoginName}");
var fb = new LEWriter();
fb.WriteByte(4);
fb.WriteByte(5);
fb.WriteByte(1);
await SendCompressedEResponseWithDump(conn, fb.ToArray(), "char_play_fallback");
}
}
private async Task<bool> WaitForPeer24(RRConnection conn, int msTimeout = 1500, int pollMs = 10)
{
int waited = 0;
while (waited < msTimeout)
{
if (_peerId24.TryGetValue(conn.ConnId, out var pid) && pid != 0u)
{
Debug.Log($"[Wire] WaitForPeer24: got peer=0x{pid:X6} after {waited}ms");
return true;
}
await Task.Delay(pollMs);
waited += pollMs;
}
Debug.LogWarning($"[Wire] WaitForPeer24: timed out after {msTimeout}ms; peer unknown");
return false;
}
private async Task<bool> EnsurePeerThenSendCharConnected(RRConnection conn)
{
await WaitForPeer24(conn);
// Even if peer isn't known yet, E-lane uses MSG_SOURCE/DEST constants (gateway semantics),
// so we go ahead and send 4/0 to wake the Character UI.
await SendCharacterConnectedResponse(conn);
return true;
}
private async Task InitiateWorldEntry(RRConnection conn)
{
await SendGoToZone_V2(conn, "Town");
}
private async Task HandleCharacterCreate(RRConnection conn, byte[] data)
{
Debug.Log($"[Game] HandleCharacterCreate: Character creation request from client {conn.ConnId}");
Debug.Log($"[Game] HandleCharacterCreate: Data ({data.Length} bytes): {BitConverter.ToString(data)}");
string characterName = $"{conn.LoginName}_NewHero";
uint newCharId = (uint)(conn.ConnId * 100 + 1);
try
{
var newCharacter = Server.Game.Objects.NewPlayer(characterName);
newCharacter.ID = newCharId;
Debug.Log($"[Game] HandleCharacterCreate: Created DFC character with ID={newCharId}");
if (!_persistentCharacters.TryGetValue(conn.LoginName, out var existing))
{
existing = new List<Server.Game.GCObject>();
_persistentCharacters[conn.LoginName] = existing;
Debug.Log($"[Game] HandleCharacterCreate: No existing list for {conn.LoginName}; created new list");
}
existing.Add(newCharacter);
Debug.Log($"[Game] HandleCharacterCreate: Persisted new DFC character (ID: {newCharId}) for {conn.LoginName}. Total now: {existing.Count}");
}
catch (Exception persistEx)
{
Debug.LogError($"[Game] HandleCharacterCreate: *** ERROR persisting DFC character *** {persistEx.Message}");
Debug.LogError($"[Game] HandleCharacterCreate: *** STACK TRACE *** {persistEx.StackTrace}");
}
var response = new LEWriter();
response.WriteByte(4);
response.WriteByte(2);
response.WriteByte(1);
response.WriteUInt32(newCharId);
await SendCompressedEResponseWithDump(conn, response.ToArray(), "char_create_4_2");
Debug.Log($"[Game] HandleCharacterCreate: Sent DFC character creation success for {characterName} (ID: {newCharId})");
await Task.Delay(100);
await SendUpdatedCharacterList(conn, newCharId, characterName);
}
private async Task SendUpdatedCharacterList(RRConnection conn, uint charId, string charName)
{
Debug.Log($"[Game] SendUpdatedCharacterList: Sending DFC list with newly created character");
try
{
if (!_persistentCharacters.TryGetValue(conn.LoginName, out var chars))
{
Debug.LogWarning($"[Game] SendUpdatedCharacterList: No persistent list found after create; falling back to single DFC entry build");
var w = new LEWriter();
w.WriteByte(4);
w.WriteByte(3);
w.WriteByte(1);
var newCharacter = Server.Game.Objects.NewPlayer(charName);
newCharacter.ID = charId;
w.WriteUInt32(charId);
WriteGoSendPlayer(w, newCharacter);
var innerSingle = w.ToArray();
Debug.Log($"[SEND][inner] CH=4,TYPE=3 (updated single DFC) : {BitConverter.ToString(innerSingle)} (len={innerSingle.Length})");
Debug.Log($"[SEND][E][prep] 4/3(DFC SINGLE) peer=0x{GetClientId24(conn.ConnId):X6} dest=0x01 sub=0x0F innerLen={innerSingle.Length}");
await SendCompressedEResponseWithDump(conn, innerSingle, "charlist_single");
Debug.Log($"[Game] SendUpdatedCharacterList: Sent updated DFC character list (SINGLE fallback) with new character (ID {charId})");
return;
}
else
{
Debug.Log($"[Game] SendUpdatedCharacterList: Found persistent list (count={chars.Count}); delegating to SendCharacterList() for DFC format");
}
}
catch (Exception ex)
{
Debug.LogWarning($"[Game] SendUpdatedCharacterList: Pre-flight check warning: {ex.Message}");
}
await SendCharacterList(conn);
}
private async Task SendGroupConnectedResponse(RRConnection conn)
{
var w = new LEWriter();
w.WriteByte(9);
w.WriteByte(0);
await SendCompressedEResponseWithDump(conn, w.ToArray(), "group_connected_9_0");
await Task.Delay(50);
// await SendGoToZone_V2(conn, "Town");
}
private async Task HandleGroupChannelMessages(RRConnection conn, byte messageType)
{
switch (messageType)
{
case 0:
await SendGoToZone_V2(conn, "Town");
break;
default:
Debug.LogWarning($"[Game] Unhandled group msg 0x{messageType:X2}");
break;
}
}
private async Task SendGoToZone_V2(RRConnection conn, string zoneName)
{
// ✅ REMOVED THE CHECK THAT WAS BLOCKING MESSAGES
Debug.Log($"[Game] SendGoToZone: Sending player to zone '{zoneName}'");
try
{
// Step 1: Group connect - E-lane
var groupWriter = new LEWriter();
groupWriter.WriteByte(9);
groupWriter.WriteByte(48);
groupWriter.WriteUInt32(33752069);
groupWriter.WriteByte(1);
groupWriter.WriteByte(1);
await SendCompressedEResponseWithDump(conn, groupWriter.ToArray(), "group_connect_9_48");
await Task.Delay(300);
// Step 2: Zone connect - E-lane
var w = new LEWriter();
w.WriteByte(13);
w.WriteByte(0);
w.WriteCString(zoneName);
w.WriteUInt32(30);
w.WriteByte(0);
w.WriteUInt32(1);
await SendCompressedEResponseWithDump(conn, w.ToArray(), "zone_connect_13_0");
// Step 3: ClientEntity bootstrap - A-lane
/* await Task.Delay(80);
await SendCE_RandomSeed_A(conn);
await Task.Delay(80);*/
Debug.Log($"[Game] SendGoToZone: Sent initial messages, waiting for client Zone Join request");
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendGoToZone: Error: {ex.Message}");
}
}
private async Task HandleZoneChannelMessages(RRConnection conn, byte messageType, byte[] data)
{
Debug.Log($"[Game] HandleZoneChannelMessages: *** RECEIVED *** Zone channel message type {messageType} from {conn.LoginName}");
Debug.Log($"[Game] HandleZoneChannelMessages: Message data length: {data.Length} bytes");
switch (messageType)
{
case 6:
Debug.Log($"[Game] HandleZoneChannelMessages: Zone/6 detected - Client wants to join zone");
Debug.Log($"[Game] HandleZoneChannelMessages: Calling HandleZoneJoinRequest...");
await HandleZoneJoinRequest(conn);
Debug.Log($"[Game] HandleZoneChannelMessages: HandleZoneJoinRequest completed");
break;
case 8:
// Zone/8 is SENT by server, not received from client
Debug.LogWarning($"[Game] HandleZoneChannelMessages: ⚠️ Received Zone/8 from client - This is unusual!");
Debug.LogWarning($"[Game] HandleZoneChannelMessages: Zone/8 is normally SENT by server, not received");
Debug.LogWarning($"[Game] HandleZoneChannelMessages: Ignoring this message");
break;
case 1:
Debug.Log($"[Game] HandleZoneChannelMessages: Zone/1 received from client - Zone Ready response");
// Client acknowledging zone ready - usually we don't need to handle this
break;
case 5:
Debug.Log($"[Game] HandleZoneChannelMessages: Zone/5 received from client - Instance Count response");
// Client acknowledging instance count - usually we don't need to handle this
break;
default:
Debug.LogWarning($"[Game] HandleZoneChannelMessages: ⚠️ Unhandled zone message type 0x{messageType:X2}");
Debug.LogWarning($"[Game] HandleZoneChannelMessages: Data: {BitConverter.ToString(data)}");
Debug.LogWarning($"[Game] HandleZoneChannelMessages: This message type is not implemented");
break;
}
Debug.Log($"[Game] HandleZoneChannelMessages: *** FINISHED *** Processing Zone/{messageType}");
}
/// <summary>
/// Handles the complete zone join sequence when a client requests to enter a zone.
/// This method orchestrates all the messages needed to transition the client from
/// the character selection screen into the actual game world.
/// ORDER IS CRITICAL: Must match GO server sequence exactly or client will reject.
/// </summary>
/// <param name="conn">The player's connection</param>
/// <summary>
/// Handles the complete zone join sequence when a client requests to enter a zone.
/// This method orchestrates all the messages needed to transition the client from
/// the character selection screen into the actual game world.
/// ORDER IS CRITICAL: Must match GO server sequence exactly or client will reject.
/// </summary>
/// <param name="conn">The player's connection</param>
/* private async Task HandleZoneJoin(RRConnection conn)
{
if (conn.ZoneInitialized)
{
Debug.LogWarning($"[Game] HandleZoneJoin: Zone already initialized for {conn.LoginName}, ignoring duplicate 13/6 request");
return;
}
Debug.Log($"[Game] HandleZoneJoin: Client requested zone join (13/6)");
conn.ZoneInitialized = true;
// Send zone ready on E-lane
var zoneReady = new LEWriter();
zoneReady.WriteByte(13);
zoneReady.WriteByte(1);
zoneReady.WriteUInt32(1);
zoneReady.WriteUInt16(0x12);
for (int i = 0; i < 0x12; i++)
zoneReady.WriteUInt32(0xFFFFFFFF);
await SendCompressedEResponseWithDump(conn, zoneReady.ToArray(), "zone_ready_13_1");
// Send instance count on E-lane
var instanceCount = new LEWriter();
instanceCount.WriteByte(13);
instanceCount.WriteByte(5);
instanceCount.WriteUInt32(1);
instanceCount.WriteUInt32(1);
await SendCompressedEResponseWithDump(conn, instanceCount.ToArray(), "zone_instance_count_13_5");
await Task.Delay(100);
Debug.Log($"[Game] HandleZoneJoin: Building spawn data");
// BUILD SPAWN DATA (without BeginStream/EndStream - those are added below)
var spawnData = BuildPlayerSpawnData(conn);
if (spawnData == null)
{
Debug.LogError($"[Game] Failed to build spawn");
return;
}
Debug.Log($"[Game] ✅ Built spawn data: {spawnData.Length} bytes");
// ADD THIS DIAGNOSTIC HERE
Debug.Log($"[DIAGNOSTIC] First 4 bytes of spawn data: {BitConverter.ToString(spawnData.Take(4).ToArray())}");
// Should log: 01-0B-00-FF if correct
Debug.Log($"[Game] Spawn data first 50 bytes: {BitConverter.ToString(spawnData.Take(50).ToArray())}");
Debug.Log($"[Game] Spawn data last 10 bytes: {BitConverter.ToString(spawnData.Skip(Math.Max(0, spawnData.Length - 10)).ToArray())}");
// ✅ CRITICAL: Wrap in BeginStream + spawn operations + EndStreamConnected + EndStream
var finalData = new LEWriter();
finalData.WriteByte(0x07); // BeginStream
finalData.WriteBytes(spawnData); // All the spawn operations
finalData.WriteByte(0x46); // EndStreamConnected (70 decimal)
finalData.WriteByte(0x06); // EndStream
var finalBytes = finalData.ToArray();
Debug.Log($"[Game] ✅ Final wrapped data: {finalBytes.Length} bytes (original: {spawnData.Length})");
Debug.Log($"[Game] Final data first 50 bytes: {BitConverter.ToString(finalBytes.Take(50).ToArray())}");
Debug.Log($"[Game] Final data last 10 bytes: {BitConverter.ToString(finalBytes.Skip(Math.Max(0, finalBytes.Length - 10)).ToArray())}");
// Send on A-lane
await SendCompressedAResponseWithDump(conn, 0x01, 0x0F, finalBytes, "player_spawn_wrapped");
Debug.Log($"[Game] ✅ Sent wrapped spawn data");
// Send Zone/8 ready
await Task.Delay(100);
var zoneReadyFinal = new LEWriter();
zoneReadyFinal.WriteByte(13);
zoneReadyFinal.WriteByte(8);
await SendCompressedEResponseWithDump(conn, zoneReadyFinal.ToArray(), "zone_ready_final_13_8");
Debug.Log($"[Game] ✅ Sent final Zone/8 ready message");
Debug.Log($"[Game] HandleZoneJoin: Completed");
}*/
private async Task HandleZoneJoinRequest(RRConnection conn)
{
Debug.Log($"[Game] HandleZoneJoinRequest: ==================== ENTRY ====================");
Debug.Log($"[Game] HandleZoneJoinRequest: Client {conn.LoginName} sent Zone/6 (join request)");
// ✅ PREVENT DUPLICATE ZONE JOINS
if (conn.ZoneInitialized)
{
Debug.LogWarning($"[Game] HandleZoneJoinRequest: ⚠️ Zone already initialized for {conn.LoginName}, ignoring duplicate 13/6 request");
return;
}
Debug.Log($"[Game] HandleZoneJoinRequest: ⭐ Processing zone join for {conn.LoginName} ⭐");
// Mark as initialized NOW to prevent race conditions
conn.ZoneInitialized = true;
Debug.Log($"[Game] HandleZoneJoinRequest: Set ZoneInitialized=true for {conn.LoginName}");
// ==================== STEP 1: Send Zone Ready (13/1) ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 1] Building Zone Ready message (13/1)...");
var zoneReady = new LEWriter();
zoneReady.WriteByte(13); // Zone channel
zoneReady.WriteByte(1); // Ready message
zoneReady.WriteUInt32(1);
zoneReady.WriteUInt16(0x12);
for (int i = 0; i < 0x12; i++)
zoneReady.WriteUInt32(0xFFFFFFFF);
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 1] Sending Zone Ready (13/1) - {zoneReady.ToArray().Length} bytes");
await SendCompressedEResponseWithDump(conn, zoneReady.ToArray(), "zone_ready_13_1");
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 1] Zone Ready (13/1) sent successfully");
// ⭐ CRITICAL: Wait for client to process 13/1
await Task.Delay(50);
Debug.Log($"[Game] HandleZoneJoinRequest: Waited 50ms for client to process Zone Ready");
// ==================== STEP 2: Send Instance Count (13/5) ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 2] Building Instance Count message (13/5)...");
var instanceCount = new LEWriter();
instanceCount.WriteByte(13); // Zone channel
instanceCount.WriteByte(5); // Instance count message
instanceCount.WriteUInt32(1); // Current instance
instanceCount.WriteUInt32(1); // Total instances
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 2] Sending Instance Count (13/5) - {instanceCount.ToArray().Length} bytes");
await SendCompressedEResponseWithDump(conn, instanceCount.ToArray(), "zone_instance_count_13_5");
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 2] Instance Count (13/5) sent successfully");
// ==================== CRITICAL: Wait for client to reach State 115 and be ready for A-lane ====================
Debug.Log($"[Game] HandleZoneJoinRequest: ⭐⭐⭐ WAITING 500ms for client to enter State 115 and prepare for A-lane messages ⭐⭐⭐");
await Task.Delay(500);
Debug.Log($"[Game] HandleZoneJoinRequest: Client should now be in State 115 and ready for spawn data");
// ==================== STEP 3: Prepare for Spawn Data ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 3] ⭐⭐⭐ NOW BUILDING SPAWN DATA ⭐⭐⭐");
// CREATE QUEUE IF NEEDED
if (!_messageQueues.ContainsKey(conn.LoginName))
{
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 3] Creating new MessageQueue for {conn.LoginName}");
_messageQueues[conn.LoginName] = new MessageQueue(
async (d, t, data, tag) => await SendCompressedAResponseWithDump(conn, d, t, data, tag),
() => _spawnedPlayers.Contains(conn.LoginName)
);
_messageQueues[conn.LoginName].Start();
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 3] MessageQueue created and started for {conn.LoginName}");
}
else
{
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 3] MessageQueue already exists for {conn.LoginName}");
}
// ==================== STEP 4: Queue Spawn Operations ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 4] ⭐ Calling QueuePlayerSpawnOperations ⭐");
QueuePlayerSpawnOperations(conn);
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 4] QueuePlayerSpawnOperations completed");
// MARK SPAWNED
_spawnedPlayers.Add(conn.LoginName);
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 4] Marked {conn.LoginName} as spawned in _spawnedPlayers");
// ==================== STEP 5: Send Spawn Data Immediately ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 5] ⭐⭐⭐ STOPPING QUEUE AND PROCESSING IMMEDIATELY ⭐⭐⭐");
_messageQueues[conn.LoginName].Stop();
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 5] MessageQueue background loop stopped");
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 5] ⏰ Current time: {DateTime.Now:HH:mm:ss.fff}");
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 5] Calling ProcessImmediately to send all queued spawn operations...");
await _messageQueues[conn.LoginName].ProcessImmediately();
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 5] ⏰ Spawn sent at: {DateTime.Now:HH:mm:ss.fff}");
Debug.Log($"[Game] HandleZoneJoinRequest: ✅✅✅ [STEP 5] SPAWN DATA SENT ON A-LANE! Client should receive message 385!");
// ==================== STEP 6: Send Zone/8 Final Ready ====================
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 6] Waiting 200ms before sending final Zone/8...");
await Task.Delay(200);
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 6] Building final Zone Ready message (13/8)...");
var zoneReadyFinal = new LEWriter();
zoneReadyFinal.WriteByte(13); // Zone channel
zoneReadyFinal.WriteByte(8); // Final ready message
Debug.Log($"[Game] HandleZoneJoinRequest: [STEP 6] Sending Zone/8 (spawn complete) - {zoneReadyFinal.ToArray().Length} bytes");
await SendCompressedEResponseWithDump(conn, zoneReadyFinal.ToArray(), "zone_ready_final_13_8");
Debug.Log($"[Game] HandleZoneJoinRequest: ✅ [STEP 6] Zone/8 (final ready) sent successfully");
// ==================== COMPLETE ====================
Debug.Log($"[Game] HandleZoneJoinRequest: ⭐⭐⭐ SEQUENCE COMPLETE ⭐⭐⭐");
Debug.Log($"[Game] HandleZoneJoinRequest: Summary:");
Debug.Log($"[Game] HandleZoneJoinRequest: - Sent 13/1 (Zone Ready)");
Debug.Log($"[Game] HandleZoneJoinRequest: - Sent 13/5 (Instance Count)");
Debug.Log($"[Game] HandleZoneJoinRequest: - Waited 500ms for client State 115");
Debug.Log($"[Game] HandleZoneJoinRequest: - Sent A-lane spawn data with message 385");
Debug.Log($"[Game] HandleZoneJoinRequest: - Sent 13/8 (Final Ready)");
Debug.Log($"[Game] HandleZoneJoinRequest: Player {conn.LoginName} should now be spawned in the world!");
Debug.Log($"[Game] HandleZoneJoinRequest: ==================== EXIT ====================");
}
private void QueuePlayerSpawnOperations(RRConnection conn)
{
Debug.Log($"[Game] QueuePlayerSpawnOperations: *** STARTING *** for {conn.LoginName}");
if (!_selectedCharacter.TryGetValue(conn.LoginName, out var character))
{
Debug.LogError($"[Game] QueuePlayerSpawnOperations: ERROR - No character found for {conn.LoginName}");
return;
}
var player = character;
var avatar = player.Children.FirstOrDefault(c => c.NativeClass == "Avatar");
if (avatar == null)
{
Debug.LogError($"[Game] QueuePlayerSpawnOperations: ERROR - No avatar found for player {player.ID}");
return;
}
Debug.Log($"[Game] QueuePlayerSpawnOperations: Found player ID={player.ID}, avatar ID={avatar.ID}");
// Reassign IDs
_nextEntityId = 10;
ReassignEntityIDs(player);
Debug.Log($"[Game] QueuePlayerSpawnOperations: Reassigned IDs - Player={player.ID:X4}, Avatar={avatar.ID:X4}");
var queue = _messageQueues[conn.LoginName];
int operationCount = 0;
/*// ⭐⭐⭐ CRITICAL: Add BeginStream (0x07) FIRST - This starts the ClientEntity channel ⭐⭐⭐
var beginStream = new LEWriter();
beginStream.WriteByte(0x07); // BeginStream / ClientEntity channel marker
queue.Enqueue(beginStream.ToArray());
operationCount++;
Debug.Log($"[Game] ⭐ Queued operation {operationCount}: BeginStream (0x07) - ClientEntity channel start ({beginStream.ToArray().Length} bytes)");*/
// OPERATION 1: Create Avatar
var op1 = new LEWriter();
op1.WriteByte(0x01); // Create
op1.WriteUInt16((ushort)avatar.ID);
op1.WriteByte(0xFF); // String lookup
op1.WriteCString(avatar.GCClass);
queue.Enqueue(op1.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Avatar (ID={avatar.ID:X4}, {op1.ToArray().Length} bytes)");
// OPERATION 2: Create Player
var op2 = new LEWriter();
op2.WriteByte(0x01); // Create
op2.WriteUInt16((ushort)player.ID);
op2.WriteByte(0xFF); // String lookup
op2.WriteCString(player.GCClass);
queue.Enqueue(op2.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Player (ID={player.ID:X4}, {op2.ToArray().Length} bytes)");
// OPERATION 3: Init Player
var op3 = new LEWriter();
op3.WriteByte(0x02); // Init
op3.WriteUInt16((ushort)player.ID);
op3.WriteCString(player.Name);
op3.WriteUInt32(0);
op3.WriteUInt32(0);
op3.WriteByte(0xFF);
op3.WriteUInt32(1);
op3.WriteUInt32(1001);
op3.WriteUInt32(1000);
op3.WriteByte(0x00);
op3.WriteCString("RainbowRunners");
op3.WriteUInt32(0);
queue.Enqueue(op3.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Init Player (Name={player.Name}, {op3.ToArray().Length} bytes)");
// Get components
var manipulators = avatar.Children.FirstOrDefault(c => c.NativeClass == "Manipulators");
var equipment = avatar.Children.FirstOrDefault(c => c.NativeClass == "Equipment");
var questManager = player.Children.FirstOrDefault(c => c.NativeClass == "QuestManager");
var dialogManager = player.Children.FirstOrDefault(c => c.NativeClass == "DialogManager");
var unitContainer = avatar.Children.FirstOrDefault(c => c.NativeClass == "UnitContainer");
var modifiers = avatar.Children.FirstOrDefault(c => c.NativeClass == "Modifiers");
var skills = avatar.Children.FirstOrDefault(c => c.NativeClass == "Skills");
var unitBehavior = avatar.Children.FirstOrDefault(c => c.NativeClass == "UnitBehavior");
Debug.Log($"[Game] QueuePlayerSpawnOperations: Component check - " +
$"Manipulators={manipulators != null}, Equipment={equipment != null}, " +
$"QuestManager={questManager != null}, DialogManager={dialogManager != null}, " +
$"UnitContainer={unitContainer != null}, Modifiers={modifiers != null}, " +
$"Skills={skills != null}, UnitBehavior={unitBehavior != null}");
// OPERATION 4: Create Manipulators
if (manipulators != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)manipulators.ID);
op.WriteByte(0xFF);
op.WriteCString("manipulators"); // lowercase!
op.WriteByte(0x01);
op.WriteByte(0x00);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Manipulators (ID={manipulators.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 5: Create Equipment
if (equipment != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)equipment.ID);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.equipment"); // lowercase!
op.WriteByte(0x01);
op.WriteByte(0x00);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Equipment (ID={equipment.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 6: Create QuestManager (with full init data)
if (questManager != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)player.ID);
op.WriteUInt16((ushort)questManager.ID);
op.WriteByte(0xFF);
op.WriteCString(questManager.GCClass);
op.WriteByte(0x01);
// Full QuestManager init data
op.WriteUInt32(0x01);
op.WriteByte(0x01);
op.WriteCString("Hello");
op.WriteCString("HelloAgain");
op.WriteUInt32(0x01);
op.WriteByte(0x01);
op.WriteCString("HelloAgainAgain");
op.WriteCString("HelloAgainAgainAgain");
op.WriteUInt32(0x01);
op.WriteCString("Hi");
op.WriteCString("HiAgain");
op.WriteCString("HiAgainAgain");
op.WriteByte(0x00);
op.WriteUInt16(0x00);
op.WriteUInt16(0x00);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create QuestManager (ID={questManager.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 7: Create DialogManager
if (dialogManager != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)player.ID);
op.WriteUInt16((ushort)dialogManager.ID);
op.WriteByte(0xFF);
op.WriteCString(dialogManager.GCClass);
// NO 0x01 flag for DialogManager
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create DialogManager (ID={dialogManager.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 8: Create UnitContainer
if (unitContainer != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)unitContainer.ID);
op.WriteByte(0xFF);
op.WriteCString("UnitContainer"); // ✅ CORRECT
op.WriteByte(0x01);
op.WriteUInt32(1);
op.WriteUInt32(1);
op.WriteByte(0x03);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.Inventory");
op.WriteByte(0x0C);
op.WriteByte(0x01);
op.WriteByte(0x00);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.Bank");
op.WriteByte(0x0D);
op.WriteByte(0x01);
op.WriteByte(0x00);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.TradeInventory");
op.WriteByte(0x0E);
op.WriteByte(0x01);
op.WriteByte(0x00);
op.WriteByte(0x00);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create UnitContainer (ID={unitContainer.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 9: Create Modifiers
if (modifiers != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)modifiers.ID);
op.WriteByte(0xFF);
op.WriteCString("modifiers"); // lowercase!
op.WriteByte(0x01);
op.WriteUInt32(0x00);
op.WriteUInt32(0x00);
op.WriteByte(0x00);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Modifiers (ID={modifiers.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 10: Create Skills
if (skills != null)
{
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)skills.ID);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.skills"); // already lowercase
op.WriteByte(0x01);
op.WriteUInt32(0xFFFFFFFF);
op.WriteByte(0x00);
op.WriteByte(0x01);
op.WriteByte(0xFF);
op.WriteCString("skills.professions.Warrior");
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create Skills (ID={skills.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION 11: Create UnitBehavior
if (unitBehavior != null)
{
Debug.Log($"[DEBUG] UnitBehavior.GCClass = '{unitBehavior.GCClass}' (length: {unitBehavior.GCClass.Length})");
Debug.Log($"[DEBUG] UnitBehavior.NativeClass = '{unitBehavior.NativeClass}'");
var op = new LEWriter();
op.WriteByte(0x32);
op.WriteUInt16((ushort)avatar.ID);
op.WriteUInt16((ushort)unitBehavior.ID);
op.WriteByte(0xFF);
op.WriteCString("avatar.base.unitbehavior"); // LOWERCASE!
op.WriteByte(0x01);
op.WriteByte(0xFF);
op.WriteByte(0x00);
op.WriteByte(0x00);
op.WriteByte(0x01);
op.WriteByte(0x01);
op.WriteUInt32(0x00);
op.WriteByte(0x00);
op.WriteByte(0x01);
queue.Enqueue(op.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Create UnitBehavior (ID={unitBehavior.ID:X4}, {op.ToArray().Length} bytes)");
}
// OPERATION LAST: Init Avatar
var initAvatar = new LEWriter();
initAvatar.WriteByte(0x02);
initAvatar.WriteUInt16((ushort)avatar.ID);
initAvatar.WriteByte(0x04);
initAvatar.WriteInt32((int)(2400.0f * 256));
initAvatar.WriteInt32((int)(2400.0f * 256));
initAvatar.WriteInt32(0);
initAvatar.WriteInt32(0);
initAvatar.WriteByte(0x01);
initAvatar.WriteUInt16(0);
initAvatar.WriteByte(0x00);
initAvatar.WriteByte(0x00);
initAvatar.WriteByte(0x00);
initAvatar.WriteByte(0x00);
initAvatar.WriteByte(0x00);
initAvatar.WriteByte(0x07);
initAvatar.WriteByte(72);
initAvatar.WriteUInt16(0);
initAvatar.WriteUInt16(0);
initAvatar.WriteUInt16(0);
initAvatar.WriteByte(0x40);
initAvatar.WriteUInt16(13);
initAvatar.WriteUInt32(230528);
initAvatar.WriteUInt32(0);
initAvatar.WriteUInt16(10);
initAvatar.WriteUInt16(10);
initAvatar.WriteUInt16(10);
initAvatar.WriteUInt16(10);
initAvatar.WriteUInt16(0);
initAvatar.WriteUInt16(0);
initAvatar.WriteUInt32(0);
initAvatar.WriteUInt32(0);
initAvatar.WriteByte(0);
initAvatar.WriteByte(0);
initAvatar.WriteByte(0);
queue.Enqueue(initAvatar.ToArray());
operationCount++;
Debug.Log($"[Game] Queued operation {operationCount}: Init Avatar (ID={avatar.ID:X4}, {initAvatar.ToArray().Length} bytes)");
// ⭐⭐⭐ CRITICAL FIX: Add EndStreamConnected (0x46) - This is MESSAGE 385! ⭐⭐⭐
var endStreamConnected = new LEWriter();
endStreamConnected.WriteByte(0x46); // EndStreamConnected - MESSAGE 385 (Channel 7, Type 70)
queue.Enqueue(endStreamConnected.ToArray());
operationCount++;
Debug.Log($"[Game] ⭐ Queued operation {operationCount}: EndStreamConnected (0x46) - THIS IS MESSAGE 385! ({endStreamConnected.ToArray().Length} bytes)");
// ⭐⭐⭐ CRITICAL FIX: Add EndStream (0x06) marker ⭐⭐⭐
var endStream = new LEWriter();
endStream.WriteByte(0x06); // EndStream marker
queue.Enqueue(endStream.ToArray());
operationCount++;
Debug.Log($"[Game] ⭐ Queued operation {operationCount}: EndStream (0x06) - Stream terminator ({endStream.ToArray().Length} bytes)");
Debug.Log($"[Game] ✅✅✅ QueuePlayerSpawnOperations: COMPLETE - Queued {operationCount} total operations for {conn.LoginName}");
Debug.Log($"[Game] Queue now contains {operationCount} messages ready for immediate processing");
}
private byte[] BuildPlayerSpawnData(RRConnection conn)
{
if (!_selectedCharacter.TryGetValue(conn.LoginName, out var character))
{
Debug.LogError($"[Game] No character found");
return null;
}
var player = character;
var avatar = character.Children.FirstOrDefault(c => c.NativeClass == "Avatar");
if (avatar == null)
{
Debug.LogError($"[Game] No avatar found");
return null;
}
Debug.Log($"[SPAWN-DEBUG] Avatar.GCClass = '{avatar.GCClass}'");
Debug.Log($"[SPAWN-DEBUG] Avatar.NativeClass = '{avatar.NativeClass}'");
Debug.Log($"[SPAWN-DEBUG] Player.GCClass = '{player.GCClass}'");
Debug.Log($"[SPAWN-DEBUG] Player.NativeClass = '{player.NativeClass}'");
// ADD CORRUPTION CHECK HERE
Debug.Log($"[CORRUPTION-CHECK] Checking all avatar children for GCClass corruption:");
foreach (var child in avatar.Children)
{
Debug.Log($"[CORRUPTION-CHECK] {child.NativeClass}: GCClass='{child.GCClass}' (length={child.GCClass?.Length})");
if (child.GCClass != null && child.GCClass.Length > 50)
{
Debug.LogError($"[CORRUPTION-CHECK] ❌ CORRUPTED! {child.NativeClass} has {child.GCClass.Length} chars!");
Debug.LogError($"[CORRUPTION-CHECK] Corrupted value: '{child.GCClass}'");
// FIX CORRUPTED VALUES
switch (child.NativeClass)
{
case "UnitBehavior":
Debug.Log($"[CORRUPTION-FIX] Fixing UnitBehavior from {child.GCClass.Length} chars to 'avatar.base.UnitBehavior'");
child.GCClass = "avatar.base.UnitBehavior";
break;
case "Manipulators":
Debug.Log($"[CORRUPTION-FIX] Fixing Manipulators to 'Manipulators'");
child.GCClass = "Manipulators";
break;
case "Equipment":
Debug.Log($"[CORRUPTION-FIX] Fixing Equipment to 'avatar.base.Equipment'");
child.GCClass = "avatar.base.Equipment";
break;
case "UnitContainer":
Debug.Log($"[CORRUPTION-FIX] Fixing UnitContainer to 'UnitContainer'");
child.GCClass = "UnitContainer";
break;
case "Modifiers":
Debug.Log($"[CORRUPTION-FIX] Fixing Modifiers to 'Modifiers'");
child.GCClass = "Modifiers";
break;
case "Skills":
Debug.Log($"[CORRUPTION-FIX] Fixing Skills to 'avatar.base.skills'");
child.GCClass = "avatar.base.skills";
break;
}
}
}
// CHECK PLAYER CHILDREN TOO
Debug.Log($"[CORRUPTION-CHECK] Checking player children:");
foreach (var child in player.Children.Where(c => c.NativeClass != "Avatar"))
{
Debug.Log($"[CORRUPTION-CHECK] {child.NativeClass}: GCClass='{child.GCClass}' (length={child.GCClass?.Length})");
if (child.GCClass != null && child.GCClass.Length > 50)
{
Debug.LogError($"[CORRUPTION-CHECK] ❌ CORRUPTED! {child.NativeClass} has {child.GCClass.Length} chars!");
}
}
_nextEntityId = 10;
ReassignEntityIDs(player);
Debug.Log($"[Game] BuildPlayerSpawnData: Player ID={player.ID:X4}, Avatar ID={avatar.ID:X4}");
var writer = new LEWriter();
int bytePos = 0;
// ADD THIS RIGHT HERE - AT THE VERY START
//writer.WriteByte(0x07); // ClientEntityChannel (from BeginStream)
// Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x07 (ClientEntityChannel - BeginStream)");
// ADD SIZE CHECKPOINT
Debug.Log($"[SIZE-CHECK] Starting size: {writer.ToArray().Length} bytes");
// Create Avatar - using GCClass like GO server does
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Create Entity)");
writer.WriteUInt16((ushort)avatar.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{avatar.ID:X4} (Avatar ID)");
bytePos += 2;
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
int avatarClassLen = avatar.GCClass.Length;
writer.WriteCString(avatar.GCClass);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + avatarClassLen:D4}: '{avatar.GCClass}' + null");
bytePos += avatarClassLen + 1;
Debug.Log($"[Game] Created Avatar: ID={avatar.ID:X4}, GCClass={avatar.GCClass}");
// ADD SIZE CHECKPOINT
Debug.Log($"[SIZE-CHECK] After Avatar create: {writer.ToArray().Length} bytes");
// Create Player - using GCClass like GO server does
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Create Entity)");
writer.WriteUInt16((ushort)player.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{player.ID:X4} (Player ID)");
bytePos += 2;
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
int playerClassLen = player.GCClass.Length;
writer.WriteCString(player.GCClass);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + playerClassLen:D4}: '{player.GCClass}' + null");
bytePos += playerClassLen + 1;
Debug.Log($"[Game] Created Player: ID={player.ID:X4}, GCClass={player.GCClass}");
// ADD SIZE CHECKPOINT
Debug.Log($"[SIZE-CHECK] After Player create: {writer.ToArray().Length} bytes");
// Init Player
writer.WriteByte(0x02);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x02 (Init Entity)");
writer.WriteUInt16((ushort)player.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{player.ID:X4} (Player ID)");
bytePos += 2;
int nameLen = player.Name.Length;
writer.WriteCString(player.Name);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + nameLen:D4}: '{player.Name}' + null");
bytePos += nameLen + 1;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteUInt32(1001);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1001)");
bytePos += 4;
writer.WriteUInt32(1000);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1000)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteCString("RainbowRunners");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 14:D4}: 'RainbowRunners' + null");
bytePos += 15;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
Debug.Log($"[Game] Initialized Player");
// ADD SIZE CHECKPOINT
Debug.Log($"[SIZE-CHECK] After Player init: {writer.ToArray().Length} bytes");
// Create components
var componentOrder = new[] { "Manipulators", "Equipment", "QuestManager", "DialogManager", "UnitContainer", "Modifiers", "Skills", "UnitBehavior" };
foreach (var compName in componentOrder)
{
GCObject component = null;
GCObject parentEntity = null;
if (compName == "QuestManager" || compName == "DialogManager")
{
component = player.Children.FirstOrDefault(c => c.NativeClass == compName);
parentEntity = player;
}
else
{
component = avatar.Children.FirstOrDefault(c => c.NativeClass == compName);
parentEntity = avatar;
}
if (component == null)
{
Debug.LogWarning($"[Game] Component {compName} not found, skipping");
continue;
}
Debug.Log($"[SPAWN-COMPONENT] Creating {compName}: Parent={parentEntity.ID:X4} ({parentEntity.NativeClass}), Component={component.ID:X4}");
// ADD SIZE BEFORE COMPONENT
int sizeBeforeComponent = writer.ToArray().Length;
writer.WriteByte(0x32);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x32 (Create Component)");
writer.WriteUInt16((ushort)parentEntity.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{parentEntity.ID:X4} (Parent ID)");
bytePos += 2;
writer.WriteUInt16((ushort)component.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{component.ID:X4} (Component ID)");
bytePos += 2;
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
// FIX: Force lowercase for certain components to match Go server
string gcClassToWrite = component.GCClass;
if (compName == "UnitBehavior")
{
gcClassToWrite = "avatar.base.unitbehavior"; // lowercase!
}
else if (compName == "Manipulators")
{
gcClassToWrite = "manipulators"; // lowercase!
}
else if (compName == "Equipment")
{
gcClassToWrite = "avatar.base.equipment"; // lowercase!
}
else if (compName == "Modifiers")
{
gcClassToWrite = "modifiers"; // lowercase!
}
int compClassLen = gcClassToWrite.Length;
writer.WriteCString(gcClassToWrite);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + compClassLen:D4}: '{gcClassToWrite}' + null");
bytePos += compClassLen + 1;
if (compName != "DialogManager")
{
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Component flag)");
}
else
{
Debug.Log($"[SPAWN-BYTES] Skipping 0x01 flag for DialogManager");
}
// Component init data
Debug.Log($"[SPAWN-COMPONENT-INIT] Writing init data for {compName}");
int initStartPos = bytePos;
if (compName == "Manipulators")
{
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Manipulators init)");
}
else if (compName == "Equipment")
{
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Equipment init)");
}
else if (compName == "QuestManager")
{
// FIX: QuestManager needs full init data from Go server
Debug.Log($"[SPAWN-BYTES] QuestManager FULL init data start");
writer.WriteUInt32(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0x01)");
bytePos += 4;
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteCString("Hello");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 5:D4}: 'Hello' + null");
bytePos += 6;
writer.WriteCString("HelloAgain");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 10:D4}: 'HelloAgain' + null");
bytePos += 11;
writer.WriteUInt32(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0x01)");
bytePos += 4;
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteCString("HelloAgainAgain");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 15:D4}: 'HelloAgainAgain' + null");
bytePos += 16;
writer.WriteCString("HelloAgainAgainAgain");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 20:D4}: 'HelloAgainAgainAgain' + null");
bytePos += 21;
writer.WriteUInt32(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0x01)");
bytePos += 4;
writer.WriteCString("Hi");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 2:D4}: 'Hi' + null");
bytePos += 3;
writer.WriteCString("HiAgain");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 7:D4}: 'HiAgain' + null");
bytePos += 8;
writer.WriteCString("HiAgainAgain");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 12:D4}: 'HiAgainAgain' + null");
bytePos += 13;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (questgiver count)");
writer.WriteUInt16(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0) (active quest count)");
bytePos += 2;
writer.WriteUInt16(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0) (checkpoint count)");
bytePos += 2;
}
else if (compName == "DialogManager")
{
Debug.Log($"[SPAWN-BYTES] DialogManager has no init data");
}
else if (compName == "UnitContainer")
{
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteByte(0x03);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x03 (Inventory count)");
// FIX: Inventory IDs should be 12, 13, 14 not 11, 12, 13
// Inventory 1
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.Inventory");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 21:D4}: 'avatar.base.Inventory' + null");
bytePos += 22;
writer.WriteByte(12); // CHANGED FROM 11 TO 12
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0C (Inventory ID - FIXED)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
// Inventory 2
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.Bank");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 16:D4}: 'avatar.base.Bank' + null");
bytePos += 17;
writer.WriteByte(13); // CHANGED FROM 12 TO 13
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0D (Inventory ID - FIXED)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
// Inventory 3
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.TradeInventory");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 27:D4}: 'avatar.base.TradeInventory' + null");
bytePos += 28;
writer.WriteByte(14); // CHANGED FROM 13 TO 14
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0E (Inventory ID - FIXED)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (UnitContainer end)");
}
else if (compName == "Modifiers")
{
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Modifiers end)");
}
else if (compName == "Skills")
{
writer.WriteUInt32(0xFFFFFFFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0xFFFFFFFF)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteCString("skills.professions.Warrior");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 27:D4}: 'skills.professions.Warrior' + null");
bytePos += 28;
}
else if (compName == "UnitBehavior")
{
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (UnitBehavior end)");
}
Debug.Log($"[SPAWN-COMPONENT-INIT] {compName} init wrote {bytePos - initStartPos} bytes");
Debug.Log($"[Game] Created component: {compName}, GCClass={component.GCClass}");
// ADD SIZE AFTER COMPONENT
int sizeAfterComponent = writer.ToArray().Length;
Debug.Log($"[SIZE-CHECK] After {compName}: {sizeAfterComponent} bytes (added {sizeAfterComponent - sizeBeforeComponent} bytes)");
}
// Init Avatar last
Debug.Log($"[SPAWN-AVATAR-INIT] Starting Avatar init at byte {bytePos}");
// ADD SIZE BEFORE AVATAR INIT
int sizeBeforeAvatarInit = writer.ToArray().Length;
Debug.Log($"[SIZE-CHECK] Before Avatar init: {sizeBeforeAvatarInit} bytes");
writer.WriteByte(0x02);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x02 (Init Entity)");
writer.WriteUInt16((ushort)avatar.ID);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: 0x{avatar.ID:X4} (Avatar ID)");
bytePos += 2;
// FIX: WorldEntity.WriteInit() writes a UInt32 for flags, not a byte!
writer.WriteUInt32(0x04); // WorldEntity flags (4 bytes)
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0x04) (WorldEntity flags)");
bytePos += 4;
writer.WriteInt32((int)(2400.0f * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(X={2400.0f * 256})");
bytePos += 4;
writer.WriteInt32((int)(2400.0f * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Y={2400.0f * 256})");
bytePos += 4;
writer.WriteInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Z=0)");
bytePos += 4;
writer.WriteInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Rot=0)");
bytePos += 4;
// FIX: WorldEntity.WriteInit() writes init flags byte after position/rotation
writer.WriteByte(0x01); // WorldEntity init flags
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (WorldEntity init flags)");
// Now continue with Unit.WriteInit() data
writer.WriteByte(0x07); // Unit flags
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x07 (Unit flags)");
writer.WriteByte(72); // Level
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x48 (72 decimal - Level)");
writer.WriteUInt16(0); // UnitUnkUint16_0
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt16(0); // UnitUnkUint16_1
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt16(0); // Parent ID (Unit flag 0x01)
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0) (Parent ID)");
bytePos += 2;
// CRITICAL FIX: Unit.WriteInit() HP/MP because flags 0x07 has 0x02 and 0x04 set
Debug.Log($"[UNIT-HP-MP] Unit flags 0x07 has HP flag (0x02) and MP flag (0x04) set, writing HP/MP");
writer.WriteUInt32(230528); // Unit HP (900*256)
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(230528) Unit HP from flags");
bytePos += 4;
writer.WriteUInt32(0); // Unit MP
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0) Unit MP from flags");
bytePos += 4;
// Hero.WriteInit() data starts here - NO 0x40 BYTE!
Debug.Log($"[HERO-INIT] Starting Hero.WriteInit() data");
writer.WriteUInt32(1000); // ExpThisLevel
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1000) Hero ExpThisLevel");
bytePos += 4;
// Hero stats (6 UInt16 values)
writer.WriteUInt16(10); // Strength
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10) Hero Strength");
bytePos += 2;
writer.WriteUInt16(10); // Agility
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10) Hero Agility");
bytePos += 2;
writer.WriteUInt16(10); // Endurance
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10) Hero Endurance");
bytePos += 2;
writer.WriteUInt16(10); // Intellect
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10) Hero Intellect");
bytePos += 2;
writer.WriteUInt16(0); // StatPointsRemaining
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0) Hero StatPointsRemaining");
bytePos += 2;
writer.WriteUInt16(0); // RespecSomething
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0) Hero RespecSomething");
bytePos += 2;
// Hero unknown values
writer.WriteUInt32(0); // HeroUnk0
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0) Hero HeroUnk0");
bytePos += 4;
writer.WriteUInt32(0); // HeroUnk1
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0) Hero HeroUnk1");
bytePos += 4;
// Avatar.WriteInit() - just the three bytes for face/hair
Debug.Log($"[AVATAR-SPECIFIC] Writing Avatar-specific data (3 bytes)");
writer.WriteByte(0); // FaceVariant
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Avatar FaceVariant)");
writer.WriteByte(0); // HairStyle
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Avatar HairStyle)");
writer.WriteByte(0); // HairColour
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Avatar HairColour)");
// ADD THIS RIGHT HERE - BEFORE THE Debug.Log("Initialized Avatar last")
// writer.WriteByte(0x46); // 70 decimal - "Now connected" (from EndStreamConnected)
// Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x46 (EndStreamConnected - Now connected)");
Debug.Log($"[Game] Initialized Avatar last");
var spawnBytes = writer.ToArray();
Debug.Log($"[SPAWN] Total bytes: {spawnBytes.Length} (should be 834 with full QuestManager data)");
Debug.Log($"[SPAWN] Byte position counter: {bytePos}");
Debug.Log($"[SPAWN] Last 10 bytes HEX: {BitConverter.ToString(spawnBytes.Skip(Math.Max(0, spawnBytes.Length - 10)).ToArray())}");
Debug.Log($"[SPAWN] First 50 bytes HEX: {BitConverter.ToString(spawnBytes.Take(50).ToArray())}");
// DIAGNOSTIC CHECK FOR EXTRA BYTES
if (spawnBytes.Length != 834)
{
Debug.LogError($"[SPAWN-ERROR] ❌ Wrong size! Expected 834, got {spawnBytes.Length}");
if (spawnBytes.Length > 834)
{
Debug.LogError($"[SPAWN-ERROR] Extra bytes starting at position 834: {BitConverter.ToString(spawnBytes.Skip(834).ToArray())}");
}
}
else
{
Debug.Log($"[SPAWN-OK] ✅ Spawn data is correct size (834 bytes)");
}
if (spawnBytes.Length >= 450)
{
Debug.Log($"[SPAWN] Bytes 100-150: {BitConverter.ToString(spawnBytes.Skip(100).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 200-250: {BitConverter.ToString(spawnBytes.Skip(200).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 300-350: {BitConverter.ToString(spawnBytes.Skip(300).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 400-450: {BitConverter.ToString(spawnBytes.Skip(400).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 500-550: {BitConverter.ToString(spawnBytes.Skip(500).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 600-650: {BitConverter.ToString(spawnBytes.Skip(600).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 700-750: {BitConverter.ToString(spawnBytes.Skip(700).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 800-834: {BitConverter.ToString(spawnBytes.Skip(800).Take(34).ToArray())}");
}
return spawnBytes;
}
/*private async Task SendPlayerEntitySpawnGO(RRConnection conn)
{
if (!_selectedCharacter.TryGetValue(conn.LoginName, out var character))
{
Debug.LogError($"[Game] No character found");
return;
}
var player = character;
var avatar = character.Children.FirstOrDefault(c => c.NativeClass == "Avatar");
if (avatar == null)
{
Debug.LogError($"[Game] No avatar found");
return;
}
// Debug logging to verify values
Debug.Log($"[SPAWN-DEBUG] Avatar.GCClass = '{avatar.GCClass}'");
Debug.Log($"[SPAWN-DEBUG] Avatar.NativeClass = '{avatar.NativeClass}'");
Debug.Log($"[SPAWN-DEBUG] Player.GCClass = '{player.GCClass}'");
Debug.Log($"[SPAWN-DEBUG] Player.NativeClass = '{player.NativeClass}'");
_nextEntityId = 10;
ReassignEntityIDs(player);
Debug.Log($"[Game] SendPlayerEntitySpawnGO: Player ID={player.ID:X4}, Avatar ID={avatar.ID:X4}");
var writer = new LEWriter();
int bytePos = 0;
// Add BeginStream with ClientEntityChannel
// writer.WriteByte(0x07); // ClientEntityChannel
// Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x07 (BeginStream)");
// Create Avatar - using GCClass like GO server does
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Create Entity)");
writer.WriteByte((byte)((avatar.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((avatar.ID >> 8) & 0xFF):X2} (Avatar ID High)");
writer.WriteByte((byte)(avatar.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(avatar.ID & 0xFF):X2} (Avatar ID Low)");
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
int avatarClassLen = avatar.GCClass.Length;
writer.WriteCString(avatar.GCClass);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + avatarClassLen:D4}: '{avatar.GCClass}' + null");
bytePos += avatarClassLen + 1;
Debug.Log($"[Game] Created Avatar: ID={avatar.ID:X4}, GCClass={avatar.GCClass}");
// Create Player - using GCClass like GO server does
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Create Entity)");
writer.WriteByte((byte)((player.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((player.ID >> 8) & 0xFF):X2} (Player ID High)");
writer.WriteByte((byte)(player.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(player.ID & 0xFF):X2} (Player ID Low)");
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
int playerClassLen = player.GCClass.Length;
writer.WriteCString(player.GCClass);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + playerClassLen:D4}: '{player.GCClass}' + null");
bytePos += playerClassLen + 1;
Debug.Log($"[Game] Created Player: ID={player.ID:X4}, GCClass={player.GCClass}");
// Init Player
writer.WriteByte(0x02);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x02 (Init Entity)");
writer.WriteByte((byte)((player.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((player.ID >> 8) & 0xFF):X2} (Player ID High)");
writer.WriteByte((byte)(player.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(player.ID & 0xFF):X2} (Player ID Low)");
int nameLen = player.Name.Length;
writer.WriteCString(player.Name);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + nameLen:D4}: '{player.Name}' + null");
bytePos += nameLen + 1;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteUInt32(1001);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1001)");
bytePos += 4;
writer.WriteUInt32(1000);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1000)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteCString("RainbowRunners");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 14:D4}: 'RainbowRunners' + null");
bytePos += 15;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
Debug.Log($"[Game] Initialized Player");
// Create components
var componentOrder = new[] { "Manipulators", "Equipment", "QuestManager", "DialogManager", "UnitContainer", "Modifiers", "Skills", "UnitBehavior" };
foreach (var compName in componentOrder)
{
GCObject component = null;
GCObject parentEntity = null;
if (compName == "QuestManager" || compName == "DialogManager")
{
component = player.Children.FirstOrDefault(c => c.NativeClass == compName);
parentEntity = player;
}
else
{
component = avatar.Children.FirstOrDefault(c => c.NativeClass == compName);
parentEntity = avatar;
}
if (component == null)
{
Debug.LogWarning($"[Game] Component {compName} not found, skipping");
continue;
}
Debug.Log($"[SPAWN-COMPONENT] Creating {compName}: Parent={parentEntity.ID:X4} ({parentEntity.NativeClass}), Component={component.ID:X4}");
writer.WriteByte(0x32);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x32 (Create Component)");
writer.WriteByte((byte)((parentEntity.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((parentEntity.ID >> 8) & 0xFF):X2} (Parent ID High)");
writer.WriteByte((byte)(parentEntity.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(parentEntity.ID & 0xFF):X2} (Parent ID Low)");
writer.WriteByte((byte)((component.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((component.ID >> 8) & 0xFF):X2} (Component ID High)");
writer.WriteByte((byte)(component.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(component.ID & 0xFF):X2} (Component ID Low)");
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (String marker)");
int compClassLen = component.GCClass.Length;
writer.WriteCString(component.GCClass);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + compClassLen:D4}: '{component.GCClass}' + null");
bytePos += compClassLen + 1;
if (compName != "DialogManager")
{
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (Component flag)");
}
else
{
Debug.Log($"[SPAWN-BYTES] Skipping 0x01 flag for DialogManager");
}
// Component init data
Debug.Log($"[SPAWN-COMPONENT-INIT] Writing init data for {compName}");
int initStartPos = bytePos;
if (compName == "Manipulators")
{
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Manipulators init)");
}
else if (compName == "Equipment")
{
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Equipment init)");
}
else if (compName == "QuestManager")
{
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (QuestManager init)");
}
else if (compName == "DialogManager")
{
Debug.Log($"[SPAWN-BYTES] DialogManager has no init data");
}
else if (compName == "UnitContainer")
{
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteUInt32(1);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(1)");
bytePos += 4;
writer.WriteByte(0x03);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x03 (Inventory count)");
// Hardcode inventory 1
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.Inventory");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 21:D4}: 'avatar.base.Inventory' + null");
bytePos += 22;
writer.WriteByte(11);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0B (Inventory ID)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
// Hardcode inventory 2
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.Bank");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 16:D4}: 'avatar.base.Bank' + null");
bytePos += 17;
writer.WriteByte(12);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0C (Inventory ID)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
// Hardcode inventory 3 - TradeInventory from GO source
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF (Inventory marker)");
writer.WriteCString("avatar.base.TradeInventory");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 27:D4}: 'avatar.base.TradeInventory' + null");
bytePos += 28; // FIXED: 27 chars + 1 null = 28 bytes
writer.WriteByte(13);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x0D (Inventory ID)");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (UnitContainer end)");
}
else if (compName == "Modifiers")
{
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Modifiers end)");
}
else if (compName == "Skills")
{
writer.WriteUInt32(0xFFFFFFFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0xFFFFFFFF)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteCString("skills.professions.Warrior");
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 27:D4}: 'skills.professions.Warrior' + null");
bytePos += 28;
}
else if (compName == "UnitBehavior")
{
writer.WriteByte(0xFF);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0xFF");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteUInt32(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00");
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01 (UnitBehavior end)");
}
Debug.Log($"[SPAWN-COMPONENT-INIT] {compName} init wrote {bytePos - initStartPos} bytes");
Debug.Log($"[Game] Created component: {compName}, GCClass={component.GCClass}");
}
// Init Avatar last
Debug.Log($"[SPAWN-AVATAR-INIT] Starting Avatar init at byte {bytePos}");
writer.WriteByte(0x02);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x02 (Init Entity)");
writer.WriteByte((byte)((avatar.ID >> 8) & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)((avatar.ID >> 8) & 0xFF):X2} (Avatar ID High)");
writer.WriteByte((byte)(avatar.ID & 0xFF));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x{(byte)(avatar.ID & 0xFF):X2} (Avatar ID Low)");
writer.WriteByte(0x04);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x04 (Avatar init flag)");
writer.WriteInt32((int)(2400.0f * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(X={2400.0f * 256})");
bytePos += 4;
writer.WriteInt32((int)(2400.0f * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Y={2400.0f * 256})");
bytePos += 4;
writer.WriteInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Z=0)");
bytePos += 4;
writer.WriteInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: Int32(Rot=0)");
bytePos += 4;
writer.WriteByte(0x01);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x01");
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
// Add these 5 zero bytes to match GO server
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Extra padding 1)");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Extra padding 2)");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Extra padding 3)");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Extra padding 4)");
writer.WriteByte(0x00);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Extra padding 5)");
writer.WriteByte(0x07);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x07");
writer.WriteByte(72);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x48 (72 decimal)");
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt32((uint)(1000 * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(Health={1000 * 256})");
bytePos += 4;
writer.WriteUInt32((uint)(500 * 256));
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(Mana={500 * 256})");
bytePos += 4;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt16(10);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10)");
bytePos += 2;
writer.WriteUInt16(10);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10)");
bytePos += 2;
writer.WriteUInt16(10);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10)");
bytePos += 2;
writer.WriteUInt16(10);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(10)");
bytePos += 2;
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt16(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 1:D4}: UInt16(0)");
bytePos += 2;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
writer.WriteUInt32(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos:D4}-{bytePos + 3:D4}: UInt32(0)");
bytePos += 4;
// Write the three zero bytes
writer.WriteByte(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Final padding 1)");
writer.WriteByte(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Final padding 2)");
writer.WriteByte(0);
Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x00 (Final padding 3)");
Debug.Log($"[Game] Initialized Avatar last");
// Write both bytes together like GO server
// writer.WriteByte(70); // Now connected (decimal 70 = 0x46)
// Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x46 (70 decimal - Now Connected)");
// writer.WriteByte(0x06); // End stream
/// Debug.Log($"[SPAWN-BYTES] Pos {bytePos++:D4}: 0x06 (End Stream)");
var spawnBytes = writer.ToArray();
Debug.Log($"[SPAWN] Total bytes: {spawnBytes.Length} (expected 541 for GO)");
Debug.Log($"[SPAWN] Byte position counter: {bytePos}");
Debug.Log($"[SPAWN] Last 10 bytes HEX: {BitConverter.ToString(spawnBytes.Skip(Math.Max(0, spawnBytes.Length - 10)).ToArray())}");
Debug.Log($"[SPAWN] First 50 bytes HEX: {BitConverter.ToString(spawnBytes.Take(50).ToArray())}");
// Log sections for comparison
if (spawnBytes.Length >= 200)
{
Debug.Log($"[SPAWN] Bytes 100-150: {BitConverter.ToString(spawnBytes.Skip(100).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 200-250: {BitConverter.ToString(spawnBytes.Skip(200).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 300-350: {BitConverter.ToString(spawnBytes.Skip(300).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 400-450: {BitConverter.ToString(spawnBytes.Skip(400).Take(50).ToArray())}");
Debug.Log($"[SPAWN] Bytes 500-540: {BitConverter.ToString(spawnBytes.Skip(500).Take(40).ToArray())}");
}
await SendCompressedAResponseWithDump(conn, 0x01, 0x0F, spawnBytes, "player_entity_spawn_complete");
}*/
private async Task HandleZoneConnected(RRConnection conn)
{
Debug.Log($"[Game] HandleZoneConnected: Sending zone connected message to client {conn.ConnId}");
// Keep this message simple - the client expects just the channel and message type
var w = new LEWriter();
w.WriteByte(13); // Zone channel
w.WriteByte(0); // Connected message type
// No additional data - the client is expecting a simple message
await SendCompressedEResponseWithDump(conn, w.ToArray(), "zone_connected_13_0");
Debug.Log($"[Game] HandleZoneConnected: Zone connected message sent to client {conn.ConnId}");
}
private async Task HandleZoneReady(RRConnection conn)
{
Debug.Log($"[Game] HandleZoneReady: ⭐⭐⭐ CLIENT SENT ZONE/8 - NOW READY FOR SPAWN! ⭐⭐⭐");
// CREATE QUEUE IF NEEDED
if (!_messageQueues.ContainsKey(conn.LoginName))
{
_messageQueues[conn.LoginName] = new MessageQueue(
async (d, t, data, tag) => await SendCompressedAResponseWithDump(conn, d, t, data, tag),
() => _spawnedPlayers.Contains(conn.LoginName)
);
_messageQueues[conn.LoginName].Start();
Debug.Log($"[Game] ✅ MessageQueue STARTED for {conn.LoginName}");
}
// QUEUE SPAWN OPERATIONS
Debug.Log($"[Game] HandleZoneReady: Building and queueing spawn operations...");
QueuePlayerSpawnOperations(conn);
// MARK SPAWNED
_spawnedPlayers.Add(conn.LoginName);
Debug.Log($"[Game] ✅ Marked {conn.LoginName} as spawned");
// STOP BACKGROUND LOOP AND SEND IMMEDIATELY
_messageQueues[conn.LoginName].Stop();
Debug.Log($"[Game] ✅ Stopped queue background loop");
Debug.Log($"[Game] HandleZoneReady: Calling ProcessImmediately to send all spawn data...");
await _messageQueues[conn.LoginName].ProcessImmediately();
Debug.Log($"[Game] ✅✅✅ SPAWN DATA SENT - Client should now receive message 385!");
Debug.Log($"[Game] HandleZoneReady: Completed successfully");
}
private async Task HandleZoneReadyResponse(RRConnection conn)
{
var w = new LEWriter();
w.WriteByte(13);
w.WriteByte(1);
// await SendCompressedEResponseWithDump(conn, w.ToArray(), "zone_ready_resp_13_1");
}
private async Task HandleZoneInstanceCount(RRConnection conn)
{
Debug.Log($"[Game] HandleZoneInstanceCount: Sending zone instance count message to client {conn.ConnId}");
var w = new LEWriter();
w.WriteByte(13); // Zone channel
w.WriteByte(5); // Instance count message type
w.WriteUInt32(1); // Number of instances
await SendCompressedEResponseWithDump(conn, w.ToArray(), "zone_instance_count_13_5");
Debug.Log($"[Game] HandleZoneInstanceCount: Zone instance count message sent to client {conn.ConnId}");
}
private async Task HandleType31(RRConnection conn, Server.Game.LEReader reader)
{
// unchanged — logs + ack
Debug.Log($"[Game] HandleType31: remaining {reader.Remaining}");
await SendType31Ack(conn);
}
private async Task SendType31Ack(RRConnection conn)
{
try
{
var response = new LEWriter();
response.WriteByte(4);
response.WriteByte(1);
response.WriteUInt32(0);
await SendCompressedEResponseWithDump(conn, response.ToArray(), "type31_ack");
}
catch (Exception ex)
{
Debug.LogError($"[Game] SendType31Ack: {ex.Message}");
}
}
// ===================== E-lane receive/dispatch ==============================
private async Task HandleCompressedE(RRConnection conn, Server.Game.LEReader reader)
{
Debug.Log($"[Game] HandleCompressedE: (zlib1) remaining={reader.Remaining}");
// parse zlib1 header that client sends back (mirror our BuildCompressedEPayload_Zlib1)
const int MIN_HDR = 3 + 3 + 1 + 3 + 5 + 4;
if (reader.Remaining < MIN_HDR)
{
Debug.LogError($"[Game] HandleCompressedE: insufficient {reader.Remaining}");
return;
}
uint msgDest = reader.ReadUInt24();
uint compLen = reader.ReadUInt24();
byte zero = reader.ReadByte();
uint msgSource = reader.ReadUInt24();
byte b1 = reader.ReadByte(); // 01
byte b2 = reader.ReadByte(); // 00
byte b3 = reader.ReadByte(); // 01
byte b4 = reader.ReadByte(); // 00
byte b5 = reader.ReadByte(); // 00
uint unclen = reader.ReadUInt32();
int zLen = (int)compLen - 12;
if (zLen < 0 || reader.Remaining < zLen)
{
Debug.LogError($"[Game] HandleCompressedE: bad zLen={zLen} remaining={reader.Remaining}");
return;
}
byte[] comp = reader.ReadBytes(zLen);
byte[] inner;
try
{
inner = (zLen == 0 || unclen == 0) ? Array.Empty<byte>() : ZlibUtil.Inflate(comp, unclen);
}
catch (Exception ex)
{
Debug.LogError($"[Game] HandleCompressedE: inflate failed {ex.Message}");
return;
}
// inner begins with [channel][type]
await ProcessUncompressedEMessage(conn, inner);
}
private async Task ProcessUncompressedEMessage(RRConnection conn, byte[] inner)
{
// Never echo empty/short frames back to the client — they crash the entity manager.
if (inner.Length < 2)
{
Debug.Log("[E] Dropping empty/short E-lane frame (len < 2).");
return;
}
byte channel = inner[0];
byte type = inner[1];
// ADD THESE LOGS
Debug.Log($"[Game] ProcessUncompressedEMessage: *** E-LANE Channel={channel} Type=0x{type:X2} ***");
LogIncomingMessage($"E-Lane-Channel{channel}-Type{type:X2}", inner);
// Retail client keep-alive: 0/2 -> respond with exactly two bytes (0,2)
if (channel == 0 && type == 2)
{
var ack = new LEWriter();
ack.WriteByte(0x00);
ack.WriteByte(0x02);
await SendCompressedEResponseWithDump(conn, ack.ToArray(), "e_ack_0_2");
return;
}
// Everything else goes through normal channel handling.
await HandleChannelMessage(conn, inner);
}
// ===========================================================================
private async Task HandleType06(RRConnection conn, Server.Game.LEReader reader)
{
Debug.Log($"[Game] HandleType06: For client {conn.ConnId}");
}
private async Task<byte[]> SendMessage0x10(RRConnection conn, byte channel, byte[] body, string fullDumpTag = null)
{
uint clientId = GetClientId24(conn.ConnId);
uint bodyLen = (uint)(body?.Length ?? 0);
var w = new LEWriter();
w.WriteByte(0x10);
w.WriteUInt24((int)clientId); // peer is u24
// Force u24 body length EXACTLY (3 bytes). Avoid any buggy helper.
w.WriteByte((byte)(bodyLen & 0xFF));
w.WriteByte((byte)((bodyLen >> 8) & 0xFF));
w.WriteByte((byte)((bodyLen >> 16) & 0xFF));
w.WriteByte(channel);
if (bodyLen > 0) w.WriteBytes(body);
var payload = w.ToArray();
// Sanity: 0x10 frame must be 1+3+3+1 + bodyLen = 8 + bodyLen
int expected = 8 + (int)bodyLen;
if (payload.Length != expected)
Debug.LogError($"[Wire][0x10] BAD SIZE: got={payload.Length} expected={expected}");
if (!string.IsNullOrEmpty(fullDumpTag))
DumpUtil.DumpFullFrame(fullDumpTag, payload);
await conn.Stream.WriteAsync(payload, 0, payload.Length);
Debug.Log($"[Wire][0x10] Sent peer=0x{clientId:X6} bodyLen(u24)={bodyLen} ch=0x{channel:X2} total={payload.Length}");
return payload;
}
private uint GetClientId24(int connId) => _peerId24.TryGetValue(connId, out var id) ? id : 0u;
public void Stop()
{
lock (_gameLoopLock) { _gameLoopRunning = false; }
Debug.Log("[Game] Server stopping");
}
private async Task SendCE_Interval_A(RRConnection conn)
{
var writer = new LEWriter();
writer.WriteByte(0x07); // Channel 7
writer.WriteByte(0x0D); // Interval opcode
writer.WriteInt32(1);
writer.WriteInt32(100);
writer.WriteInt32(0);
writer.WriteInt32(0);
writer.WriteUInt16(100);
writer.WriteUInt16(20);
writer.WriteByte(0x06); // EndStream
await SendCompressedAResponseWithDump(conn, writer.ToArray(), "interval_7_13");
}
private async Task SendCE_RandomSeed_A(RRConnection conn, uint seed = 0xC0FFEE01)
{
var body = new LEWriter();
body.WriteByte(7); // ClientEntity channel
body.WriteByte(12); // Op_RandomSeed
body.WriteUInt32(seed);
body.WriteByte(0x06); // EndStream
await SendCompressedAResponseWithDump(conn, body.ToArray(), "ce_randseed_7_12");
}
private async Task SendCE_Connect_A(RRConnection conn)
{
// The Go server ends a CE bootstrap stream with a single opcode 70 (Connected).
// No extras, no length, no tail — just [7][70].
var body = new LEWriter();
body.WriteByte(7); // ClientEntity channel
body.WriteByte(70); // Op_Connected (end-of-stream indicator)
await SendCompressedAResponseWithDump(conn, body.ToArray(), "ce_connected_7_70");
}
private static void WriteInventoryChild(Server.Game.LEWriter w, string gcType, byte inventoryId)
{
w.WriteByte(0xFF); // lookup by string
WriteCString(w, gcType); // e.g., "avatar.base.Inventory"
w.WriteByte(inventoryId); // Inventory ID (1 = backpack, 2 = bank)
w.WriteByte(0x01); // Cannot be 0 (from GO server)
w.WriteByte(0x00); // Item count (0 = empty inventory)
}
public class RRConnection
{
public int ConnId { get; }
public TcpClient Client { get; }
public NetworkStream Stream { get; }
public string LoginName { get; set; } = "";
public bool IsConnected { get; set; } = true;
public bool ZoneInitialized { get; set; } = false;
public RRConnection(int connId, TcpClient client, NetworkStream stream)
{
ConnId = connId;
Client = client;
Stream = stream;
}
// === Added: Proper handling of server list response from Go server ===
private void HandleServerList(Server.Game.LEReader reader)
{
byte serverCount = reader.ReadByte();
byte lastServerId = reader.ReadByte();
for (int i = 0; i < serverCount; i++)
{
byte serverId = reader.ReadByte();
uint ip = reader.ReadUInt32();
uint port = reader.ReadUInt32(); // Go writes port as UInt32
ushort currentPlayers = reader.ReadUInt16();
ushort maxPlayers = reader.ReadUInt16();
byte status = reader.ReadByte();
Debug.Log($"[ServerList] ID={serverId} IP={ip} Port={port} Online={currentPlayers}/{maxPlayers} Status={status}");
// TODO: Integrate with your _playerCharacters or server/character selection system
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment