1#![expect(clippy::wildcard_imports)]
5
6mod auth;
7mod battle;
8mod chat;
9mod cheat;
10mod city;
11mod continent;
12mod infrastructure;
13mod market;
14mod military;
15mod npc;
16mod player;
17mod ranking;
18mod report;
19mod round;
20mod user;
21mod world;
22
23use crate::authorization::Authorization;
24use crate::circuit_breaker::CircuitBreaker;
25use crate::error::{Error, Result};
26use crate::http::{self, USER_AGENT};
27use crate::retry::Retry;
28use crate::server::ServerAddr;
29use crate::websocket::WebSocketClient;
30use bon::Builder;
31use futures::future::BoxFuture;
32use local_ip_address::local_ip;
33use nil_core::event::Event;
34use nil_core::player::PlayerId;
35use nil_core::world::config::WorldId;
36use nil_crypto::password::Password;
37use nil_payload::request::auth::AuthorizeRequest;
38use nil_payload::request::world::LeaveRequest;
39use nil_payload::response::server::*;
40use nil_server_types::ServerKind;
41use nil_server_types::auth::Token;
42use serde::{Deserialize, Serialize};
43use std::borrow::Cow;
44use std::net::{IpAddr, SocketAddrV4};
45use std::sync::nonpoison::Mutex;
46use std::sync::{Arc, Weak};
47
48pub struct Client {
49 server: ServerAddr,
50 world_id: Option<WorldId>,
51 authorization: Option<Authorization>,
52 websocket: Option<WebSocketClient>,
53 circuit_breaker: Arc<Mutex<CircuitBreaker>>,
54 retry: Retry,
55 user_agent: Cow<'static, str>,
56}
57
58impl Client {
59 #[inline]
60 pub fn new(server: ServerAddr) -> Self {
61 Self {
62 server,
63 world_id: None,
64 authorization: None,
65 websocket: None,
66 circuit_breaker: Arc::new(Mutex::default()),
67 retry: Retry::with_attempts(2),
68 user_agent: Cow::Borrowed(USER_AGENT),
69 }
70 }
71
72 #[inline]
73 pub fn new_local(addr: SocketAddrV4) -> Self {
74 Self::new(ServerAddr::Local { addr })
75 }
76
77 #[inline]
78 pub fn new_remote() -> Self {
79 Self::new(ServerAddr::Remote)
80 }
81
82 pub async fn update<OnEvent>(
83 &mut self,
84 options: ClientOptions,
85 on_event: Option<OnEvent>,
86 ) -> Result<()>
87 where
88 OnEvent: Fn(Event) -> BoxFuture<'static, ()> + Send + Sync + 'static,
89 {
90 let update = Update {
91 client: self,
92 server: options.server,
93 world_id: options.world_id,
94 world_password: options.world_password,
95 player_id: options.player_id,
96 player_password: options.player_password,
97 authorization_token: options.authorization_token,
98 on_event,
99 };
100
101 update.execute().await
102 }
103
104 pub async fn stop(&mut self) {
105 if let Some(world) = self.world_id
106 && self.authorization.is_some()
107 {
108 let req = LeaveRequest { world };
109 if let Err(err) = self.leave(req).await {
110 tracing::error!(message = %err, error = ?err);
111 }
112 }
113
114 self.world_id = None;
115 self.authorization = None;
116 self.websocket = None;
117 }
118
119 pub fn server_addr(&self) -> ServerAddr {
120 let mut addr = self.server;
121 if let ServerAddr::Local { addr } = &mut addr
122 && addr.ip().is_loopback()
123 && let Ok(ip) = local_ip()
124 && let IpAddr::V4(ip) = ip
125 {
126 addr.set_ip(ip);
127 }
128
129 addr
130 }
131
132 #[inline]
133 pub fn world(&self) -> Option<WorldId> {
134 self.world_id
135 }
136
137 #[inline]
138 pub fn user_agent(&self) -> &str {
139 &self.user_agent
140 }
141
142 #[inline]
143 pub fn set_user_agent(&mut self, user_agent: &str) {
144 self.user_agent = Cow::Owned(user_agent.to_owned());
145 }
146
147 #[inline]
148 pub fn is_local(&self) -> bool {
149 self.server.is_local()
150 }
151
152 #[inline]
153 pub fn is_remote(&self) -> bool {
154 self.server.is_remote()
155 }
156
157 fn circuit_breaker(&self) -> Weak<Mutex<CircuitBreaker>> {
158 Arc::downgrade(&self.circuit_breaker)
159 }
160
161 pub async fn get_server_kind(&self) -> Result<GetServerKindResponse> {
163 http::json_get("get-server-kind")
164 .server(self.server)
165 .retry(&self.retry)
166 .circuit_breaker(self.circuit_breaker())
167 .user_agent(&self.user_agent)
168 .send()
169 .await
170 }
171
172 pub async fn is_ready(&self) -> bool {
174 http::get("")
175 .server(self.server)
176 .retry(&self.retry)
177 .circuit_breaker(self.circuit_breaker())
178 .user_agent(&self.user_agent)
179 .send()
180 .await
181 .map(|()| true)
182 .unwrap_or_else(|err| {
183 tracing::error!(message = %err, error = ?err);
184 false
185 })
186 }
187
188 pub async fn version(&self) -> Result<VersionResponse> {
190 http::json_get("version")
191 .server(self.server)
192 .retry(&self.retry)
193 .circuit_breaker(self.circuit_breaker())
194 .user_agent(&self.user_agent)
195 .send()
196 .await
197 }
198}
199
200impl Default for Client {
201 fn default() -> Self {
202 Self::new_remote()
203 }
204}
205
206#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
207#[serde(rename_all = "camelCase")]
208#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
209#[cfg_attr(feature = "typescript", ts(export, optional_fields = nullable))]
210pub struct ClientOptions {
211 #[builder(start_fn, into)]
212 pub server: ServerAddr,
213
214 #[serde(default)]
215 #[builder(into)]
216 pub world_id: Option<WorldId>,
217
218 #[serde(default)]
219 #[builder(into)]
220 pub world_password: Option<Password>,
221
222 #[serde(default)]
223 #[builder(into)]
224 pub player_id: Option<PlayerId>,
225
226 #[serde(default)]
227 #[builder(into)]
228 pub player_password: Option<Password>,
229
230 #[serde(default)]
231 #[builder(into)]
232 pub authorization_token: Option<Token>,
233}
234
235struct Update<'a, OnEvent>
236where
237 OnEvent: Fn(Event) -> BoxFuture<'static, ()> + Send + Sync + 'static,
238{
239 client: &'a mut Client,
240 server: ServerAddr,
241 world_id: Option<WorldId>,
242 world_password: Option<Password>,
243 player_id: Option<PlayerId>,
244 player_password: Option<Password>,
245 authorization_token: Option<Token>,
246 on_event: Option<OnEvent>,
247}
248
249impl<OnEvent> Update<'_, OnEvent>
250where
251 OnEvent: Fn(Event) -> BoxFuture<'static, ()> + Send + Sync + 'static,
252{
253 async fn execute(self) -> Result<()> {
254 self.client.stop().await;
255 self.client.world_id = self.world_id;
256
257 if self.server != self.client.server {
258 self.client.server = self.server;
259 self
260 .client
261 .circuit_breaker
262 .set(CircuitBreaker::new());
263 }
264
265 if self.client.server.is_remote()
266 && let Some(token) = self.authorization_token
267 && let Some(id) = self
268 .client
269 .validate_token((&token).into())
270 .await?
271 .0
272 && self
273 .player_id
274 .as_ref()
275 .is_none_or(|it| it == &id)
276 && let Ok(authorization) = Authorization::new(token)
277 {
278 self.client.authorization = Some(authorization);
279 } else if let Some(player) = self.player_id {
280 let req = AuthorizeRequest {
281 player,
282 password: self.player_password,
283 };
284
285 self.client.authorization = self
286 .client
287 .authorize(req)
288 .await
289 .map(|token| Some(Authorization::new(&token.0)))?
290 .transpose()
291 .inspect_err(|err| tracing::error!(message = %err, error = ?err))
292 .map_err(|_| Error::FailedToAuthenticate)?;
293 }
294
295 if self.client.world_id.is_none()
296 && self.client.server.is_local()
297 && let ServerKind::Local { id } = self.client.get_server_kind().await?.0
298 {
299 self.client.world_id = Some(id);
300 }
301
302 if let Some(world_id) = self.client.world_id
303 && let Some(on_event) = self.on_event
304 && let Some(authorization) = self.client.authorization.clone()
305 {
306 let websocket = WebSocketClient::connect(self.client.server)
307 .world_id(world_id)
308 .maybe_world_password(self.world_password)
309 .authorization(authorization)
310 .circuit_breaker(self.client.circuit_breaker())
311 .user_agent(&self.client.user_agent)
312 .on_event(on_event)
313 .call()
314 .await?;
315
316 self.client.websocket = Some(websocket);
317 }
318
319 Ok(())
320 }
321}