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