Skip to main content

nil_client/client/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4#![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  /// Endpoint: `GET /get-server-kind`
162  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  /// Endpoint: `GET /`
173  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  /// Endpoint: `GET /version`
189  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}