Commit d323f7a9 authored by hex's avatar hex

optimize tcp weak-network handling and ocgcore performance baseline

parent 0f33a907
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
......@@ -14,9 +15,148 @@ public static class TcpHelper
static NetworkStream networkStream = null;
static bool canjoin = true;
const int SioKeepAliveVals = -1744830460;
static readonly object stateLock = new object();
static ConnectionState state = null;
static readonly ConcurrentQueue<byte[]> injectedIncoming = new ConcurrentQueue<byte[]>();
static long injectedIncomingBytes = 0;
static int injectedIncomingPackets = 0;
static int joinInProgress = 0;
static int generationCounter = 0;
static int disconnectGeneration = 0;
static Thread receiverThread = null;
static Thread senderThread = null;
public static int ConnectTimeoutMs = 10000;
public static int KeepAliveTimeMs = 20000;
public static int KeepAliveIntervalMs = 5000;
public static int DuelIdleHeartbeatMs = 15000;
public static int SendRetryDelayMs = 50;
public static int MaxTransientSendRetry = 3;
public static int SendTimeoutMs = 9999 * 1000;
public static int ReceiveTimeoutMs = 9999 * 1000;
public static bool TcpNoDelay = true;
public static bool TcpKeepAlive = true;
public static int OutgoingQueueLimitBytes = 4 * 1024 * 1024;
public static int OutgoingQueueLimitPackets = 4096;
public static int IncomingQueueLimitBytes = 8 * 1024 * 1024;
public static int IncomingQueueLimitPackets = 8192;
static bool roomListChecking = false;
sealed class ConnectionState : IDisposable
{
public readonly int Generation;
public readonly TcpClient Client;
public readonly NetworkStream Stream;
public readonly Socket Socket;
public readonly ConcurrentQueue<byte[]> Incoming = new ConcurrentQueue<byte[]>();
public readonly ConcurrentQueue<byte[]> Outgoing = new ConcurrentQueue<byte[]>();
public readonly AutoResetEvent OutgoingSignal = new AutoResetEvent(false);
public readonly CancellationTokenSource Cts = new CancellationTokenSource();
public volatile bool Closing = false;
public int DisconnectRequested = 0;
public long IncomingBytes = 0;
public int IncomingPackets = 0;
public long OutgoingBytes = 0;
public int OutgoingPackets = 0;
public int LastReceiveTick = 0;
public int LastSendTick = 0;
public int LastHeartbeatTick = 0;
public ConnectionState(int generation, TcpClient client)
{
Generation = generation;
Client = client;
Stream = client.GetStream();
Socket = client.Client;
int now = Environment.TickCount;
LastReceiveTick = now;
LastSendTick = now;
LastHeartbeatTick = now;
}
public void Dispose()
{
try
{
OutgoingSignal.Dispose();
}
catch { }
try
{
Cts.Dispose();
}
catch { }
}
}
public static void Disconnect(bool userInitiated = true)
{
onDisConnected = false;
Interlocked.Exchange(ref disconnectGeneration, 0);
CloseActiveConnection();
}
static int TickNow()
{
return Environment.TickCount;
}
static bool IsElapsed(int fromTick, int durationMs)
{
return unchecked(TickNow() - fromTick) >= durationMs;
}
static bool IsTransientSocketError(SocketError socketError)
{
return socketError == SocketError.WouldBlock
|| socketError == SocketError.IOPending
|| socketError == SocketError.NoBufferSpaceAvailable
|| socketError == SocketError.TimedOut
|| socketError == SocketError.Interrupted
|| socketError == SocketError.InProgress
|| socketError == SocketError.TryAgain;
}
static void TryDuelIdleHeartbeat(ConnectionState localState)
{
if (localState == null || localState.Closing)
return;
Program program = Program.I();
if (program == null || program.ocgcore == null)
return;
if (program.ocgcore.condition != Ocgcore.Condition.duel)
return;
int lastReceive = Volatile.Read(ref localState.LastReceiveTick);
int lastSend = Volatile.Read(ref localState.LastSendTick);
int lastActivity = unchecked(lastSend - lastReceive) > 0 ? lastSend : lastReceive;
if (!IsElapsed(lastActivity, DuelIdleHeartbeatMs))
return;
int lastHeartbeat = Volatile.Read(ref localState.LastHeartbeatTick);
if (!IsElapsed(lastHeartbeat, DuelIdleHeartbeatMs))
return;
Volatile.Write(ref localState.LastHeartbeatTick, TickNow());
CtosMessage_TimeConfirm();
}
public static void join(
string ipString,
string name,
......@@ -25,209 +165,427 @@ public static class TcpHelper
string version
)
{
if (canjoin)
if (Interlocked.CompareExchange(ref joinInProgress, 1, 0) != 0)
{
return;
}
TcpClient client = null;
ConnectionState newState = null;
try
{
if (tcpClient == null || tcpClient.Connected == false)
onDisConnected = false;
roomListChecking = pswString == "L";
CloseActiveConnection();
int port = int.Parse(portString);
client = new TcpClientWithTimeout(
ipString,
port,
ConnectTimeoutMs
).Connect();
ConfigureSocket(client);
int generation = Interlocked.Increment(ref generationCounter);
newState = new ConnectionState(generation, client);
try
{
canjoin = false;
try
{
tcpClient = new TcpClientWithTimeout(
ipString,
int.Parse(portString),
3000
).Connect();
networkStream = tcpClient.GetStream();
Thread t = new Thread(receiver);
t.Start();
CtosMessage_ExternalAddress(ipString);
CtosMessage_PlayerInfo(name);
if (pswString == "L")
{
roomListChecking = true;
}
else
{
roomListChecking = false;
}
CtosMessage_JoinGame(pswString, version);
}
catch (Exception e)
{
Program.DEBUGLOG("onDisConnected 10");
}
canjoin = true;
newState.Stream.ReadTimeout = ReceiveTimeoutMs;
}
catch { }
try
{
newState.Stream.WriteTimeout = SendTimeoutMs;
}
catch { }
lock (stateLock)
{
state = newState;
tcpClient = client;
networkStream = newState.Stream;
}
receiverThread = new Thread(ReceiverLoop);
receiverThread.IsBackground = true;
receiverThread.Start(newState);
senderThread = new Thread(SenderLoop);
senderThread.IsBackground = true;
senderThread.Start(newState);
CtosMessage_ExternalAddress(ipString);
CtosMessage_PlayerInfo(name);
CtosMessage_JoinGame(pswString, version);
}
else
catch (Exception e)
{
onDisConnected = true;
Program.DEBUGLOG("onDisConnected 1");
Program.DEBUGLOG("onDisConnected 10: " + e.Message);
try
{
client?.Close();
}
catch { }
try
{
newState?.Dispose();
}
catch { }
CloseActiveConnection();
}
finally
{
Interlocked.Exchange(ref joinInProgress, 0);
}
}
public static void receiver()
static void ConfigureSocket(TcpClient client)
{
if (client == null)
return;
try
{
client.NoDelay = TcpNoDelay;
}
catch { }
try
{
client.Client.NoDelay = TcpNoDelay;
}
catch { }
try
{
while (
tcpClient != null && networkStream != null && tcpClient.Connected && Program.Running
)
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, TcpKeepAlive);
}
catch { }
try
{
byte[] keepAlive = new byte[12];
BitConverter.GetBytes((uint)1).CopyTo(keepAlive, 0);
BitConverter.GetBytes((uint)KeepAliveTimeMs).CopyTo(keepAlive, 4);
BitConverter.GetBytes((uint)KeepAliveIntervalMs).CopyTo(keepAlive, 8);
client.Client.IOControl((IOControlCode)SioKeepAliveVals, keepAlive, null);
}
catch { }
try
{
client.Client.SendTimeout = SendTimeoutMs;
client.Client.ReceiveTimeout = ReceiveTimeoutMs;
}
catch { }
}
static void ReceiverLoop(object obj)
{
var localState = (ConnectionState)obj;
try
{
var token = localState.Cts.Token;
while (!token.IsCancellationRequested && Program.Running)
{
byte[] data = SocketMaster.ReadPacket(networkStream);
addDateJumoLine(data);
byte[] data = SocketMaster.ReadPacket(localState.Stream, token);
if (data == null)
{
RequestDisconnect(localState, "onDisConnected 2");
break;
}
Volatile.Write(ref localState.LastReceiveTick, TickNow());
if (!TryEnqueueIncoming(localState, data))
{
break;
}
}
onDisConnected = true;
Program.DEBUGLOG("onDisConnected 2");
}
catch (Exception e)
{
onDisConnected = true;
Program.DEBUGLOG("onDisConnected 3");
RequestDisconnect(localState, "onDisConnected 3: " + e.Message);
}
}
static bool TryEnqueueIncoming(ConnectionState localState, byte[] data)
{
if (data == null)
return false;
long bytes = Interlocked.Add(ref localState.IncomingBytes, data.Length);
int packets = Interlocked.Increment(ref localState.IncomingPackets);
if (bytes > IncomingQueueLimitBytes || packets > IncomingQueueLimitPackets)
{
RequestDisconnect(localState, "onDisConnected incoming overflow");
return false;
}
localState.Incoming.Enqueue(data);
return true;
}
// For offline duel core / replay engine: inject packets into the same main-thread dispatcher.
public static void addDateJumoLine(byte[] data)
{
Monitor.Enter(datas);
if (data == null)
return;
long bytes = Interlocked.Add(ref injectedIncomingBytes, data.Length);
int packets = Interlocked.Increment(ref injectedIncomingPackets);
if (bytes > IncomingQueueLimitBytes || packets > IncomingQueueLimitPackets)
{
Interlocked.Add(ref injectedIncomingBytes, -data.Length);
Interlocked.Decrement(ref injectedIncomingPackets);
return;
}
injectedIncoming.Enqueue(data);
}
static void RequestDisconnect(ConnectionState localState, string debugLog)
{
if (localState == null)
return;
if (Interlocked.Exchange(ref localState.DisconnectRequested, 1) != 0)
return;
localState.Closing = true;
try
{
datas.Add(data);
localState.Cts.Cancel();
}
catch (System.Exception e)
catch { }
try
{
// Debug.Log(e);
localState.OutgoingSignal.Set();
}
Monitor.Exit(datas);
}
catch { }
public static bool onDisConnected = false;
bool shouldNotifyMainThread = false;
lock (stateLock)
{
shouldNotifyMainThread = ReferenceEquals(state, localState);
}
static List<byte[]> datas = new List<byte[]>();
if (shouldNotifyMainThread)
{
Interlocked.Exchange(ref disconnectGeneration, localState.Generation);
onDisConnected = true;
Program.DEBUGLOG(debugLog);
}
}
public static void preFrameFunction()
static void CloseActiveConnection()
{
if (datas.Count > 0)
ConnectionState oldState = null;
lock (stateLock)
{
if (Monitor.TryEnter(datas))
oldState = state;
state = null;
}
if (oldState != null)
{
oldState.Closing = true;
try
{
for (int i = 0; i < datas.Count; i++)
oldState.Cts.Cancel();
}
catch { }
try
{
oldState.OutgoingSignal.Set();
}
catch { }
try
{
if (oldState.Client != null)
{
try
{
MemoryStream memoryStream = new MemoryStream(datas[i]);
BinaryReader r = new BinaryReader(memoryStream);
var ms = (StocMessage)(r.ReadByte());
switch (ms)
if (oldState.Client.Connected)
{
case StocMessage.GameMsg:
Program.I().room.StocMessage_GameMsg(r);
break;
case StocMessage.ErrorMsg:
Program.I().room.StocMessage_ErrorMsg(r);
break;
case StocMessage.SelectHand:
Program.I().room.StocMessage_SelectHand(r);
break;
case StocMessage.SelectTp:
Program.I().room.StocMessage_SelectTp(r);
break;
case StocMessage.HandResult:
Program.I().room.StocMessage_HandResult(r);
break;
case StocMessage.TpResult:
Program.I().room.StocMessage_TpResult(r);
break;
case StocMessage.ChangeSide:
Program.I().room.StocMessage_ChangeSide(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.WaitingSide:
Program.I().room.StocMessage_WaitingSide(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.DeckCount:
Program.I().room.StocMessage_DeckCount(r);
break;
case StocMessage.CreateGame:
Program.I().room.StocMessage_CreateGame(r);
break;
case StocMessage.JoinGame:
Program.I().room.StocMessage_JoinGame(r);
break;
case StocMessage.TypeChange:
Program.I().room.StocMessage_TypeChange(r);
break;
case StocMessage.LeaveGame:
Program.I().room.StocMessage_LeaveGame(r);
break;
case StocMessage.DuelStart:
Program.I().room.StocMessage_DuelStart(r);
break;
case StocMessage.DuelEnd:
Program.I().room.StocMessage_DuelEnd(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.Replay:
Program.I().room.StocMessage_Replay(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.TimeLimit:
Program.I().ocgcore.StocMessage_TimeLimit(r);
break;
case StocMessage.Chat:
Program.I().room.StocMessage_Chat(r);
break;
case StocMessage.HsPlayerEnter:
Program.I().room.StocMessage_HsPlayerEnter(r);
break;
case StocMessage.HsPlayerChange:
Program.I().room.StocMessage_HsPlayerChange(r);
break;
case StocMessage.HsWatchChange:
Program.I().room.StocMessage_HsWatchChange(r);
break;
case StocMessage.TeammateSurrender:
Program.I().room.StocMessage_TeammateSurrender(r);
break;
case YGOSharp.Network.Enums.StocMessage.RoomList:
((Room)Program.I().room).StocMessage_RoomList(r);
break;
default:
break;
oldState.Socket.Shutdown(SocketShutdown.Both);
}
}
catch (System.Exception e)
catch { }
try
{
// Program.DEBUGLOG(e);
oldState.Client.Close();
}
catch { }
}
datas.Clear();
Monitor.Exit(datas);
}
catch { }
try
{
oldState.Stream.Close();
}
catch { }
oldState.Dispose();
}
if (onDisConnected == true)
tcpClient = null;
networkStream = null;
}
public static volatile bool onDisConnected = false;
static void DispatchIncomingPacket(byte[] packet)
{
try
{
onDisConnected = false;
Program.I().ocgcore.setDefaultReturnServant();
MemoryStream memoryStream = new MemoryStream(packet);
BinaryReader r = new BinaryReader(memoryStream);
var ms = (StocMessage)(r.ReadByte());
switch (ms)
{
case StocMessage.GameMsg:
Program.I().room.StocMessage_GameMsg(r);
break;
case StocMessage.ErrorMsg:
Program.I().room.StocMessage_ErrorMsg(r);
break;
case StocMessage.SelectHand:
Program.I().room.StocMessage_SelectHand(r);
break;
case StocMessage.SelectTp:
Program.I().room.StocMessage_SelectTp(r);
break;
case StocMessage.HandResult:
Program.I().room.StocMessage_HandResult(r);
break;
case StocMessage.TpResult:
Program.I().room.StocMessage_TpResult(r);
break;
case StocMessage.ChangeSide:
Program.I().room.StocMessage_ChangeSide(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.WaitingSide:
Program.I().room.StocMessage_WaitingSide(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.DeckCount:
Program.I().room.StocMessage_DeckCount(r);
break;
case StocMessage.CreateGame:
Program.I().room.StocMessage_CreateGame(r);
break;
case StocMessage.JoinGame:
Program.I().room.StocMessage_JoinGame(r);
break;
case StocMessage.TypeChange:
Program.I().room.StocMessage_TypeChange(r);
break;
case StocMessage.LeaveGame:
Program.I().room.StocMessage_LeaveGame(r);
break;
case StocMessage.DuelStart:
Program.I().room.StocMessage_DuelStart(r);
break;
case StocMessage.DuelEnd:
Program.I().room.StocMessage_DuelEnd(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.Replay:
Program.I().room.StocMessage_Replay(r);
// TcpHelper.SaveRecord();
break;
case StocMessage.TimeLimit:
Program.I().ocgcore.StocMessage_TimeLimit(r);
break;
case StocMessage.Chat:
Program.I().room.StocMessage_Chat(r);
break;
case StocMessage.HsPlayerEnter:
Program.I().room.StocMessage_HsPlayerEnter(r);
break;
case StocMessage.HsPlayerChange:
Program.I().room.StocMessage_HsPlayerChange(r);
break;
case StocMessage.HsWatchChange:
Program.I().room.StocMessage_HsWatchChange(r);
break;
case StocMessage.TeammateSurrender:
Program.I().room.StocMessage_TeammateSurrender(r);
break;
case YGOSharp.Network.Enums.StocMessage.RoomList:
((Room)Program.I().room).StocMessage_RoomList(r);
break;
default:
break;
}
}
catch (System.Exception e)
{
// Program.DEBUGLOG(e);
}
}
try
public static void preFrameFunction()
{
ConnectionState localState = null;
lock (stateLock)
{
localState = state;
}
TryDuelIdleHeartbeat(localState);
if (localState != null && !ReferenceEquals(tcpClient, localState.Client))
{
RequestDisconnect(localState, "onDisConnected external close");
}
while (injectedIncoming.TryDequeue(out var injectedPacket))
{
Interlocked.Add(ref injectedIncomingBytes, -injectedPacket.Length);
Interlocked.Decrement(ref injectedIncomingPackets);
DispatchIncomingPacket(injectedPacket);
}
if (localState != null)
{
while (localState.Incoming.TryDequeue(out var packet))
{
if (tcpClient != null)
{
if (tcpClient.Connected)
{
tcpClient.Client.Shutdown(SocketShutdown.Both);
}
tcpClient.Close();
}
Interlocked.Add(ref localState.IncomingBytes, -packet.Length);
Interlocked.Decrement(ref localState.IncomingPackets);
DispatchIncomingPacket(packet);
}
catch (Exception e)
}
if (onDisConnected == true)
{
onDisConnected = false;
int gen = Interlocked.Exchange(ref disconnectGeneration, 0);
bool closeNow = false;
lock (stateLock)
{
// Debug.LogWarning("Socket cleanup failed: " + e.Message);
closeNow = gen != 0 && state != null && state.Generation == gen;
}
if (gen != 0 && !closeNow)
return; // stale disconnect from previous connection, ignore
Program.I().ocgcore.setDefaultReturnServant();
if (closeNow)
CloseActiveConnection();
tcpClient = null;
if (Program.I().ocgcore.isShowed == false)
{
if (Program.I().menu.isShowed == false)
......@@ -260,47 +618,130 @@ public static class TcpHelper
public static void Send(Package message)
{
if (tcpClient != null && tcpClient.Connected)
ConnectionState localState = null;
lock (stateLock)
{
// 用线程池代替 Thread
System.Threading.ThreadPool.QueueUserWorkItem(sender, message);
localState = state;
}
}
static object locker = new object();
if (localState == null || localState.Closing)
return;
static void sender(object state)
{
try
{
Package message = (Package)state;
byte[] data = message.Data.get();
byte[] frame = BuildFrame(message);
if (frame == null)
return;
long bytes = Interlocked.Add(ref localState.OutgoingBytes, frame.Length);
int packets = Interlocked.Increment(ref localState.OutgoingPackets);
if (bytes > OutgoingQueueLimitBytes || packets > OutgoingQueueLimitPackets)
{
Interlocked.Add(ref localState.OutgoingBytes, -frame.Length);
Interlocked.Decrement(ref localState.OutgoingPackets);
RequestDisconnect(localState, "onDisConnected outgoing overflow");
return;
}
localState.Outgoing.Enqueue(frame);
localState.OutgoingSignal.Set();
}
catch (Exception e)
{
RequestDisconnect(localState, "onDisConnected 5: " + e.Message);
}
}
// 预分配足够的 buffer,避免多余的内存流
int totalLen = 2 + 1 + data.Length;
byte[] s = new byte[totalLen];
static byte[] BuildFrame(Package message)
{
if (message == null || message.Data == null)
return null;
byte[] data = message.Data.get();
// 写入长度(short,包含功能码长度)
short len = (short)(data.Length + 1);
s[0] = (byte)(len & 0xFF);
s[1] = (byte)((len >> 8) & 0xFF);
int totalLen = 2 + 1 + data.Length;
byte[] s = new byte[totalLen];
// 写入功能码
s[2] = (byte)message.Fuction;
ushort len = (ushort)(data.Length + 1);
s[0] = (byte)(len & 0xFF);
s[1] = (byte)((len >> 8) & 0xFF);
s[2] = (byte)message.Fuction;
Buffer.BlockCopy(data, 0, s, 3, data.Length);
return s;
}
// 写入数据
Buffer.BlockCopy(data, 0, s, 3, data.Length);
static void SenderLoop(object obj)
{
var localState = (ConnectionState)obj;
var token = localState.Cts.Token;
// 只锁发送
lock (locker)
try
{
while (!token.IsCancellationRequested && Program.Running)
{
tcpClient.Client.Send(s);
if (!localState.Outgoing.TryDequeue(out var frame))
{
localState.OutgoingSignal.WaitOne(100);
continue;
}
Interlocked.Add(ref localState.OutgoingBytes, -frame.Length);
Interlocked.Decrement(ref localState.OutgoingPackets);
try
{
SendAll(localState.Socket, frame, token);
Volatile.Write(ref localState.LastSendTick, TickNow());
}
catch (Exception e)
{
RequestDisconnect(localState, "onDisConnected 5: " + e.Message);
break;
}
}
}
catch (Exception e)
{
onDisConnected = true;
Program.DEBUGLOG("onDisConnected 5: " + e.Message + "\n" + e.StackTrace);
RequestDisconnect(localState, "onDisConnected sender loop: " + e.Message);
}
}
static void SendAll(Socket socket, byte[] buffer, CancellationToken token)
{
if (socket == null || buffer == null)
return;
int offset = 0;
int retry = 0;
while (offset < buffer.Length)
{
if (token.IsCancellationRequested)
return;
try
{
int sent = socket.Send(buffer, offset, buffer.Length - offset, SocketFlags.None);
if (sent <= 0)
throw new IOException("socket send returned 0");
offset += sent;
retry = 0;
}
catch (SocketException socketException)
{
if (
IsTransientSocketError(socketException.SocketErrorCode)
&& retry < MaxTransientSendRetry
)
{
retry++;
Thread.Sleep(SendRetryDelayMs * retry);
continue;
}
throw;
}
}
}
......@@ -870,30 +1311,72 @@ public static class BinaryExtensions
public class SocketMaster
{
static byte[] ReadFull(NetworkStream stream, int length)
const int HeaderLength = 2;
const int MaxPayloadLength = 0xFFFF;
static byte[] ReadFull(NetworkStream stream, int length, CancellationToken token)
{
if (stream == null)
return null;
if (length == 0)
return Array.Empty<byte>();
if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length));
var buf = new byte[length];
int rlen = 0;
while (rlen < buf.Length)
{
int currentLength = stream.Read(buf, rlen, buf.Length - rlen);
rlen += currentLength;
if (currentLength == 0)
if (token.IsCancellationRequested)
return null;
int currentLength = 0;
try
{
currentLength = stream.Read(buf, rlen, buf.Length - rlen);
}
catch (IOException ioEx) when (IsTimeout(ioEx))
{
continue;
}
catch (SocketException se) when (se.SocketErrorCode == SocketError.TimedOut)
{
TcpHelper.onDisConnected = true;
Program.DEBUGLOG("onDisConnected 6");
break;
continue;
}
catch (ObjectDisposedException)
{
return null;
}
rlen += currentLength;
if (currentLength == 0)
return null;
}
return buf;
}
public static byte[] ReadPacket(NetworkStream stream)
static bool IsTimeout(IOException ioEx)
{
var hdr = ReadFull(stream, 2);
if (ioEx == null)
return false;
if (ioEx.InnerException is SocketException se)
{
return se.SocketErrorCode == SocketError.TimedOut;
}
return false;
}
public static byte[] ReadPacket(NetworkStream stream, CancellationToken token)
{
var hdr = ReadFull(stream, HeaderLength, token);
if (hdr == null)
return null;
var plen = BitConverter.ToUInt16(hdr, 0);
var buf = ReadFull(stream, plen);
if (plen == 0 || plen > MaxPayloadLength)
return null;
var buf = ReadFull(stream, plen, token);
return buf;
}
}
......@@ -1009,4 +1492,4 @@ public class TcpClientWithTimeout
exception = ex;
}
}
}
\ No newline at end of file
}
......@@ -64,9 +64,9 @@ public class MyCard : WindowServantSP
{
TerminateRequest();
}
if (TcpHelper.tcpClient != null && TcpHelper.tcpClient.Connected)
if (TcpHelper.tcpClient != null)
{
TcpHelper.tcpClient.Close();
TcpHelper.Disconnect(true);
}
}
......
......@@ -48,6 +48,20 @@ public class Ocgcore : ServantWithCardDescription
List<linkMask> linkMaskList = new List<linkMask>();
readonly List<gameCard> realizeToClearCards = new List<gameCard>(64);
readonly List<gameCard> realizeOpponentMonsterCards = new List<gameCard>(16);
readonly List<gameCard> realizeOpponentSpellCards = new List<gameCard>(16);
readonly List<GPS> realizeLinkPositions = new List<GPS>(32);
readonly List<linkMask> realizeLinkMaskRemoveBuffer = new List<linkMask>(16);
readonly List<gameCard> realizeOpponentHandLine = new List<gameCard>(16);
readonly List<thunder_locator> realizeThunderRemoveBuffer = new List<thunder_locator>(16);
readonly List<gameCard> realizeMyPendulumCards = new List<gameCard>(4);
readonly List<gameCard> realizeOpponentPendulumCards = new List<gameCard>(4);
readonly List<gameCard> realizeEmptyOverlayList = new List<gameCard>(0);
readonly Dictionary<int, List<gameCard>> realizeOverlayMap = new Dictionary<int, List<gameCard>>(64);
readonly Stack<List<gameCard>> realizeOverlayListPool = new Stack<List<gameCard>>();
linkMask makeLinkMask(GPS p)
{
linkMask ma = new linkMask();
......@@ -100,6 +114,106 @@ public class Ocgcore : ServantWithCardDescription
}
}
static int OverlayKey(GPS p)
{
return ((int)p.controller << 24) | ((int)p.location << 16) | ((int)p.sequence << 8);
}
static int OverlayKeyFromCard(gameCard card)
{
return ((int)card.p.controller << 24)
| ((int)(card.p.location | (UInt32)CardLocation.Overlay) << 16)
| ((int)card.p.sequence << 8);
}
void ClearOverlayMapCache()
{
foreach (var pair in realizeOverlayMap)
{
pair.Value.Clear();
realizeOverlayListPool.Push(pair.Value);
}
realizeOverlayMap.Clear();
}
void BuildOverlayMapCache()
{
ClearOverlayMapCache();
for (int i = 0; i < cards.Count; i++)
{
gameCard card = cards[i];
if (card.gameObject.activeInHierarchy == false)
{
continue;
}
if ((card.p.location & (UInt32)CardLocation.Overlay) == 0)
{
continue;
}
int key = OverlayKey(card.p);
List<gameCard> list;
if (realizeOverlayMap.TryGetValue(key, out list) == false)
{
if (realizeOverlayListPool.Count > 0)
{
list = realizeOverlayListPool.Pop();
}
else
{
list = new List<gameCard>(4);
}
realizeOverlayMap.Add(key, list);
}
list.Add(card);
}
}
List<gameCard> GetOverlayElementsFromCache(gameCard card)
{
if (card == null)
{
return null;
}
if ((card.p.location & (UInt32)CardLocation.Overlay) > 0)
{
return null;
}
List<gameCard> list;
realizeOverlayMap.TryGetValue(OverlayKeyFromCard(card), out list);
return list;
}
void SetLocationCount(
TMPro.TextMeshPro textmesh,
int count,
int faceUpCount,
CardLocation location
)
{
if (count < 2)
{
textmesh.text = "";
return;
}
if (location == CardLocation.Extra)
{
textmesh.text = count.ToString() + "(" + faceUpCount.ToString() + ")";
}
else
{
textmesh.text = count.ToString();
}
}
gameCardCondition get_point_worldcondition(GPS p)
{
gameCardCondition return_value = gameCardCondition.floating_clickable;
......@@ -826,10 +940,8 @@ public class Ocgcore : ServantWithCardDescription
if (TcpHelper.tcpClient.Connected)
{
setDefaultReturnServant();
TcpHelper.tcpClient.Client.Shutdown(0);
TcpHelper.tcpClient.Close();
}
TcpHelper.tcpClient = null;
TcpHelper.Disconnect(true);
}
returnTo();
}
......@@ -838,13 +950,7 @@ public class Ocgcore : ServantWithCardDescription
{
if (TcpHelper.tcpClient != null)
{
/*if (TcpHelper.tcpClient.Connected)
{
setDefaultReturnServant();
TcpHelper.tcpClient.Client.Shutdown(0);
TcpHelper.tcpClient.Close();
} */
TcpHelper.tcpClient = null;
TcpHelper.Disconnect(true);
}
returnTo();
}
......@@ -7307,30 +7413,190 @@ public class Ocgcore : ServantWithCardDescription
{
someCardIsShowed = false;
float real = (Program.fieldSize - 1) * 0.9f + 1f;
realizeToClearCards.Clear();
realizeOpponentMonsterCards.Clear();
realizeOpponentSpellCards.Clear();
realizeLinkPositions.Clear();
realizeLinkMaskRemoveBuffer.Clear();
realizeOpponentHandLine.Clear();
realizeThunderRemoveBuffer.Clear();
realizeMyPendulumCards.Clear();
realizeOpponentPendulumCards.Clear();
int myDeckCount = 0;
int myExtraCount = 0;
int myExtraFaceUpCount = 0;
int myGraveCount = 0;
int myRemovedCount = 0;
int opDeckCount = 0;
int opExtraCount = 0;
int opExtraFaceUpCount = 0;
int opGraveCount = 0;
int opRemovedCount = 0;
int myFieldCode = 0;
int opFieldCode = 0;
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
cards[i].cookie_cared = false;
cards[i].p_line_off();
cards[i].sortButtons();
cards[i].opMonsterWithBackGroundCard = false;
cards[i].isMinBlockMode = false;
cards[i].overFatherCount = 0;
gameCard card = cards[i];
card.cookie_cared = false;
card.p_line_off();
card.sortButtons();
card.opMonsterWithBackGroundCard = false;
card.isMinBlockMode = false;
card.overFatherCount = 0;
if (card.p.location == (uint)CardLocation.Unknown)
{
realizeToClearCards.Add(card);
}
if (card.p.location == (uint)CardLocation.Search)
{
card.isShowed = true;
}
if ((card.p.location & (UInt32)CardLocation.Overlay) == 0)
{
if (card.p.controller == 1)
{
if ((card.p.location & (UInt32)CardLocation.MonsterZone) > 0)
{
realizeOpponentMonsterCards.Add(card);
}
if ((card.p.location & (UInt32)CardLocation.SpellZone) > 0)
{
realizeOpponentSpellCards.Add(card);
}
}
if ((card.p.location & (UInt32)CardLocation.SpellZone) > 0)
{
if (
Program.I().setting.setting.Vfield.value
&& card.p.sequence == 5
&& (card.p.position & (Int32)CardPosition.FaceUp) > 0
)
{
if (card.p.controller == 0)
{
myFieldCode = card.get_data().Id;
}
else
{
opFieldCode = card.get_data().Id;
}
}
}
}
if (card.p.controller == 0)
{
if ((card.p.location & (UInt32)CardLocation.Deck) > 0)
{
myDeckCount++;
}
if ((card.p.location & (UInt32)CardLocation.Extra) > 0)
{
myExtraCount++;
if ((card.p.position & (UInt32)CardPosition.FaceUp) > 0)
{
myExtraFaceUpCount++;
}
}
if ((card.p.location & (UInt32)CardLocation.Grave) > 0)
{
myGraveCount++;
}
if ((card.p.location & (UInt32)CardLocation.Removed) > 0)
{
myRemovedCount++;
}
}
else
{
if ((card.p.location & (UInt32)CardLocation.Deck) > 0)
{
opDeckCount++;
}
if ((card.p.location & (UInt32)CardLocation.Extra) > 0)
{
opExtraCount++;
if ((card.p.position & (UInt32)CardPosition.FaceUp) > 0)
{
opExtraFaceUpCount++;
}
}
if ((card.p.location & (UInt32)CardLocation.Grave) > 0)
{
opGraveCount++;
}
if ((card.p.location & (UInt32)CardLocation.Removed) > 0)
{
opRemovedCount++;
}
}
}
List<gameCard> to_clear = new List<gameCard>();
for (int i = 0; i < realizeToClearCards.Count; i++)
{
realizeToClearCards[i].hide();
}
bool usePendulumDisplay = Program.I().setting.setting.Vpedium.value;
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
gameCard card = cards[i];
if (card.gameObject.activeInHierarchy == false || card.cookie_cared)
{
if (cards[i].p.location == (uint)CardLocation.Unknown)
continue;
}
if (
(card.p.location & (UInt32)CardLocation.Hand) > 0
&& card.p.controller == 1
)
{
realizeOpponentHandLine.Add(card);
}
if ((card.p.location & (UInt32)CardLocation.SpellZone) == 0)
{
continue;
}
if (usePendulumDisplay)
{
if ((card.p.sequence == 0 || card.p.sequence == 4) == false)
{
to_clear.Add(cards[i]);
continue;
}
if ((card.get_data().Type & (int)CardType.Pendulum) == 0)
{
continue;
}
}
else
{
if (card.p.sequence != 6 && card.p.sequence != 7)
{
continue;
}
}
for (int i = 0; i < to_clear.Count; i++)
{
to_clear[i].hide();
if (card.p.controller == 0)
{
realizeMyPendulumCards.Add(card);
}
else
{
realizeOpponentPendulumCards.Add(card);
}
}
//for (int i = 0; i < cards.Count; i++) if (cards[i].gameObject.activeInHierarchy)
......@@ -7387,16 +7653,6 @@ public class Ocgcore : ServantWithCardDescription
}
}
}
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
if (cards[i].p.location == (uint)CardLocation.Search)
{
cards[i].isShowed = true;
}
}
List<List<gameCard>> lines = new List<List<gameCard>>();
UInt32 preController = 9999;
UInt32 preLocation = 9999;
......@@ -7482,39 +7738,20 @@ public class Ocgcore : ServantWithCardDescription
gameField.isLong = false;
List<gameCard> op_m = new List<gameCard>();
List<gameCard> op_s = new List<gameCard>();
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
if (cards[i].p.controller == 1)
{
if ((cards[i].p.location & (UInt32)CardLocation.Overlay) == 0)
{
if ((cards[i].p.location & (UInt32)CardLocation.MonsterZone) > 0)
{
op_m.Add(cards[i]);
}
if ((cards[i].p.location & (UInt32)CardLocation.SpellZone) > 0)
{
op_s.Add(cards[i]);
}
}
}
}
for (int m = 0; m < op_m.Count; m++)
for (int m = 0; m < realizeOpponentMonsterCards.Count; m++)
{
if ((op_m[m].p.position & (UInt32)CardPosition.FaceUp) > 0)
if ((realizeOpponentMonsterCards[m].p.position & (UInt32)CardPosition.FaceUp) > 0)
{
for (int s = 0; s < op_s.Count; s++)
for (int s = 0; s < realizeOpponentSpellCards.Count; s++)
{
if (op_m[m].p.sequence == op_s[s].p.sequence)
if (
realizeOpponentMonsterCards[m].p.sequence
== realizeOpponentSpellCards[s].p.sequence
)
{
if (op_m[m].p.sequence < 5)
if (realizeOpponentMonsterCards[m].p.sequence < 5)
{
op_m[m].opMonsterWithBackGroundCard = true;
realizeOpponentMonsterCards[m].opMonsterWithBackGroundCard = true;
//op_m[m].isMinBlockMode = true;
if (Program.getVerticalTransparency() >= 0.5f)
{
......@@ -7650,7 +7887,7 @@ public class Ocgcore : ServantWithCardDescription
}
}
List<GPS> linkPs = new List<GPS>();
List<GPS> linkPs = realizeLinkPositions;
for (int curHang = 2; curHang <= 4; curHang++)
{
......@@ -7815,7 +8052,7 @@ public class Ocgcore : ServantWithCardDescription
}
}
List<linkMask> removeList = new List<linkMask>();
List<linkMask> removeList = realizeLinkMaskRemoveBuffer;
for (int i = 0; i < linkMaskList.Count; i++)
{
......@@ -7843,7 +8080,6 @@ public class Ocgcore : ServantWithCardDescription
}
removeList.Clear();
removeList = null;
for (int i = 0; i < linkMaskList.Count; i++)
{
......@@ -7852,19 +8088,7 @@ public class Ocgcore : ServantWithCardDescription
gameField.Update();
//op hand
List<gameCard> line = new List<gameCard>();
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
if (cards[i].cookie_cared == false)
{
if (
(cards[i].p.location & (UInt32)CardLocation.Hand) > 0
&& cards[i].p.controller == 1
)
{
line.Add(cards[i]);
}
}
List<gameCard> line = realizeOpponentHandLine;
for (int index = 0; index < line.Count; index++)
{
Vector3 want_position = Vector3.zero;
......@@ -7898,10 +8122,13 @@ public class Ocgcore : ServantWithCardDescription
gameField.thunders[i].needDestroy = true;
}
BuildOverlayMapCache();
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
List<gameCard> overlayed_cards = GCS_cardGetOverlayElements(cards[i]);
List<gameCard> overlayed_cards = GetOverlayElementsFromCache(cards[i]);
int overlayCount = overlayed_cards != null ? overlayed_cards.Count : 0;
int overC = 0;
if (Program.getVerticalTransparency() > 0.5f)
{
......@@ -7910,17 +8137,20 @@ public class Ocgcore : ServantWithCardDescription
&& (cards[i].p.location & (Int32)CardLocation.Onfield) > 0
)
{
overC = overlayed_cards.Count;
overC = overlayCount;
}
}
cards[i].set_overlay_light(overC);
cards[i].set_overlay_see_button(overlayed_cards.Count > 0);
for (int x = 0; x < overlayed_cards.Count; x++)
cards[i].set_overlay_see_button(overlayCount > 0);
if (overlayCount > 0)
{
overlayed_cards[x].overFatherCount = overlayed_cards.Count;
if (overlayed_cards[x].isShowed)
for (int x = 0; x < overlayCount; x++)
{
animation_thunder(overlayed_cards[x].gameObject, cards[i].gameObject);
overlayed_cards[x].overFatherCount = overlayCount;
if (overlayed_cards[x].isShowed)
{
animation_thunder(overlayed_cards[x].gameObject, cards[i].gameObject);
}
}
}
foreach (var item in cards[i].target)
......@@ -7935,7 +8165,7 @@ public class Ocgcore : ServantWithCardDescription
}
}
List<thunder_locator> needRemoveThunder = new List<thunder_locator>();
List<thunder_locator> needRemoveThunder = realizeThunderRemoveBuffer;
for (int i = 0; i < gameField.thunders.Count; i++)
{
if (gameField.thunders[i].needDestroy == true)
......@@ -7950,36 +8180,15 @@ public class Ocgcore : ServantWithCardDescription
}
needRemoveThunder.Clear();
ClearOverlayMapCache();
//p effect
gameField.relocatePnums(Program.I().setting.setting.Vpedium.value);
if (Program.I().setting.setting.Vpedium.value == true)
gameField.relocatePnums(usePendulumDisplay);
if (usePendulumDisplay == true)
{
List<gameCard> my_p_cards = new List<gameCard>();
List<gameCard> my_p_cards = realizeMyPendulumCards;
List<gameCard> op_p_cards = new List<gameCard>();
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
if (cards[i].cookie_cared == false)
{
if ((cards[i].p.location & (UInt32)CardLocation.SpellZone) > 0)
{
if (cards[i].p.sequence == 0 || cards[i].p.sequence == 4)
{
if ((cards[i].get_data().Type & (int)CardType.Pendulum) > 0)
{
if (cards[i].p.controller == 0)
{
my_p_cards.Add(cards[i]);
}
else
{
op_p_cards.Add(cards[i]);
}
}
}
}
}
List<gameCard> op_p_cards = realizeOpponentPendulumCards;
if (MasterRule >= 4)
{
......@@ -8108,29 +8317,9 @@ public class Ocgcore : ServantWithCardDescription
{
//p effect pain
List<gameCard> my_p_cards = new List<gameCard>();
List<gameCard> op_p_cards = new List<gameCard>();
List<gameCard> my_p_cards = realizeMyPendulumCards;
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
if (cards[i].cookie_cared == false)
{
if ((cards[i].p.location & (UInt32)CardLocation.SpellZone) > 0)
{
if (cards[i].p.sequence == 6 || cards[i].p.sequence == 7)
{
if (cards[i].p.controller == 0)
{
my_p_cards.Add(cards[i]);
}
else
{
op_p_cards.Add(cards[i]);
}
}
}
}
List<gameCard> op_p_cards = realizeOpponentPendulumCards;
gameField.mePHole = false;
gameField.opPHole = false;
......@@ -8197,49 +8386,8 @@ public class Ocgcore : ServantWithCardDescription
if (Program.I().setting.setting.Vfield.value)
{
int code = 0;
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
if (
((cards[i].p.location & (UInt32)CardLocation.SpellZone) > 0)
&& cards[i].p.sequence == 5
)
{
if (cards[i].p.controller == 0)
{
if ((cards[i].p.position & (Int32)CardPosition.FaceUp) > 0)
{
code = cards[i].get_data().Id;
}
}
}
}
gameField.set(0, code);
code = 0;
for (int i = 0; i < cards.Count; i++)
if (cards[i].gameObject.activeInHierarchy)
{
if (
((cards[i].p.location & (UInt32)CardLocation.SpellZone) > 0)
&& cards[i].p.sequence == 5
)
{
if (cards[i].p.controller == 1)
{
if ((cards[i].p.position & (Int32)CardPosition.FaceUp) > 0)
{
code = cards[i].get_data().Id;
}
}
}
}
gameField.set(1, code);
gameField.set(0, myFieldCode);
gameField.set(1, opFieldCode);
}
else
{
......@@ -8305,14 +8453,14 @@ public class Ocgcore : ServantWithCardDescription
{
gameInfo.removeHashedButton("swap");
}
animation_count(gameField.LOCATION_DECK_0, CardLocation.Deck, 0);
animation_count(gameField.LOCATION_EXTRA_0, CardLocation.Extra, 0);
animation_count(gameField.LOCATION_GRAVE_0, CardLocation.Grave, 0);
animation_count(gameField.LOCATION_REMOVED_0, CardLocation.Removed, 0);
animation_count(gameField.LOCATION_DECK_1, CardLocation.Deck, 1);
animation_count(gameField.LOCATION_EXTRA_1, CardLocation.Extra, 1);
animation_count(gameField.LOCATION_GRAVE_1, CardLocation.Grave, 1);
animation_count(gameField.LOCATION_REMOVED_1, CardLocation.Removed, 1);
SetLocationCount(gameField.LOCATION_DECK_0, myDeckCount, 0, CardLocation.Deck);
SetLocationCount(gameField.LOCATION_EXTRA_0, myExtraCount, myExtraFaceUpCount, CardLocation.Extra);
SetLocationCount(gameField.LOCATION_GRAVE_0, myGraveCount, 0, CardLocation.Grave);
SetLocationCount(gameField.LOCATION_REMOVED_0, myRemovedCount, 0, CardLocation.Removed);
SetLocationCount(gameField.LOCATION_DECK_1, opDeckCount, 0, CardLocation.Deck);
SetLocationCount(gameField.LOCATION_EXTRA_1, opExtraCount, opExtraFaceUpCount, CardLocation.Extra);
SetLocationCount(gameField.LOCATION_GRAVE_1, opGraveCount, 0, CardLocation.Grave);
SetLocationCount(gameField.LOCATION_REMOVED_1, opRemovedCount, 0, CardLocation.Removed);
gameField.realize();
Program.notGo(gameInfo.realize);
Program.go(50, gameInfo.realize);
......@@ -8585,21 +8733,8 @@ public class Ocgcore : ServantWithCardDescription
}
}
}
if (count < 2)
{
textmesh.text = "";
}
else
{
if (location == CardLocation.Extra)
{
textmesh.text = count.ToString() + "(" + countU.ToString() + ")";
}
else
{
textmesh.text = count.ToString();
}
}
SetLocationCount(textmesh, count, countU, location);
}
float camera_max = -17.5f;
......@@ -8713,6 +8848,27 @@ public class Ocgcore : ServantWithCardDescription
public List<gameCard> GCS_cardGetOverlayElements(gameCard c)
{
if (c == null)
{
return realizeToClearCards;
}
if ((c.p.location & (UInt32)CardLocation.Overlay) > 0)
{
return realizeToClearCards;
}
if (realizeOverlayMap.Count > 0)
{
List<gameCard> cached = GetOverlayElementsFromCache(c);
if (cached != null)
{
return cached;
}
return realizeEmptyOverlayList;
}
List<gameCard> cas = new List<gameCard>();
if (c != null)
{
......
......@@ -1592,7 +1592,7 @@ public class Program : MonoBehaviour
Running = false;
try
{
TcpHelper.tcpClient.Close();
TcpHelper.Disconnect(true);
}
catch (Exception e)
{
......
......@@ -291,10 +291,7 @@ public class SelectServer : WindowServantSP
Program.I().shiftToServant(Program.I().menu);
if (TcpHelper.tcpClient != null)
{
if (TcpHelper.tcpClient.Connected)
{
TcpHelper.tcpClient.Close();
}
TcpHelper.Disconnect(true);
}
}
......
# OCGCore 性能与稳定性问题记录(2026-02-07)
## 背景
- 项目:`ygopro2_unity2021`(Unity 2021)
- 目标平台重点:iOS 移动端
- 当前阶段:已完成一轮 TCP 弱网优化与 `Ocgcore.cs` 重点性能排查
## 已完成的优化(当前工作区)
### 1) TCP 弱网优化(不改服务端协议)
- 文件:`Assets/SibylSystem/MonoHelpers/TcpHelper.cs`
- 要点:
- 连接阶段参数更适配移动端(连接超时、KeepAlive 参数)
- 新增对局空闲期心跳(`CtosMessage_TimeConfirm`),避免“玩家思考无操作”期间误断线
- 发送失败增加短重试,提升瞬时抖动下的可用性
### 2) `realize()` 主流程降分配与降重复扫描
- 文件:`Assets/SibylSystem/Ocgcore/Ocgcore.cs`
- 要点:
- 将高频临时列表改为复用缓冲,减少 GC 压力
- 合并多处统计循环,单次遍历同时收集 deck/extra/grave/removed 计数
- 新增 overlay 映射缓存,避免反复 O(N²) 查找叠放素材
- 将库/额外/墓地/除外计数显示改为集中写入,减少重复调用
## 当前仍存在的主要性能热点
### P0:全量刷新触发频率高
- 位置:`Assets/SibylSystem/Ocgcore/Ocgcore.cs:7412`
- 现象:`realize()` 仍是全局大函数,且在消息处理中触发频繁。
- 风险:在移动端(尤其 iOS)高频对局消息下,主线程抖动明显。
### P0:按 GPS 查卡仍为线性扫描
- 位置:`Assets/SibylSystem/Ocgcore/Ocgcore.cs:8789`
- 现象:`GCS_cardGet()` 每次查找都遍历 `cards`,对高频消息处理放大开销。
- 风险:对局越长、卡越多,查找成本越高。
### P1:局部统计函数重复扫全表
- 位置:
- `Assets/SibylSystem/Ocgcore/Ocgcore.cs:2803``countLocation`
- `Assets/SibylSystem/Ocgcore/Ocgcore.cs:2822``countLocationSequence`
- 现象:多处消息处理和 UI 刷新仍依赖全表计数。
### P1:整理阶段全量排序成本高
- 位置:`Assets/SibylSystem/Ocgcore/Ocgcore.cs:9075`
- 现象:`arrangeCards()` 进行全量 `Sort + 重写 sequence`
- 风险:若触发频率高,会出现额外 CPU 开销与潜在映射时序问题。
### P1:特效对象频繁 Instantiate/Destroy
- 位置示例:`Assets/SibylSystem/Ocgcore/Ocgcore.cs:5722``Assets/SibylSystem/Ocgcore/Ocgcore.cs:5762`
- 现象:指示物增减等消息内循环创建销毁特效对象。
- 风险:高频时造成 GC 与帧时间尖刺。
### P2:部分辅助函数仍有短生命周期 List 分配
- 位置:`Assets/SibylSystem/Ocgcore/Ocgcore.cs:6397``Assets/SibylSystem/Ocgcore/Ocgcore.cs:6431`
- 现象:`MHS_getBundle` / `MHS_resizeBundle` 仍使用即时新建列表。
## 功能稳定性风险(与“确认卡片”相关)
### ConfirmDecktop 定位来源不一致风险
- 位置:
- `Assets/SibylSystem/Ocgcore/Ocgcore.cs:2471`
- `Assets/SibylSystem/Ocgcore/Ocgcore.cs:4811`
- 现象:读取了 `ReadShortGPS()`,但后续定位卡片时使用“本地重算 sequence / 固定 Deck”的方式。
- 风险:当本地状态与消息发送时状态不完全同步,或确认来源并非严格 Deck 语义时,可能出现“展示与确认不是同一卡”的体验。
## 建议的后续执行顺序
1. 先做 `GCS_cardGet` 的索引化(字典缓存 + 变更点增量维护)
2. 再做 `realize()` 的脏区刷新(按 location/owner 增量更新)
3. 为 Confirm 系列消息统一“以消息 GPS 为唯一定位源”
4. 最后处理特效对象池与细节内存分配
## 验证建议(iOS/弱网)
- 使用 Network Link Conditioner / Charles / `tc netem` 组合测试:
- 高延迟(150ms~500ms)、丢包(2%~10%)、抖动(50ms~150ms)
- 长时间“仅思考无操作”场景(5~10 分钟)
- 观察项:
- 是否误断线
- 心跳是否仅在对局空闲阶段触发
- `realize()` 相关帧时间峰值与 GC 分配是否下降
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment