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