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