Created
October 9, 2025 03:22
-
-
Save timspurgeon/7fcd13774f2ceb8b0f6f28d6544c6b08 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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