veloren_server/sys/msg/
character_screen.rs

1#[cfg(not(feature = "worldgen"))]
2use crate::test_world::World;
3#[cfg(feature = "worldgen")]
4use world::{IndexOwned, World};
5
6use crate::{
7    EditableSettings,
8    automod::AutoMod,
9    character_creator,
10    client::Client,
11    persistence::{character_loader::CharacterLoader, character_updater::CharacterUpdater},
12};
13#[cfg(feature = "worldgen")]
14use common::terrain::TerrainChunkSize;
15use common::{
16    comp::{Admin, AdminRole, ChatType, Content, Player, Presence, Waypoint},
17    event::{
18        ChatEvent, ClientDisconnectEvent, DeleteCharacterEvent, EmitExt, InitializeCharacterEvent,
19        InitializeSpectatorEvent,
20    },
21    event_emitters,
22    resources::Time,
23    uid::Uid,
24};
25use common_ecs::{Job, Origin, Phase, System};
26use common_net::msg::{ClientGeneral, ServerGeneral};
27use specs::{
28    Entities, Join, ReadExpect, ReadStorage, SystemData, WriteExpect, WriteStorage, shred,
29};
30use std::sync::{Arc, atomic::Ordering};
31use tracing::debug;
32
33event_emitters! {
34    struct Events[Emitters] {
35        init_spectator: InitializeSpectatorEvent,
36        init_character_data: InitializeCharacterEvent,
37        delete_character: DeleteCharacterEvent,
38        client_disconnect: ClientDisconnectEvent,
39        chat: ChatEvent,
40    }
41}
42
43impl Sys {
44    #[cfg_attr(feature = "worldgen", expect(clippy::too_many_arguments))] // Shhhh, go bother someone else clippy
45    fn handle_client_character_screen_msg(
46        emitters: &mut Emitters,
47        entity: specs::Entity,
48        client: &Client,
49        character_loader: &ReadExpect<'_, CharacterLoader>,
50        character_updater: &mut WriteExpect<'_, CharacterUpdater>,
51        uids: &ReadStorage<'_, Uid>,
52        players: &ReadStorage<'_, Player>,
53        admins: &ReadStorage<'_, Admin>,
54        presences: &ReadStorage<'_, Presence>,
55        editable_settings: &ReadExpect<'_, EditableSettings>,
56        censor: &ReadExpect<'_, Arc<censor::Censor>>,
57        automod: &AutoMod,
58        msg: ClientGeneral,
59        time: Time,
60        #[cfg(feature = "worldgen")] index: &ReadExpect<'_, IndexOwned>,
61        world: &ReadExpect<'_, Arc<World>>,
62    ) -> Result<(), crate::error::Error> {
63        let mut send_join_messages = || -> Result<(), crate::error::Error> {
64            // Give the player a welcome message
65            let localized_description = editable_settings
66                .server_description
67                .get(client.locale.as_deref());
68            if !localized_description.is_none_or(|d| d.motd.is_empty()) {
69                client.send(ServerGeneral::server_msg(
70                    ChatType::CommandInfo,
71                    localized_description.map_or(Content::Plain("".to_string()), |d| {
72                        Content::Plain(d.motd.to_owned())
73                    }),
74                ))?;
75            }
76
77            // Warn them about automod
78            if automod.enabled() {
79                client.send(ServerGeneral::server_msg(
80                    ChatType::CommandInfo,
81                    Content::Plain(
82                        "Automatic moderation is enabled: play nice and have fun!".to_string(),
83                    ),
84                ))?;
85            }
86
87            if client.client_type.emit_login_events()
88                && !client.login_msg_sent.load(Ordering::Relaxed)
89                && let Some(player_uid) = uids.get(entity)
90            {
91                emitters.emit(ChatEvent {
92                    msg: ChatType::Online(*player_uid).into_plain_msg(""),
93                    from_client: false,
94                });
95
96                client.login_msg_sent.store(true, Ordering::Relaxed);
97            }
98            Ok(())
99        };
100        match msg {
101            // Request spectator state
102            ClientGeneral::Spectate(requested_view_distances) => {
103                if let Some(admin) = admins.get(entity)
104                    && admin.0 >= AdminRole::Moderator
105                {
106                    send_join_messages()?;
107
108                    emitters.emit(InitializeSpectatorEvent(entity, requested_view_distances));
109                } else {
110                    debug!("dropped Spectate msg from unprivileged client")
111                }
112            },
113            ClientGeneral::Character(character_id, requested_view_distances) => {
114                if let Some(player) = players.get(entity) {
115                    // NOTE: Because clients retain their Uid when exiting to the character
116                    // selection screen, we rely on this check to prevent them from immediately
117                    // re-entering in-game in the same tick so that we can synchronize their
118                    // removal without this being mixed up (and e.g. telling other clients to
119                    // delete the re-joined version) or requiring more complex handling to avoid
120                    // this.
121                    if presences.contains(entity) {
122                        debug!("player already ingame, aborting");
123                    } else if character_updater.has_pending_database_action(character_id) {
124                        debug!("player recently logged out pending persistence, aborting");
125                        client.send(ServerGeneral::CharacterDataLoadResult(Err(
126                            "You have recently logged out, please wait a few seconds and try again"
127                                .to_string(),
128                        )))?;
129                    } else if character_updater.disconnect_all_clients_requested() {
130                        // If we're in the middle of disconnecting all clients due to a persistence
131                        // transaction failure, prevent new logins
132                        // temporarily.
133                        debug!(
134                            "Rejecting player login while pending disconnection of all players is \
135                             in progress"
136                        );
137                        client.send(ServerGeneral::CharacterDataLoadResult(Err(
138                            "The server is currently recovering from an error, please wait a few \
139                             seconds and try again"
140                                .to_string(),
141                        )))?;
142                    } else {
143                        // Send a request to load the character's component data from the
144                        // DB. Once loaded, persisted components such as stats and inventory
145                        // will be inserted for the entity
146                        character_loader.load_character_data(
147                            entity,
148                            player.uuid().to_string(),
149                            character_id,
150                        );
151
152                        send_join_messages()?;
153
154                        // Start inserting non-persisted/default components for the entity
155                        // while we load the DB data
156                        emitters.emit(InitializeCharacterEvent {
157                            entity,
158                            character_id,
159                            requested_view_distances,
160                        });
161                    }
162                } else {
163                    debug!("Client is not yet registered");
164                    client.send(ServerGeneral::CharacterDataLoadResult(Err(String::from(
165                        "Failed to fetch player entity",
166                    ))))?
167                }
168            },
169            ClientGeneral::RequestCharacterList => {
170                if let Some(player) = players.get(entity) {
171                    character_loader.load_character_list(entity, player.uuid().to_string())
172                }
173            },
174            ClientGeneral::CreateCharacter {
175                alias,
176                mainhand,
177                offhand,
178                body,
179                hardcore,
180                #[cfg(feature = "worldgen")]
181                start_site,
182                #[cfg(not(feature = "worldgen"))]
183                    start_site: _,
184            } => {
185                if censor.check(&alias) {
186                    debug!(?alias, "denied alias as it contained a banned word");
187                    client.send(ServerGeneral::CharacterActionError(format!(
188                        "Alias '{}' contains a banned word",
189                        alias
190                    )))?;
191                } else if let Some(player) = players.get(entity) {
192                    #[cfg(feature = "worldgen")]
193                    let waypoint = start_site.and_then(|site_idx| {
194                        // TODO: This corresponds to the ID generation logic in
195                        // `world/src/lib.rs`. Really, we should have
196                        // a way to consistently refer to sites, but that's a job for rtsim2
197                        // and the site changes that it will require. Until then, this code is
198                        // very hacky.
199                        world
200                            .civs()
201                            .sites
202                            .iter()
203                            .find(|(_, site)| site.site_tmp.map(|i| i.id()) == Some(site_idx))
204                            .map(Some)
205                            .unwrap_or_else(|| {
206                                tracing::error!(
207                                    "Tried to create character with starting site index {}, but \
208                                     such a site does not exist",
209                                    site_idx
210                                );
211                                None
212                            })
213                            .map(|(_, site)| {
214                                let wpos2d = TerrainChunkSize::center_wpos(site.center);
215                                Waypoint::new(
216                                    world.find_accessible_pos(index.as_index_ref(), wpos2d, false),
217                                    time,
218                                )
219                            })
220                    });
221                    #[cfg(not(feature = "worldgen"))]
222                    let waypoint = Some(Waypoint::new(world.get_center().with_z(10).as_(), time));
223                    if let Err(error) = character_creator::create_character(
224                        entity,
225                        player.uuid().to_string(),
226                        alias,
227                        mainhand.clone(),
228                        offhand.clone(),
229                        body,
230                        hardcore,
231                        character_updater,
232                        waypoint,
233                    ) {
234                        debug!(
235                            ?error,
236                            ?mainhand,
237                            ?offhand,
238                            ?body,
239                            "Denied creating character because of invalid input."
240                        );
241                        client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
242                    }
243                }
244            },
245            ClientGeneral::EditCharacter { id, alias, body } => {
246                if censor.check(&alias) {
247                    debug!(?alias, "denied alias as it contained a banned word");
248                    client.send(ServerGeneral::CharacterActionError(format!(
249                        "Alias '{}' contains a banned word",
250                        alias
251                    )))?;
252                } else if let Some(player) = players.get(entity)
253                    && let Err(error) = character_creator::edit_character(
254                        entity,
255                        player.uuid().to_string(),
256                        id,
257                        alias,
258                        body,
259                        character_updater,
260                    )
261                {
262                    debug!(
263                        ?error,
264                        ?body,
265                        "Denied editing character because of invalid input."
266                    );
267                    client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
268                }
269            },
270            ClientGeneral::DeleteCharacter(character_id) => {
271                if let Some(player) = players.get(entity) {
272                    emitters.emit(DeleteCharacterEvent {
273                        entity,
274                        requesting_player_uuid: player.uuid().to_string(),
275                        character_id,
276                    });
277                }
278            },
279            _ => {
280                debug!("Kicking possibly misbehaving client due to invalid character request");
281                emitters.emit(ClientDisconnectEvent(
282                    entity,
283                    common::comp::DisconnectReason::NetworkError,
284                ));
285            },
286        }
287        Ok(())
288    }
289}
290
291#[derive(SystemData)]
292pub struct Data<'a> {
293    entities: Entities<'a>,
294    events: Events<'a>,
295    character_loader: ReadExpect<'a, CharacterLoader>,
296    character_updater: WriteExpect<'a, CharacterUpdater>,
297    uids: ReadStorage<'a, Uid>,
298    clients: WriteStorage<'a, Client>,
299    players: ReadStorage<'a, Player>,
300    admins: ReadStorage<'a, Admin>,
301    presences: ReadStorage<'a, Presence>,
302    editable_settings: ReadExpect<'a, EditableSettings>,
303    censor: ReadExpect<'a, Arc<censor::Censor>>,
304    automod: ReadExpect<'a, AutoMod>,
305    time: ReadExpect<'a, Time>,
306    #[cfg(feature = "worldgen")]
307    index: ReadExpect<'a, IndexOwned>,
308    world: ReadExpect<'a, Arc<World>>,
309}
310
311/// This system will handle new messages from clients
312#[derive(Default)]
313pub struct Sys;
314impl<'a> System<'a> for Sys {
315    type SystemData = Data<'a>;
316
317    const NAME: &'static str = "msg::character_screen";
318    const ORIGIN: Origin = Origin::Server;
319    const PHASE: Phase = Phase::Create;
320
321    fn run(_job: &mut Job<Self>, mut data: Self::SystemData) {
322        let mut emitters = data.events.get_emitters();
323
324        for (entity, client) in (&data.entities, &mut data.clients).join() {
325            let _ = super::try_recv_all(client, 1, |client, msg| {
326                Self::handle_client_character_screen_msg(
327                    &mut emitters,
328                    entity,
329                    client,
330                    &data.character_loader,
331                    &mut data.character_updater,
332                    &data.uids,
333                    &data.players,
334                    &data.admins,
335                    &data.presences,
336                    &data.editable_settings,
337                    &data.censor,
338                    &data.automod,
339                    msg,
340                    *data.time,
341                    #[cfg(feature = "worldgen")]
342                    &data.index,
343                    &data.world,
344                )
345            });
346        }
347    }
348}