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