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