1
1
use std:: { borrow:: Cow , collections:: BTreeSet , ops:: Index } ;
2
2
3
- use anyhow:: Context ;
3
+ use anyhow:: { Context , bail } ;
4
4
use flecs_ecs:: prelude:: * ;
5
5
use glam:: DVec3 ;
6
6
use hyperion_crafting:: { Action , CraftingRegistry , RecipeBookState } ;
@@ -20,7 +20,7 @@ use valence_registry::{BiomeRegistry, RegistryCodec};
20
20
use valence_server:: entity:: EntityKind ;
21
21
use valence_text:: IntoText ;
22
22
23
- use crate :: simulation:: { MovementTracking , PacketState , Pitch } ;
23
+ use crate :: simulation:: { IgnMap , MovementTracking , PacketState , Pitch } ;
24
24
25
25
mod list;
26
26
pub use list:: * ;
@@ -43,12 +43,12 @@ use crate::{
43
43
clippy:: too_many_arguments,
44
44
reason = "todo: we should refactor at some point"
45
45
) ]
46
- #[ instrument( skip_all, fields( name = name) ) ]
46
+ #[ instrument( skip_all, fields( name = & * * * name) ) ]
47
47
pub fn player_join_world (
48
48
entity : & EntityView < ' _ > ,
49
49
compose : & Compose ,
50
50
uuid : uuid:: Uuid ,
51
- name : & str ,
51
+ name : & Name ,
52
52
io : ConnectionId ,
53
53
position : & Position ,
54
54
yaw : & Yaw ,
@@ -68,6 +68,7 @@ pub fn player_join_world(
68
68
) > ,
69
69
crafting_registry : & CraftingRegistry ,
70
70
config : & Config ,
71
+ ign_map : & IgnMap ,
71
72
) -> anyhow:: Result < ( ) > {
72
73
static CACHED_DATA : once_cell:: sync:: OnceCell < bytes:: Bytes > = once_cell:: sync:: OnceCell :: new ( ) ;
73
74
@@ -321,7 +322,7 @@ pub fn player_join_world(
321
322
. add_packet ( & pkt)
322
323
. context ( "failed to send player list packet" ) ?;
323
324
324
- let player_name = vec ! [ name] ;
325
+ let player_name = vec ! [ & * * * name] ;
325
326
326
327
compose
327
328
. broadcast (
@@ -373,6 +374,64 @@ pub fn player_join_world(
373
374
374
375
bundle. unicast ( io) ?;
375
376
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
+
376
435
info ! ( "{name} joined the world" ) ;
377
436
378
437
Ok ( ( ) )
@@ -552,6 +611,7 @@ impl Module for PlayerJoinModule {
552
611
& Compose ( $) ,
553
612
& CraftingRegistry ( $) ,
554
613
& Config ( $) ,
614
+ & IgnMap ( $) ,
555
615
& Uuid ,
556
616
& Name ,
557
617
& Position ,
@@ -570,6 +630,7 @@ impl Module for PlayerJoinModule {
570
630
compose,
571
631
crafting_registry,
572
632
config,
633
+ ign_map,
573
634
uuid,
574
635
name,
575
636
position,
@@ -602,6 +663,7 @@ impl Module for PlayerJoinModule {
602
663
& query,
603
664
crafting_registry,
604
665
config,
666
+ ign_map,
605
667
) {
606
668
warn ! ( "player_join_world error: {e:?}" ) ;
607
669
compose. io_buf ( ) . shutdown ( * stream_id, & world) ;
0 commit comments