GeoSus
This commit is contained in:
@@ -132,7 +132,20 @@ public class GameClient : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Disconnect(string reason = "User disconnected")
|
||||
/// <summary>
|
||||
/// Tears down the socket and crypto session. When `transient` is true
|
||||
/// (network drop, decrypt-failure cascade, anything we expect to retry),
|
||||
/// the lobby/role/task/state caches are preserved so the post-reconnect
|
||||
/// flow can re-associate via Reconnect(LobbyId). Default false matches
|
||||
/// pre-P9 behavior (full state wipe) for explicit user disconnects.
|
||||
///
|
||||
/// Critical for the P9 reconnect bug: previously every Disconnect path
|
||||
/// nuked LobbyId, so by the time GameManager_Network's reconnect coroutine
|
||||
/// fired, the client had no idea which lobby it had been in - the
|
||||
/// post-handshake Reconnect call had nothing to send and the server
|
||||
/// answered the next vote/action with NOT_IN_LOBBY.
|
||||
/// </summary>
|
||||
public void Disconnect(string reason = "User disconnected", bool transient = false)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_tcpClient?.Close();
|
||||
@@ -140,15 +153,22 @@ public class GameClient : IDisposable
|
||||
_stream = null;
|
||||
_encryption?.Dispose();
|
||||
_encryption = null;
|
||||
|
||||
LobbyId = null;
|
||||
JoinCode = null;
|
||||
CurrentLobbyState = null;
|
||||
MyRole = null;
|
||||
MyTasks.Clear();
|
||||
PlayerPositions.Clear();
|
||||
Bodies.Clear();
|
||||
|
||||
|
||||
if (!transient)
|
||||
{
|
||||
LobbyId = null;
|
||||
JoinCode = null;
|
||||
CurrentLobbyState = null;
|
||||
MyRole = null;
|
||||
MyTasks.Clear();
|
||||
PlayerPositions.Clear();
|
||||
Bodies.Clear();
|
||||
}
|
||||
// PlayerPositions are stale anyway after a drop, but we keep them so
|
||||
// the UI doesn't blink avatars off-map mid-meeting; the next position
|
||||
// broadcast overwrites them. LastEventId is intentionally preserved
|
||||
// so the Reconnect message can ask the server for missed events.
|
||||
|
||||
Dispatcher.Post(() => OnDisconnected?.Invoke(reason));
|
||||
}
|
||||
|
||||
@@ -236,7 +256,8 @@ public class GameClient : IDisposable
|
||||
decryptFailures++;
|
||||
if (decryptFailures >= 3)
|
||||
{
|
||||
Disconnect("Too many decryption failures");
|
||||
// Transient: keep LobbyId for the reconnect coroutine.
|
||||
Disconnect("Too many decryption failures", transient: true);
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
@@ -253,7 +274,9 @@ public class GameClient : IDisposable
|
||||
}
|
||||
catch (Exception ex) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
Disconnect($"Connection error: {ex.Message}");
|
||||
// Transient: TCP RST / read failure is exactly what reconnect was
|
||||
// designed for. Keep LobbyId so post-reconnect flow can re-attach.
|
||||
Disconnect($"Connection error: {ex.Message}", transient: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,7 +531,7 @@ public class GameClient : IDisposable
|
||||
|
||||
#region Game Actions
|
||||
|
||||
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500)
|
||||
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500, GameSettingsOverrides? settings = null)
|
||||
{
|
||||
Send(new CreateLobby
|
||||
{
|
||||
@@ -516,7 +539,8 @@ public class GameClient : IDisposable
|
||||
PlayAreaRadius = playAreaRadius,
|
||||
ImpostorCount = impostorCount,
|
||||
TaskCount = taskCount,
|
||||
Password = password
|
||||
Password = password,
|
||||
Settings = settings
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,11 @@ public enum PlayerRole { Crew, Impostor }
|
||||
public enum PlayerState { Alive, Dead }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
// NOTE: `Voting` is reserved-but-unused on the wire as of 2026. The server
|
||||
// keeps the entire vote cycle inside `Meeting` and uses MeetingStartedPayload
|
||||
// timestamps (DiscussionEndTime / VotingEndTime) to distinguish sub-phases.
|
||||
// The enum value is preserved here for serialization compatibility with old
|
||||
// saves; new code should not assign it.
|
||||
public enum GamePhase { Lobby, Loading, Playing, Meeting, Voting, Ended }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
@@ -184,6 +189,14 @@ public class CreateLobby : Message
|
||||
|
||||
[JsonProperty("taskCount")]
|
||||
public int TaskCount { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// P13b: optional per-lobby settings overrides supplied by the host.
|
||||
/// Any field left null falls through to the server's current default
|
||||
/// (snapshotted at lobby creation, immutable thereafter for this lobby).
|
||||
/// </summary>
|
||||
[JsonProperty("settings")]
|
||||
public GameSettingsOverrides? Settings { get; set; }
|
||||
}
|
||||
|
||||
public class CreateLobbyResponse : Message
|
||||
@@ -623,17 +636,26 @@ public class PlayerEjectedPayload
|
||||
public PlayerRole Role { get; set; }
|
||||
}
|
||||
|
||||
public class TaskStartedPayload
|
||||
{
|
||||
[JsonProperty("clientUuid")]
|
||||
public string ClientUuid { get; set; } = "";
|
||||
|
||||
[JsonProperty("taskId")]
|
||||
public string TaskId { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TaskCompletedPayload
|
||||
{
|
||||
[JsonProperty("clientUuid")]
|
||||
public string ClientUuid { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("taskId")]
|
||||
public string TaskId { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("totalCompleted")]
|
||||
public int TotalCompleted { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("totalTasks")]
|
||||
public int TotalTasks { get; set; }
|
||||
}
|
||||
@@ -713,10 +735,10 @@ public class RepairStartedPayload
|
||||
{
|
||||
[JsonProperty("sabotageId")]
|
||||
public string SabotageId { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("stationId")]
|
||||
public string StationId { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("playerId")]
|
||||
public string PlayerId { get; set; } = "";
|
||||
}
|
||||
@@ -725,10 +747,10 @@ public class RepairStoppedPayload
|
||||
{
|
||||
[JsonProperty("sabotageId")]
|
||||
public string SabotageId { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("stationId")]
|
||||
public string StationId { get; set; } = "";
|
||||
|
||||
|
||||
[JsonProperty("playerId")]
|
||||
public string PlayerId { get; set; } = "";
|
||||
}
|
||||
@@ -790,6 +812,162 @@ public class LobbyState
|
||||
/// <summary>True if map data has been loaded (or Overpass is disabled)</summary>
|
||||
[JsonProperty("mapDataReady")]
|
||||
public bool MapDataReady { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// P13b: full per-lobby settings snapshot. Clients use this for HUD
|
||||
/// (button visibility, countdown timings, etc.) instead of hardcoded
|
||||
/// values. Always populated for new server builds; old client builds
|
||||
/// can ignore the field.
|
||||
/// </summary>
|
||||
[JsonProperty("settings")]
|
||||
public GameSettings? Settings { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// P13b: per-lobby gameplay settings on the wire. Server populates this from
|
||||
/// its per-lobby snapshot so clients can drive HUD logic from authoritative
|
||||
/// values rather than hardcoded constants.
|
||||
/// </summary>
|
||||
public class GameSettings
|
||||
{
|
||||
// Round shape
|
||||
[JsonProperty("maxPlayers")]
|
||||
public int MaxPlayers { get; set; }
|
||||
|
||||
[JsonProperty("impostorCount")]
|
||||
public int ImpostorCount { get; set; }
|
||||
|
||||
[JsonProperty("taskCount")]
|
||||
public int TaskCount { get; set; }
|
||||
|
||||
[JsonProperty("tiePolicy")]
|
||||
public string TiePolicy { get; set; } = "NoEject";
|
||||
|
||||
// Distances (m)
|
||||
[JsonProperty("killDistanceM")]
|
||||
public double KillDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("reportDistanceM")]
|
||||
public double ReportDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("taskStartDistanceM")]
|
||||
public double TaskStartDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("meetingArrivalRadiusM")]
|
||||
public double MeetingArrivalRadiusM { get; set; }
|
||||
|
||||
[JsonProperty("emergencyMeetingCallRadiusM")]
|
||||
public double EmergencyMeetingCallRadiusM { get; set; }
|
||||
|
||||
[JsonProperty("repairStationDistanceM")]
|
||||
public double RepairStationDistanceM { get; set; }
|
||||
|
||||
// Cooldowns / counts
|
||||
[JsonProperty("killCooldownMs")]
|
||||
public int KillCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("emergencyMeetingCooldownMs")]
|
||||
public int EmergencyMeetingCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("maxEmergencyMeetingsPerPlayer")]
|
||||
public int MaxEmergencyMeetingsPerPlayer { get; set; }
|
||||
|
||||
// Meeting phases (ms)
|
||||
[JsonProperty("arrivalBaseMs")]
|
||||
public int ArrivalBaseMs { get; set; }
|
||||
|
||||
[JsonProperty("allowedLateMs")]
|
||||
public int AllowedLateMs { get; set; }
|
||||
|
||||
[JsonProperty("discussionPhaseMs")]
|
||||
public int DiscussionPhaseMs { get; set; }
|
||||
|
||||
[JsonProperty("votingPhaseMs")]
|
||||
public int VotingPhaseMs { get; set; }
|
||||
|
||||
// Sabotage
|
||||
[JsonProperty("sabotageCooldownMs")]
|
||||
public int SabotageCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("commsBlackoutDurationMs")]
|
||||
public int CommsBlackoutDurationMs { get; set; }
|
||||
|
||||
[JsonProperty("criticalMeltdownDeadlineMs")]
|
||||
public int CriticalMeltdownDeadlineMs { get; set; }
|
||||
|
||||
[JsonProperty("repairStationHoldMs")]
|
||||
public int RepairStationHoldMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// P13b: host-supplied overrides at CreateLobby. Every field is nullable so
|
||||
/// the host can opt into changing only what they care about; null = use the
|
||||
/// server's current default at the moment of lobby creation.
|
||||
/// </summary>
|
||||
public class GameSettingsOverrides
|
||||
{
|
||||
[JsonProperty("maxPlayers")]
|
||||
public int? MaxPlayers { get; set; }
|
||||
|
||||
[JsonProperty("impostorCount")]
|
||||
public int? ImpostorCount { get; set; }
|
||||
|
||||
[JsonProperty("taskCount")]
|
||||
public int? TaskCount { get; set; }
|
||||
|
||||
[JsonProperty("tiePolicy")]
|
||||
public string? TiePolicy { get; set; }
|
||||
|
||||
[JsonProperty("killDistanceM")]
|
||||
public double? KillDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("reportDistanceM")]
|
||||
public double? ReportDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("taskStartDistanceM")]
|
||||
public double? TaskStartDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("meetingArrivalRadiusM")]
|
||||
public double? MeetingArrivalRadiusM { get; set; }
|
||||
|
||||
[JsonProperty("emergencyMeetingCallRadiusM")]
|
||||
public double? EmergencyMeetingCallRadiusM { get; set; }
|
||||
|
||||
[JsonProperty("repairStationDistanceM")]
|
||||
public double? RepairStationDistanceM { get; set; }
|
||||
|
||||
[JsonProperty("killCooldownMs")]
|
||||
public int? KillCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("emergencyMeetingCooldownMs")]
|
||||
public int? EmergencyMeetingCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("maxEmergencyMeetingsPerPlayer")]
|
||||
public int? MaxEmergencyMeetingsPerPlayer { get; set; }
|
||||
|
||||
[JsonProperty("arrivalBaseMs")]
|
||||
public int? ArrivalBaseMs { get; set; }
|
||||
|
||||
[JsonProperty("allowedLateMs")]
|
||||
public int? AllowedLateMs { get; set; }
|
||||
|
||||
[JsonProperty("discussionPhaseMs")]
|
||||
public int? DiscussionPhaseMs { get; set; }
|
||||
|
||||
[JsonProperty("votingPhaseMs")]
|
||||
public int? VotingPhaseMs { get; set; }
|
||||
|
||||
[JsonProperty("sabotageCooldownMs")]
|
||||
public int? SabotageCooldownMs { get; set; }
|
||||
|
||||
[JsonProperty("commsBlackoutDurationMs")]
|
||||
public int? CommsBlackoutDurationMs { get; set; }
|
||||
|
||||
[JsonProperty("criticalMeltdownDeadlineMs")]
|
||||
public int? CriticalMeltdownDeadlineMs { get; set; }
|
||||
|
||||
[JsonProperty("repairStationHoldMs")]
|
||||
public int? RepairStationHoldMs { get; set; }
|
||||
}
|
||||
|
||||
// Map data classes for rendering - compact format from server
|
||||
|
||||
Reference in New Issue
Block a user