Skip to main content

veloren_server/sys/msg/
character_screen.rs

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