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 { /// Comms Blackout - Cannot report bodies or call emergency meetings CommsBlackout, /// Critical Meltdown - Must be repaired in time or impostors win (requires 2 simultaneous repairs) 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 CompletedTaskIds { get; set; } = new(); public List 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 ArrivedPlayers { get; set; } = new(); public Dictionary Votes { get; set; } = new(); // Voter -> Target (null = skip) public DateTime? LastVoteChangeTime { get; set; } } /// /// Active sabotage instance /// 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; } /// /// Repair stations for this sabotage. /// CommsBlackout: 1 station /// CriticalMeltdown: 2 stations that need simultaneous activation /// public List RepairStations { get; set; } = new(); /// /// How many stations need to be simultaneously active to repair. /// CommsBlackout: 1, CriticalMeltdown: 2 /// public int RequiredSimultaneousRepairs { get; set; } = 1; } /// /// Repair station for fixing sabotages /// 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; } /// /// Has this station been successfully repaired /// public bool IsRepaired { get; set; } /// /// How long player must hold to complete repair at this station /// public int RepairDurationMs { get; set; } = 3000; } #endregion #region Map Data /// /// Complete map data for a play area - sent to clients for rendering /// public class MapData { public Position Center { get; set; } public double RadiusMeters { get; set; } public DateTime FetchedAt { get; set; } /// Buildings to render (polygons) public List Buildings { get; set; } = new(); /// Walkable pathways (lines) public List Pathways { get; set; } = new(); /// Parks, green areas (polygons) public List Areas { get; set; } = new(); /// Points of interest (single points) public List PointsOfInterest { get; set; } = new(); /// All positions reachable from center via walkable paths (within play area) [JsonIgnore] // Too large for network, computed server-side only public List ReachablePositions { get; set; } = new(); } /// Building outline for rendering public class MapBuilding { public required string Id { get; set; } public List Outline { get; set; } = new(); public string BuildingType { get; set; } = "yes"; public string? Name { get; set; } public int Levels { get; set; } = 1; } /// Walkable pathway for rendering and reachability public class MapPathway { public required string Id { get; set; } public List 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; } } /// Area like park or garden public class MapArea { public required string Id { get; set; } public List Outline { get; set; } = new(); public MapAreaType AreaType { get; set; } public string? Name { get; set; } } /// Point of interest for task placement hints public class MapPOI { public required string Id { get; set; } public Position Position { get; set; } public MapPOIType PoiType { get; set; } public string? Name { get; set; } } /// Simplified map data sent to clients (smaller payload) public class MapDataPayload { public Position Center { get; set; } public double RadiusMeters { get; set; } /// Buildings: [[lat,lon,lat,lon,...], ...] public List Buildings { get; set; } = new(); /// Building types: ["residential", "commercial", ...] public List BuildingTypes { get; set; } = new(); /// Pathways: [[lat,lon,lat,lon,...], ...] public List Pathways { get; set; } = new(); /// Pathway types: [0=footway, 1=steps, ...] public List PathwayTypes { get; set; } = new(); /// Areas (parks): [[lat,lon,lat,lon,...], ...] public List Areas { get; set; } = new(); /// Area types public List AreaTypes { get; set; } = new(); /// POIs: [lat, lon, type, lat, lon, type, ...] public List POIs { get; set; } = new(); /// Convert from full MapData to compact payload 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"; } /// /// Client confirms it received map data and is ready to play /// 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 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? 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; } } /// /// Sent when owner clicks StartGame - clients should show loading screen /// public class GameStartingPayload { public string? InitiatorId { get; set; } public string Message { get; set; } = "Loading map data..."; } /// /// Sent when map data is ready and distributed to all clients /// public class MapDataReadyPayload { public MapDataPayload? MapData { get; set; } public Position PlayAreaCenter { get; set; } public double PlayAreaRadius { get; set; } } /// /// Sent when a client confirms they received map data /// public class PlayerMapDataReceivedPayload { public required string ClientUuid { get; set; } public string DisplayName { get; set; } = ""; public int PlayersReady { get; set; } public int TotalPlayers { get; set; } } /// /// Sent when all clients have confirmed map data and game actually starts /// 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? 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 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 Winners { get; set; } = new(); } public class ReturnedToLobbyPayload { public string Message { get; set; } = ""; } 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 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 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 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 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; } /// Map data for client rendering (null if Overpass disabled or failed) public MapDataPayload? MapData { get; set; } /// True if map data has been loaded (or Overpass is disabled) 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 Players { get; set; } = new(); public List Bodies { get; set; } = new(); public List 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 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 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 data) { return JsonSerializer.Deserialize(data, JsonOptions.Default); } } #endregion