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 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 pub day_length: f64,
195 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 #[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 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 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 pub fn singleplayer(path: &Path) -> Self {
302 let load = Self::load(path);
303 Self {
304 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 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 }
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 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
373const MIGRATION_UPGRADE_GUARANTEE: &str = "Any valid file of an old verison should be able to \
383 successfully migrate to the latest version.";
384
385#[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 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}