veloren_server/settings/
mod.rs

1pub mod admin;
2pub mod banlist;
3mod editable;
4pub mod server_description;
5pub mod server_physics;
6pub mod whitelist;
7
8pub use editable::{EditableSetting, Error as SettingError};
9
10pub use admin::{AdminRecord, Admins};
11pub use banlist::{
12    Ban, BanEntry, BanError, BanErrorKind, BanInfo, BanKind, BanOperation, BanOperationError,
13    BanRecord, Banlist,
14};
15pub use server_description::ServerDescriptions;
16pub use whitelist::{Whitelist, WhitelistInfo, WhitelistRecord};
17
18use chrono::Utc;
19use common::{
20    calendar::{Calendar, CalendarEvent},
21    consts::DAY_LENGTH_DEFAULT,
22    resources::BattleMode,
23    rtsim::WorldSettings,
24};
25use core::time::Duration;
26use portpicker::pick_unused_port;
27use serde::{Deserialize, Serialize};
28use std::{
29    fmt::Display,
30    fs,
31    net::{Ipv4Addr, Ipv6Addr, SocketAddr},
32    path::{Path, PathBuf},
33};
34use tracing::{error, warn};
35use world::sim::{DEFAULT_WORLD_SEED, FileOpts};
36
37use self::server_description::ServerDescription;
38
39use self::server_physics::ServerPhysicsForceList;
40
41const CONFIG_DIR: &str = "server_config";
42const SETTINGS_FILENAME: &str = "settings.ron";
43const WHITELIST_FILENAME: &str = "whitelist.ron";
44const BANLIST_FILENAME: &str = "banlist.ron";
45const SERVER_DESCRIPTION_FILENAME: &str = "description.ron";
46const ADMINS_FILENAME: &str = "admins.ron";
47const SERVER_PHYSICS_FORCE_FILENAME: &str = "server_physics_force.ron";
48
49pub const SINGLEPLAYER_SERVER_NAME: &str = "Singleplayer";
50
51#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
52pub enum ServerBattleMode {
53    Global(BattleMode),
54    PerPlayer { default: BattleMode },
55}
56
57impl Default for ServerBattleMode {
58    fn default() -> Self { Self::Global(BattleMode::PvP) }
59}
60
61impl ServerBattleMode {
62    pub fn allow_choosing(&self) -> bool {
63        match self {
64            ServerBattleMode::Global { .. } => false,
65            ServerBattleMode::PerPlayer { .. } => true,
66        }
67    }
68
69    pub fn default_mode(&self) -> BattleMode {
70        match self {
71            ServerBattleMode::Global(mode) => *mode,
72            ServerBattleMode::PerPlayer { default: mode } => *mode,
73        }
74    }
75}
76
77impl From<ServerBattleMode> for veloren_query_server::proto::ServerBattleMode {
78    fn from(value: ServerBattleMode) -> Self {
79        use veloren_query_server::proto::ServerBattleMode as QueryBattleMode;
80
81        match value {
82            ServerBattleMode::Global(mode) => match mode {
83                BattleMode::PvP => QueryBattleMode::GlobalPvP,
84                BattleMode::PvE => QueryBattleMode::GlobalPvE,
85            },
86            ServerBattleMode::PerPlayer { .. } => QueryBattleMode::PerPlayer,
87        }
88    }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum Protocol {
93    Quic {
94        address: SocketAddr,
95        cert_file_path: PathBuf,
96        key_file_path: PathBuf,
97    },
98    Tcp {
99        address: SocketAddr,
100    },
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct GameplaySettings {
105    #[serde(default)]
106    pub battle_mode: ServerBattleMode,
107    #[serde(default)]
108    // explosion_burn_marks by players
109    pub explosion_burn_marks: bool,
110}
111
112impl Default for GameplaySettings {
113    fn default() -> Self {
114        Self {
115            battle_mode: ServerBattleMode::default(),
116            explosion_burn_marks: true,
117        }
118    }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
122pub struct ModerationSettings {
123    #[serde(default)]
124    pub banned_words_files: Vec<PathBuf>,
125    #[serde(default)]
126    pub automod: bool,
127    #[serde(default)]
128    pub admins_exempt: bool,
129}
130
131impl ModerationSettings {
132    pub fn load_banned_words(&self, data_dir: &Path) -> Vec<String> {
133        let mut banned_words = Vec::new();
134        for fname in self.banned_words_files.iter() {
135            let mut path = with_config_dir(data_dir);
136            path.push(fname);
137            match std::fs::File::open(&path) {
138                Ok(file) => match ron::de::from_reader(&file) {
139                    Ok(mut words) => banned_words.append(&mut words),
140                    Err(error) => error!(?error, ?file, "Couldn't read banned words file"),
141                },
142                Err(error) => error!(?error, ?path, "Couldn't open banned words file"),
143            }
144        }
145        banned_words
146    }
147}
148
149impl Default for ModerationSettings {
150    fn default() -> Self {
151        Self {
152            banned_words_files: Vec::new(),
153            automod: false,
154            admins_exempt: true,
155        }
156    }
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize)]
160pub enum CalendarMode {
161    None,
162    Auto,
163    Timezone(chrono_tz::Tz),
164    Events(Vec<CalendarEvent>),
165}
166
167impl Default for CalendarMode {
168    fn default() -> Self { Self::Auto }
169}
170
171impl CalendarMode {
172    pub fn calendar_now(&self) -> Calendar {
173        match self {
174            CalendarMode::None => Calendar::default(),
175            CalendarMode::Auto => Calendar::from_tz(None),
176            CalendarMode::Timezone(tz) => Calendar::from_tz(Some(*tz)),
177            CalendarMode::Events(events) => Calendar::from_events(events.clone()),
178        }
179    }
180}
181
182#[derive(Clone, Debug, Serialize, Deserialize)]
183#[serde(default)]
184pub struct Settings {
185    pub gameserver_protocols: Vec<Protocol>,
186    pub auth_server_address: Option<String>,
187    pub query_address: Option<SocketAddr>,
188    pub max_players: u16,
189    pub world_seed: u32,
190    pub server_name: String,
191    pub start_time: f64,
192    /// Length of a day in minutes.
193    pub day_length: f64,
194    /// When set to None, loads the default map file (if available); otherwise,
195    /// uses the value of the file options to decide how to proceed.
196    pub map_file: Option<FileOpts>,
197    pub max_view_distance: Option<u32>,
198    pub max_player_group_size: u32,
199    pub client_timeout: Duration,
200    pub max_player_for_kill_broadcast: Option<usize>,
201    pub calendar_mode: CalendarMode,
202
203    /// Experimental feature. No guaranteed forwards-compatibility, may be
204    /// removed at *any time* with no migration.
205    #[serde(default, skip_serializing)]
206    pub experimental_terrain_persistence: bool,
207
208    #[serde(default)]
209    pub gameplay: GameplaySettings,
210    #[serde(default)]
211    pub moderation: ModerationSettings,
212
213    #[serde(default)]
214    pub world: WorldSettings,
215}
216
217impl Default for Settings {
218    fn default() -> Self {
219        Self {
220            gameserver_protocols: vec![
221                Protocol::Tcp {
222                    address: SocketAddr::from((Ipv6Addr::UNSPECIFIED, 14004)),
223                },
224                Protocol::Tcp {
225                    address: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14004)),
226                },
227            ],
228            auth_server_address: Some("https://auth.veloren.net".into()),
229            query_address: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14006))),
230            world_seed: DEFAULT_WORLD_SEED,
231            server_name: "Veloren Server".into(),
232            max_players: 100,
233            day_length: DAY_LENGTH_DEFAULT,
234            start_time: 9.0 * 3600.0,
235            map_file: None,
236            max_view_distance: Some(65),
237            max_player_group_size: 6,
238            calendar_mode: CalendarMode::Auto,
239            client_timeout: Duration::from_secs(40),
240            max_player_for_kill_broadcast: None,
241            experimental_terrain_persistence: false,
242            gameplay: GameplaySettings::default(),
243            moderation: ModerationSettings::default(),
244            world: WorldSettings::default(),
245        }
246    }
247}
248
249impl Settings {
250    /// path: Directory that contains the server config directory
251    pub fn load(path: &Path) -> Self {
252        let path = Self::get_settings_path(path);
253
254        let mut settings = if let Ok(file) = fs::File::open(&path) {
255            match ron::de::from_reader(file) {
256                Ok(x) => x,
257                Err(e) => {
258                    let default_settings = Self::default();
259                    let template_path = path.with_extension("template.ron");
260                    warn!(
261                        ?e,
262                        "Failed to parse setting file! Falling back to default settings and \
263                         creating a template file for you to migrate your current settings file: \
264                         {}",
265                        template_path.display()
266                    );
267                    if let Err(e) = default_settings.save_to_file(&template_path) {
268                        error!(?e, "Failed to create template settings file")
269                    }
270                    default_settings
271                },
272            }
273        } else {
274            let default_settings = Self::default();
275
276            if let Err(e) = default_settings.save_to_file(&path) {
277                error!(?e, "Failed to create default settings file!");
278            }
279            default_settings
280        };
281
282        settings.validate();
283        settings
284    }
285
286    fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
287        // Create dir if it doesn't exist
288        if let Some(dir) = path.parent() {
289            fs::create_dir_all(dir)?;
290        }
291        let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
292            .expect("Failed serialize settings.");
293
294        fs::write(path, ron.as_bytes())?;
295
296        Ok(())
297    }
298
299    /// path: Directory that contains the server config directory
300    pub fn singleplayer(path: &Path) -> Self {
301        let load = Self::load(path);
302        Self {
303            // BUG: theoretically another process can grab the port between here and server
304            // creation, however the time window is quite small.
305            gameserver_protocols: vec![Protocol::Tcp {
306                address: SocketAddr::from((
307                    Ipv4Addr::LOCALHOST,
308                    pick_unused_port().expect("Failed to find unused port!"),
309                )),
310            }],
311            auth_server_address: None,
312            // If loading the default map file, make sure the seed is also default.
313            world_seed: if load.map_file.is_some() {
314                load.world_seed
315            } else {
316                DEFAULT_WORLD_SEED
317            },
318            server_name: SINGLEPLAYER_SERVER_NAME.to_owned(),
319            max_players: 100,
320            max_view_distance: None,
321            client_timeout: Duration::from_secs(180),
322            ..load // Fill in remaining fields from server_settings.ron.
323        }
324    }
325
326    fn get_settings_path(path: &Path) -> PathBuf {
327        let mut path = with_config_dir(path);
328        path.push(SETTINGS_FILENAME);
329        path
330    }
331
332    fn validate(&mut self) {
333        const INVALID_SETTING_MSG: &str =
334            "Invalid value for setting in userdata/server/server_config/settings.ron.";
335
336        let default_values = Settings::default();
337
338        if self.day_length <= 0.0 {
339            warn!(
340                "{} Setting: day_length, Value: {}. Set day_length to it's default value of {}. \
341                 Help: day_length must be a positive floating point value above 0.",
342                INVALID_SETTING_MSG, self.day_length, default_values.day_length
343            );
344            self.day_length = default_values.day_length;
345        }
346    }
347
348    /// Derive a coefficient that is the relatively speed of the in-game
349    /// day/night cycle compared to reality.
350    pub fn day_cycle_coefficient(&self) -> f64 { 1440.0 / self.day_length }
351}
352
353pub enum InvalidSettingsError {
354    InvalidDayDuration,
355}
356impl Display for InvalidSettingsError {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        match self {
359            InvalidSettingsError::InvalidDayDuration => {
360                f.write_str("Invalid settings error: Day length was invalid (zero or negative).")
361            },
362        }
363    }
364}
365
366pub fn with_config_dir(path: &Path) -> PathBuf {
367    let mut path = PathBuf::from(path);
368    path.push(CONFIG_DIR);
369    path
370}
371
372/// Our upgrade guarantee is that if validation succeeds
373/// for an old version, then migration to the next version must always succeed
374/// and produce a valid settings file for that version (if we need to change
375/// this in the future, it should require careful discussion).  Therefore, we
376/// would normally panic if the upgrade produced an invalid settings file, which
377/// we would perform by doing the following post-validation (example
378/// is given for a hypothetical upgrade from Whitelist_V1 to Whitelist_V2):
379///
380/// Ok(Whitelist_V2::try_into().expect())
381const MIGRATION_UPGRADE_GUARANTEE: &str = "Any valid file of an old verison should be able to \
382                                           successfully migrate to the latest version.";
383
384/// Combines all the editable settings into one struct that is stored in the ecs
385#[derive(Clone)]
386pub struct EditableSettings {
387    pub whitelist: Whitelist,
388    pub banlist: Banlist,
389    pub server_description: ServerDescriptions,
390    pub admins: Admins,
391    pub server_physics_force_list: ServerPhysicsForceList,
392}
393
394impl EditableSettings {
395    pub fn load(data_dir: &Path) -> Self {
396        Self {
397            whitelist: Whitelist::load(data_dir),
398            banlist: Banlist::load(data_dir),
399            server_description: ServerDescriptions::load(data_dir),
400            admins: Admins::load(data_dir),
401            server_physics_force_list: ServerPhysicsForceList::load(data_dir),
402        }
403    }
404
405    pub fn singleplayer(data_dir: &Path) -> Self {
406        let load = Self::load(data_dir);
407
408        let mut server_description = ServerDescriptions::default();
409        server_description
410            .descriptions
411            .insert("en".to_string(), ServerDescription {
412                motd: "Who needs friends anyway?".to_string(),
413                rules: None,
414            });
415
416        let mut admins = Admins::default();
417        // TODO: Let the player choose if they want to use admin commands or not
418        admins.insert(
419            crate::login_provider::derive_singleplayer_uuid(),
420            AdminRecord {
421                username_when_admined: Some("singleplayer".into()),
422                date: Utc::now(),
423                role: admin::Role::Admin,
424            },
425        );
426
427        Self {
428            server_description,
429            admins,
430            ..load
431        }
432    }
433}