Savestate stuff.

This commit is contained in:
IQuant 2024-08-01 20:07:12 +03:00
parent 29b0296528
commit 0e8c97ccab
11 changed files with 252 additions and 42 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
target target
mat_data.txt mat_data.txt
save_state

View file

@ -1997,7 +1997,7 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]] [[package]]
name = "noita-proxy" name = "noita-proxy"
version = "0.15.3" version = "0.16.0"
dependencies = [ dependencies = [
"argh", "argh",
"bincode", "bincode",

View file

@ -5,7 +5,7 @@ resolver = "2"
[package] [package]
name = "noita-proxy" name = "noita-proxy"
description = "Noita Entangled Worlds companion app." description = "Noita Entangled Worlds companion app."
version = "0.15.3" version = "0.16.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -88,3 +88,10 @@ connect_settings_autostart = Start the game automatically
Higher-values-result-in-less-performance-impact = Higher values result in less performance impact. Higher-values-result-in-less-performance-impact = Higher values result in less performance impact.
World-will-be-synced-every-this-many-frames = World will be synced every this many frames. World-will-be-synced-every-this-many-frames = World will be synced every this many frames.
## Savestate
New-game = New game
Continue = Continue
savestate_desc = Savestate from a previous run has been detected. Do you wish to continue that run, or to start a new game (and reset the savestate)?
An-in-progress-run-has-been-detected = An in-progress run has been detected.

View file

@ -88,3 +88,10 @@ connect_settings_autostart = ゲームを自動的に開始する
Higher-values-result-in-less-performance-impact = Higher values result in less performance impact. Higher-values-result-in-less-performance-impact = Higher values result in less performance impact.
World-will-be-synced-every-this-many-frames = World will be synced every this many frames. World-will-be-synced-every-this-many-frames = World will be synced every this many frames.
## Savestate
New-game = New game
Continue = Continue
savestate_desc = Savestate from a previous run has been detected. Do you wish to continue that run, or to start a new game (and reset the savestate)?
An-in-progress-run-has-been-detected = An in-progress run has been detected.

View file

@ -69,4 +69,10 @@ error_lobby_does_not_exist = Лобби не существует.
## Game settings ## Game settings
Higher-values-result-in-less-performance-impact = Higher values result in less performance impact Higher-values-result-in-less-performance-impact = Higher values result in less performance impact
World-will-be-synced-every-this-many-frames = World will be synced every this many frames World-will-be-synced-every-this-many-frames = World will be synced every this many frames
## Savestate
New-game = New game
Continue = Continue
savestate_desc = Savestate from a previous run has been detected. Do you wish to continue that run, or to start a new game (and reset the savestate)?
An-in-progress-run-has-been-detected = An in-progress run has been detected.

View file

@ -2,4 +2,5 @@
pub mod mod_manager; pub mod mod_manager;
pub mod noita_launcher; pub mod noita_launcher;
pub mod releases; pub mod releases;
pub mod save_state;
pub mod self_update; pub mod self_update;

View file

@ -0,0 +1,89 @@
use std::{
fs,
path::PathBuf,
sync::{
atomic::{self, AtomicBool},
Arc,
},
};
use tracing::{error, info, warn};
pub trait SaveStateEntry: bitcode::Encode + bitcode::DecodeOwned {
const FILENAME: &'static str;
}
struct SaveStateInner {
game_started: AtomicBool,
}
/// Allows persisting extra run state (like chunks). Cleared between runs.
#[derive(Clone)]
pub struct SaveState {
inner: Arc<SaveStateInner>,
path: PathBuf,
has_savestate: bool,
}
impl SaveState {
pub(crate) fn new(path: PathBuf) -> Self {
let has_savestate = path.join("run_info.bit").exists();
info!("Has savestate: {has_savestate}");
if let Err(err) = fs::create_dir_all(&path) {
error!("Error while creating directories: {err}");
}
let path = path.canonicalize().unwrap_or(path);
info!("Will save to: {}", path.display());
Self {
path,
inner: Arc::new(SaveStateInner {
game_started: false.into(),
}),
has_savestate,
}
}
pub(crate) fn save<D: SaveStateEntry>(&self, data: &D) {
if !self.inner.game_started.load(atomic::Ordering::SeqCst) {
info!("Skipping save of {}, game not started yet", D::FILENAME);
return;
}
let path = self.path_for_filename(D::FILENAME);
let encoded = bitcode::encode(data);
if let Err(err) = fs::write(&path, encoded) {
error!("Error while saving to {:?}: {err}", D::FILENAME);
}
info!("Saved {}", path.display());
}
pub(crate) fn load<D: SaveStateEntry>(&self) -> Option<D> {
let path = self.path_for_filename(D::FILENAME);
let data = fs::read(&path)
.inspect_err(|err| warn!("Could not read {:?}: {err}", D::FILENAME))
.ok()?;
bitcode::decode(&data)
.inspect_err(|err| error!("Could not decode {:?}: {err}", D::FILENAME))
.ok()
}
pub(crate) fn mark_game_started(&self) {
self.inner
.game_started
.store(true, atomic::Ordering::SeqCst);
}
pub(crate) fn reset(&self) {
fs::remove_dir_all(&self.path).ok();
fs::create_dir_all(&self.path).ok();
}
/// true if had a savestate initially.
pub(crate) fn has_savestate(&self) -> bool {
self.has_savestate
}
fn path_for_filename(&self, filename: &str) -> PathBuf {
self.path.join(format!("{filename}.bit"))
}
}

View file

@ -1,12 +1,9 @@
use std::{ use std::{
fmt::Display, fmt::Display, net::SocketAddr, ops::Deref, sync::{atomic::Ordering, Arc}, thread::JoinHandle, time::Duration
net::SocketAddr,
sync::{atomic::Ordering, Arc},
time::Duration,
}; };
use bitcode::{Decode, Encode}; use bitcode::{Decode, Encode};
use bookkeeping::noita_launcher::{LaunchTokenResult, NoitaLauncher}; use bookkeeping::{noita_launcher::{LaunchTokenResult, NoitaLauncher}, save_state::SaveState};
use clipboard::{ClipboardContext, ClipboardProvider}; use clipboard::{ClipboardContext, ClipboardProvider};
use eframe::egui::{ use eframe::egui::{
self, Align2, Button, Color32, Context, DragValue, FontDefinitions, FontFamily, InnerResponse, self, Align2, Button, Color32, Context, DragValue, FontDefinitions, FontFamily, InnerResponse,
@ -15,7 +12,7 @@ use eframe::egui::{
use egui_plot::{Plot, PlotPoint, PlotUi, Text}; use egui_plot::{Plot, PlotPoint, PlotUi, Text};
use lang::{set_current_locale, tr, LANGS}; use lang::{set_current_locale, tr, LANGS};
use mod_manager::{Modmanager, ModmanagerSettings}; use mod_manager::{Modmanager, ModmanagerSettings};
use net::{omni::PeerVariant, steam_networking::ExtraPeerState, NetManagerInit}; use net::{omni::PeerVariant, steam_networking::ExtraPeerState, NetManagerInit, RunInfo};
use self_update::SelfUpdateManager; use self_update::SelfUpdateManager;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use steamworks::{LobbyId, SteamAPIInitError}; use steamworks::{LobbyId, SteamAPIInitError};
@ -61,11 +58,28 @@ impl Default for GameSettings {
} }
} }
pub struct NetManStopOnDrop(pub Arc<net::NetManager>, Option<JoinHandle<()>>);
impl Deref for NetManStopOnDrop {
type Target = Arc<net::NetManager>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for NetManStopOnDrop {
fn drop(&mut self) {
self.0.continue_running.store(false, Ordering::Relaxed);
self.1.take().unwrap().join().unwrap();
}
}
enum AppState { enum AppState {
Connect, Connect,
ModManager, ModManager,
Netman { Netman {
netman: Arc<net::NetManager>, netman: NetManStopOnDrop,
noita_launcher: NoitaLauncher, noita_launcher: NoitaLauncher,
}, },
Error { Error {
@ -73,6 +87,7 @@ enum AppState {
}, },
SelfUpdate, SelfUpdate,
LangPick, LangPick,
AskSavestateReset,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -103,7 +118,8 @@ pub struct App {
state: AppState, state: AppState,
modmanager: Modmanager, modmanager: Modmanager,
steam_state: Result<steam_helper::SteamState, SteamAPIInitError>, steam_state: Result<steam_helper::SteamState, SteamAPIInitError>,
saved_state: AppSavedState, app_saved_state: AppSavedState,
run_save_state: SaveState,
modmanager_settings: ModmanagerSettings, modmanager_settings: ModmanagerSettings,
self_update: SelfUpdateManager, self_update: SelfUpdateManager,
show_map_plot: bool, show_map_plot: bool,
@ -170,7 +186,7 @@ impl App {
state, state,
modmanager: Modmanager::default(), modmanager: Modmanager::default(),
steam_state: steam_helper::SteamState::new(), steam_state: steam_helper::SteamState::new(),
saved_state, app_saved_state: saved_state,
modmanager_settings, modmanager_settings,
self_update: SelfUpdateManager::new(), self_update: SelfUpdateManager::new(),
show_map_plot: false, show_map_plot: false,
@ -178,6 +194,7 @@ impl App {
lobby_id_field: "".to_string(), lobby_id_field: "".to_string(),
args, args,
can_start_automatically: false, can_start_automatically: false,
run_save_state: SaveState::new("./save_state/".into())
} }
} }
@ -187,13 +204,14 @@ impl App {
} else { } else {
None None
}; };
let my_nickname = self.saved_state.nickname.clone().or(steam_nickname); let my_nickname = self.app_saved_state.nickname.clone().or(steam_nickname);
NetManagerInit { my_nickname } NetManagerInit { my_nickname, save_state: self.run_save_state.clone() }
} }
fn change_state_to_netman(&mut self, netman: Arc<net::NetManager>) { fn change_state_to_netman(&mut self, netman: Arc<net::NetManager>) {
let handle = netman.clone().start();
self.state = AppState::Netman { self.state = AppState::Netman {
netman, netman: NetManStopOnDrop(netman, Some(handle)),
noita_launcher: NoitaLauncher::new( noita_launcher: NoitaLauncher::new(
&self.modmanager_settings.game_exe_path, &self.modmanager_settings.game_exe_path,
self.args.launch_cmd.as_deref(), self.args.launch_cmd.as_deref(),
@ -207,15 +225,21 @@ impl App {
let peer = Peer::host(bind_addr, None).unwrap(); let peer = Peer::host(bind_addr, None).unwrap();
let netman = net::NetManager::new(PeerVariant::Tangled(peer), self.get_netman_init()); let netman = net::NetManager::new(PeerVariant::Tangled(peer), self.get_netman_init());
self.set_netman_settings(&netman); self.set_netman_settings(&netman);
netman.clone().start();
self.change_state_to_netman(netman); self.change_state_to_netman(netman);
} }
fn set_netman_settings(&mut self, netman: &Arc<net::NetManager>) { fn set_netman_settings(&mut self, netman: &Arc<net::NetManager>) {
let run_info: Option<RunInfo> = self.run_save_state.load();
let mut settings = netman.settings.lock().unwrap(); let mut settings = netman.settings.lock().unwrap();
*settings = self.saved_state.game_settings.clone(); *settings = self.app_saved_state.game_settings.clone();
if !self.saved_state.game_settings.use_constant_seed { if !self.app_saved_state.game_settings.use_constant_seed {
settings.seed = rand::random(); if let Some(info) = run_info {
settings.seed = info.seed;
info!("Using saved seed: {}", settings.seed);
} else {
settings.seed = rand::random();
info!("Using random seed: {}", settings.seed);
}
} else { } else {
info!("Using constant seed: {}", settings.seed); info!("Using constant seed: {}", settings.seed);
} }
@ -225,7 +249,6 @@ impl App {
fn start_connect(&mut self, addr: SocketAddr) { fn start_connect(&mut self, addr: SocketAddr) {
let peer = Peer::connect(addr, None).unwrap(); let peer = Peer::connect(addr, None).unwrap();
let netman = net::NetManager::new(PeerVariant::Tangled(peer), self.get_netman_init()); let netman = net::NetManager::new(PeerVariant::Tangled(peer), self.get_netman_init());
netman.clone().start();
self.change_state_to_netman(netman); self.change_state_to_netman(netman);
} }
@ -236,7 +259,6 @@ impl App {
); );
let netman = net::NetManager::new(PeerVariant::Steam(peer), self.get_netman_init()); let netman = net::NetManager::new(PeerVariant::Steam(peer), self.get_netman_init());
self.set_netman_settings(&netman); self.set_netman_settings(&netman);
netman.clone().start();
self.change_state_to_netman(netman); self.change_state_to_netman(netman);
} }
@ -253,13 +275,12 @@ impl App {
); );
let netman = net::NetManager::new(PeerVariant::Steam(peer), self.get_netman_init()); let netman = net::NetManager::new(PeerVariant::Steam(peer), self.get_netman_init());
netman.clone().start();
self.change_state_to_netman(netman); self.change_state_to_netman(netman);
} }
fn connect_screen(&mut self, ctx: &egui::Context) { fn connect_screen(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
if self.saved_state.times_started % 20 == 0 { if self.app_saved_state.times_started % 20 == 0 {
let image = egui::Image::new(egui::include_image!("../assets/longleg.png")) let image = egui::Image::new(egui::include_image!("../assets/longleg.png"))
.texture_options(TextureOptions::NEAREST); .texture_options(TextureOptions::NEAREST);
image.paint_at(ui, ui.ctx().screen_rect()); image.paint_at(ui, ui.ctx().screen_rect());
@ -293,7 +314,7 @@ impl App {
ui.set_min_size(ui.available_size()); ui.set_min_size(ui.available_size());
let lang_label = self let lang_label = self
.saved_state .app_saved_state
.lang_id .lang_id
.clone() .clone()
.unwrap_or_default() .unwrap_or_default()
@ -309,7 +330,7 @@ impl App {
} }
let secret_active = ui.input(|i| i.modifiers.ctrl && i.key_down(Key::D)); let secret_active = ui.input(|i| i.modifiers.ctrl && i.key_down(Key::D));
if secret_active && ui.button("reset all data").clicked() { if secret_active && ui.button("reset all data").clicked() {
self.saved_state = Default::default(); self.app_saved_state = Default::default();
self.modmanager_settings = Default::default(); self.modmanager_settings = Default::default();
self.state = AppState::LangPick; self.state = AppState::LangPick;
} }
@ -370,8 +391,8 @@ impl App {
self.start_server(); self.start_server();
} }
ui.text_edit_singleline(&mut self.saved_state.addr); ui.text_edit_singleline(&mut self.app_saved_state.addr);
let addr = self.saved_state.addr.parse(); let addr = self.app_saved_state.addr.parse();
ui.add_enabled_ui(addr.is_ok(), |ui| { ui.add_enabled_ui(addr.is_ok(), |ui| {
if ui.button(tr("ip_connect")).clicked() { if ui.button(tr("ip_connect")).clicked() {
if let Ok(addr) = addr { if let Ok(addr) = addr {
@ -386,7 +407,7 @@ impl App {
fn show_game_settings(&mut self, ui: &mut Ui) { fn show_game_settings(&mut self, ui: &mut Ui) {
heading_with_underline(ui, tr("connect_settings")); heading_with_underline(ui, tr("connect_settings"));
let game_settings = &mut self.saved_state.game_settings; let game_settings = &mut self.app_saved_state.game_settings;
ui.label(tr("connect_settings_debug")); ui.label(tr("connect_settings_debug"));
ui.checkbox( ui.checkbox(
@ -445,7 +466,7 @@ impl App {
heading_with_underline(ui, tr("connect_settings_local")); heading_with_underline(ui, tr("connect_settings_local"));
ui.checkbox( ui.checkbox(
&mut self.saved_state.start_game_automatically, &mut self.app_saved_state.start_game_automatically,
tr("connect_settings_autostart"), tr("connect_settings_autostart"),
); );
} }
@ -494,6 +515,14 @@ impl App {
ctx.set_fonts(font_definitions); ctx.set_fonts(font_definitions);
} }
fn switch_to_connect(&mut self) {
self.state = if self.run_save_state.has_savestate() {
AppState::AskSavestateReset
} else {
AppState::Connect
};
}
} }
fn draw_bg(ui: &mut Ui) { fn draw_bg(ui: &mut Ui) {
@ -596,7 +625,7 @@ impl eframe::App for App {
if accept_local && !local_connected { if accept_local && !local_connected {
match noita_launcher.launch_token() { match noita_launcher.launch_token() {
LaunchTokenResult::Ok(mut token) => { LaunchTokenResult::Ok(mut token) => {
let start_auto = self.can_start_automatically && self.saved_state.start_game_automatically; let start_auto = self.can_start_automatically && self.app_saved_state.start_game_automatically;
if start_auto || ui.button(tr("launcher_start_game")).clicked() { if start_auto || ui.button(tr("launcher_start_game")).clicked() {
info!("Starting the game now"); info!("Starting the game now");
token.start_game(); token.start_game();
@ -646,7 +675,7 @@ impl eframe::App for App {
egui::Window::new(tr("connect_settings")).open(&mut show).show(ctx, |ui| { egui::Window::new(tr("connect_settings")).open(&mut show).show(ctx, |ui| {
self.show_game_settings(ui); self.show_game_settings(ui);
if ui.button(tr("netman_apply_settings")).clicked() { if ui.button(tr("netman_apply_settings")).clicked() {
*netman.pending_settings.lock().unwrap() = self.saved_state.game_settings.clone(); *netman.pending_settings.lock().unwrap() = self.app_saved_state.game_settings.clone();
} }
}); });
self.show_settings = show; self.show_settings = show;
@ -677,7 +706,7 @@ impl eframe::App for App {
) )
}); });
if self.modmanager.is_done() { if self.modmanager.is_done() {
self.state = AppState::Connect; self.switch_to_connect();
} }
} }
AppState::SelfUpdate => { AppState::SelfUpdate => {
@ -700,7 +729,7 @@ impl eframe::App for App {
ui.set_max_width(200.0); ui.set_max_width(200.0);
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
if ui.button(lang.name()).clicked() { if ui.button(lang.name()).clicked() {
self.saved_state.lang_id = Some(lang.id()); self.app_saved_state.lang_id = Some(lang.id());
set_current_locale(lang.id()) set_current_locale(lang.id())
} }
}); });
@ -710,11 +739,29 @@ impl eframe::App for App {
} }
}); });
} }
AppState::AskSavestateReset => {
egui::Window::new(tr("An-in-progress-run-has-been-detected"))
.auto_sized()
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.label(tr("savestate_desc"));
ui.horizontal(|ui| {
if ui.button(tr("Continue")).clicked() {
self.state = AppState::Connect;
}
if ui.button(tr("New-game")).clicked() {
self.state = AppState::Connect;
self.run_save_state.reset();
}
});
}
);
},
}; };
} }
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, &self.saved_state); eframe::set_value(storage, eframe::APP_KEY, &self.app_saved_state);
eframe::set_value(storage, MODMANAGER, &self.modmanager_settings); eframe::set_value(storage, MODMANAGER, &self.modmanager_settings);
} }
} }

View file

@ -1,3 +1,4 @@
use bitcode::{Decode, Encode};
use messages::{MessageRequest, NetMsg}; use messages::{MessageRequest, NetMsg};
use omni::OmniPeerId; use omni::OmniPeerId;
use proxy_opt::ProxyOpt; use proxy_opt::ProxyOpt;
@ -8,7 +9,7 @@ use std::{
io::{self, Write}, io::{self, Write},
net::{SocketAddr, TcpListener, TcpStream}, net::{SocketAddr, TcpListener, TcpStream},
sync::{atomic::AtomicBool, Arc, Mutex}, sync::{atomic::AtomicBool, Arc, Mutex},
thread, thread::{self, JoinHandle},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tracing::debug; use tracing::debug;
@ -18,7 +19,10 @@ use tangled::Reliability;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tungstenite::{accept, WebSocket}; use tungstenite::{accept, WebSocket};
use crate::GameSettings; use crate::{
bookkeeping::save_state::{SaveState, SaveStateEntry},
GameSettings,
};
pub mod messages; pub mod messages;
mod proxy_opt; mod proxy_opt;
@ -48,6 +52,15 @@ pub(crate) fn ws_encode_mod(peer: omni::OmniPeerId, data: &[u8]) -> tungstenite:
tungstenite::Message::Binary(buf) tungstenite::Message::Binary(buf)
} }
#[derive(Encode, Decode)]
pub(crate) struct RunInfo {
pub(crate) seed: u64,
}
impl SaveStateEntry for RunInfo {
const FILENAME: &'static str = "run_info";
}
pub(crate) struct NetInnerState { pub(crate) struct NetInnerState {
pub(crate) ws: Option<WebSocket<TcpStream>>, pub(crate) ws: Option<WebSocket<TcpStream>>,
world: WorldManager, world: WorldManager,
@ -75,6 +88,7 @@ pub mod omni;
pub struct NetManagerInit { pub struct NetManagerInit {
pub my_nickname: Option<String>, pub my_nickname: Option<String>,
pub save_state: SaveState,
} }
pub struct NetManager { pub struct NetManager {
@ -157,7 +171,11 @@ impl NetManager {
let mut state = NetInnerState { let mut state = NetInnerState {
ws: None, ws: None,
world: WorldManager::new(self.is_host(), self.peer.my_id().unwrap()), world: WorldManager::new(
self.is_host(),
self.peer.my_id().unwrap(),
self.init_settings.save_state.clone(),
),
}; };
let mut last_iter = Instant::now(); let mut last_iter = Instant::now();
@ -299,6 +317,7 @@ impl NetManager {
} }
fn on_ws_connection(self: &Arc<NetManager>, state: &mut NetInnerState) { fn on_ws_connection(self: &Arc<NetManager>, state: &mut NetInnerState) {
self.init_settings.save_state.mark_game_started();
info!("New stream connected"); info!("New stream connected");
let stream_ref = &state.ws.as_ref().unwrap().get_ref(); let stream_ref = &state.ws.as_ref().unwrap().get_ref();
stream_ref.set_nonblocking(true).ok(); stream_ref.set_nonblocking(true).ok();
@ -378,7 +397,7 @@ impl NetManager {
} }
} }
pub fn start(self: Arc<NetManager>) { pub fn start(self: Arc<NetManager>) -> JoinHandle<()> {
info!("Starting netmanager"); info!("Starting netmanager");
thread::spawn(move || { thread::spawn(move || {
let result = self.clone().start_inner(); let result = self.clone().start_inner();
@ -388,7 +407,7 @@ impl NetManager {
} }
self.stopped self.stopped
.store(true, std::sync::atomic::Ordering::Relaxed); .store(true, std::sync::atomic::Ordering::Relaxed);
}); })
} }
fn resend_game_settings(&self) { fn resend_game_settings(&self) {
@ -408,6 +427,7 @@ impl NetManager {
Some("game_over") => { Some("game_over") => {
if self.is_host() { if self.is_host() {
info!("Game over, resending game settings"); info!("Game over, resending game settings");
self.init_settings.save_state.reset();
{ {
let mut settings = self.pending_settings.lock().unwrap().clone(); let mut settings = self.pending_settings.lock().unwrap().clone();
if !settings.use_constant_seed { if !settings.use_constant_seed {
@ -453,3 +473,17 @@ impl NetManager {
} }
} }
} }
impl Drop for NetManager {
fn drop(&mut self) {
if self.is_host() {
let run_info = RunInfo {
seed: self.settings.lock().unwrap().seed,
};
self.init_settings.save_state.save(&run_info);
info!("Saved run info");
} else {
info!("Skip saving run info: not a host");
}
}
}

View file

@ -8,6 +8,8 @@ use world_model::{ChunkCoord, ChunkData, ChunkDelta, WorldModel};
pub use world_model::encoding::NoitaWorldUpdate; pub use world_model::encoding::NoitaWorldUpdate;
use crate::bookkeeping::save_state::{SaveState, SaveStateEntry};
use super::{ use super::{
messages::{Destination, MessageRequest}, messages::{Destination, MessageRequest},
omni::OmniPeerId, omni::OmniPeerId,
@ -109,6 +111,7 @@ impl ChunkState {
pub(crate) struct WorldManager { pub(crate) struct WorldManager {
is_host: bool, is_host: bool,
my_peer_id: OmniPeerId, my_peer_id: OmniPeerId,
save_state: SaveState,
/// We receive changes from other clients here, intending to send them to Noita. /// We receive changes from other clients here, intending to send them to Noita.
inbound_model: WorldModel, inbound_model: WorldModel,
/// We use that to create changes to be sent to other clients. /// We use that to create changes to be sent to other clients.
@ -129,15 +132,17 @@ pub(crate) struct WorldManager {
} }
impl WorldManager { impl WorldManager {
pub(crate) fn new(is_host: bool, my_peer_id: OmniPeerId) -> Self { pub(crate) fn new(is_host: bool, my_peer_id: OmniPeerId, save_state: SaveState) -> Self {
let chunk_storage = save_state.load().unwrap_or_default();
WorldManager { WorldManager {
is_host, is_host,
my_peer_id, my_peer_id,
save_state,
inbound_model: Default::default(), inbound_model: Default::default(),
outbound_model: Default::default(), outbound_model: Default::default(),
authority_map: Default::default(), authority_map: Default::default(),
// TODO this needs to be persisted between proxy restarts. // TODO this needs to be persisted between proxy restarts.
chunk_storage: Default::default(), chunk_storage,
chunk_state: Default::default(), chunk_state: Default::default(),
emitted_messages: Default::default(), emitted_messages: Default::default(),
current_update: 0, current_update: 0,
@ -423,6 +428,19 @@ impl WorldManager {
} }
} }
impl Drop for WorldManager {
fn drop(&mut self) {
if self.is_host {
self.save_state.save(&self.chunk_storage);
info!("Saved chunk data");
}
}
}
impl SaveStateEntry for FxHashMap<ChunkCoord, ChunkData> {
const FILENAME: &'static str = "world_chunks";
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{fs::File, io::BufReader}; use std::{fs::File, io::BufReader};