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