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