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