Merge pull request 'GameClient' (#1) from GameClient into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
285
Assets/ClientSDK/Encryption.cs
Normal file
285
Assets/ClientSDK/Encryption.cs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GeoSus.Client
|
||||||
|
{
|
||||||
|
// Klientská strana šifrování - generuje session key, šifruje RSA, AES-CBC session
|
||||||
|
// Používá AES-CBC místo AES-GCM pro kompatibilitu s Unity
|
||||||
|
public class ClientEncryption : IDisposable
|
||||||
|
{
|
||||||
|
private byte[] _sessionKey;
|
||||||
|
private byte[] _sessionIv;
|
||||||
|
private long _nonceCounter;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
// Kontrola, zda je session key nastaven
|
||||||
|
public bool HasSessionKey => _sessionKey != null && _sessionIv != null;
|
||||||
|
|
||||||
|
// Generuje nový session key a IV
|
||||||
|
public void GenerateSessionKey()
|
||||||
|
{
|
||||||
|
_sessionKey = new byte[32]; // AES-256
|
||||||
|
_sessionIv = new byte[16]; // CBC IV (16 bytes)
|
||||||
|
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(_sessionKey);
|
||||||
|
rng.GetBytes(_sessionIv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] SessionKey => _sessionKey ?? throw new InvalidOperationException("Session key not generated");
|
||||||
|
public byte[] SessionIV => _sessionIv ?? throw new InvalidOperationException("Session IV not generated");
|
||||||
|
|
||||||
|
// Zašifruje session key pomocí RSA public key serveru
|
||||||
|
public (string EncryptedKey, string EncryptedIV) EncryptSessionKeyForServer(string rsaPublicKeyPem)
|
||||||
|
{
|
||||||
|
if (_sessionKey == null || _sessionIv == null)
|
||||||
|
throw new InvalidOperationException("Session key not generated");
|
||||||
|
|
||||||
|
using (var rsa = RSA.Create())
|
||||||
|
{
|
||||||
|
// Parse PEM - extrahuj Base64 obsah
|
||||||
|
var pemLines = rsaPublicKeyPem.Split('\n');
|
||||||
|
var base64 = new StringBuilder();
|
||||||
|
foreach (var line in pemLines)
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (!trimmed.StartsWith("-----") && !string.IsNullOrEmpty(trimmed))
|
||||||
|
{
|
||||||
|
base64.Append(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyBytes = Convert.FromBase64String(base64.ToString());
|
||||||
|
|
||||||
|
// Unity kompatibilní import - parsujeme SubjectPublicKeyInfo ručně
|
||||||
|
ImportSubjectPublicKeyInfoManual(rsa, keyBytes);
|
||||||
|
|
||||||
|
// Používáme OaepSHA1 pro Unity kompatibilitu (OaepSHA256 není podporován)
|
||||||
|
var encryptedKey = rsa.Encrypt(_sessionKey, RSAEncryptionPadding.OaepSHA1);
|
||||||
|
var encryptedIv = rsa.Encrypt(_sessionIv, RSAEncryptionPadding.OaepSHA1);
|
||||||
|
|
||||||
|
return (Convert.ToBase64String(encryptedKey), Convert.ToBase64String(encryptedIv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ručně parsuje SubjectPublicKeyInfo (DER) a importuje RSA klíč - Unity kompatibilní
|
||||||
|
private static void ImportSubjectPublicKeyInfoManual(RSA rsa, byte[] subjectPublicKeyInfo)
|
||||||
|
{
|
||||||
|
// SubjectPublicKeyInfo ::= SEQUENCE {
|
||||||
|
// algorithm AlgorithmIdentifier,
|
||||||
|
// subjectPublicKey BIT STRING }
|
||||||
|
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
// Outer SEQUENCE
|
||||||
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
||||||
|
throw new InvalidOperationException("Invalid SubjectPublicKeyInfo");
|
||||||
|
ReadLength(subjectPublicKeyInfo, ref index);
|
||||||
|
|
||||||
|
// AlgorithmIdentifier SEQUENCE - skip it
|
||||||
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
||||||
|
throw new InvalidOperationException("Invalid AlgorithmIdentifier");
|
||||||
|
int algLen = ReadLength(subjectPublicKeyInfo, ref index);
|
||||||
|
index += algLen;
|
||||||
|
|
||||||
|
// BIT STRING containing RSAPublicKey
|
||||||
|
if (subjectPublicKeyInfo[index++] != 0x03)
|
||||||
|
throw new InvalidOperationException("Invalid BIT STRING");
|
||||||
|
ReadLength(subjectPublicKeyInfo, ref index);
|
||||||
|
index++; // Skip unused bits byte (should be 0)
|
||||||
|
|
||||||
|
// RSAPublicKey SEQUENCE
|
||||||
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
||||||
|
throw new InvalidOperationException("Invalid RSAPublicKey");
|
||||||
|
ReadLength(subjectPublicKeyInfo, ref index);
|
||||||
|
|
||||||
|
// Modulus INTEGER
|
||||||
|
byte[] modulus = ReadInteger(subjectPublicKeyInfo, ref index);
|
||||||
|
|
||||||
|
// Exponent INTEGER
|
||||||
|
byte[] exponent = ReadInteger(subjectPublicKeyInfo, ref index);
|
||||||
|
|
||||||
|
var parameters = new RSAParameters
|
||||||
|
{
|
||||||
|
Modulus = modulus,
|
||||||
|
Exponent = exponent
|
||||||
|
};
|
||||||
|
rsa.ImportParameters(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadLength(byte[] data, ref int index)
|
||||||
|
{
|
||||||
|
int length = data[index++];
|
||||||
|
if ((length & 0x80) != 0)
|
||||||
|
{
|
||||||
|
int numBytes = length & 0x7F;
|
||||||
|
length = 0;
|
||||||
|
for (int i = 0; i < numBytes; i++)
|
||||||
|
{
|
||||||
|
length = (length << 8) | data[index++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ReadInteger(byte[] data, ref int index)
|
||||||
|
{
|
||||||
|
if (data[index++] != 0x02)
|
||||||
|
throw new InvalidOperationException("Expected INTEGER");
|
||||||
|
int length = ReadLength(data, ref index);
|
||||||
|
|
||||||
|
// Skip leading zero if present (used for positive sign in DER)
|
||||||
|
int originalLength = length;
|
||||||
|
int start = index;
|
||||||
|
if (length > 1 && data[start] == 0x00)
|
||||||
|
{
|
||||||
|
start++;
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] result = new byte[length];
|
||||||
|
Buffer.BlockCopy(data, start, result, 0, length);
|
||||||
|
index += originalLength;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Šifruje zprávu pomocí AES-256-CBC s HMAC
|
||||||
|
public byte[] Encrypt(byte[] plaintext)
|
||||||
|
{
|
||||||
|
if (_sessionKey == null || _sessionIv == null)
|
||||||
|
throw new InvalidOperationException("Session key not set");
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Generuj unikátní IV pro tuto zprávu
|
||||||
|
var iv = GetNextIV();
|
||||||
|
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = _sessionKey;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
byte[] ciphertext;
|
||||||
|
using (var encryptor = aes.CreateEncryptor())
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
||||||
|
{
|
||||||
|
cs.Write(plaintext, 0, plaintext.Length);
|
||||||
|
}
|
||||||
|
ciphertext = ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute HMAC pro integritu
|
||||||
|
byte[] hmac;
|
||||||
|
using (var hmacSha = new HMACSHA256(_sessionKey))
|
||||||
|
{
|
||||||
|
var toSign = new byte[iv.Length + ciphertext.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, toSign, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(ciphertext, 0, toSign, iv.Length, ciphertext.Length);
|
||||||
|
hmac = hmacSha.ComputeHash(toSign);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Výstup: [16 bytes IV][32 bytes HMAC][ciphertext]
|
||||||
|
var result = new byte[16 + 32 + ciphertext.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, result, 0, 16);
|
||||||
|
Buffer.BlockCopy(hmac, 0, result, 16, 32);
|
||||||
|
Buffer.BlockCopy(ciphertext, 0, result, 48, ciphertext.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dešifruje zprávu pomocí AES-256-CBC s HMAC ověřením
|
||||||
|
public byte[] Decrypt(byte[] encrypted)
|
||||||
|
{
|
||||||
|
if (_sessionKey == null)
|
||||||
|
throw new InvalidOperationException("Session key not set");
|
||||||
|
|
||||||
|
if (encrypted.Length < 48) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var iv = new byte[16];
|
||||||
|
var hmac = new byte[32];
|
||||||
|
var ciphertext = new byte[encrypted.Length - 48];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(encrypted, 0, iv, 0, 16);
|
||||||
|
Buffer.BlockCopy(encrypted, 16, hmac, 0, 32);
|
||||||
|
Buffer.BlockCopy(encrypted, 48, ciphertext, 0, ciphertext.Length);
|
||||||
|
|
||||||
|
// Ověř HMAC
|
||||||
|
byte[] expectedHmac;
|
||||||
|
using (var hmacSha = new HMACSHA256(_sessionKey))
|
||||||
|
{
|
||||||
|
var toVerify = new byte[iv.Length + ciphertext.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, toVerify, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(ciphertext, 0, toVerify, iv.Length, ciphertext.Length);
|
||||||
|
expectedHmac = hmacSha.ComputeHash(toVerify);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time compare
|
||||||
|
var diff = 0;
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
diff |= hmac[i] ^ expectedHmac[i];
|
||||||
|
}
|
||||||
|
if (diff != 0) return null; // HMAC mismatch
|
||||||
|
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = _sessionKey;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
using (var decryptor = aes.CreateDecryptor())
|
||||||
|
using (var ms = new MemoryStream(ciphertext))
|
||||||
|
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
||||||
|
using (var output = new MemoryStream())
|
||||||
|
{
|
||||||
|
cs.CopyTo(output);
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (CryptographicException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GetNextIV()
|
||||||
|
{
|
||||||
|
if (_sessionIv == null)
|
||||||
|
throw new InvalidOperationException("Session IV not set");
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
Buffer.BlockCopy(_sessionIv, 0, iv, 0, 8);
|
||||||
|
|
||||||
|
var counter = System.Threading.Interlocked.Increment(ref _nonceCounter);
|
||||||
|
var counterBytes = BitConverter.GetBytes(counter);
|
||||||
|
Buffer.BlockCopy(counterBytes, 0, iv, 8, 8);
|
||||||
|
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_sessionKey != null)
|
||||||
|
{
|
||||||
|
Array.Clear(_sessionKey, 0, _sessionKey.Length);
|
||||||
|
_sessionKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
Assets/ClientSDK/EventDispatcher.cs
Normal file
73
Assets/ClientSDK/EventDispatcher.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace GeoSus.Client
|
||||||
|
{
|
||||||
|
// Event dispatcher pro Unity main thread
|
||||||
|
// Unity může přidat SynchronizationContext, nebo polling z Update()
|
||||||
|
public class EventDispatcher
|
||||||
|
{
|
||||||
|
private readonly Queue<Action> _pendingActions = new Queue<Action>();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
private SynchronizationContext? _syncContext;
|
||||||
|
|
||||||
|
public EventDispatcher()
|
||||||
|
{
|
||||||
|
// Pokusíme se zachytit aktuální synchronization context (Unity main thread)
|
||||||
|
_syncContext = SynchronizationContext.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volat z networking vlákna - naplánuje callback na main thread
|
||||||
|
public void Post(Action action)
|
||||||
|
{
|
||||||
|
if (_syncContext != null)
|
||||||
|
{
|
||||||
|
_syncContext.Post(_ => action(), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback - přidáme do fronty pro polling
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_pendingActions.Enqueue(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volat z Unity Update() pokud není SynchronizationContext
|
||||||
|
public void ProcessPendingActions()
|
||||||
|
{
|
||||||
|
Action[] actions;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_pendingActions.Count == 0) return;
|
||||||
|
actions = _pendingActions.ToArray();
|
||||||
|
_pendingActions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var action in actions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"EventDispatcher error: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int PendingCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _pendingActions.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
607
Assets/ClientSDK/GameClient.cs
Normal file
607
Assets/ClientSDK/GameClient.cs
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GeoSus.Client
|
||||||
|
{
|
||||||
|
// Hlavní klientská třída pro připojení k serveru
|
||||||
|
public class GameClient : IDisposable
|
||||||
|
{
|
||||||
|
private TcpClient? _tcpClient;
|
||||||
|
private NetworkStream? _stream;
|
||||||
|
private ClientEncryption? _encryption;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private Task? _receiveTask;
|
||||||
|
private int _clientSeq;
|
||||||
|
private readonly object _sendLock = new object();
|
||||||
|
private bool _handshakeComplete;
|
||||||
|
|
||||||
|
public string ClientUuid { get; }
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public bool IsConnected => _tcpClient?.Connected ?? false;
|
||||||
|
public bool IsReady => IsConnected && _handshakeComplete && (_encryption?.HasSessionKey ?? false);
|
||||||
|
public EventDispatcher Dispatcher { get; }
|
||||||
|
|
||||||
|
// Events - voláno na main thread přes dispatcher
|
||||||
|
public event Action? OnConnected;
|
||||||
|
public event Action<string>? OnDisconnected;
|
||||||
|
public event Action<string>? OnError;
|
||||||
|
public event Action<Message>? OnMessage;
|
||||||
|
public event Action<GameEvent>? OnGameEvent;
|
||||||
|
|
||||||
|
// Lobby state
|
||||||
|
public string? LobbyId { get; private set; }
|
||||||
|
public string? JoinCode { get; private set; }
|
||||||
|
public LobbyState? CurrentLobbyState { get; private set; }
|
||||||
|
public PlayerRole? MyRole { get; private set; }
|
||||||
|
public List<GameTask> MyTasks { get; } = new List<GameTask>();
|
||||||
|
public Position MyPosition { get; set; }
|
||||||
|
public Dictionary<string, PlayerPositionInfo> PlayerPositions { get; } = new Dictionary<string, PlayerPositionInfo>();
|
||||||
|
public List<Body> Bodies { get; } = new List<Body>();
|
||||||
|
public int Ping { get; private set; }
|
||||||
|
public long LastEventId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Returns true if this client is the current lobby owner</summary>
|
||||||
|
public bool IsOwner => CurrentLobbyState?.OwnerId == ClientUuid;
|
||||||
|
|
||||||
|
public GameClient(string clientUuid, string displayName)
|
||||||
|
{
|
||||||
|
ClientUuid = clientUuid;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Dispatcher = new EventDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Connection
|
||||||
|
|
||||||
|
public async Task<bool> ConnectAsync(string host, int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tcpClient = new TcpClient();
|
||||||
|
await _tcpClient.ConnectAsync(host, port);
|
||||||
|
_stream = _tcpClient.GetStream();
|
||||||
|
_encryption = new ClientEncryption();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Handshake
|
||||||
|
if (!await PerformHandshakeAsync())
|
||||||
|
{
|
||||||
|
Disconnect("Handshake failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spustíme příjem zpráv
|
||||||
|
_receiveTask = Task.Run(() => ReceiveLoopAsync(_cts.Token));
|
||||||
|
|
||||||
|
Dispatcher.Post(() => OnConnected?.Invoke());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Dispatcher.Post(() => OnError?.Invoke(ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> PerformHandshakeAsync()
|
||||||
|
{
|
||||||
|
if (_stream == null || _encryption == null) return false;
|
||||||
|
|
||||||
|
// 1. ClientHello
|
||||||
|
var hello = new ClientHello
|
||||||
|
{
|
||||||
|
ClientUuid = ClientUuid,
|
||||||
|
DisplayName = DisplayName
|
||||||
|
};
|
||||||
|
await SendPlainAsync(hello);
|
||||||
|
|
||||||
|
// 2. ServerHello
|
||||||
|
var serverHelloData = await ReadMessageAsync();
|
||||||
|
if (serverHelloData == null) return false;
|
||||||
|
|
||||||
|
var serverHello = MessageSerializer.Deserialize(serverHelloData) as ServerHello;
|
||||||
|
if (serverHello == null) return false;
|
||||||
|
|
||||||
|
// 3. Generujeme session key a šifrujeme RSA
|
||||||
|
_encryption.GenerateSessionKey();
|
||||||
|
var (encKey, encIv) = _encryption.EncryptSessionKeyForServer(serverHello.RsaPublicKeyPem);
|
||||||
|
|
||||||
|
var keyExchange = new KeyExchange
|
||||||
|
{
|
||||||
|
EncryptedSessionKey = encKey,
|
||||||
|
EncryptedIV = encIv
|
||||||
|
};
|
||||||
|
await SendPlainAsync(keyExchange);
|
||||||
|
|
||||||
|
// 4. KeyExchangeAck (šifrovaně)
|
||||||
|
var ackData = await ReadMessageAsync();
|
||||||
|
if (ackData == null) return false;
|
||||||
|
|
||||||
|
var decrypted = _encryption.Decrypt(ackData);
|
||||||
|
if (decrypted == null) return false;
|
||||||
|
|
||||||
|
var ack = MessageSerializer.Deserialize(decrypted) as KeyExchangeAck;
|
||||||
|
if (ack?.Status == "success")
|
||||||
|
{
|
||||||
|
_handshakeComplete = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect(string reason = "User disconnected")
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_tcpClient?.Close();
|
||||||
|
_tcpClient = null;
|
||||||
|
_stream = null;
|
||||||
|
_encryption?.Dispose();
|
||||||
|
_encryption = null;
|
||||||
|
|
||||||
|
LobbyId = null;
|
||||||
|
JoinCode = null;
|
||||||
|
CurrentLobbyState = null;
|
||||||
|
MyRole = null;
|
||||||
|
MyTasks.Clear();
|
||||||
|
PlayerPositions.Clear();
|
||||||
|
Bodies.Clear();
|
||||||
|
|
||||||
|
Dispatcher.Post(() => OnDisconnected?.Invoke(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Sending
|
||||||
|
|
||||||
|
public void Send(Message message)
|
||||||
|
{
|
||||||
|
if (_stream == null || _encryption == null || !IsConnected) return;
|
||||||
|
|
||||||
|
message.ClientSeq = Interlocked.Increment(ref _clientSeq);
|
||||||
|
if (string.IsNullOrEmpty(message.ActionId))
|
||||||
|
{
|
||||||
|
message.ActionId = Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
var plain = MessageSerializer.Serialize(message);
|
||||||
|
var encrypted = _encryption.Encrypt(plain);
|
||||||
|
|
||||||
|
lock (_sendLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SendData(encrypted);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Dispatcher.Post(() => OnError?.Invoke($"Send error: {ex.Message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendPlainAsync(Message message)
|
||||||
|
{
|
||||||
|
if (_stream == null) return;
|
||||||
|
var data = MessageSerializer.Serialize(message);
|
||||||
|
await SendDataAsync(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendData(byte[] data)
|
||||||
|
{
|
||||||
|
if (_stream == null) return;
|
||||||
|
|
||||||
|
var lengthBuffer = BitConverter.GetBytes(data.Length);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(lengthBuffer);
|
||||||
|
|
||||||
|
_stream.Write(lengthBuffer, 0, 4);
|
||||||
|
_stream.Write(data, 0, data.Length);
|
||||||
|
_stream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendDataAsync(byte[] data)
|
||||||
|
{
|
||||||
|
if (_stream == null) return;
|
||||||
|
|
||||||
|
var lengthBuffer = BitConverter.GetBytes(data.Length);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(lengthBuffer);
|
||||||
|
|
||||||
|
await _stream.WriteAsync(lengthBuffer, 0, 4);
|
||||||
|
await _stream.WriteAsync(data, 0, data.Length);
|
||||||
|
await _stream.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Receiving
|
||||||
|
|
||||||
|
private async Task ReceiveLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
int decryptFailures = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested && IsConnected)
|
||||||
|
{
|
||||||
|
var data = await ReadMessageAsync();
|
||||||
|
if (data == null) break;
|
||||||
|
|
||||||
|
var decrypted = _encryption?.Decrypt(data);
|
||||||
|
if (decrypted == null)
|
||||||
|
{
|
||||||
|
decryptFailures++;
|
||||||
|
if (decryptFailures >= 3)
|
||||||
|
{
|
||||||
|
Disconnect("Too many decryption failures");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptFailures = 0;
|
||||||
|
|
||||||
|
var message = MessageSerializer.Deserialize(decrypted);
|
||||||
|
if (message != null)
|
||||||
|
{
|
||||||
|
ProcessMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Disconnect($"Connection error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> ReadMessageAsync()
|
||||||
|
{
|
||||||
|
if (_stream == null) return null;
|
||||||
|
|
||||||
|
var lengthBuffer = new byte[4];
|
||||||
|
var read = await _stream.ReadAsync(lengthBuffer, 0, 4);
|
||||||
|
if (read < 4) return null;
|
||||||
|
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(lengthBuffer);
|
||||||
|
var length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
|
if (length <= 0 || length > 1048576) return null;
|
||||||
|
|
||||||
|
var buffer = new byte[length];
|
||||||
|
var totalRead = 0;
|
||||||
|
while (totalRead < length)
|
||||||
|
{
|
||||||
|
read = await _stream.ReadAsync(buffer, totalRead, length - totalRead);
|
||||||
|
if (read == 0) return null;
|
||||||
|
totalRead += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessMessage(Message message)
|
||||||
|
{
|
||||||
|
// Zpracujeme speciální typy
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case CreateLobbyResponse r:
|
||||||
|
if (r.Success)
|
||||||
|
{
|
||||||
|
LobbyId = r.LobbyId;
|
||||||
|
JoinCode = r.JoinCode;
|
||||||
|
CurrentLobbyState = r.LobbyState;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JoinLobbyResponse r:
|
||||||
|
if (r.Success)
|
||||||
|
{
|
||||||
|
LobbyId = r.LobbyId;
|
||||||
|
CurrentLobbyState = r.LobbyState;
|
||||||
|
JoinCode = r.LobbyState?.JoinCode;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PositionBroadcast b:
|
||||||
|
ProcessPositionBroadcast(b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Pong p:
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
Ping = (int)(now - p.ClientTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameEvent evt:
|
||||||
|
ProcessGameEvent(evt);
|
||||||
|
Dispatcher.Post(() => OnGameEvent?.Invoke(evt));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.Post(() => OnMessage?.Invoke(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessPositionBroadcast(PositionBroadcast broadcast)
|
||||||
|
{
|
||||||
|
PlayerPositions.Clear();
|
||||||
|
foreach (var player in broadcast.Players)
|
||||||
|
{
|
||||||
|
PlayerPositions[player.ClientUuid] = player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessGameEvent(GameEvent evt)
|
||||||
|
{
|
||||||
|
LastEventId = evt.EventId;
|
||||||
|
|
||||||
|
switch (evt.EventType)
|
||||||
|
{
|
||||||
|
case "PlayerJoined":
|
||||||
|
// Add player to lobby state
|
||||||
|
var joinedPayload = evt.GetPayload<PlayerJoinedPayload>();
|
||||||
|
if (joinedPayload != null && CurrentLobbyState?.Players != null)
|
||||||
|
{
|
||||||
|
// Check if player already exists
|
||||||
|
bool exists = CurrentLobbyState.Players.Any(p => p.ClientUuid == joinedPayload.ClientUuid);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Players.Add(new PlayerInfo
|
||||||
|
{
|
||||||
|
ClientUuid = joinedPayload.ClientUuid,
|
||||||
|
DisplayName = joinedPayload.DisplayName,
|
||||||
|
IsOwner = false,
|
||||||
|
IsReady = false,
|
||||||
|
State = PlayerState.Alive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PlayerLeft":
|
||||||
|
// Remove player from lobby state
|
||||||
|
var leftPayload = evt.GetPayload<PlayerLeftPayload>();
|
||||||
|
if (leftPayload != null && CurrentLobbyState?.Players != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Players.RemoveAll(p => p.ClientUuid == leftPayload.ClientUuid);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HostChanged":
|
||||||
|
// Update lobby owner
|
||||||
|
var hostPayload = evt.GetPayload<HostChangedPayload>();
|
||||||
|
if (hostPayload != null && CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.OwnerId = hostPayload.NewHostId;
|
||||||
|
// Update IsOwner flag on all players
|
||||||
|
foreach (var player in CurrentLobbyState.Players)
|
||||||
|
{
|
||||||
|
player.IsOwner = player.ClientUuid == hostPayload.NewHostId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GameStarting":
|
||||||
|
// Game is entering loading phase - update lobby state if available
|
||||||
|
if (CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Phase = GamePhase.Loading;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "MapDataReady":
|
||||||
|
// Map data received - store it and send confirmation
|
||||||
|
var mapDataPayload = evt.GetPayload<MapDataReadyPayload>();
|
||||||
|
if (mapDataPayload != null && CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.MapData = mapDataPayload.MapData;
|
||||||
|
CurrentLobbyState.MapDataReady = true;
|
||||||
|
}
|
||||||
|
// Send confirmation to server
|
||||||
|
Send(new MapDataReceived());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GameStarted":
|
||||||
|
// Game officially started - update phase
|
||||||
|
if (CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Phase = GamePhase.Playing;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "RoleAssigned":
|
||||||
|
var rolePayload = evt.GetPayload<RoleAssignedPayload>();
|
||||||
|
if (rolePayload != null && rolePayload.ClientUuid == ClientUuid)
|
||||||
|
{
|
||||||
|
MyRole = rolePayload.Role;
|
||||||
|
MyTasks.Clear();
|
||||||
|
if (rolePayload.Tasks != null)
|
||||||
|
{
|
||||||
|
MyTasks.AddRange(rolePayload.Tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PlayerKilled":
|
||||||
|
var killPayload = evt.GetPayload<PlayerKilledPayload>();
|
||||||
|
if (killPayload != null)
|
||||||
|
{
|
||||||
|
Bodies.Add(new Body
|
||||||
|
{
|
||||||
|
BodyId = killPayload.BodyId,
|
||||||
|
VictimId = killPayload.VictimId,
|
||||||
|
Location = killPayload.Location
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "MeetingStarted":
|
||||||
|
if (CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Phase = GamePhase.Meeting;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "VotingClosed":
|
||||||
|
Bodies.Clear(); // Bodies zmizí po meetingu
|
||||||
|
if (CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Phase = GamePhase.Playing;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GameEnded":
|
||||||
|
if (CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
CurrentLobbyState.Phase = GamePhase.Ended;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Game Actions
|
||||||
|
|
||||||
|
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500)
|
||||||
|
{
|
||||||
|
Send(new CreateLobby
|
||||||
|
{
|
||||||
|
PlayAreaCenter = center,
|
||||||
|
PlayAreaRadius = playAreaRadius,
|
||||||
|
ImpostorCount = impostorCount,
|
||||||
|
TaskCount = taskCount,
|
||||||
|
Password = password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void JoinLobby(string joinCode, string? password = null)
|
||||||
|
{
|
||||||
|
Send(new JoinLobby
|
||||||
|
{
|
||||||
|
JoinCode = joinCode.ToUpperInvariant(),
|
||||||
|
Password = password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LeaveLobby()
|
||||||
|
{
|
||||||
|
Send(new LeaveLobby());
|
||||||
|
LobbyId = null;
|
||||||
|
JoinCode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartGame()
|
||||||
|
{
|
||||||
|
Send(new StartGame());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReturnToLobby()
|
||||||
|
{
|
||||||
|
Send(new ReturnToLobby());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdatePosition(Position position)
|
||||||
|
{
|
||||||
|
MyPosition = position;
|
||||||
|
Send(new UpdatePosition { Position = position });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Kill(string targetUuid)
|
||||||
|
{
|
||||||
|
Send(new KillAttempt { TargetClientUuid = targetUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReportBody(string bodyId)
|
||||||
|
{
|
||||||
|
Send(new ReportBody { BodyId = bodyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CallEmergencyMeeting()
|
||||||
|
{
|
||||||
|
Send(new CallEmergencyMeeting());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Vote(string? targetUuid)
|
||||||
|
{
|
||||||
|
Send(new CastVote { TargetClientUuid = targetUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pokus o dokončení tasku. Server ověří že hráč je na správné pozici.
|
||||||
|
/// </summary>
|
||||||
|
public void CompleteTask(string taskId)
|
||||||
|
{
|
||||||
|
Send(new TaskComplete { TaskId = taskId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendPing()
|
||||||
|
{
|
||||||
|
Send(new Ping { ClientTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reconnect(string lobbyId)
|
||||||
|
{
|
||||||
|
Send(new Reconnect { LobbyId = lobbyId, LastEventId = LastEventId });
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
public Body? FindNearbyBody(double maxDistance)
|
||||||
|
{
|
||||||
|
foreach (var body in Bodies)
|
||||||
|
{
|
||||||
|
if (MyPosition.DistanceTo(body.Location) <= maxDistance)
|
||||||
|
{
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? FindNearbyPlayer(double maxDistance, bool aliveOnly = true)
|
||||||
|
{
|
||||||
|
foreach (var (uuid, info) in PlayerPositions)
|
||||||
|
{
|
||||||
|
if (uuid == ClientUuid) continue;
|
||||||
|
if (aliveOnly && info.State != PlayerState.Alive) continue;
|
||||||
|
|
||||||
|
if (MyPosition.DistanceTo(info.Position) <= maxDistance)
|
||||||
|
{
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameTask? FindNearbyTask(double maxDistance)
|
||||||
|
{
|
||||||
|
foreach (var task in MyTasks)
|
||||||
|
{
|
||||||
|
if (MyPosition.DistanceTo(task.Location) <= maxDistance)
|
||||||
|
{
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volat z Unity Update() pro zpracování callbacků
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
Dispatcher.ProcessPendingActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disconnect("Disposed");
|
||||||
|
_encryption?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1054
Assets/ClientSDK/Protocol.cs
Normal file
1054
Assets/ClientSDK/Protocol.cs
Normal file
File diff suppressed because it is too large
Load Diff
1992
Assets/ClientSDK/SimulatorClient.cs
Normal file
1992
Assets/ClientSDK/SimulatorClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
91
Assets/GameManager/GameManager.cs
Normal file
91
Assets/GameManager/GameManager.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using GeoSus.Client;
|
||||||
|
using Subsystems;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using TMPro;
|
||||||
|
/*
|
||||||
|
GameManager - hlavní tøida pro správu hry
|
||||||
|
GameManager_Network - subsystém pro správu komunikace se serverem
|
||||||
|
GameManager_Game - subsystém pro správu logiky hry (sabotáže, tasky, atd.)
|
||||||
|
GameManager_Map - subsystém pro správu mapy a prostøedí
|
||||||
|
GameManager_Input - subsystém pro správu vstupu od hráèe
|
||||||
|
GameManager_UI - subsystém pro správu uživatelského rozhraní
|
||||||
|
GamaManager_Stats - subsystém pro správu statistik pro server
|
||||||
|
*/
|
||||||
|
public class GameManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Subsystems")]
|
||||||
|
protected GameManager_Network networkSubsystem;
|
||||||
|
protected GameManager_UI uiSubsystem;
|
||||||
|
|
||||||
|
|
||||||
|
protected GameClient gameClient;
|
||||||
|
[Header("Player Info")]
|
||||||
|
public string displayName;
|
||||||
|
|
||||||
|
[Header("UI Elements")]
|
||||||
|
public Canvas JoinCreateLobby;
|
||||||
|
public Canvas InLobby;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
DontDestroyOnLoad(this);
|
||||||
|
if (displayName == null || displayName == "")
|
||||||
|
{
|
||||||
|
displayName = "Player_" + Random.Range(1000, 9999).ToString();
|
||||||
|
}
|
||||||
|
gameClient = new GameClient(GenerateUUID(), /*displayName*/ GenerateUsername());
|
||||||
|
uiSubsystem = new GameManager_UI(gameClient, JoinCreateLobby, InLobby);
|
||||||
|
networkSubsystem = new GameManager_Network(gameClient);
|
||||||
|
networkSubsystem.OpenConection();
|
||||||
|
}
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (gameClient.CurrentLobbyState != null)
|
||||||
|
{
|
||||||
|
uiSubsystem.UpdateLobbyUI();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected string GenerateUUID()
|
||||||
|
{
|
||||||
|
string UUID = System.Guid.NewGuid().ToString();
|
||||||
|
Debug.Log(UUID);
|
||||||
|
return UUID;
|
||||||
|
}
|
||||||
|
protected string GenerateUsername()
|
||||||
|
{
|
||||||
|
string Username = Random.Range(0,10).ToString() + Random.Range(0, 10).ToString() + Random.Range(0, 10).ToString() + Random.Range(0, 10).ToString();
|
||||||
|
Debug.Log(Username);
|
||||||
|
return Username;
|
||||||
|
}
|
||||||
|
public void CreateLobbyButton()
|
||||||
|
{
|
||||||
|
networkSubsystem.CrateLobby(50.0755, 14.4378);
|
||||||
|
}
|
||||||
|
public void JoinLobbyButton()
|
||||||
|
{
|
||||||
|
TMP_InputField joinCode = JoinCreateLobby.transform.Find("InputCode").GetComponent<TMP_InputField>();
|
||||||
|
if (joinCode.text != null && joinCode.text != "")
|
||||||
|
{
|
||||||
|
networkSubsystem.JoinLobby(joinCode.text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log("Join code is empty!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void LeaveLobbyButton()
|
||||||
|
{
|
||||||
|
networkSubsystem.LeaveLobby();
|
||||||
|
}
|
||||||
|
void OnApplicationQuit()
|
||||||
|
{
|
||||||
|
gameClient.Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Assets/GameManager/GameManager_Network.cs
Normal file
152
Assets/GameManager/GameManager_Network.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using GeoSus.Client;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Subsystems
|
||||||
|
{
|
||||||
|
public class GameManager_Network
|
||||||
|
{
|
||||||
|
private const string _serverAddress = "geosus.honzuvkod.dev";
|
||||||
|
private const int _serverPort = 7777;
|
||||||
|
private GameClient _gameClient;
|
||||||
|
public async void OpenConection()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Task<bool> state = _gameClient.ConnectAsync(_serverAddress, _serverPort);
|
||||||
|
await state;
|
||||||
|
if (state.Result)
|
||||||
|
{
|
||||||
|
Debug.Log("Connected to server.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log("Failed to connect to server");
|
||||||
|
}
|
||||||
|
await Task.Delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GameManager_Network(GameClient gameClient)
|
||||||
|
{
|
||||||
|
_gameClient = gameClient;
|
||||||
|
RegisterEventHandlers();
|
||||||
|
}
|
||||||
|
public void RegisterEventHandlers()
|
||||||
|
{
|
||||||
|
_gameClient.OnConnected += OnConnected;
|
||||||
|
_gameClient.OnDisconnected += OnDisconnected;
|
||||||
|
_gameClient.OnError += OnError;
|
||||||
|
_gameClient.OnMessage += OnMessage;
|
||||||
|
_gameClient.OnGameEvent += OnGameEvent;
|
||||||
|
}
|
||||||
|
private void OnConnected()
|
||||||
|
{
|
||||||
|
Debug.Log("Successfully connected to the server.");
|
||||||
|
}
|
||||||
|
private void OnDisconnected(string reason)
|
||||||
|
{
|
||||||
|
Debug.Log($"Host disconnected due to {reason}");
|
||||||
|
}
|
||||||
|
private void OnError(string error)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Network error: {error}");
|
||||||
|
}
|
||||||
|
private void OnMessage(Message message)
|
||||||
|
{
|
||||||
|
switch (message.Type)
|
||||||
|
{
|
||||||
|
case "GameEvent":
|
||||||
|
OnGameEvent(message as GameEvent);
|
||||||
|
break;
|
||||||
|
case "CreateLobbyResponse":
|
||||||
|
Debug.Log("Received CreateLobbyResponse message");
|
||||||
|
HandleCreateLobbyResponse(message as CreateLobbyResponse);
|
||||||
|
break;
|
||||||
|
case "JoinLobbyResponse":
|
||||||
|
Debug.Log("Received JoinLobbyResponse message");
|
||||||
|
HandleJoinLobbyResponse(message as JoinLobbyResponse);
|
||||||
|
break;
|
||||||
|
case "Ack":
|
||||||
|
Debug.Log("Received Ack message");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Debug.Log("Received message of type: " + message.Type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void OnGameEvent(GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
switch (gameEvent.Type)
|
||||||
|
{
|
||||||
|
case "PlayerJoined":
|
||||||
|
Debug.Log($"Player {gameEvent.GetPayload<PlayerJoinedPayload>().DisplayName} joined");
|
||||||
|
HandlePlayerJoined(gameEvent);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Debug.Log("Received GameEvent of type: " + gameEvent.Type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void HandleCreateLobbyResponse(CreateLobbyResponse message)
|
||||||
|
{
|
||||||
|
if (message.Success)
|
||||||
|
{
|
||||||
|
Debug.Log("Lobby created successfully. Join Code: " + message.JoinCode + ", Lobby ID: " + message.LobbyId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("Failed to create lobby: " + message.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void HandleJoinLobbyResponse(JoinLobbyResponse message)
|
||||||
|
{
|
||||||
|
if (message.Success)
|
||||||
|
{
|
||||||
|
Debug.Log("Lobby created successfully." + ", Lobby ID: " + message.LobbyId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("Failed to create lobby: " + message.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void HandlePlayerJoined(GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
var payload = gameEvent.GetPayload<PlayerJoinedPayload>();
|
||||||
|
_gameClient.CurrentLobbyState.Players.Add(new PlayerInfo
|
||||||
|
{
|
||||||
|
ClientUuid = payload.ClientUuid,
|
||||||
|
DisplayName = payload.DisplayName,
|
||||||
|
IsOwner = false,
|
||||||
|
IsReady = false,
|
||||||
|
State = PlayerState.Alive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void CrateLobby(double lat, double lon)
|
||||||
|
{
|
||||||
|
_gameClient.CreateLobby(new Position(lat, lon));
|
||||||
|
}
|
||||||
|
public void JoinLobby(string joinCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_gameClient.JoinLobby(joinCode);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("Error joining lobby: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void LeaveLobby()
|
||||||
|
{
|
||||||
|
_gameClient.Disconnect();
|
||||||
|
Application.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
35
Assets/GameManager/GameManager_UI.cs
Normal file
35
Assets/GameManager/GameManager_UI.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using Subsystems;
|
||||||
|
using GeoSus.Client;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Subsystems
|
||||||
|
{
|
||||||
|
public class GameManager_UI
|
||||||
|
{
|
||||||
|
private GameClient _gameClient;
|
||||||
|
private Canvas _CreateJoinLobby;
|
||||||
|
private Canvas _InLobby;
|
||||||
|
public GameManager_UI(GameClient gameClient, Canvas CreateJoinLobby, Canvas InLobby)
|
||||||
|
{
|
||||||
|
_gameClient = gameClient;
|
||||||
|
_CreateJoinLobby = CreateJoinLobby;
|
||||||
|
_InLobby = InLobby;
|
||||||
|
_CreateJoinLobby.enabled = true;
|
||||||
|
_InLobby.enabled = false;
|
||||||
|
}
|
||||||
|
public void UpdateLobbyUI()
|
||||||
|
{
|
||||||
|
_InLobby.enabled = true;
|
||||||
|
_CreateJoinLobby.enabled = false;
|
||||||
|
var playerList = _InLobby.transform.Find("PlayerList").GetComponent<TMPro.TMP_Text>();
|
||||||
|
playerList.text = "";
|
||||||
|
foreach (var player in _gameClient.CurrentLobbyState.Players)
|
||||||
|
{
|
||||||
|
playerList.text += player.DisplayName + "\n";
|
||||||
|
}
|
||||||
|
_InLobby.transform.Find("JoinCode").GetComponent<TMPro.TMP_Text>().text = _gameClient.CurrentLobbyState.JoinCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Assets/GameManager/ITask.cs
Normal file
53
Assets/GameManager/ITask.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using GeoSus.Client;
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public enum TaskType
|
||||||
|
{
|
||||||
|
Task //TODO: Typy úkolù
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface ITask
|
||||||
|
{
|
||||||
|
public string TaskID { get; } // Unikátní ID úkolu pro server
|
||||||
|
public TaskType TaskType { get; } // Typ úkolu
|
||||||
|
public string TaskName { get; } // Viditelný název úkolu
|
||||||
|
public (double, double) TaskLocation { get; } // Polohy na mapì
|
||||||
|
public bool IsCompleted { get; } // Stav dokonèení úkolu
|
||||||
|
|
||||||
|
void Initialize(Action<ITask> onCompleted); // Vytvoøení tasku + naètení postupu
|
||||||
|
void ExitTask(Action<ITask> onExit); // Pøi opuštìní úkolu poslat hotovo / uložit postup / reset
|
||||||
|
void Complete(); // Oznaèit úkol jako dokonèený, poslat na server a zavøít
|
||||||
|
|
||||||
|
}
|
||||||
|
/* Ukázoková implementace ITask
|
||||||
|
public class Wires : ITask{
|
||||||
|
public string TaskID { get; set; } // Unikátní ID úkolu pro server
|
||||||
|
public TaskType TaskType { get; set; } // Typ úkolu
|
||||||
|
public string TaskName { get; set; } // Viditelný název úkolu
|
||||||
|
public (double, double) TaskLocation { get; set; } // Poloha na mapì
|
||||||
|
public bool IsCompleted { get; private set; } // Stav dokonèení úkolu
|
||||||
|
private Action<ITask> _onCompleted;
|
||||||
|
|
||||||
|
public void Initialize(Action<ITask> onCompleted) // Vytvoøení tasku
|
||||||
|
{
|
||||||
|
IsCompleted = false;
|
||||||
|
_onCompleted = onCompleted;
|
||||||
|
}
|
||||||
|
public void ExitTask(Action<ITask> onExit) //Zavøení tasku
|
||||||
|
{
|
||||||
|
onExit?.Invoke(this);
|
||||||
|
}
|
||||||
|
public void Complete() // Dokonèení tasku a zavøení
|
||||||
|
{
|
||||||
|
IsCompleted = true;
|
||||||
|
_onCompleted?.Invoke(this);
|
||||||
|
ExitTask(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
2245
Assets/Scenes/Client.unity
Normal file
2245
Assets/Scenes/Client.unity
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,12 @@ EditorBuildSettings:
|
|||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Scenes:
|
m_Scenes:
|
||||||
- enabled: 1
|
- enabled: 0
|
||||||
path: Assets/Scenes/SampleScene.unity
|
path: Assets/Scenes/SampleScene.unity
|
||||||
guid: 99c9720ab356a0642a771bea13969a05
|
guid: 3e95f16d8e50b3341925e51e50768027
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/Client.unity
|
||||||
|
guid: 8f736798e2d13f14f903b26a2df0eed8
|
||||||
m_configObjects:
|
m_configObjects:
|
||||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
||||||
m_UseUCBPForAssetBundles: 0
|
m_UseUCBPForAssetBundles: 0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
UnityConnectSettings:
|
UnityConnectSettings:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 1
|
serializedVersion: 1
|
||||||
m_Enabled: 0
|
m_Enabled: 1
|
||||||
m_TestMode: 0
|
m_TestMode: 0
|
||||||
m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events
|
m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events
|
||||||
m_EventUrl: https://cdp.cloud.unity3d.com/v1/events
|
m_EventUrl: https://cdp.cloud.unity3d.com/v1/events
|
||||||
|
|||||||
Reference in New Issue
Block a user