Files
Server/Protocol.cs
Bandwidth 796ba0906d fixes 2
2026-04-26 18:33:24 +02:00

986 lines
30 KiB
C#

namespace GeoSus.Server;
using System.Text.Json;
using System.Text.Json.Serialization;
#region Základní typy
public record struct Position(double Lat, double Lon)
{
// Haversine vzdálenost v metrech
public double DistanceTo(Position other)
{
const double R = 6371000; // Poloměr Země v metrech
var lat1 = Lat * Math.PI / 180;
var lat2 = other.Lat * Math.PI / 180;
var dLat = (other.Lat - Lat) * Math.PI / 180;
var dLon = (other.Lon - Lon) * Math.PI / 180;
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(lat1) * Math.Cos(lat2) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
return R * c;
}
}
public enum PlayerRole { Crew, Impostor }
public enum PlayerState { Alive, Dead }
public enum CheatStatus { Ok, Warn, Restrict, Kicked }
public enum GamePhase { Lobby, Loading, Playing, Meeting, Voting, Ended }
public enum TaskType { Instant }
public enum MeetingType { BodyReport, Emergency }
// Sabotage system
public enum SabotageType
{
/// <summary>Comms Blackout - Cannot report bodies or call emergency meetings</summary>
CommsBlackout,
/// <summary>Critical Meltdown - Must be repaired in time or impostors win (requires 2 simultaneous repairs)</summary>
CriticalMeltdown
}
public enum SabotageState
{
Inactive,
Active,
Repaired
}
// Map data types
public enum PathType
{
Footway,
Steps,
Cycleway,
Residential,
Service,
Road,
Track,
Other
}
public enum MapAreaType
{
Park,
Playground,
Garden,
Water,
Other
}
public enum MapPOIType
{
FoodDrink,
Finance,
Health,
Education,
Transport,
Shop,
Amenity,
Other
}
#endregion
#region Herní entity
public class Player
{
public required string ClientUuid { get; set; }
public required string DisplayName { get; set; }
public Position Position { get; set; }
public PlayerRole Role { get; set; } = PlayerRole.Crew;
public PlayerState State { get; set; } = PlayerState.Alive;
public CheatStatus CheatStatus { get; set; } = CheatStatus.Ok;
public int CheatScore { get; set; }
public DateTime ConnectedAt { get; set; } = DateTime.UtcNow;
public DateTime LastPositionUpdate { get; set; } = DateTime.UtcNow;
public DateTime? LastKillTime { get; set; }
public DateTime? LastEmergencyMeetingTime { get; set; }
public int EmergencyMeetingsUsed { get; set; }
public List<string> CompletedTaskIds { get; set; } = new();
public List<GameTask> Tasks { get; set; } = new(); // Per-player tasks
public string? CurrentTaskId { get; set; }
public DateTime? TaskStartTime { get; set; }
public bool IsOwner { get; set; }
public bool IsReady { get; set; }
public long LastEventId { get; set; }
public int Ping { get; set; }
// Historie pozic pro anti-cheat
public Queue<(Position Pos, DateTime Time)> PositionHistory { get; set; } = new();
}
public class Body
{
public required string BodyId { get; set; }
public required string VictimId { get; set; }
public required string KillerId { get; set; }
public Position Location { get; set; }
public DateTime KilledAt { get; set; } = DateTime.UtcNow;
public bool Reported { get; set; }
public string? ReportedBy { get; set; }
}
public class GameTask
{
public required string TaskId { get; set; }
public required string Name { get; set; }
public Position Location { get; set; }
public TaskType Type { get; set; } = TaskType.Instant;
}
public class Meeting
{
public required string MeetingId { get; set; }
public MeetingType Type { get; set; }
public string? ReportedBodyId { get; set; }
public required string CallerId { get; set; }
public Position MeetingLocation { get; set; }
public DateTime StartTime { get; set; } = DateTime.UtcNow;
public DateTime ArrivalDeadline { get; set; }
public DateTime? DiscussionEndTime { get; set; }
public DateTime VotingEndTime { get; set; }
public HashSet<string> ArrivedPlayers { get; set; } = new();
public Dictionary<string, string?> Votes { get; set; } = new(); // Voter -> Target (null = skip)
public DateTime? LastVoteChangeTime { get; set; }
}
/// <summary>
/// Active sabotage instance
/// </summary>
public class Sabotage
{
public required string SabotageId { get; set; }
public SabotageType Type { get; set; }
public SabotageState State { get; set; } = SabotageState.Active;
public required string InitiatorId { get; set; }
public DateTime StartTime { get; set; } = DateTime.UtcNow;
public DateTime? Deadline { get; set; } // For critical meltdown
public DateTime? RepairedAt { get; set; }
public string? RepairedBy { get; set; }
/// <summary>
/// Repair stations for this sabotage.
/// CommsBlackout: 1 station
/// CriticalMeltdown: 2 stations that need simultaneous activation
/// </summary>
public List<RepairStation> RepairStations { get; set; } = new();
/// <summary>
/// How many stations need to be simultaneously active to repair.
/// CommsBlackout: 1, CriticalMeltdown: 2
/// </summary>
public int RequiredSimultaneousRepairs { get; set; } = 1;
}
/// <summary>
/// Repair station for fixing sabotages
/// </summary>
public class RepairStation
{
public required string StationId { get; set; }
public required string Name { get; set; }
public Position Location { get; set; }
public bool IsBeingRepaired { get; set; }
public string? RepairingPlayerId { get; set; }
public DateTime? RepairStartTime { get; set; }
/// <summary>
/// Has this station been successfully repaired
/// </summary>
public bool IsRepaired { get; set; }
/// <summary>
/// How long player must hold to complete repair at this station
/// </summary>
public int RepairDurationMs { get; set; } = 3000;
}
#endregion
#region Map Data
/// <summary>
/// Complete map data for a play area - sent to clients for rendering
/// </summary>
public class MapData
{
public Position Center { get; set; }
public double RadiusMeters { get; set; }
public DateTime FetchedAt { get; set; }
/// <summary>Buildings to render (polygons)</summary>
public List<MapBuilding> Buildings { get; set; } = new();
/// <summary>Walkable pathways (lines)</summary>
public List<MapPathway> Pathways { get; set; } = new();
/// <summary>Parks, green areas (polygons)</summary>
public List<MapArea> Areas { get; set; } = new();
/// <summary>Points of interest (single points)</summary>
public List<MapPOI> PointsOfInterest { get; set; } = new();
/// <summary>All positions reachable from center via walkable paths (within play area)</summary>
[JsonIgnore] // Too large for network, computed server-side only
public List<Position> ReachablePositions { get; set; } = new();
}
/// <summary>Building outline for rendering</summary>
public class MapBuilding
{
public required string Id { get; set; }
public List<Position> Outline { get; set; } = new();
public string BuildingType { get; set; } = "yes";
public string? Name { get; set; }
public int Levels { get; set; } = 1;
}
/// <summary>Walkable pathway for rendering and reachability</summary>
public class MapPathway
{
public required string Id { get; set; }
public List<Position> Points { get; set; } = new();
public PathType PathType { get; set; }
public string? Name { get; set; }
public bool IsWalkable { get; set; } = true;
public double Width { get; set; } = 2.0;
[JsonIgnore]
public bool IsFullyReachable { get; set; }
[JsonIgnore]
public bool IsPartiallyReachable { get; set; }
/// <summary>
/// P11: True only when this pathway is unambiguously publicly accessible
/// for foot traffic - no `access=private`, no `foot=no`, and the highway
/// type itself is one we trust to be a place a player can safely and
/// legally stand. Server-internal (JsonIgnore) - clients don't need it.
/// Used by OverpassService.GetPubliclyAccessiblePositions to constrain
/// task placement to "absolutely public" geometry per Bandwidth's brutal
/// filter directive. Falls back to any-pathway-point when this set is
/// empty (rural / forest / open-field scenario).
/// </summary>
[JsonIgnore]
public bool IsPubliclyAccessible { get; set; }
}
/// <summary>Area like park or garden</summary>
public class MapArea
{
public required string Id { get; set; }
public List<Position> Outline { get; set; } = new();
public MapAreaType AreaType { get; set; }
public string? Name { get; set; }
}
/// <summary>Point of interest for task placement hints</summary>
public class MapPOI
{
public required string Id { get; set; }
public Position Position { get; set; }
public MapPOIType PoiType { get; set; }
public string? Name { get; set; }
}
/// <summary>Simplified map data sent to clients (smaller payload)</summary>
public class MapDataPayload
{
public Position Center { get; set; }
public double RadiusMeters { get; set; }
/// <summary>Buildings: [[lat,lon,lat,lon,...], ...]</summary>
public List<double[]> Buildings { get; set; } = new();
/// <summary>Building types: ["residential", "commercial", ...]</summary>
public List<string> BuildingTypes { get; set; } = new();
/// <summary>Pathways: [[lat,lon,lat,lon,...], ...]</summary>
public List<double[]> Pathways { get; set; } = new();
/// <summary>Pathway types: [0=footway, 1=steps, ...]</summary>
public List<int> PathwayTypes { get; set; } = new();
/// <summary>Areas (parks): [[lat,lon,lat,lon,...], ...]</summary>
public List<double[]> Areas { get; set; } = new();
/// <summary>Area types</summary>
public List<int> AreaTypes { get; set; } = new();
/// <summary>POIs: [lat, lon, type, lat, lon, type, ...]</summary>
public List<double> POIs { get; set; } = new();
/// <summary>Convert from full MapData to compact payload</summary>
public static MapDataPayload FromMapData(MapData data)
{
var payload = new MapDataPayload
{
Center = data.Center,
RadiusMeters = data.RadiusMeters
};
foreach (var b in data.Buildings)
{
var coords = new double[b.Outline.Count * 2];
for (int i = 0; i < b.Outline.Count; i++)
{
coords[i * 2] = b.Outline[i].Lat;
coords[i * 2 + 1] = b.Outline[i].Lon;
}
payload.Buildings.Add(coords);
payload.BuildingTypes.Add(b.BuildingType);
}
foreach (var p in data.Pathways)
{
var coords = new double[p.Points.Count * 2];
for (int i = 0; i < p.Points.Count; i++)
{
coords[i * 2] = p.Points[i].Lat;
coords[i * 2 + 1] = p.Points[i].Lon;
}
payload.Pathways.Add(coords);
payload.PathwayTypes.Add((int)p.PathType);
}
foreach (var a in data.Areas)
{
var coords = new double[a.Outline.Count * 2];
for (int i = 0; i < a.Outline.Count; i++)
{
coords[i * 2] = a.Outline[i].Lat;
coords[i * 2 + 1] = a.Outline[i].Lon;
}
payload.Areas.Add(coords);
payload.AreaTypes.Add((int)a.AreaType);
}
foreach (var poi in data.PointsOfInterest)
{
payload.POIs.Add(poi.Position.Lat);
payload.POIs.Add(poi.Position.Lon);
payload.POIs.Add((int)poi.PoiType);
}
return payload;
}
}
#endregion
#region Protokol - Zprávy
public abstract class Message
{
[JsonPropertyName("type")]
public abstract string Type { get; }
[JsonPropertyName("clientSeq")]
public int ClientSeq { get; set; }
[JsonPropertyName("actionId")]
public string? ActionId { get; set; }
}
// Handshake
public class ClientHello : Message
{
public override string Type => "ClientHello";
public string ProtocolVersion { get; set; } = "1.0";
public required string ClientUuid { get; set; }
public string? DisplayName { get; set; }
}
public class ServerHello : Message
{
public override string Type => "ServerHello";
public required string RsaPublicKeyPem { get; set; }
public required string ServerId { get; set; }
}
public class KeyExchange : Message
{
public override string Type => "KeyExchange";
public required string EncryptedSessionKey { get; set; }
public required string EncryptedIV { get; set; }
}
public class KeyExchangeAck : Message
{
public override string Type => "KeyExchangeAck";
public string Status { get; set; } = "success";
}
// Lobby
public class CreateLobby : Message
{
public override string Type => "CreateLobby";
public string? Password { get; set; }
public Position? PlayAreaCenter { get; set; }
public double PlayAreaRadius { get; set; } = 500; // metry
public int ImpostorCount { get; set; } = 1;
public int TaskCount { get; set; } = 5;
}
public class CreateLobbyResponse : Message
{
public override string Type => "CreateLobbyResponse";
public bool Success { get; set; }
public string? JoinCode { get; set; }
public string? LobbyId { get; set; }
public string? Error { get; set; }
public LobbyState? LobbyState { get; set; }
}
public class JoinLobby : Message
{
public override string Type => "JoinLobby";
public required string JoinCode { get; set; }
public string? Password { get; set; }
}
public class JoinLobbyResponse : Message
{
public override string Type => "JoinLobbyResponse";
public bool Success { get; set; }
public string? LobbyId { get; set; }
public string? Error { get; set; }
public LobbyState? LobbyState { get; set; }
}
public class LeaveLobby : Message
{
public override string Type => "LeaveLobby";
}
public class KickPlayer : Message
{
public override string Type => "KickPlayer";
public required string TargetClientUuid { get; set; }
}
public class StartGame : Message
{
public override string Type => "StartGame";
}
public class ReturnToLobby : Message
{
public override string Type => "ReturnToLobby";
}
/// <summary>
/// Client confirms it received map data and is ready to play
/// </summary>
public class MapDataReceived : Message
{
public override string Type => "MapDataReceived";
}
// Hra
public class UpdatePosition : Message
{
public override string Type => "UpdatePosition";
public Position Position { get; set; }
}
public class PositionBroadcast : Message
{
public override string Type => "PositionBroadcast";
public List<PlayerPositionInfo> Players { get; set; } = new();
}
public class PlayerPositionInfo
{
public required string ClientUuid { get; set; }
public Position Position { get; set; }
public PlayerState State { get; set; }
}
public class KillAttempt : Message
{
public override string Type => "KillAttempt";
public required string TargetClientUuid { get; set; }
}
public class ReportBody : Message
{
public override string Type => "ReportBody";
public required string BodyId { get; set; }
}
public class CallEmergencyMeeting : Message
{
public override string Type => "CallEmergencyMeeting";
}
public class CastVote : Message
{
public override string Type => "CastVote";
public string? TargetClientUuid { get; set; } // null = skip
}
public class TaskStart : Message
{
public override string Type => "TaskStart";
public required string TaskId { get; set; }
}
public class TaskProgress : Message
{
public override string Type => "TaskProgress";
public required string TaskId { get; set; }
public int Step { get; set; } = 1;
}
public class TaskComplete : Message
{
public override string Type => "TaskComplete";
public required string TaskId { get; set; }
}
// Ping/Heartbeat
public class Ping : Message
{
public override string Type => "Ping";
public long ClientTime { get; set; }
}
public class Pong : Message
{
public override string Type => "Pong";
public long ClientTime { get; set; }
public long ServerTime { get; set; }
}
// Reconnect
public class Reconnect : Message
{
public override string Type => "Reconnect";
public required string LobbyId { get; set; }
public long LastEventId { get; set; }
}
public class ReconnectResponse : Message
{
public override string Type => "ReconnectResponse";
public bool Success { get; set; }
public string? Error { get; set; }
public LobbySnapshot? Snapshot { get; set; }
public List<GameEvent>? MissedEvents { get; set; }
}
// Ack
public class Ack : Message
{
public override string Type => "Ack";
public int AckedSeq { get; set; }
public bool Success { get; set; }
public string? Error { get; set; }
}
// Error
public class ErrorMessage : Message
{
public override string Type => "Error";
public required string ErrorCode { get; set; }
public required string ErrorText { get; set; }
}
// Sabotage messages
public class StartSabotage : Message
{
public override string Type => "StartSabotage";
public SabotageType SabotageType { get; set; }
public Position? TargetLocation { get; set; } // For zone lockdown
}
public class ActivateRepairStation : Message
{
public override string Type => "ActivateRepairStation";
public required string StationId { get; set; }
}
public class DeactivateRepairStation : Message
{
public override string Type => "DeactivateRepairStation";
public required string StationId { get; set; }
}
#endregion
#region Eventy
public class GameEvent : Message
{
public override string Type => "GameEvent";
public long EventId { get; set; }
public long ServerSeq { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string? Actor { get; set; }
public required string EventType { get; set; }
public object? Payload { get; set; }
}
// Payload typy pro eventy
public class LobbyCreatedPayload
{
public required string LobbyId { get; set; }
public required string JoinCode { get; set; }
public required string OwnerId { get; set; }
}
public class PlayerJoinedPayload
{
public required string ClientUuid { get; set; }
public required string DisplayName { get; set; }
}
public class PlayerLeftPayload
{
public required string ClientUuid { get; set; }
public string? Reason { get; set; }
}
/// <summary>
/// Sent when owner clicks StartGame - clients should show loading screen
/// </summary>
public class GameStartingPayload
{
public string? InitiatorId { get; set; }
public string Message { get; set; } = "Loading map data...";
}
/// <summary>
/// Sent when map data is ready and distributed to all clients
/// </summary>
public class MapDataReadyPayload
{
public MapDataPayload? MapData { get; set; }
public Position PlayAreaCenter { get; set; }
public double PlayAreaRadius { get; set; }
}
/// <summary>
/// Sent when a client confirms they received map data
/// </summary>
public class PlayerMapDataReceivedPayload
{
public required string ClientUuid { get; set; }
public string DisplayName { get; set; } = "";
public int PlayersReady { get; set; }
public int TotalPlayers { get; set; }
}
/// <summary>
/// Sent when all clients have confirmed map data and game actually starts
/// </summary>
public class GameStartedPayload
{
public int ImpostorCount { get; set; }
public int TaskCount { get; set; }
}
public class RoleAssignedPayload
{
public required string ClientUuid { get; set; }
public PlayerRole Role { get; set; }
public List<GameTask>? Tasks { get; set; }
}
public class PlayerKilledPayload
{
public required string VictimId { get; set; }
public required string KillerId { get; set; }
public required string BodyId { get; set; }
public Position Location { get; set; }
}
public class BodyReportedPayload
{
public required string ReporterId { get; set; }
public required string BodyId { get; set; }
public required string VictimId { get; set; }
}
public class EmergencyMeetingCalledPayload
{
public required string CallerId { get; set; }
}
public class MeetingStartedPayload
{
public required string MeetingId { get; set; }
public MeetingType Type { get; set; }
public Position MeetingLocation { get; set; }
public DateTime ArrivalDeadline { get; set; }
public DateTime? DiscussionEndTime { get; set; }
public DateTime VotingEndTime { get; set; }
}
public class PlayerArrivedAtMeetingPayload
{
public required string ClientUuid { get; set; }
public required string MeetingId { get; set; }
}
public class PlayerVotedPayload
{
public required string VoterId { get; set; }
public string? TargetId { get; set; } // null = skip
}
public class VotingClosedPayload
{
public Dictionary<string, int> VoteCounts { get; set; } = new();
public string? EjectedPlayerId { get; set; }
public bool WasTie { get; set; }
}
public class PlayerEjectedPayload
{
public required string ClientUuid { get; set; }
public PlayerRole Role { get; set; }
}
public class TaskStartedPayload
{
public required string ClientUuid { get; set; }
public required string TaskId { get; set; }
}
public class TaskCompletedPayload
{
public required string ClientUuid { get; set; }
public required string TaskId { get; set; }
public int TotalCompleted { get; set; }
public int TotalTasks { get; set; }
}
public class GameEndedPayload
{
public required string WinningFaction { get; set; } // "Crew" nebo "Impostor"
public required string Reason { get; set; }
public List<string> Winners { get; set; } = new();
}
public class ReturnedToLobbyPayload
{
public string Message { get; set; } = "";
}
// ── P12: Admin lobby manipulation event payloads ────────────────────────────
// Broadcast by LobbyActor when an admin uses the new admin-panel endpoints
// (force-phase, edit-settings). Spectators and live clients re-render against
// these so they see admin overrides without having to reload.
public class PhaseChangedPayload
{
public GamePhase Phase { get; set; }
}
public class LobbySettingsChangedPayload
{
public double Radius { get; set; }
public int ImpostorCount { get; set; }
public int TaskCount { get; set; }
public string TiePolicy { get; set; } = "NoEject";
}
public class HostChangedPayload
{
public required string NewHostId { get; set; }
public required string PreviousHostId { get; set; }
}
public class CheatDetectedPayload
{
public required string ClientUuid { get; set; }
public required string Violation { get; set; }
public int NewCheatScore { get; set; }
public CheatStatus NewStatus { get; set; }
}
// Sabotage event payloads
public class SabotageStartedPayload
{
public required string SabotageId { get; set; }
public SabotageType Type { get; set; }
public required string InitiatorId { get; set; }
public DateTime? Deadline { get; set; } // For critical meltdown
public List<RepairStationInfo> RepairStations { get; set; } = new();
public int RequiredSimultaneousRepairs { get; set; }
}
public class RepairStationInfo
{
public required string StationId { get; set; }
public required string Name { get; set; }
public Position Location { get; set; }
public int RepairDurationMs { get; set; }
}
public class RepairStartedPayload
{
public required string SabotageId { get; set; }
public required string StationId { get; set; }
public required string PlayerId { get; set; }
}
public class RepairStoppedPayload
{
public required string SabotageId { get; set; }
public required string StationId { get; set; }
public required string PlayerId { get; set; }
}
public class RepairProgressPayload
{
public required string SabotageId { get; set; }
public required string StationId { get; set; }
public required string PlayerId { get; set; }
public int ProgressMs { get; set; }
public int RequiredMs { get; set; }
}
public class SabotageRepairedPayload
{
public required string SabotageId { get; set; }
public SabotageType Type { get; set; }
public List<string> RepairerIds { get; set; } = new(); // Players who helped repair
}
public class SabotageMeltdownPayload
{
public required string SabotageId { get; set; }
// Impostor wins - game ends
}
#endregion
#region State
public class LobbyState
{
public required string LobbyId { get; set; }
public required string JoinCode { get; set; }
public string? OwnerId { get; set; }
public GamePhase Phase { get; set; } = GamePhase.Lobby;
public List<PlayerInfo> Players { get; set; } = new();
public Position PlayAreaCenter { get; set; }
public double PlayAreaRadius { get; set; }
public int ImpostorCount { get; set; }
public bool HasPassword { get; set; }
public DateTime CreatedAt { get; set; }
/// <summary>Map data for client rendering (null if Overpass disabled or failed)</summary>
public MapDataPayload? MapData { get; set; }
/// <summary>True if map data has been loaded (or Overpass is disabled)</summary>
public bool MapDataReady { get; set; } = true;
}
public class PlayerInfo
{
public required string ClientUuid { get; set; }
public required string DisplayName { get; set; }
public bool IsOwner { get; set; }
public bool IsReady { get; set; }
public PlayerState State { get; set; }
// Role je viditelná pouze pro daného hráče, ne v broadcast
}
public class LobbySnapshot
{
public required string LobbyId { get; set; }
public long LastEventId { get; set; }
public DateTime Timestamp { get; set; }
public required string Checksum { get; set; }
public GamePhase Phase { get; set; }
public List<Player> Players { get; set; } = new();
public List<Body> Bodies { get; set; } = new();
public List<GameTask> Tasks { get; set; } = new();
public Meeting? CurrentMeeting { get; set; }
public Position PlayAreaCenter { get; set; }
public double PlayAreaRadius { get; set; }
public int ImpostorCount { get; set; }
public TiePolicy TiePolicy { get; set; }
}
#endregion
#region Serializace
public static class MessageSerializer
{
private static readonly Dictionary<string, Type> MessageTypes = new()
{
["ClientHello"] = typeof(ClientHello),
["ServerHello"] = typeof(ServerHello),
["KeyExchange"] = typeof(KeyExchange),
["KeyExchangeAck"] = typeof(KeyExchangeAck),
["CreateLobby"] = typeof(CreateLobby),
["CreateLobbyResponse"] = typeof(CreateLobbyResponse),
["JoinLobby"] = typeof(JoinLobby),
["JoinLobbyResponse"] = typeof(JoinLobbyResponse),
["LeaveLobby"] = typeof(LeaveLobby),
["KickPlayer"] = typeof(KickPlayer),
["StartGame"] = typeof(StartGame),
["ReturnToLobby"] = typeof(ReturnToLobby),
["MapDataReceived"] = typeof(MapDataReceived),
["UpdatePosition"] = typeof(UpdatePosition),
["PositionBroadcast"] = typeof(PositionBroadcast),
["KillAttempt"] = typeof(KillAttempt),
["ReportBody"] = typeof(ReportBody),
["CallEmergencyMeeting"] = typeof(CallEmergencyMeeting),
["CastVote"] = typeof(CastVote),
["TaskStart"] = typeof(TaskStart),
["TaskProgress"] = typeof(TaskProgress),
["TaskComplete"] = typeof(TaskComplete),
["Ping"] = typeof(Ping),
["Pong"] = typeof(Pong),
["Reconnect"] = typeof(Reconnect),
["ReconnectResponse"] = typeof(ReconnectResponse),
["Ack"] = typeof(Ack),
["Error"] = typeof(ErrorMessage),
["GameEvent"] = typeof(GameEvent),
["StartSabotage"] = typeof(StartSabotage),
["ActivateRepairStation"] = typeof(ActivateRepairStation),
["DeactivateRepairStation"] = typeof(DeactivateRepairStation)
};
public static byte[] Serialize(Message msg)
{
return JsonSerializer.SerializeToUtf8Bytes(msg, msg.GetType(), JsonOptions.Default);
}
public static byte[] Serialize(GameEvent evt)
{
return JsonSerializer.SerializeToUtf8Bytes(evt, JsonOptions.Default);
}
public static Message? Deserialize(ReadOnlySpan<byte> data)
{
// Nejdřív zjistíme typ
using var doc = JsonDocument.Parse(data.ToArray());
if (!doc.RootElement.TryGetProperty("type", out var typeProp))
return null;
var typeName = typeProp.GetString();
if (typeName == null || !MessageTypes.TryGetValue(typeName, out var type))
return null;
return (Message?)JsonSerializer.Deserialize(data, type, JsonOptions.Default);
}
public static GameEvent? DeserializeEvent(ReadOnlySpan<byte> data)
{
return JsonSerializer.Deserialize<GameEvent>(data, JsonOptions.Default);
}
}
#endregion