Skip to main content

nil_core/
savedata.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::chat::Chat;
5use crate::continent::Continent;
6use crate::error::{AnyResult, Error, Result};
7use crate::military::Military;
8use crate::npc::bot::BotManager;
9use crate::npc::precursor::PrecursorManager;
10use crate::player::{Player, PlayerManager, PlayerStatus};
11use crate::ranking::Ranking;
12use crate::report::ReportManager;
13use crate::round::{Round, RoundId};
14use crate::world::config::{WorldConfig, WorldName};
15use crate::world::stats::WorldStats;
16use anyhow::bail;
17use flate2::Compression;
18use flate2::bufread::GzDecoder;
19use flate2::write::GzEncoder;
20use jiff::Zoned;
21use semver::Version;
22use serde::de::DeserializeOwned;
23use serde::{Deserialize, Serialize};
24use std::fmt;
25use std::io::{Read, Write};
26use tar::Archive;
27
28const MINIFY: bool = cfg!(any(
29  not(debug_assertions),
30  target_os = "android",
31  target_os = "ios"
32));
33
34type TarBuilder<'a> = tar::Builder<GzEncoder<&'a mut Vec<u8>>>;
35
36#[derive(Clone, Debug, Deserialize, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct Savedata {
39  pub(crate) round: Round,
40  pub(crate) continent: Continent,
41  pub(crate) player_manager: PlayerManager,
42  pub(crate) bot_manager: BotManager,
43  pub(crate) precursor_manager: PrecursorManager,
44  pub(crate) military: Military,
45  pub(crate) ranking: Ranking,
46  pub(crate) report: ReportManager,
47  pub(crate) config: WorldConfig,
48  pub(crate) stats: WorldStats,
49  pub(crate) chat: Chat,
50}
51
52impl Savedata {
53  pub fn read(bytes: &[u8]) -> Result<Self> {
54    read_tar(bytes, "world")
55      .inspect_err(|err| tracing::error!(message = %err, error = ?err))
56      .map_err(|_| Error::FailedToReadSavedata)
57  }
58
59  pub(crate) fn write(&mut self, buffer: &mut Vec<u8>) -> Result<()> {
60    for player in self.player_manager.players_mut() {
61      *player.status_mut() = PlayerStatus::Inactive;
62    }
63
64    write_tar(buffer, self)
65      .inspect_err(|err| tracing::error!(message = %err, error = ?err))
66      .map_err(|_| Error::FailedToWriteSavedata)
67  }
68
69  pub fn players(&self) -> impl Iterator<Item = &Player> {
70    self.player_manager.players()
71  }
72}
73
74#[derive(Clone, Debug, Deserialize, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct SavedataInfo {
77  world_name: WorldName,
78  round: RoundId,
79  version: Version,
80  saved_at: Zoned,
81}
82
83impl SavedataInfo {
84  fn new(data: &Savedata) -> AnyResult<Self> {
85    Ok(Self {
86      world_name: data.config.name(),
87      round: data.round.id(),
88      version: Version::parse(env!("CARGO_PKG_VERSION"))?,
89      saved_at: Zoned::now(),
90    })
91  }
92
93  pub fn read(bytes: &[u8]) -> Result<Self> {
94    read_tar(bytes, "info")
95      .inspect_err(|err| tracing::error!(message = %err, error = ?err))
96      .map_err(|_| Error::FailedToReadSavedata)
97  }
98}
99
100fn read_tar<T>(bytes: &[u8], entry_name: &str) -> AnyResult<T>
101where
102  T: DeserializeOwned,
103{
104  let decoder = GzDecoder::new(bytes);
105  let mut archive = Archive::new(decoder);
106
107  for entry in archive.entries()? {
108    let mut entry = entry?;
109    if let Ok(entry_path) = entry.path()
110      && let Some(path) = entry_path.to_str()
111      && path == entry_name
112    {
113      let size = entry.size().try_into()?;
114      let mut buffer = Vec::with_capacity(size);
115      entry.read_to_end(&mut buffer)?;
116      return Ok(serde_json::from_slice(&buffer)?);
117    }
118  }
119
120  bail!("Entry not found: {entry_name}");
121}
122
123fn write_tar(buffer: &mut Vec<u8>, data: &Savedata) -> AnyResult<()> {
124  let encoder = GzEncoder::new(buffer, Compression::best());
125  let mut tar_builder = TarBuilder::new(encoder);
126
127  let info = SavedataInfo::new(data)?;
128  append(&mut tar_builder, &info, "info")?;
129  append(&mut tar_builder, data, "world")?;
130
131  tar_builder.into_inner()?.finish()?.flush()?;
132
133  Ok(())
134}
135
136fn append<T>(builder: &mut TarBuilder, value: &T, path: &str) -> AnyResult<()>
137where
138  T: ?Sized + Serialize,
139{
140  let bytes = if MINIFY {
141    serde_json::to_vec(value)?
142  } else {
143    serde_json::to_vec_pretty(value)?
144  };
145
146  let mut header = tar::Header::new_gnu();
147  header.set_size(bytes.len().try_into()?);
148  header.set_cksum();
149
150  builder.append_data(&mut header, path, bytes.as_slice())?;
151
152  Ok(())
153}
154
155pub struct SaveHandle(Box<dyn FnOnce(Vec<u8>) + Send + Sync>);
156
157impl SaveHandle {
158  pub fn new<F>(f: F) -> Self
159  where
160    F: FnOnce(Vec<u8>) + Send + Sync + 'static,
161  {
162    Self(Box::new(f))
163  }
164
165  #[inline]
166  pub fn save(self, data: Vec<u8>) {
167    (self.0)(data);
168  }
169}
170
171impl fmt::Debug for SaveHandle {
172  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173    f.debug_tuple("SaveHandle")
174      .finish_non_exhaustive()
175  }
176}