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 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}