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