Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "runecast-protocol"
version = "0.20.0"
version = "0.21.0"
edition = "2021"
description = "Protocol definitions for RuneCast WebSocket communication"

Expand Down
19 changes: 19 additions & 0 deletions src/protocol/client_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ pub enum ClientMessage {
previous_game_id: String,
},

/// Switch the current lobby's auto-rematch setting. Any lobby member may send
/// it; the server broadcasts `LobbyChange::AutoRematchChanged` in a
/// `LobbyDelta`, and sending the current value again changes nothing.
SetAutoRematch { enabled: bool },

// ========================================================================
// Admin Messages
// ========================================================================
Expand Down Expand Up @@ -322,6 +327,7 @@ impl ClientMessage {
Self::VoteForTimer { .. } => "vote_for_timer",
Self::TriggerRematch { .. } => "trigger_rematch",
Self::LeaveRematch { .. } => "leave_rematch",
Self::SetAutoRematch { .. } => "set_auto_rematch",
Self::AdminGetGames => "admin_get_games",
Self::AdminDeleteGame { .. } => "admin_delete_game",
Self::PlayerDisconnected { .. } => "player_disconnected",
Expand Down Expand Up @@ -351,6 +357,7 @@ impl ClientMessage {
| Self::VoteForTimer { .. }
| Self::TriggerRematch { .. }
| Self::LeaveRematch { .. }
| Self::SetAutoRematch { .. }
| Self::AdminGetGames
| Self::AdminDeleteGame { .. }
)
Expand Down Expand Up @@ -577,4 +584,16 @@ mod tests {
}
));
}

#[test]
fn set_auto_rematch_parses_and_needs_a_lobby() {
let msg: ClientMessage =
serde_json::from_str(r#"{"type":"set_auto_rematch","enabled":false}"#).unwrap();
assert!(matches!(
msg,
ClientMessage::SetAutoRematch { enabled: false }
));
assert_eq!(msg.message_type(), "set_auto_rematch");
assert!(msg.requires_lobby());
}
}
30 changes: 30 additions & 0 deletions src/protocol/server_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,12 +838,20 @@ pub struct LobbySnapshot {
#[serde_as(as = "Option<serde_with::DisplayFromStr>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_id: Option<i64>,
/// Whether a finished game arms a rematch countdown (`true`) or an opt-in
/// (`false`). Absent from backends before v0.48.0, so it reads as on.
#[serde(default = "default_true")]
pub auto_rematch: bool,
}

fn default_max_players() -> u8 {
6
}

fn default_true() -> bool {
true
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1524,6 +1532,7 @@ mod tests {
max_players: 6,
sandbox_config: None,
host_id: None,
auto_rematch: true,
};
snap.sandbox_config = Some(GameConfig::default());

Expand All @@ -1545,6 +1554,7 @@ mod tests {
max_players: 6,
sandbox_config: None,
host_id: None,
auto_rematch: true,
};
assert!(
!serde_json::to_string(&plain)
Expand Down Expand Up @@ -1573,6 +1583,7 @@ mod tests {
max_players: 6,
sandbox_config: None,
host_id: Some(snowflake),
auto_rematch: true,
};
let json = serde_json::to_string(&snap).unwrap();
// Quoted = string on the wire; an unquoted number would be the bug.
Expand Down Expand Up @@ -1828,4 +1839,23 @@ mod tests {
other => panic!("wrong variant: {other:?}"),
}
}

#[test]
fn a_snapshot_without_auto_rematch_reads_as_on() {
let json = r#"{"lobby_id":"l","lobby_type":"channel","players":[],"games":[]}"#;
let snap: LobbySnapshot = serde_json::from_str(json).unwrap();
assert!(
snap.auto_rematch,
"an older server's snapshot must read as auto-rematch on"
);
}

#[test]
fn a_snapshot_carries_auto_rematch_off() {
let json = r#"{"lobby_id":"l","lobby_type":"channel","players":[],"games":[],"auto_rematch":false}"#;
let snap: LobbySnapshot = serde_json::from_str(json).unwrap();
assert!(!snap.auto_rematch);
let out = serde_json::to_string(&snap).unwrap();
assert!(out.contains(r#""auto_rematch":false"#), "{out}");
}
}
66 changes: 66 additions & 0 deletions src/protocol/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,23 @@ pub enum RematchCountdownState {
game_type: GameType,
},

/// Auto-rematch is off for the lobby, or was switched off mid-countdown: no
/// timer. Each eligible player who presses Play Again (`TriggerRematch`)
/// joins `ready`; the rematch starts for `ready` only, once it reaches
/// `needed`. Exists only while at least `needed` players are eligible.
OptIn {
/// Players who pressed Play Again.
#[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
ready: Vec<i64>,
/// The previous game's players who may still opt in (a superset of `ready`).
#[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
eligible: Vec<i64>,
/// How many must be ready before the rematch starts (`REMATCH_MIN_PLAYERS`).
needed: u8,
/// The game pool/game type for the rematch
game_type: GameType,
},

/// A player triggered an early start
Starting {
/// Who triggered the early start (None if countdown expired naturally)
Expand Down Expand Up @@ -601,6 +618,13 @@ pub enum LobbyChange {

/// Host changed
HostChanged { new_host_id: String },

/// The lobby's auto-rematch setting changed. Any lobby member may change it.
AutoRematchChanged {
enabled: bool,
#[serde_as(as = "serde_with::DisplayFromStr")]
changed_by: i64,
},
}

/// Changes to game state (for delta updates).
Expand Down Expand Up @@ -1969,4 +1993,46 @@ mod tests {
assert_eq!(p.username, "ada_l");
assert!(p.name.is_none());
}

#[test]
fn opt_in_state_round_trips_with_string_ids() {
let s = RematchCountdownState::OptIn {
ready: vec![1],
eligible: vec![1, 2],
needed: 2,
game_type: GameType::Open,
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains(r#""status":"opt_in""#), "{json}");
assert!(json.contains(r#""ready":["1"]"#), "{json}");
assert!(json.contains(r#""eligible":["1","2"]"#), "{json}");
assert!(json.contains(r#""needed":2"#), "{json}");
let back: RematchCountdownState = serde_json::from_str(&json).unwrap();
assert!(
matches!(&back, RematchCountdownState::OptIn { ready, eligible, needed: 2, game_type: GameType::Open }
if ready == &[1] && eligible == &[1, 2]),
"{back:?}"
);
}

#[test]
fn auto_rematch_changed_round_trips() {
let c = LobbyChange::AutoRematchChanged {
enabled: false,
changed_by: 42,
};
let json = serde_json::to_string(&c).unwrap();
assert_eq!(
json,
r#"{"change_type":"auto_rematch_changed","enabled":false,"changed_by":"42"}"#
);
let back: LobbyChange = serde_json::from_str(&json).unwrap();
assert!(matches!(
back,
LobbyChange::AutoRematchChanged {
enabled: false,
changed_by: 42
}
));
}
}
Loading