noita_entangled_worlds/noita-proxy/src/net.rs

805 lines
31 KiB
Rust

use bitcode::{Decode, Encode};
use messages::{MessageRequest, NetMsg};
use omni::OmniPeerId;
use proxy_opt::ProxyOpt;
use shared::{NoitaInbound, NoitaOutbound};
use socket2::{Domain, Socket, Type};
use std::fs::{create_dir, remove_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU16, Ordering};
use std::sync::{Arc, Mutex};
use std::{
env,
fmt::Display,
io::{self},
net::{SocketAddr, TcpListener, TcpStream},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use world::{world_info::WorldInfo, NoitaWorldUpdate, WorldManager};
use tangled::Reliability;
use tracing::{error, info, warn};
use tungstenite::{accept, WebSocket};
use crate::mod_manager::ModmanagerSettings;
use crate::player_cosmetics::{create_player_png, PlayerPngDesc};
use crate::{
bookkeeping::save_state::{SaveState, SaveStateEntry},
recorder::Recorder,
DefaultSettings, GameSettings, PlayerColor, UXSettings,
};
pub mod messages;
mod proxy_opt;
pub mod steam_networking;
pub mod world;
fn ws_bitcode(proxy_kv: &NoitaInbound) -> tungstenite::Message {
tungstenite::Message::Binary(bitcode::encode(proxy_kv))
}
pub(crate) fn ws_encode_proxy(key: &'static str, value: impl Display) -> tungstenite::Message {
let mut buf = Vec::new();
buf.push(2);
write!(buf, "{} {}", key, value).unwrap();
ws_bitcode(&NoitaInbound::RawMessage(buf))
}
pub fn ws_encode_proxy_bin(key: u8, data: &[u8]) -> tungstenite::Message {
let mut buf = Vec::new();
buf.push(3);
buf.push(key);
buf.extend(data);
ws_bitcode(&NoitaInbound::RawMessage(buf))
}
pub(crate) fn ws_encode_mod(peer: OmniPeerId, data: &[u8]) -> tungstenite::Message {
let mut buf = Vec::new();
buf.push(1u8);
buf.extend_from_slice(&peer.0.to_le_bytes());
buf.extend_from_slice(data);
ws_bitcode(&NoitaInbound::RawMessage(buf))
}
// pub(crate) fn ws_encode_proxy(key: &'static str, value: impl Display) -> tungstenite::Message {
// ws_bitcode(&NoitaInbound::ProxyKV(ProxyKV {
// key: key.to_owned(),
// value: value.to_string(),
// }))
// }
// pub fn ws_encode_proxy_bin(key: u8, data: &[u8]) -> tungstenite::Message {
// ws_bitcode(&NoitaInbound::ProxyKVBin(ProxyKVBin {
// key,
// value: data.to_owned(),
// }))
// }
// pub(crate) fn ws_encode_mod(peer: OmniPeerId, data: &[u8]) -> tungstenite::Message {
// ws_bitcode(&NoitaInbound::ModMessage(ModMessage {
// peer: peer.into(),
// value: data.to_owned(),
// }))
// }
pub struct DebugMarker {
pub x: f64,
pub y: f64,
pub message: String,
}
#[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) ws: Option<WebSocket<TcpStream>>,
world: WorldManager,
recorder: Option<Recorder>,
}
impl NetInnerState {
pub(crate) fn try_ws_write(&mut self, data: tungstenite::Message) {
if let Some(ws) = &mut self.ws {
if let Some(recorder) = &mut self.recorder {
recorder.record_msg(&data);
}
if let Err(err) = ws.write(data) {
error!("Error occured while sending to websocket: {}", err);
self.ws = None;
self.recorder = None;
};
}
}
pub(crate) fn try_ws_write_option(&mut self, key: &str, value: impl ProxyOpt) {
let mut buf = Vec::new();
buf.push(2);
value.write_opt(&mut buf, key);
let message = ws_bitcode(&NoitaInbound::RawMessage(buf));
self.try_ws_write(message);
}
}
pub mod omni;
pub struct NetManagerInit {
pub my_nickname: Option<String>,
pub save_state: SaveState,
pub player_color: PlayerColor,
pub ux_settings: UXSettings,
pub cosmetics: (bool, bool, bool),
pub mod_path: PathBuf,
pub player_path: PathBuf,
pub modmanager_settings: ModmanagerSettings,
pub player_png_desc: PlayerPngDesc,
pub noita_port: u16,
}
pub struct NetManager {
pub peer: omni::PeerVariant,
pub pending_settings: Mutex<GameSettings>,
pub settings: Mutex<GameSettings>,
pub continue_running: AtomicBool,
pub accept_local: AtomicBool,
pub local_connected: AtomicBool,
pub stopped: AtomicBool,
pub error: Mutex<Option<io::Error>>,
pub init_settings: NetManagerInit,
pub world_info: WorldInfo,
pub enable_recorder: AtomicBool,
pub end_run: AtomicBool,
pub debug_markers: Mutex<Vec<DebugMarker>>,
pub friendly_fire_team: AtomicI32,
pub friendly_fire: AtomicBool,
pub ban_list: Mutex<Vec<OmniPeerId>>,
pub kick_list: Mutex<Vec<OmniPeerId>>,
pub no_more_players: AtomicBool,
dont_kick: Mutex<Vec<OmniPeerId>>,
pub dirty: AtomicBool,
pub actual_noita_port: AtomicU16,
}
impl NetManager {
pub fn new(peer: omni::PeerVariant, init: NetManagerInit) -> Arc<Self> {
Self {
peer,
pending_settings: Mutex::new(GameSettings::default()),
settings: Mutex::new(GameSettings::default()),
continue_running: AtomicBool::new(true),
accept_local: AtomicBool::new(false),
local_connected: AtomicBool::new(false),
stopped: AtomicBool::new(false),
error: Default::default(),
init_settings: init,
world_info: Default::default(),
enable_recorder: AtomicBool::new(false),
end_run: AtomicBool::new(false),
debug_markers: Default::default(),
friendly_fire_team: AtomicI32::new(-2),
friendly_fire: AtomicBool::new(false),
ban_list: Default::default(),
kick_list: Default::default(),
no_more_players: AtomicBool::new(false),
dont_kick: Default::default(),
dirty: AtomicBool::new(false),
actual_noita_port: AtomicU16::new(0),
}
.into()
}
pub(crate) fn send(&self, peer: OmniPeerId, msg: &NetMsg, reliability: Reliability) {
let encoded = lz4_flex::compress_prepend_size(&bitcode::encode(msg));
self.peer.send(peer, encoded.clone(), reliability).ok(); // TODO log
}
pub(crate) fn broadcast(&self, msg: &NetMsg, reliability: Reliability) {
let encoded = lz4_flex::compress_prepend_size(&bitcode::encode(msg));
let len = encoded.len();
if let Err(err) = self.peer.broadcast(encoded, reliability) {
warn!("Error while broadcasting message of len {}: {}", len, err)
}
}
fn clean_dir(path: PathBuf) {
let tmp = path.parent().unwrap().join("tmp");
if tmp.exists() {
remove_dir_all(tmp.clone()).ok();
}
create_dir(tmp).ok();
}
pub(crate) fn start_inner(
self: Arc<NetManager>,
player_path: PathBuf,
mut cli: bool,
) -> io::Result<()> {
Self::clean_dir(player_path.clone());
if !self.init_settings.cosmetics.0 {
File::create(player_path.parent().unwrap().join("tmp/no_crown"))?;
}
if !self.init_settings.cosmetics.1 {
File::create(player_path.parent().unwrap().join("tmp/no_amulet"))?;
}
if !self.init_settings.cosmetics.2 {
File::create(player_path.parent().unwrap().join("tmp/no_amulet_gem"))?;
}
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
// This allows several proxies to listen on the same address.
// While this works, I couldn't get Noita to reliably connect to correct proxy instances on my os (linux).
if env::var_os("NP_ALLOW_REUSE_ADDR").is_some() {
info!("Address reuse allowed");
if let Err(err) = socket.set_reuse_address(true) {
error!("Could not allow to reuse address: {}", err)
}
#[cfg(target_os = "linux")]
if let Err(err) = socket.set_reuse_port(true) {
error!("Could not allow to reuse port: {}", err)
}
}
let address: SocketAddr = env::var("NP_NOITA_ADDR")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or_else(|| {
SocketAddr::new("127.0.0.1".parse().unwrap(), self.init_settings.noita_port)
});
info!("Listening for noita connection on {}", address);
let address = address.into();
socket.bind(&address)?;
socket.listen(1)?;
socket.set_nonblocking(true)?;
let actual_port = socket.local_addr()?.as_socket().unwrap().port();
self.actual_noita_port.store(actual_port, Ordering::Relaxed);
info!("Actual Noita port: {actual_port}");
let local_server: TcpListener = socket.into();
let is_host = self.is_host();
info!("Is host: {is_host}");
let mut state = NetInnerState {
ws: None,
recorder: None,
world: WorldManager::new(
is_host,
self.peer.my_id(),
self.init_settings.save_state.clone(),
),
};
let mut last_iter = Instant::now();
// Create appearance files for local player.
create_player_png(
self.peer.my_id(),
&self.init_settings.mod_path,
&self.init_settings.player_path,
&self.init_settings.player_png_desc,
self.is_host(),
);
let mut timer = Instant::now();
while self.continue_running.load(Ordering::Relaxed) {
if cli {
if let Some(n) = self.peer.lobby_id() {
println!("Lobby ID: {}", n.raw());
cli = false
}
}
if self.friendly_fire.load(Ordering::Relaxed) && timer.elapsed().as_secs() > 4 {
let team = self.friendly_fire_team.load(Ordering::Relaxed);
state.try_ws_write_option("friendly_fire_team", (team + 1) as u32);
timer = Instant::now()
}
if self.end_run.load(Ordering::Relaxed) {
for id in self.peer.iter_peer_ids() {
self.send(id, &NetMsg::EndRun, Reliability::Reliable);
}
state.try_ws_write(ws_encode_proxy("end_run", self.peer.my_id().to_string()));
self.end_run(&mut state);
self.end_run.store(false, Ordering::Relaxed);
}
self.local_connected
.store(state.ws.is_some(), Ordering::Relaxed);
if state.ws.is_none() && self.accept_local.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(10));
if let Ok((stream, addr)) = local_server.accept() {
info!("New stream incoming from {}", addr);
stream.set_nodelay(true).ok();
stream.set_nonblocking(false).ok();
state.ws = accept(stream)
.inspect_err(|e| error!("Could not init websocket: {}", e))
.ok();
if state.ws.is_some() {
if self.enable_recorder.load(Ordering::Relaxed) {
state.recorder = Some(Recorder::default());
}
self.on_ws_connection(&mut state);
}
}
}
if let Some(ws) = &mut state.ws {
if let Err(err) = ws.flush() {
warn!("Websocket flush not ok: {err}");
}
}
let mut to_kick = self.kick_list.lock().unwrap();
let mut dont_kick = self.dont_kick.lock().unwrap();
if self.no_more_players.load(Ordering::Relaxed) {
if dont_kick.is_empty() {
dont_kick.extend(self.peer.iter_peer_ids())
} else {
for peer in self.peer.iter_peer_ids() {
if !dont_kick.contains(&peer) {
to_kick.push(peer);
}
}
}
} else {
dont_kick.clear()
}
let list = self.ban_list.lock().unwrap();
for peer in list.iter() {
if self.peer.iter_peer_ids().contains(peer) {
to_kick.push(*peer)
}
}
for peer in to_kick.iter() {
info!("player kicked: {}", peer);
state.try_ws_write(ws_encode_proxy("leave", peer.as_hex()));
state.world.handle_peer_left(*peer);
self.send(*peer, &NetMsg::Kick, Reliability::Reliable);
self.broadcast(
&NetMsg::PeerDisconnected { id: *peer },
Reliability::Reliable,
);
}
to_kick.clear();
for net_event in self.peer.recv() {
match net_event {
omni::OmniNetworkEvent::PeerConnected(id) => {
self.broadcast(&NetMsg::Welcome, Reliability::Reliable);
info!("Peer connected {id}");
if self.peer.my_id() == self.peer.host_id() {
info!("Sending start game message");
self.send(
id,
&NetMsg::StartGame {
settings: self.settings.lock().unwrap().clone(),
},
Reliability::Reliable,
);
}
if id != self.peer.my_id() {
// Create temporary appearance files for new player.
info!("Created temporary appearance for {id}");
create_player_png(
id,
&self.init_settings.mod_path,
&self.init_settings.player_path,
&PlayerPngDesc::default(),
id == self.peer.host_id(),
);
info!("Sending PlayerColor to {id}");
self.send(
id,
&NetMsg::PlayerColor(
self.init_settings.player_png_desc,
self.is_host(),
Some(self.peer.my_id()),
),
Reliability::Reliable,
);
}
state.try_ws_write(ws_encode_proxy("join", id.as_hex()));
}
omni::OmniNetworkEvent::PeerDisconnected(id) => {
state.try_ws_write(ws_encode_proxy("leave", id.as_hex()));
state.world.handle_peer_left(id);
}
omni::OmniNetworkEvent::Message { src, data } => {
let Some(net_msg) = lz4_flex::decompress_size_prepended(&data)
.ok()
.and_then(|decomp| bitcode::decode::<NetMsg>(&decomp).ok())
else {
continue;
};
match net_msg {
NetMsg::Welcome => {}
NetMsg::PeerDisconnected { id } => {
info!("player kicked: {}", id);
state.try_ws_write(ws_encode_proxy("leave", id.as_hex()));
state.world.handle_peer_left(id);
}
NetMsg::EndRun => state.try_ws_write(ws_encode_proxy(
"end_run",
self.peer.my_id().to_string(),
)),
NetMsg::StartGame { settings } => {
*self.settings.lock().unwrap() = settings;
info!("Settings updated");
self.accept_local.store(true, Ordering::SeqCst);
state.world.reset();
}
NetMsg::ModRaw { data } => {
state.try_ws_write(ws_encode_mod(src, &data));
}
NetMsg::ModCompressed { data } => {
if let Ok(decompressed) = lz4_flex::decompress_size_prepended(&data)
{
state.try_ws_write(ws_encode_mod(src, &decompressed));
}
}
NetMsg::WorldMessage(msg) => state.world.handle_msg(src, msg),
NetMsg::PlayerColor(rgb, host, pong) => {
info!("Player appearance created for {}", src);
// Create proper appearance files for new player.
create_player_png(
src,
&self.init_settings.mod_path,
&self.init_settings.player_path,
&rgb,
host,
);
if let Some(id) = pong {
self.send(
id,
&NetMsg::PlayerColor(
self.init_settings.player_png_desc,
self.is_host(),
None,
),
Reliability::Reliable,
);
}
}
NetMsg::Kick => std::process::exit(0),
}
}
}
}
// Handle all available messages from Noita.
while let Some(ws) = &mut state.ws {
let msg = ws.read();
match msg {
Ok(msg) => {
if let tungstenite::Message::Binary(msg) = msg {
if let Ok(msg) = bitcode::decode(&msg) {
self.handle_mod_message_2(msg, &mut state);
}
}
}
Err(tungstenite::Error::Io(io_err))
if io_err.kind() == io::ErrorKind::WouldBlock
|| io_err.kind() == io::ErrorKind::TimedOut =>
{
break
}
Err(err) => {
warn!("Game closed (Lost connection to noita instance: {})", err);
state.ws = None;
}
}
}
for msg in state.world.get_emitted_msgs() {
self.do_message_request(msg)
}
state.world.update();
// TODO maybe shouldn't be always enabled.
*self.debug_markers.lock().unwrap() = state.world.get_debug_markers();
let updates = state.world.get_noita_updates();
for update in updates {
state.try_ws_write(ws_encode_proxy_bin(0, &update));
}
// Don't do excessive busy-waiting;
let min_update_time = Duration::from_millis(1);
let elapsed = last_iter.elapsed();
if elapsed < min_update_time {
thread::sleep(min_update_time - elapsed);
}
last_iter = Instant::now();
}
Ok(())
}
fn do_message_request(&self, request: impl Into<MessageRequest<NetMsg>>) {
let request: MessageRequest<NetMsg> = request.into();
match request.dst {
messages::Destination::Peer(peer) => {
self.send(peer, &request.msg, request.reliability);
}
messages::Destination::Host => {
self.send(self.peer.host_id(), &request.msg, request.reliability);
}
messages::Destination::Broadcast => self.broadcast(&request.msg, request.reliability),
}
}
fn on_ws_connection(self: &Arc<NetManager>, state: &mut NetInnerState) {
self.init_settings.save_state.mark_game_started();
info!("New stream connected");
let stream_ref = &state.ws.as_ref().unwrap().get_ref();
stream_ref.set_nonblocking(true).ok();
stream_ref
.set_read_timeout(Some(Duration::from_millis(1)))
.expect("can set read timeout");
// Set write timeout to a somewhat high value just in case.
stream_ref
.set_write_timeout(Some(Duration::from_secs(5)))
.expect("can set write timeout");
let settings = self.settings.lock().unwrap();
let def = DefaultSettings::default();
state.try_ws_write(ws_encode_proxy("seed", settings.seed));
let my_id = self.peer.my_id();
state.try_ws_write(ws_encode_proxy("peer_id", format!("{:016x}", my_id.0)));
state.try_ws_write(ws_encode_proxy(
"host_id",
format!("{:016x}", self.peer.host_id().0),
));
if let Some(nickname) = &self.init_settings.my_nickname {
info!("Chosen nickname: {}", nickname);
state.try_ws_write_option("name", nickname.as_str());
} else {
info!("No nickname chosen");
}
let ff = settings.friendly_fire.unwrap_or(def.friendly_fire);
self.friendly_fire.store(ff, Ordering::Relaxed);
state.try_ws_write_option("friendly_fire", ff);
state.try_ws_write_option("debug", settings.debug_mode.unwrap_or(def.debug_mode));
state.try_ws_write_option(
"world_sync_version",
settings
.world_sync_version
.unwrap_or(def.world_sync_version),
);
state.try_ws_write_option(
"player_tether",
settings.player_tether.unwrap_or(def.player_tether),
);
state.try_ws_write_option(
"tether_length",
settings.tether_length.unwrap_or(def.tether_length),
);
state.try_ws_write_option("item_dedup", settings.item_dedup.unwrap_or(def.item_dedup));
state.try_ws_write_option(
"randomize_perks",
settings.randomize_perks.unwrap_or(def.randomize_perks),
);
state.try_ws_write_option(
"enemy_hp_scale",
settings.enemy_hp_mult.unwrap_or(def.enemy_hp_mult),
);
state.try_ws_write_option(
"world_sync_interval",
settings
.world_sync_interval
.unwrap_or(def.world_sync_interval),
);
state.try_ws_write_option("game_mode", settings.game_mode.unwrap_or(def.game_mode));
state.try_ws_write_option(
"chunk_target",
settings.chunk_target.unwrap_or(def.chunk_target),
);
state.try_ws_write_option(
"health_per_player",
settings.health_per_player.unwrap_or(def.health_per_player),
);
state.try_ws_write_option(
"enemy_sync_interval",
settings
.enemy_sync_interval
.unwrap_or(def.enemy_sync_interval),
);
state.try_ws_write_option(
"global_hp_loss",
settings.global_hp_loss.unwrap_or(def.global_hp_loss),
);
state.try_ws_write_option(
"perma_death",
settings.perma_death.unwrap_or(def.perma_death),
);
state.try_ws_write_option(
"physics_damage",
settings.physics_damage.unwrap_or(def.physics_damage),
);
let lst = settings.clone();
state.try_ws_write_option(
"perk_ban_list",
lst.perk_ban_list.unwrap_or(def.perk_ban_list).as_str(),
);
state.try_ws_write_option(
"no_material_damage",
settings
.no_material_damage
.unwrap_or(def.no_material_damage),
);
state.try_ws_write_option(
"health_lost_on_revive",
settings
.health_lost_on_revive
.unwrap_or(def.health_lost_on_revive),
);
let rgb = self.init_settings.player_color.player_main;
state.try_ws_write_option(
"mina_color",
rgb[0] as u32 + ((rgb[1] as u32) << 8) + ((rgb[2] as u32) << 16),
);
state.try_ws_write_option(
"ping_lifetime",
self.init_settings.ux_settings.ping_lifetime(),
);
state.try_ws_write_option("ping_scale", self.init_settings.ux_settings.ping_scale());
let progress = settings.progress.join(",");
state.try_ws_write_option("progress", progress.as_str());
state.try_ws_write(ws_bitcode(&NoitaInbound::Ready));
// TODO? those are currently ignored by mod
for id in self.peer.iter_peer_ids() {
state.try_ws_write(ws_encode_proxy("join", id.as_hex()));
}
info!("Settings sent")
}
fn handle_mod_message_2(&self, msg: NoitaOutbound, state: &mut NetInnerState) {
match msg {
NoitaOutbound::Raw(raw_msg) => {
match raw_msg[0] & 0b11 {
// Message to proxy
1 => {
self.handle_message_to_proxy(&raw_msg[1..], state);
}
// Broadcast
2 => {
let msg_to_send = NetMsg::ModRaw {
data: raw_msg[1..].to_owned(),
};
let reliable = raw_msg[0] & 4 > 0;
self.broadcast(
&msg_to_send,
if reliable {
Reliability::Reliable
} else {
Reliability::Unreliable
},
);
}
// Binary message to proxy
3 => self.handle_bin_message_to_proxy(&raw_msg[1..], state),
msg_variant => {
error!("Unknown msg variant from mod: {}", msg_variant)
}
}
}
}
}
pub fn start(self: Arc<NetManager>, player_path: PathBuf) -> JoinHandle<()> {
info!("Starting netmanager");
thread::spawn(move || {
let result = self.clone().start_inner(player_path, false);
if let Err(err) = result {
error!("Error in netmanager: {}", err);
*self.error.lock().unwrap() = Some(err);
}
self.stopped.store(true, Ordering::Relaxed);
})
}
fn resend_game_settings(&self) {
let settings = self.settings.lock().unwrap().clone();
self.broadcast(&NetMsg::StartGame { settings }, Reliability::Reliable);
}
fn is_host(&self) -> bool {
self.peer.is_host()
}
pub(crate) fn handle_message_to_proxy(&self, msg: &[u8], state: &mut NetInnerState) {
let msg = String::from_utf8_lossy(msg);
let mut msg = msg.split_ascii_whitespace();
let key = msg.next();
match key {
Some("game_over") => {
if self.is_host() {
info!("Game over, resending game settings");
self.end_run(state)
}
}
Some("peer_pos") => {
let peer_id = msg.next().and_then(OmniPeerId::from_hex);
let x: Option<f64> = msg.next().and_then(|s| s.parse().ok());
let y: Option<f64> = msg.next().and_then(|s| s.parse().ok());
if let (Some(peer_id), Some(x), Some(y)) = (peer_id, x, y) {
self.world_info.update_player_pos(peer_id, x, y);
}
}
Some("reset_world") => state.world.reset(),
Some("cut_through_world") => {
let x: Option<i32> = msg.next().and_then(|s| s.parse().ok());
let y_min: Option<i32> = msg.next().and_then(|s| s.parse().ok());
let y_max: Option<i32> = msg.next().and_then(|s| s.parse().ok());
let radius: Option<i32> = msg.next().and_then(|s| s.parse().ok());
let (Some(x), Some(y_min), Some(y_max), Some(radius)) = (x, y_min, y_max, radius)
else {
error!("Missing arguments in cut_through_world message");
return;
};
state.world.cut_through_world(x, y_min, y_max, radius);
}
Some("flush") => self.peer.flush(),
key => {
error!("Unknown msg from mod: {:?}", key)
}
}
}
fn handle_bin_message_to_proxy(&self, msg: &[u8], state: &mut NetInnerState) {
let key = msg[0];
let data = &msg[1..];
match key {
// world frame
0 => {
let update = NoitaWorldUpdate::load(data);
state.world.add_update(update);
}
// world end
1 => {
let pos = if data.len() > 1 {
let pos = data[1..]
.split(|b| *b == b':')
.map(|s| String::from_utf8_lossy(s).parse::<i32>().unwrap_or(0))
.collect::<Vec<i32>>();
Some(pos)
} else {
None
};
state.world.add_end(data[0], pos.as_deref());
}
key => {
error!("Unknown bin msg from mod: {:?}", key)
}
}
}
fn end_run(&self, state: &mut NetInnerState) {
self.init_settings.save_state.reset();
{
let mut settings = self.pending_settings.lock().unwrap().clone();
if !settings.use_constant_seed {
settings.seed = rand::random();
}
info!("New seed: {}", settings.seed);
settings.progress = self
.init_settings
.modmanager_settings
.get_progress()
.unwrap_or_default();
*self.settings.lock().unwrap() = settings;
state.world.reset();
self.dirty.store(false, Ordering::Relaxed);
}
self.resend_game_settings();
}
}
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");
}
}
}