Skip to content

Commit 5b5f594

Browse files
committed
fix: only allow one connection per account
Previously, if someone connected to the server twice with the same account, the server would abort because two entities cannot have the same name in flecs. In addition, having multiple players from the same account would not make sense. This code disconnects the previous player if a new player with the same name connects. This also modifies IgnMap to no longer use DeferredMap. Deferred insertions would allow for race conditions because two players with the same name could be inserted into the IgnMap with neither insertion call knowing that there's another player with the same name. The code also cannot rely on flecs to determine if there's another player with the same name using World::try_lookup because this would introduce a race condition between the try_lookup call and setting the entity name.
1 parent 05c1271 commit 5b5f594

File tree

3 files changed

+72
-28
lines changed

3 files changed

+72
-28
lines changed

crates/hyperion/src/egress/player_join/mod.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{borrow::Cow, collections::BTreeSet, ops::Index};
22

3-
use anyhow::Context;
3+
use anyhow::{Context, bail};
44
use flecs_ecs::prelude::*;
55
use glam::DVec3;
66
use hyperion_crafting::{Action, CraftingRegistry, RecipeBookState};
@@ -20,7 +20,7 @@ use valence_registry::{BiomeRegistry, RegistryCodec};
2020
use valence_server::entity::EntityKind;
2121
use valence_text::IntoText;
2222

23-
use crate::simulation::{MovementTracking, PacketState, Pitch};
23+
use crate::simulation::{IgnMap, MovementTracking, PacketState, Pitch};
2424

2525
mod list;
2626
pub use list::*;
@@ -43,12 +43,12 @@ use crate::{
4343
clippy::too_many_arguments,
4444
reason = "todo: we should refactor at some point"
4545
)]
46-
#[instrument(skip_all, fields(name = name))]
46+
#[instrument(skip_all, fields(name = &***name))]
4747
pub fn player_join_world(
4848
entity: &EntityView<'_>,
4949
compose: &Compose,
5050
uuid: uuid::Uuid,
51-
name: &str,
51+
name: &Name,
5252
io: ConnectionId,
5353
position: &Position,
5454
yaw: &Yaw,
@@ -68,6 +68,7 @@ pub fn player_join_world(
6868
)>,
6969
crafting_registry: &CraftingRegistry,
7070
config: &Config,
71+
ign_map: &IgnMap,
7172
) -> anyhow::Result<()> {
7273
static CACHED_DATA: once_cell::sync::OnceCell<bytes::Bytes> = once_cell::sync::OnceCell::new();
7374

@@ -321,7 +322,7 @@ pub fn player_join_world(
321322
.add_packet(&pkt)
322323
.context("failed to send player list packet")?;
323324

324-
let player_name = vec![name];
325+
let player_name = vec![&***name];
325326

326327
compose
327328
.broadcast(
@@ -373,6 +374,64 @@ pub fn player_join_world(
373374

374375
bundle.unicast(io)?;
375376

377+
// The player must be added to the ign map after all of its components have been set and ready
378+
// to receive play packets because other threads may attempt to process the player once it is
379+
// added to the ign map.
380+
if let Some(previous_player) = ign_map.insert((**name).clone(), entity.id()) {
381+
// Disconnect either this player or the previous player with the same username.
382+
// There are some Minecraft accounts with the same username, but this is an extremely
383+
// rare edge case which is not worth handling.
384+
let previous_player = previous_player.entity_view(world);
385+
386+
let pkt = play::DisconnectS2c {
387+
reason: "A different player with the same username as your account has joined on a \
388+
different device"
389+
.into_cow_text(),
390+
};
391+
392+
match previous_player.get_name() {
393+
None => {
394+
// previous_player must be getting processed in another thread in player_join_world
395+
// because it is in ign_map but does not have a name yet. To avoid having two
396+
// entities with the same name, which would cause flecs to abort, this code
397+
// disconnects the current player. In the worst-case scenario, both players may get
398+
// disconnected, which is okay because the players can reconnect.
399+
400+
warn!(
401+
"two players are simultanenously connecting with the same username '{name}'. \
402+
one player will be disconnected."
403+
);
404+
405+
compose.unicast(&pkt, io, system)?;
406+
compose.io_buf().shutdown(io, world);
407+
bail!("another player with the same username is joining");
408+
}
409+
Some(previous_player_name) => {
410+
// Kick the previous player with the same name. One player should only be able to connect
411+
// to the server one time simultaneously, so if the same player connects to this server
412+
// multiple times, the other connection should be disconnected. In general, this wants to
413+
// disconnect the older player connection because the alternative solution of repeatedly kicking
414+
// new player join attempts if an old player connection is somehow still alive would lead to bad
415+
// user experience.
416+
assert_eq!(previous_player_name, &***name);
417+
418+
warn!(
419+
"player {name} has joined with the same username of an already-connected \
420+
player. the previous player with the username will be disconnected."
421+
);
422+
423+
previous_player.remove_name();
424+
425+
let previous_stream_id = previous_player.get::<&ConnectionId>(|id| *id);
426+
427+
compose.unicast(&pkt, previous_stream_id, system)?;
428+
compose.io_buf().shutdown(previous_stream_id, world);
429+
}
430+
}
431+
}
432+
433+
entity.set_name(name);
434+
376435
info!("{name} joined the world");
377436

378437
Ok(())
@@ -552,6 +611,7 @@ impl Module for PlayerJoinModule {
552611
&Compose($),
553612
&CraftingRegistry($),
554613
&Config($),
614+
&IgnMap($),
555615
&Uuid,
556616
&Name,
557617
&Position,
@@ -570,6 +630,7 @@ impl Module for PlayerJoinModule {
570630
compose,
571631
crafting_registry,
572632
config,
633+
ign_map,
573634
uuid,
574635
name,
575636
position,
@@ -602,6 +663,7 @@ impl Module for PlayerJoinModule {
602663
&query,
603664
crafting_registry,
604665
config,
666+
ign_map,
605667
) {
606668
warn!("player_join_world error: {e:?}");
607669
compose.io_buf().shutdown(*stream_id, &world);

crates/hyperion/src/ingress/mod.rs

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ use crate::{
2424
},
2525
runtime::AsyncRuntime,
2626
simulation::{
27-
AiTargetable, ChunkPosition, Comms, ConfirmBlockSequences, EntitySize, IgnMap,
28-
ImmuneStatus, Name, PacketState, Pitch, Player, Position, StreamLookup, Uuid, Velocity, Xp,
29-
Yaw,
27+
AiTargetable, ChunkPosition, Comms, ConfirmBlockSequences, EntitySize, ImmuneStatus, Name,
28+
PacketState, Pitch, Player, Position, StreamLookup, Uuid, Velocity, Xp, Yaw,
3029
animation::ActiveAnimation,
3130
blocks::Blocks,
3231
handlers::PacketSwitchQuery,
@@ -81,7 +80,6 @@ fn process_login(
8180
compose: &Compose,
8281
entity: &EntityView<'_>,
8382
system: EntityView<'_>,
84-
ign_map: &IgnMap,
8583
) -> anyhow::Result<()> {
8684
debug_assert!(
8785
*login_state == PacketState::Login,
@@ -151,14 +149,12 @@ fn process_login(
151149
.unicast(&pkt, stream_id, system)
152150
.context("failed to send login success packet")?;
153151

154-
ign_map.insert(username.clone(), entity_id, world);
155-
156152
*login_state = PacketState::PendingPlay;
157153

158154
world.get::<&MetadataPrefabs>(|prefabs| {
159155
entity
160156
.is_a(prefabs.player_base)
161-
.set(Name::from(username))
157+
.set(Name::from(username.clone()))
162158
.add(id::<AiTargetable>())
163159
.set(ImmuneStatus::default())
164160
.set(Uuid::from(uuid))
@@ -275,18 +271,6 @@ impl Module for IngressModule {
275271
}
276272
});
277273

278-
system!(
279-
"update_ign_map",
280-
world,
281-
&mut IgnMap($),
282-
)
283-
.kind(id::<flecs::pipeline::OnLoad>())
284-
.each_iter(|_, _, ign_map| {
285-
let span = info_span!("update_ign_map");
286-
let _enter = span.enter();
287-
ign_map.update();
288-
});
289-
290274
system!(
291275
"generate_ingress_events",
292276
world,
@@ -422,7 +406,6 @@ impl Module for IngressModule {
422406
&mut hyperion_inventory::PlayerInventory,
423407
&mut ActiveAnimation,
424408
&hyperion_crafting::CraftingRegistry($),
425-
&IgnMap($),
426409
)
427410
.kind(id::<flecs::pipeline::OnUpdate>())
428411
.each_iter(
@@ -448,7 +431,6 @@ impl Module for IngressModule {
448431
inventory,
449432
animation,
450433
crafting_registry,
451-
ign_map,
452434
)| {
453435
let system = it.system();
454436
let world = it.world();
@@ -514,7 +496,6 @@ impl Module for IngressModule {
514496
compose,
515497
&entity,
516498
system,
517-
ign_map,
518499
) {
519500
error!("failed to process login packet: {e}");
520501
let msg = format!(

crates/hyperion/src/simulation/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{borrow::Borrow, collections::HashMap, hash::Hash, sync::Arc};
22

33
use bytemuck::{Pod, Zeroable};
4+
use dashmap::DashMap;
45
use derive_more::{Constructor, Deref, DerefMut, Display, From};
56
use flecs_ecs::prelude::*;
67
use geometry::aabb::Aabb;
@@ -115,7 +116,7 @@ impl<K: Eq + Hash, V> DeferredMap<K, V> {
115116
pub struct Name(Arc<str>);
116117

117118
#[derive(Component, Deref, DerefMut, From, Debug, Default)]
118-
pub struct IgnMap(DeferredMap<Arc<str>, Entity>);
119+
pub struct IgnMap(DashMap<Arc<str>, Entity>);
119120

120121
#[derive(Component, Debug, Default)]
121122
pub struct RaycastTravel;

0 commit comments

Comments
 (0)