Skip to content

Commit 2c9bf64

Browse files
committed
change player subscription handling a bit
1 parent d44797a commit 2c9bf64

2 files changed

Lines changed: 150 additions & 44 deletions

File tree

src/api/websocket/endpoint.rs

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ use crate::api::{
3333

3434
/// Max UUIDs in a single `SubscribePlayers` or `GetActiveCosmetics` message.
3535
const MAX_PLAYERS_PER_REQUEST: usize = 64;
36-
/// Max distinct players a connection may subscribe to at once (render distance).
37-
const MAX_PLAYER_SUBSCRIPTIONS: usize = 128;
36+
const MAX_PLAYER_SUBSCRIPTIONS: usize = 512;
37+
const REAL_PLAYER_UUID_VERSION: usize = 4;
3838

3939
fn enforce_max_players_per_request(players: &[Uuid]) -> Result<(), WebsocketError> {
4040
if players.len() > MAX_PLAYERS_PER_REQUEST {
@@ -45,6 +45,21 @@ fn enforce_max_players_per_request(players: &[Uuid]) -> Result<(), WebsocketErro
4545
Ok(())
4646
}
4747

48+
fn is_fake_player(player: &Uuid) -> bool {
49+
player.get_version_num() != REAL_PLAYER_UUID_VERSION
50+
}
51+
52+
fn empty_snapshot(rejected: Vec<Uuid>, request_id: Option<u64>) -> ClientBoundPacket {
53+
ClientBoundPacket::SubscriptionSnapshot {
54+
equipped: HashMap::new(),
55+
active_emotes: HashMap::new(),
56+
particle_colors: HashMap::new(),
57+
users: Vec::new(),
58+
rejected,
59+
request_id,
60+
}
61+
}
62+
4863
pub(super) fn router() -> ApiRouter<ApiState> {
4964
ApiRouter::new()
5065
.route("/websocket", get(self::endpoint))
@@ -377,42 +392,49 @@ async fn subscribe(
377392
state: &ApiState,
378393
connection_id: ConnectionId,
379394
players: Vec<Uuid>,
395+
request_id: Option<u64>,
380396
) -> Result<ClientBoundPacket, WebsocketError> {
381-
let requested = players.into_iter().collect::<HashSet<_>>();
382-
let newly_subscribed = {
397+
let mut seen = HashSet::new();
398+
let requested = players
399+
.into_iter()
400+
.filter(|player| seen.insert(*player))
401+
.collect::<Vec<_>>();
402+
403+
let (newly_subscribed, rejected) = {
383404
let mut connections = state.realtime.connections.write().await;
384405
let Some(connection) = connections.get_mut(&connection_id) else {
385-
return Ok(ClientBoundPacket::SubscriptionSnapshot {
386-
equipped: HashMap::new(),
387-
active_emotes: HashMap::new(),
388-
particle_colors: HashMap::new(),
389-
users: Vec::new(),
390-
});
406+
return Ok(empty_snapshot(Vec::new(), request_id));
391407
};
392408

393-
let pending = requested
394-
.iter()
395-
.filter(|player| !connection.subscriptions.contains(player))
396-
.count();
397-
if connection.subscriptions.len() + pending > MAX_PLAYER_SUBSCRIPTIONS {
398-
return Err(WebsocketError::SubscriptionLimitExceeded {
399-
limit: MAX_PLAYER_SUBSCRIPTIONS,
400-
});
409+
let mut newly_subscribed = Vec::new();
410+
let mut rejected = Vec::new();
411+
for player in requested {
412+
if is_fake_player(&player) {
413+
rejected.push(player);
414+
continue;
415+
}
416+
if connection.subscriptions.contains(&player) {
417+
continue;
418+
}
419+
if connection.subscriptions.len() >= MAX_PLAYER_SUBSCRIPTIONS {
420+
rejected.push(player);
421+
continue;
422+
}
423+
connection.subscriptions.insert(player);
424+
newly_subscribed.push(player);
401425
}
402-
403-
requested
404-
.into_iter()
405-
.filter(|player| connection.subscriptions.insert(*player))
406-
.collect::<Vec<_>>()
426+
(newly_subscribed, rejected)
407427
};
408428

429+
if !rejected.is_empty() {
430+
warn!(
431+
"Connection {connection_id} rejected {} subscription(s) (cap {MAX_PLAYER_SUBSCRIPTIONS})",
432+
rejected.len()
433+
);
434+
}
435+
409436
if newly_subscribed.is_empty() {
410-
return Ok(ClientBoundPacket::SubscriptionSnapshot {
411-
equipped: HashMap::new(),
412-
active_emotes: HashMap::new(),
413-
particle_colors: HashMap::new(),
414-
users: Vec::new(),
415-
});
437+
return Ok(empty_snapshot(rejected, request_id));
416438
}
417439

418440
{
@@ -480,6 +502,8 @@ async fn subscribe(
480502
active_emotes,
481503
particle_colors,
482504
users,
505+
rejected,
506+
request_id,
483507
})
484508
}
485509

@@ -530,14 +554,28 @@ async fn broadcast_to_watchers(
530554
}
531555
}
532556

557+
struct RequestError {
558+
error: WebsocketError,
559+
request_id: Option<u64>,
560+
}
561+
562+
impl From<WebsocketError> for RequestError {
563+
fn from(error: WebsocketError) -> Self {
564+
Self {
565+
error,
566+
request_id: None,
567+
}
568+
}
569+
}
570+
533571
async fn handle_msg(
534572
socket: &mut WebSocket,
535573
state: &ApiState,
536574
player: &entities::user::Model,
537575
connection_id: ConnectionId,
538576
msg: Result<Message, axum::Error>,
539-
) -> Result<(), WebsocketError> {
540-
let msg = msg?;
577+
) -> Result<(), RequestError> {
578+
let msg = msg.map_err(WebsocketError::from)?;
541579

542580
// Ignore control/keepalive frames. Ping/Pong carry an opaque payload (Ktor
543581
// sends a Ping every pingInterval) that is not a serializable request, and
@@ -548,7 +586,20 @@ async fn handle_msg(
548586

549587
let parsed = serde_json::from_slice::<ServerBoundPacket>(&msg.into_data())
550588
.map_err(WebsocketError::Deserialization)?;
589+
let request_id = parsed.request_id();
551590

591+
handle_packet(socket, state, player, connection_id, parsed)
592+
.await
593+
.map_err(|error| RequestError { error, request_id })
594+
}
595+
596+
async fn handle_packet(
597+
socket: &mut WebSocket,
598+
state: &ApiState,
599+
player: &entities::user::Model,
600+
connection_id: ConnectionId,
601+
parsed: ServerBoundPacket,
602+
) -> Result<(), WebsocketError> {
552603
match parsed {
553604
ServerBoundPacket::GetActiveCosmetics { players } => {
554605
enforce_max_players_per_request(&players)?;
@@ -560,9 +611,12 @@ async fn handle_msg(
560611
)
561612
.await?;
562613
}
563-
ServerBoundPacket::SubscribePlayers { players } => {
614+
ServerBoundPacket::SubscribePlayers {
615+
players,
616+
request_id,
617+
} => {
564618
enforce_max_players_per_request(&players)?;
565-
let snapshot = subscribe(state, connection_id, players).await?;
619+
let snapshot = subscribe(state, connection_id, players, request_id).await?;
566620
send_packet(socket, snapshot).await?;
567621
}
568622
ServerBoundPacket::UnsubscribePlayers { players } => {
@@ -670,8 +724,14 @@ async fn endpoint(
670724
let equipped = match load_equipped(&state, player.id).await {
671725
Ok(equipped) => equipped,
672726
Err(error) => {
673-
let _ =
674-
send_packet(&mut socket, ClientBoundPacket::Error { error }).await;
727+
let _ = send_packet(
728+
&mut socket,
729+
ClientBoundPacket::Error {
730+
error,
731+
request_id: None,
732+
},
733+
)
734+
.await;
675735
return;
676736
}
677737
};
@@ -697,15 +757,18 @@ async fn endpoint(
697757
let Some(packet) = packet else {
698758
break;
699759
};
700-
send_packet(&mut socket, packet).await
760+
send_packet(&mut socket, packet).await.map_err(RequestError::from)
701761
}
702762
};
703763

704764
match result {
705765
Ok(_) => continue,
706-
Err(WebsocketError::Fatal(_)) => break,
707-
Err(e) => {
708-
let e = ClientBoundPacket::Error { error: e };
766+
Err(RequestError {
767+
error: WebsocketError::Fatal(_),
768+
..
769+
}) => break,
770+
Err(RequestError { error, request_id }) => {
771+
let e = ClientBoundPacket::Error { error, request_id };
709772
if send_packet(&mut socket, e).await.is_err() {
710773
break;
711774
};

src/api/websocket/structs.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ pub enum WebsocketError {
2323
UnownedEmote(i32),
2424
#[error("Too many players in one request (max {limit})")]
2525
TooManyPlayersInRequest { limit: usize },
26-
#[error("Too many player subscriptions (max {limit})")]
27-
SubscriptionLimitExceeded { limit: usize },
2826
}
2927

3028
impl WebsocketError {
@@ -37,8 +35,7 @@ impl WebsocketError {
3735
Self::DatabaseQuery(_) | Self::Serialization(_) => Self::ERROR_CODES[1],
3836
Self::Deserialization(_)
3937
| Self::InvalidSlot { .. }
40-
| Self::TooManyPlayersInRequest { .. }
41-
| Self::SubscriptionLimitExceeded { .. } => Self::ERROR_CODES[2],
38+
| Self::TooManyPlayersInRequest { .. } => Self::ERROR_CODES[2],
4239
Self::UnownedCosmetic(_) | Self::UnownedEmote(_) => Self::ERROR_CODES[3],
4340
}
4441
}
@@ -92,6 +89,8 @@ pub enum ServerBoundPacket {
9289
SubscribePlayers {
9390
/// Player UUIDs to watch. Capped per request and per connection total.
9491
players: Vec<Uuid>,
92+
#[serde(default)]
93+
request_id: Option<u64>,
9594
},
9695
UnsubscribePlayers {
9796
players: Vec<Uuid>,
@@ -133,6 +132,8 @@ pub enum ClientBoundPacket {
133132
/// The subset of subscribed players that currently have a live PolyPlus
134133
/// session connected. Used to render a "uses PolyPlus" indicator.
135134
users: Vec<Uuid>,
135+
rejected: Vec<Uuid>,
136+
request_id: Option<u64>,
136137
},
137138
/// A subscribed player's PolyPlus session came online or went offline.
138139
PlayerPresence {
@@ -166,9 +167,19 @@ pub enum ClientBoundPacket {
166167
Error {
167168
#[serde(flatten)]
168169
error: WebsocketError,
170+
request_id: Option<u64>,
169171
},
170172
}
171173

174+
impl ServerBoundPacket {
175+
pub fn request_id(&self) -> Option<u64> {
176+
match self {
177+
Self::SubscribePlayers { request_id, .. } => *request_id,
178+
_ => None,
179+
}
180+
}
181+
}
182+
172183
#[cfg(test)]
173184
mod tests {
174185
use std::collections::HashMap;
@@ -203,13 +214,28 @@ mod tests {
203214
.expect("packet should parse");
204215

205216
match packet {
206-
ServerBoundPacket::SubscribePlayers { players } => {
217+
ServerBoundPacket::SubscribePlayers {
218+
players,
219+
request_id,
220+
} => {
207221
assert_eq!(players, vec![player]);
222+
assert_eq!(request_id, None);
208223
}
209224
_ => panic!("unexpected packet variant"),
210225
}
211226
}
212227

228+
#[test]
229+
fn parses_player_subscription_with_request_id() {
230+
let player = Uuid::nil();
231+
let packet: ServerBoundPacket = serde_json::from_str(&format!(
232+
r#"{{"type":"SubscribePlayers","players":["{player}"],"request_id":7}}"#
233+
))
234+
.expect("packet should parse");
235+
236+
assert_eq!(packet.request_id(), Some(7));
237+
}
238+
213239
#[test]
214240
fn serializes_subscription_snapshot_packet() {
215241
let player = Uuid::nil();
@@ -218,12 +244,29 @@ mod tests {
218244
active_emotes: HashMap::from([(player, 6)]),
219245
particle_colors: HashMap::from([(player, 0xFF_0000)]),
220246
users: vec![player],
247+
rejected: Vec::new(),
248+
request_id: Some(3),
221249
};
222250

223251
let serialized = serde_json::to_value(packet).expect("packet should serialize");
224252
assert_eq!(serialized["type"], "SubscriptionSnapshot");
225253
assert_eq!(serialized["equipped"][player.to_string()]["cape"], 1);
226254
assert_eq!(serialized["users"][0], player.to_string());
255+
assert_eq!(serialized["rejected"].as_array().map(Vec::len), Some(0));
256+
assert_eq!(serialized["request_id"], 3);
257+
}
258+
259+
#[test]
260+
fn serializes_error_packet_with_request_id() {
261+
let packet = ClientBoundPacket::Error {
262+
error: super::WebsocketError::TooManyPlayersInRequest { limit: 64 },
263+
request_id: Some(9),
264+
};
265+
266+
let serialized = serde_json::to_value(packet).expect("packet should serialize");
267+
assert_eq!(serialized["type"], "Error");
268+
assert_eq!(serialized["error_code"], "bad_request");
269+
assert_eq!(serialized["request_id"], 9);
227270
}
228271

229272
#[test]

0 commit comments

Comments
 (0)