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:
2026-02-21 10:08:32 +01:00
12 changed files with 6593 additions and 3 deletions

View 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;
}
}
}
}

View 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;
}
}
}
}
}

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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();
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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