flatland_client_lib/
client_config.rs1use std::net::SocketAddr;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_GAME_PORT: u16 = 7373;
9pub const DEFAULT_API_PORT: u16 = 7380;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct GfxWindowPrefs {
13 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub width: Option<u32>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub height: Option<u32>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub x: Option<u32>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub y: Option<u32>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
28pub struct ClientConfig {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub game_host: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub api_port: Option<u16>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub game_port: Option<u16>,
38 #[serde(default, skip_serializing_if = "GfxWindowPrefs::is_empty")]
40 pub gfx_window: GfxWindowPrefs,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub hud_view: Option<String>,
44}
45
46impl GfxWindowPrefs {
47 pub fn is_empty(&self) -> bool {
48 self.width.is_none()
49 && self.height.is_none()
50 && self.x.is_none()
51 && self.y.is_none()
52 }
53}
54
55impl ClientConfig {
56 pub fn path() -> anyhow::Result<PathBuf> {
57 let base = dirs::config_dir()
58 .ok_or_else(|| anyhow::anyhow!("could not resolve config directory"))?;
59 Ok(base.join("flatland").join("client.json"))
60 }
61
62 pub fn load() -> Self {
63 Self::path()
64 .ok()
65 .and_then(|path| std::fs::read(&path).ok())
66 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
67 .unwrap_or_default()
68 }
69
70 pub fn save(&self) -> anyhow::Result<()> {
71 let path = Self::path()?;
72 if let Some(parent) = path.parent() {
73 std::fs::create_dir_all(parent)?;
74 }
75 std::fs::write(path, serde_json::to_vec_pretty(self)?)?;
76 Ok(())
77 }
78
79 pub fn save_gfx_window(&mut self, prefs: GfxWindowPrefs) -> anyhow::Result<()> {
80 self.gfx_window = prefs;
81 self.save()
82 }
83
84 pub fn save_hud_view(&mut self, label: &str) -> anyhow::Result<()> {
86 self.hud_view = Some(label.to_string());
87 self.save()
88 }
89
90 pub fn set_game_host(&mut self, host: impl Into<String>) -> anyhow::Result<()> {
91 let host = host.into().trim().to_string();
92 if host.is_empty() {
93 anyhow::bail!("game host cannot be empty");
94 }
95 self.game_host = Some(host);
96 self.save()
97 }
98
99 pub fn game_server_addr(&self) -> SocketAddr {
100 if let Some(host) = self.game_host.as_deref() {
101 let port = self.game_port.unwrap_or(DEFAULT_GAME_PORT);
102 return parse_host_port(host, port)
103 .unwrap_or_else(|| format!("{host}:{port}").parse().expect("valid host:port"));
104 }
105 "127.0.0.1:7373".parse().expect("valid default addr")
106 }
107
108 pub fn api_base_url(&self) -> String {
109 if let Some(host) = self.game_host.as_deref() {
110 let port = self.api_port.unwrap_or(DEFAULT_API_PORT);
111 if host.starts_with("http://") || host.starts_with("https://") {
112 return host.to_string();
113 }
114 return format!("http://{host}:{port}");
115 }
116 "http://127.0.0.1:7380".into()
117 }
118}
119
120pub fn default_game_server_addr() -> SocketAddr {
121 if let Ok(addr) = std::env::var("FLATLAND_GATEWAY_ADDR") {
122 if let Ok(parsed) = addr.parse() {
123 return parsed;
124 }
125 }
126 ClientConfig::load().game_server_addr()
127}
128
129pub fn default_api_base_url() -> String {
130 if let Ok(url) = std::env::var("FLATLAND_API_URL") {
131 return url;
132 }
133 ClientConfig::load().api_base_url()
134}
135
136fn parse_host_port(host: &str, default_port: u16) -> Option<SocketAddr> {
137 if host.contains(':') {
138 host.parse().ok()
139 } else {
140 format!("{host}:{default_port}").parse().ok()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn default_is_localhost() {
150 let cfg = ClientConfig::default();
151 assert_eq!(
152 cfg.game_server_addr(),
153 "127.0.0.1:7373".parse().unwrap()
154 );
155 assert_eq!(cfg.api_base_url(), "http://127.0.0.1:7380");
156 }
157
158 #[test]
159 fn hud_view_roundtrips_in_config() {
160 let mut cfg = ClientConfig::default();
161 cfg.hud_view = Some("map".into());
162 let json = serde_json::to_string(&cfg).unwrap();
163 let loaded: ClientConfig = serde_json::from_str(&json).unwrap();
164 assert_eq!(loaded.hud_view.as_deref(), Some("map"));
165 }
166}