using System.Collections.Concurrent; using System.Diagnostics; using ClientTester.Packet; using LiteNetLib; using LiteNetLib.Utils; using Serilog; namespace ClientTester.DummyService; public class DummyClients { private NetManager manager; private EventBasedNetListener listener; private NetDataWriter? writer; public NetPeer? peer; public long clientId; // seq → 송신 타임스탬프 (Stopwatch tick) private ConcurrentDictionary pendingPings = new(); private int seqNumber; private const int MAX_PENDING_PINGS = 1000; // info private Vector3 position = new Vector3(); private int rotY = 0; private float moveSpeed = 3.5f; private float distance = 0.0f; private float preTime = 0.0f; // 이동 계산용 private static readonly Random random = new(); private readonly Stopwatch moveClock = Stopwatch.StartNew(); private float posX = 0f; private float posZ = 0f; private float dirX = 0f; private float dirZ = 0f; // 유닛 테스트용 (0 = 제한 없음) public int TestCount { get; set; } = 0; // 통계 public int SentCount { set; get; } public int ReceivedCount { set; get; } public double LastRttMs { set; get; } public double TotalRttMs { set; get; } public int RttCount { set; get; } public DummyClients(long clientId, string ip, int port, string key) { this.clientId = clientId; listener = new EventBasedNetListener(); manager = new NetManager(listener); writer = new NetDataWriter(); listener.PeerConnectedEvent += netPeer => { peer = netPeer; Log.Information("[Client {ClientId:00}] 연결됨", this.clientId); // clientID가 토큰의 hashKey라고 가정함 PacketHeader packetHeader = new PacketHeader(); packetHeader.Code = (PacketCode)1; packetHeader.BodyLength = 4 + sizeof(long); writer.Put(clientId); peer.Send(writer, DeliveryMethod.ReliableOrdered); // 초기화 }; listener.NetworkReceiveEvent += (peer, reader, channel, deliveryMethod) => { short code = reader.GetShort(); short bodyLength = reader.GetShort(); string? msg = reader.GetString(); long sentTick; if (msg != null && msg.StartsWith("Echo seq:") && int.TryParse(msg.Substring("Echo seq:".Length), out int seq) && pendingPings.TryRemove(seq, out sentTick)) { double rttMs = (Stopwatch.GetTimestamp() - sentTick) * 1000.0 / Stopwatch.Frequency; LastRttMs = rttMs; TotalRttMs += rttMs; RttCount++; } ReceivedCount++; if (TestCount > 0 && ReceivedCount >= TestCount) { peer.Disconnect(); } reader.Recycle(); }; listener.PeerDisconnectedEvent += (peer, info) => { Log.Warning("[Client {ClientId:00}] 연결 끊김: {Reason}", this.clientId, info.Reason); this.peer = null; }; manager.Start(); manager.Connect(ip, port, key); } public void UpdateDummy() { // 델타 타임 계산 (초 단위) float now = (float)moveClock.Elapsed.TotalSeconds; float delta = preTime > 0f ? now - preTime : 0.1f; preTime = now; // 남은 거리가 없으면 새 방향·목표 거리 설정 if (distance <= 0f) { // 현재 각도에서 -30~+30도 범위로 회전 rotY = (rotY + random.Next(-30, 31) + 360) % 360; float rad = rotY * MathF.PI / 180f; dirX = MathF.Sin(rad); dirZ = MathF.Cos(rad); // 3초~12초에 도달할 수 있는 거리 = moveSpeed × 랜덤 초 float seconds = 3f + (float)random.NextDouble() * 9f; distance = moveSpeed * seconds; } // 이번 틱 이동량 (남은 거리 초과 방지) float step = MathF.Min(moveSpeed * delta, distance); posX += dirX * step; posZ += dirZ * step; distance -= step; // 정수 Vector3 갱신 position.X = (int)MathF.Round(posX); position.Z = (int)MathF.Round(posZ); } public void SendTransform() { if (peer == null || writer == null) { return; } UpdateDummy(); int seq = seqNumber++; pendingPings[seq] = Stopwatch.GetTimestamp(); // 응답 없는 오래된 ping 정리 (패킷 유실 시 메모리 누수 방지) if (pendingPings.Count > MAX_PENDING_PINGS) { int cutoff = seq - MAX_PENDING_PINGS; foreach (int key in pendingPings.Keys) { if (key < cutoff) { pendingPings.TryRemove(key, out _); } } } PacketHeader packetHeader = new PacketHeader(); packetHeader.Code = 0; packetHeader.BodyLength = (ushort)$"Echo seq:{seq}".Length; writer.Put((short)packetHeader.Code); writer.Put((short)packetHeader.BodyLength); writer.Put($"Echo seq:{seq}"); // 순서보장 안함 HOL Blocking 제거 peer.Send(writer, DeliveryMethod.ReliableUnordered); SentCount++; writer.Reset(); } public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0.0; public void PollEvents() { manager.PollEvents(); } public void Stop() { manager.Stop(); } }