nil-client 0.4.23

Multiplayer strategy game
Documentation
// Copyright (C) Call of Nil contributors
// SPDX-License-Identifier: AGPL-3.0-only

#![expect(clippy::wildcard_imports)]

mod auth;
mod battle;
mod chat;
mod cheat;
mod city;
mod continent;
mod infrastructure;
mod military;
mod npc;
mod player;
mod ranking;
mod report;
mod round;
mod user;
mod world;

use crate::authorization::Authorization;
use crate::circuit_breaker::CircuitBreaker;
use crate::error::{Error, Result};
use crate::http::{self, USER_AGENT};
use crate::retry::Retry;
use crate::server::ServerAddr;
use crate::websocket::WebSocketClient;
use futures::future::BoxFuture;
use local_ip_address::local_ip;
use nil_core::event::Event;
use nil_core::player::PlayerId;
use nil_core::world::config::WorldId;
use nil_crypto::password::Password;
use nil_payload::AuthorizeRequest;
use nil_payload::world::LeaveRequest;
use nil_server_types::ServerKind;
use nil_server_types::auth::Token;
use std::borrow::Cow;
use std::net::{IpAddr, SocketAddrV4};
use std::sync::nonpoison::Mutex;
use std::sync::{Arc, Weak};

pub struct Client {
  server: ServerAddr,
  world_id: Option<WorldId>,
  authorization: Option<Authorization>,
  websocket: Option<WebSocketClient>,
  circuit_breaker: Arc<Mutex<CircuitBreaker>>,
  retry: Retry,
  user_agent: Cow<'static, str>,
}

#[bon::bon]
impl Client {
  #[inline]
  pub fn new(server: ServerAddr) -> Self {
    Self {
      server,
      world_id: None,
      authorization: None,
      websocket: None,
      circuit_breaker: Arc::new(Mutex::default()),
      retry: Retry::with_attempts(2),
      user_agent: Cow::Borrowed(USER_AGENT),
    }
  }

  #[inline]
  pub fn new_local(addr: SocketAddrV4) -> Self {
    Self::new(ServerAddr::Local { addr })
  }

  #[inline]
  pub fn new_remote() -> Self {
    Self::new(ServerAddr::Remote)
  }

  #[builder]
  pub async fn update<OnEvent>(
    &mut self,
    #[builder(start_fn)] server: ServerAddr,
    world_id: Option<WorldId>,
    world_password: Option<Password>,
    player_id: Option<PlayerId>,
    player_password: Option<Password>,
    authorization_token: Option<Token>,
    on_event: Option<OnEvent>,
  ) -> Result<()>
  where
    OnEvent: Fn(Event) -> BoxFuture<'static, ()> + Send + Sync + 'static,
  {
    self.stop().await;
    self.world_id = world_id;

    if server != self.server {
      self.server = server;
      self
        .circuit_breaker
        .set(CircuitBreaker::new());
    }

    if self.server.is_remote()
      && let Some(token) = authorization_token
      && let Some(id) = self.validate_token(&token).await?
      && player_id.as_ref().is_none_or(|it| it == &id)
      && let Ok(authorization) = Authorization::new(token)
    {
      self.authorization = Some(authorization);
    } else if let Some(player) = player_id {
      let req = AuthorizeRequest { player, password: player_password };
      self.authorization = self
        .authorize(req)
        .await
        .map(|token| Some(Authorization::new(&token)))?
        .transpose()
        .inspect_err(|err| tracing::error!(message = %err, error = ?err))
        .map_err(|_| Error::FailedToAuthenticate)?;
    }

    if self.world_id.is_none()
      && self.server.is_local()
      && let ServerKind::Local { id } = self.get_server_kind().await?
    {
      self.world_id = Some(id);
    }

    if let Some(world_id) = self.world_id
      && let Some(on_event) = on_event
      && let Some(authorization) = self.authorization.clone()
    {
      let websocket = WebSocketClient::connect(server)
        .world_id(world_id)
        .maybe_world_password(world_password)
        .authorization(authorization)
        .circuit_breaker(self.circuit_breaker())
        .user_agent(&self.user_agent)
        .on_event(on_event)
        .call()
        .await?;

      self.websocket = Some(websocket);
    }

    Ok(())
  }

  pub async fn stop(&mut self) {
    if let Some(world) = self.world_id
      && self.authorization.is_some()
    {
      let req = LeaveRequest { world };
      if let Err(err) = self.leave(req).await {
        tracing::error!(message = %err, error = ?err);
      }
    }

    self.server = ServerAddr::Remote;
    self.world_id = None;
    self.authorization = None;
    self.websocket = None;
  }

  pub fn server_addr(&self) -> ServerAddr {
    let mut addr = self.server;
    if let ServerAddr::Local { addr } = &mut addr
      && addr.ip().is_loopback()
      && let Ok(ip) = local_ip()
      && let IpAddr::V4(ip) = ip
    {
      addr.set_ip(ip);
    }

    addr
  }

  #[inline]
  pub fn server(&self) -> ServerAddr {
    self.server
  }

  #[inline]
  pub fn world(&self) -> Option<WorldId> {
    self.world_id
  }

  #[inline]
  pub fn user_agent(&self) -> &str {
    &self.user_agent
  }

  pub fn set_user_agent(&mut self, user_agent: &str) {
    self.user_agent = Cow::Owned(user_agent.to_owned());
  }

  #[inline]
  pub fn is_local(&self) -> bool {
    self.server.is_local()
  }

  #[inline]
  pub fn is_remote(&self) -> bool {
    self.server.is_remote()
  }

  fn circuit_breaker(&self) -> Weak<Mutex<CircuitBreaker>> {
    Arc::downgrade(&self.circuit_breaker)
  }

  pub async fn get_server_kind(&self) -> Result<ServerKind> {
    http::json_get("get-server-kind")
      .server(self.server)
      .retry(&self.retry)
      .circuit_breaker(self.circuit_breaker())
      .user_agent(&self.user_agent)
      .send()
      .await
  }

  pub async fn get_server_version(&self) -> Result<String> {
    http::get_text("version")
      .server(self.server)
      .retry(&self.retry)
      .circuit_breaker(self.circuit_breaker())
      .user_agent(&self.user_agent)
      .send()
      .await
  }

  pub async fn is_ready(&self) -> bool {
    http::get("")
      .server(self.server)
      .retry(&self.retry)
      .circuit_breaker(self.circuit_breaker())
      .user_agent(&self.user_agent)
      .send()
      .await
      .map(|()| true)
      .unwrap_or(false)
  }
}

impl Default for Client {
  fn default() -> Self {
    Self::new_remote()
  }
}