valence_server/
spawn.rs

1//! Handles spawning and respawning the client.
2
3use std::borrow::Cow;
4use std::collections::BTreeSet;
5
6use bevy_ecs::prelude::*;
7use bevy_ecs::query::QueryData;
8use derive_more::{Deref, DerefMut};
9use valence_entity::EntityLayerId;
10use valence_protocol::packets::play::{GameJoinS2c, PlayerRespawnS2c, PlayerSpawnPositionS2c};
11use valence_protocol::{BlockPos, GameMode, GlobalPos, Ident, VarInt, WritePacket};
12use valence_registry::tags::TagsRegistry;
13use valence_registry::{BiomeRegistry, RegistryCodec};
14
15use crate::client::{Client, ViewDistance, VisibleChunkLayer};
16use crate::layer::ChunkLayer;
17
18// Components for the join game and respawn packet.
19
20#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
21pub struct DeathLocation(pub Option<(Ident<String>, BlockPos)>);
22
23#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
24pub struct IsHardcore(pub bool);
25
26/// Hashed world seed used for biome noise.
27#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
28pub struct HashedSeed(pub u64);
29
30#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
31pub struct ReducedDebugInfo(pub bool);
32
33#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Deref, DerefMut)]
34pub struct HasRespawnScreen(pub bool);
35
36/// If the client is spawning into a debug world.
37#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
38pub struct IsDebug(pub bool);
39
40/// Changes the perceived horizon line (used for superflat worlds).
41#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
42pub struct IsFlat(pub bool);
43
44#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
45pub struct PortalCooldown(pub i32);
46
47/// The initial previous gamemode. Used for the F3+F4 gamemode switcher.
48#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug, Deref, DerefMut)]
49pub struct PrevGameMode(pub Option<GameMode>);
50
51impl Default for HasRespawnScreen {
52    fn default() -> Self {
53        Self(true)
54    }
55}
56
57/// The position and angle that clients will respawn with. Also
58/// controls the position that compasses point towards.
59#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
60pub struct RespawnPosition {
61    /// The position that clients will respawn at. This can be changed at any
62    /// time to set the position that compasses point towards.
63    pub pos: BlockPos,
64    /// The yaw angle that clients will respawn with (in degrees).
65    pub yaw: f32,
66}
67
68/// A convenient [`QueryData`] for obtaining client spawn components. Also see
69/// [`ClientSpawnQueryReadOnly`].
70#[derive(QueryData)]
71#[query_data(mutable)]
72pub struct ClientSpawnQuery {
73    pub is_hardcore: &'static mut IsHardcore,
74    pub game_mode: &'static mut GameMode,
75    pub prev_game_mode: &'static mut PrevGameMode,
76    pub hashed_seed: &'static mut HashedSeed,
77    pub view_distance: &'static mut ViewDistance,
78    pub reduced_debug_info: &'static mut ReducedDebugInfo,
79    pub has_respawn_screen: &'static mut HasRespawnScreen,
80    pub is_debug: &'static mut IsDebug,
81    pub is_flat: &'static mut IsFlat,
82    pub death_loc: &'static mut DeathLocation,
83    pub portal_cooldown: &'static mut PortalCooldown,
84}
85
86pub(super) fn initial_join(
87    codec: Res<RegistryCodec>,
88    tags: Res<TagsRegistry>,
89    mut clients: Query<(&mut Client, &VisibleChunkLayer, ClientSpawnQueryReadOnly), Added<Client>>,
90    chunk_layers: Query<&ChunkLayer>,
91) {
92    for (mut client, visible_chunk_layer, spawn) in &mut clients {
93        let Ok(chunk_layer) = chunk_layers.get(visible_chunk_layer.0) else {
94            continue;
95        };
96
97        let dimension_names: BTreeSet<Ident<Cow<str>>> = codec
98            .registry(BiomeRegistry::KEY)
99            .iter()
100            .map(|value| value.name.as_str_ident().into())
101            .collect();
102
103        let dimension_name: Ident<Cow<str>> = chunk_layer.dimension_type_name().into();
104
105        let last_death_location = spawn.death_loc.0.as_ref().map(|(id, pos)| GlobalPos {
106            dimension_name: id.as_str_ident().into(),
107            position: *pos,
108        });
109
110        // The login packet is prepended so that it's sent before all the other packets.
111        // Some packets don't work correctly when sent before the game join packet.
112        _ = client.enc.prepend_packet(&GameJoinS2c {
113            entity_id: 0, // We reserve ID 0 for clients.
114            is_hardcore: spawn.is_hardcore.0,
115            game_mode: *spawn.game_mode,
116            previous_game_mode: spawn.prev_game_mode.0.into(),
117            dimension_names: Cow::Owned(dimension_names),
118            registry_codec: Cow::Borrowed(codec.cached_codec()),
119            dimension_type_name: dimension_name.clone(),
120            dimension_name,
121            hashed_seed: spawn.hashed_seed.0 as i64,
122            max_players: VarInt(0), // Ignored by clients.
123            view_distance: VarInt(i32::from(spawn.view_distance.get())),
124            simulation_distance: VarInt(16), // TODO.
125            reduced_debug_info: spawn.reduced_debug_info.0,
126            enable_respawn_screen: spawn.has_respawn_screen.0,
127            is_debug: spawn.is_debug.0,
128            is_flat: spawn.is_flat.0,
129            last_death_location,
130            portal_cooldown: VarInt(spawn.portal_cooldown.0),
131        });
132
133        client.write_packet_bytes(tags.sync_tags_packet());
134
135        /*
136        // TODO: enable all the features?
137        q.client.write_packet(&FeatureFlags {
138            features: vec![Ident::new("vanilla").unwrap()],
139        })?;
140        */
141    }
142}
143
144pub(super) fn respawn(
145    mut clients: Query<
146        (
147            &mut Client,
148            &EntityLayerId,
149            &DeathLocation,
150            &HashedSeed,
151            &GameMode,
152            &PrevGameMode,
153            &IsDebug,
154            &IsFlat,
155        ),
156        Changed<VisibleChunkLayer>,
157    >,
158    chunk_layers: Query<&ChunkLayer>,
159) {
160    for (mut client, loc, death_loc, hashed_seed, game_mode, prev_game_mode, is_debug, is_flat) in
161        &mut clients
162    {
163        if client.is_added() {
164            // No need to respawn since we are sending the game join packet this tick.
165            continue;
166        }
167
168        let Ok(chunk_layer) = chunk_layers.get(loc.0) else {
169            continue;
170        };
171
172        let dimension_name = chunk_layer.dimension_type_name();
173
174        let last_death_location = death_loc.0.as_ref().map(|(id, pos)| GlobalPos {
175            dimension_name: id.as_str_ident().into(),
176            position: *pos,
177        });
178
179        client.write_packet(&PlayerRespawnS2c {
180            dimension_type_name: dimension_name.into(),
181            dimension_name: dimension_name.into(),
182            hashed_seed: hashed_seed.0,
183            game_mode: *game_mode,
184            previous_game_mode: prev_game_mode.0.into(),
185            is_debug: is_debug.0,
186            is_flat: is_flat.0,
187            copy_metadata: true,
188            last_death_location,
189            portal_cooldown: VarInt(0), // TODO
190        });
191    }
192}
193
194/// Sets the client's respawn and compass position.
195///
196/// This also closes the "downloading terrain" screen when first joining, so
197/// it should happen after the initial chunks are written.
198pub(super) fn update_respawn_position(
199    mut clients: Query<(&mut Client, &RespawnPosition), Changed<RespawnPosition>>,
200) {
201    for (mut client, respawn_pos) in &mut clients {
202        client.write_packet(&PlayerSpawnPositionS2c {
203            position: respawn_pos.pos,
204            angle: respawn_pos.yaw,
205        });
206    }
207}