986 lines
30 KiB
C#
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
|