Merge branch '36-savegame-dumping' into 'dev'

Add savegame dumping

See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!10
This commit is contained in:
Dominik Bernroider
2025-11-26 17:45:15 +00:00
16 changed files with 260 additions and 14 deletions

66
Cargo.lock generated
View File

@@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.3.4",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@@ -1092,7 +1092,7 @@ dependencies = [
"critical-section", "critical-section",
"foldhash 0.2.0", "foldhash 0.2.0",
"futures-channel", "futures-channel",
"getrandom", "getrandom 0.3.4",
"hashbrown 0.16.0", "hashbrown 0.16.0",
"js-sys", "js-sys",
"portable-atomic", "portable-atomic",
@@ -2079,6 +2079,27 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "directories"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "dispatch" name = "dispatch"
version = "0.2.0" version = "0.2.0"
@@ -2380,6 +2401,17 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@@ -2783,7 +2815,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.4",
"libc", "libc",
] ]
@@ -3464,6 +3496,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.48" version = "0.3.48"
@@ -3622,6 +3660,7 @@ dependencies = [
"bevy", "bevy",
"bevy_aseprite_ultra", "bevy_aseprite_ultra",
"bevy_dev_tools", "bevy_dev_tools",
"directories",
"serde", "serde",
"serde_json", "serde_json",
] ]
@@ -3754,7 +3793,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.4",
] ]
[[package]] [[package]]
@@ -3819,6 +3858,17 @@ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
] ]
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.2"
@@ -4575,7 +4625,7 @@ version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.4",
"js-sys", "js-sys",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
@@ -4620,6 +4670,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.1+wasi-0.2.4" version = "1.0.1+wasi-0.2.4"

View File

@@ -19,3 +19,4 @@ bevy_aseprite_ultra = "0.7.0"
bevy_dev_tools = "0.17.2" bevy_dev_tools = "0.17.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
directories = "6.0"

View File

@@ -1,4 +1,5 @@
pub mod phase; pub mod phase;
pub mod pom; pub mod pom;
pub mod savegame;
pub mod tile; pub mod tile;
pub mod ui; pub mod ui;

View File

@@ -1,6 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Phase { pub enum Phase {
Break { duration: f32 }, Break { duration: f32 },
Focus { duration: f32 }, Focus { duration: f32 },
@@ -51,10 +52,10 @@ impl Phase {
} }
} }
#[derive(Resource, Debug)] #[derive(Resource, Debug, Serialize, Deserialize, Clone)]
pub struct CurrentPhase(pub Phase); pub struct CurrentPhase(pub Phase);
#[derive(Resource, Debug)] #[derive(Resource, Debug, Serialize, Deserialize, Clone)]
pub struct TimerSettings { pub struct TimerSettings {
pub focus_duration: f32, pub focus_duration: f32,
pub short_break_duration: f32, pub short_break_duration: f32,
@@ -73,7 +74,7 @@ impl Default for TimerSettings {
} }
} }
#[derive(Resource, Debug, Default)] #[derive(Resource, Debug, Default, Serialize, Deserialize, Clone)]
pub struct SessionTracker { pub struct SessionTracker {
pub completed_focus_phases: u32, pub completed_focus_phases: u32,
} }

View File

@@ -1,11 +1,12 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Component)] #[derive(Component)]
pub struct Pom; pub struct Pom;
#[derive(Component)] #[derive(Component, Serialize, Deserialize, Clone, Copy)]
pub struct GridPosition { pub struct GridPosition {
pub x: u32, pub x: u32,
pub y: u32, pub y: u32,

View File

@@ -0,0 +1,33 @@
use bevy::prelude::*;
use directories::ProjectDirs;
use std::path::PathBuf;
#[derive(Resource)]
pub struct SavegamePath(pub PathBuf);
impl SavegamePath {
fn get_project_path() -> Option<PathBuf> {
let project_dirs = ProjectDirs::from("de", "demenik", "pomomon-garden");
if let Some(dirs) = project_dirs {
Some(dirs.data_local_dir().to_path_buf())
} else {
None
}
}
pub fn new(name: &str) -> Self {
let base_path = Self::get_project_path().unwrap_or_else(|| {
println!(
"Could not determine platform-specific save directory. Falling back to `./saves/"
);
PathBuf::from("./saves/")
});
if let Err(e) = std::fs::create_dir_all(&base_path) {
panic!("Failed to create save directory at {:?}: {}", base_path, e);
}
Self(base_path.join(name))
}
}

View File

@@ -1,4 +1,5 @@
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::errors::GridError; use crate::errors::GridError;
@@ -8,7 +9,7 @@ pub struct Tile {
pub y: u32, pub y: u32,
} }
#[derive(Component, Default)] #[derive(Component, Default, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum TileState { pub enum TileState {
#[default] #[default]
Unclaimed, Unclaimed,

View File

@@ -8,3 +8,8 @@ pub struct UiPhaseText;
#[derive(Component)] #[derive(Component)]
pub struct UiTimerText; pub struct UiTimerText;
#[derive(Component)]
pub enum UiStatusButton {
SavegameDump,
}

View File

@@ -33,6 +33,7 @@ fn main() {
plugins::InputPlugin, plugins::InputPlugin,
plugins::PhasePlugin, plugins::PhasePlugin,
plugins::StatusPlugin, plugins::StatusPlugin,
plugins::SavegamePlugin,
)) ))
.insert_resource(config) .insert_resource(config)
.run(); .run();

View File

@@ -1,3 +1,4 @@
pub mod interact; pub mod interact;
pub mod r#move; pub mod r#move;
pub mod phase; pub mod phase;
pub mod savegame;

4
src/messages/savegame.rs Normal file
View File

@@ -0,0 +1,4 @@
use bevy::prelude::*;
#[derive(Message)]
pub struct SavegameDumpMessage;

View File

@@ -4,6 +4,7 @@ pub mod grid;
pub mod input; pub mod input;
pub mod phase; pub mod phase;
pub mod pom; pub mod pom;
pub mod savegame;
pub mod start_screen; pub mod start_screen;
pub mod status; pub mod status;
@@ -13,5 +14,6 @@ pub use grid::GridPlugin;
pub use input::InputPlugin; pub use input::InputPlugin;
pub use phase::PhasePlugin; pub use phase::PhasePlugin;
pub use pom::PomPlugin; pub use pom::PomPlugin;
pub use savegame::SavegamePlugin;
pub use start_screen::StartScreenPlugin; pub use start_screen::StartScreenPlugin;
pub use status::StatusPlugin; pub use status::StatusPlugin;

View File

@@ -1,4 +1,8 @@
use crate::{components::phase::*, messages::phase::*, states::AppState}; use crate::{
components::phase::*,
messages::{phase::*, savegame::SavegameDumpMessage},
states::AppState,
};
use bevy::prelude::*; use bevy::prelude::*;
pub struct PhasePlugin; pub struct PhasePlugin;
@@ -63,6 +67,7 @@ fn tick_timer(
time: Res<Time>, time: Res<Time>,
mut phase_res: ResMut<CurrentPhase>, mut phase_res: ResMut<CurrentPhase>,
mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>, mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>,
mut savegame_messages: MessageWriter<SavegameDumpMessage>,
) { ) {
let delta = time.delta_secs(); let delta = time.delta_secs();
let phase = &mut phase_res.0; let phase = &mut phase_res.0;
@@ -81,6 +86,7 @@ fn tick_timer(
}; };
println!("phase ended"); println!("phase ended");
savegame_messages.write(SavegameDumpMessage);
} }
} }
_ => {} _ => {}

95
src/plugins/savegame.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::{
components::{
phase::{CurrentPhase, SessionTracker, TimerSettings},
pom::{GridPosition, Pom},
savegame::SavegamePath,
tile::{Grid, TileState},
},
messages::savegame::*,
states::AppState,
};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
pub struct SavegamePlugin;
impl Plugin for SavegamePlugin {
fn build(&self, app: &mut App) {
app.add_message::<SavegameDumpMessage>();
app.add_systems(Update, dump_savegame.run_if(in_state(AppState::GameScreen)));
}
}
#[derive(Serialize, Deserialize)]
struct SaveData {
grid_width: u32,
grid_height: u32,
tiles: Vec<Vec<TileState>>,
current_phase: CurrentPhase,
session_tracker: SessionTracker,
timer_settings: TimerSettings,
pom_position: GridPosition,
}
fn dump_savegame(
mut messages: MessageReader<SavegameDumpMessage>,
grid: Res<Grid>,
tile_query: Query<&TileState>,
phase: Res<CurrentPhase>,
tracker: Res<SessionTracker>,
settings: Res<TimerSettings>,
pom_query: Query<&GridPosition, With<Pom>>,
save_path: Res<SavegamePath>,
) {
for _ in messages.read() {
let mut tile_states = Vec::new();
for x in 0..grid.width {
let mut col = Vec::new();
for y in 0..grid.height {
if let Ok(entity) = grid.get_tile((x, y)) {
if let Ok(state) = tile_query.get(entity) {
col.push(*state);
} else {
col.push(TileState::Unclaimed);
}
} else {
col.push(TileState::Unclaimed);
}
}
tile_states.push(col);
}
let pom_pos = pom_query.single().unwrap();
let save_data = SaveData {
grid_width: grid.width,
grid_height: grid.height,
tiles: tile_states,
current_phase: phase.clone(),
session_tracker: tracker.clone(),
timer_settings: settings.clone(),
pom_position: *pom_pos,
};
match serde_json::to_string_pretty(&save_data) {
Ok(serialized) => {
if let Ok(mut file) = File::create(&save_path.0) {
if let Err(e) = file.write_all(serialized.as_bytes()) {
panic!("Failed to write save file: {}", e);
} else {
println!("Game saved to {}", save_path.0.display());
}
} else {
panic!("Failed to create save file at {}", save_path.0.display());
}
}
Err(e) => {
panic!("Failed to serialize save data: {}", e);
}
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::states::*; use crate::{components::savegame::SavegamePath, states::*};
use bevy::prelude::*; use bevy::prelude::*;
pub struct StartScreenPlugin; pub struct StartScreenPlugin;
@@ -115,6 +115,7 @@ fn setup(mut commands: Commands) {
} }
fn menu( fn menu(
mut commands: Commands,
mut next_state: ResMut<NextState<AppState>>, mut next_state: ResMut<NextState<AppState>>,
mut interaction_query: Query< mut interaction_query: Query<
(&Interaction, &ButtonType, &mut BackgroundColor), (&Interaction, &ButtonType, &mut BackgroundColor),
@@ -127,7 +128,10 @@ fn menu(
*color = PRESSED_BUTTON.into(); *color = PRESSED_BUTTON.into();
match button_type { match button_type {
ButtonType::NewGame => next_state.set(AppState::GameScreen), ButtonType::NewGame => {
commands.insert_resource(SavegamePath::new("savegame.json"));
next_state.set(AppState::GameScreen);
}
_ => (), _ => (),
}; };
} }

View File

@@ -1,5 +1,6 @@
use crate::{ use crate::{
components::{phase::CurrentPhase, ui::*}, components::{phase::CurrentPhase, ui::*},
messages::savegame::SavegameDumpMessage,
states::AppState, states::AppState,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -11,6 +12,10 @@ impl Plugin for StatusPlugin {
app.add_systems(OnEnter(AppState::GameScreen), setup); app.add_systems(OnEnter(AppState::GameScreen), setup);
app.add_systems(OnExit(AppState::GameScreen), cleanup); app.add_systems(OnExit(AppState::GameScreen), cleanup);
app.add_systems(Update, update_status.run_if(in_state(AppState::GameScreen))); app.add_systems(Update, update_status.run_if(in_state(AppState::GameScreen)));
app.add_systems(
Update,
status_buttons.run_if(in_state(AppState::GameScreen)),
);
} }
} }
@@ -43,6 +48,16 @@ fn setup(mut commands: Commands) {
Text::new("--:--"), Text::new("--:--"),
TextFont::from_font_size(16.0), TextFont::from_font_size(16.0),
TextColor(Color::WHITE) TextColor(Color::WHITE)
),
(
Button,
UiStatusButton::SavegameDump,
Node::default(),
children![
Text::new("Save"),
TextFont::from_font_size(16.0),
TextColor(Color::WHITE)
]
) )
], ],
)); ));
@@ -67,6 +82,25 @@ fn update_status(
} }
} }
fn status_buttons(
mut interaction_query: Query<
(&Interaction, &UiStatusButton),
(Changed<Interaction>, With<Button>),
>,
mut savegamedump_messages: MessageWriter<SavegameDumpMessage>,
) {
for (interaction, button_type) in &mut interaction_query {
match *interaction {
Interaction::Pressed => match button_type {
UiStatusButton::SavegameDump => {
savegamedump_messages.write(SavegameDumpMessage);
}
},
_ => {}
}
}
}
fn cleanup(mut commands: Commands, query: Query<Entity, With<UiStatusRootContainer>>) { fn cleanup(mut commands: Commands, query: Query<Entity, With<UiStatusRootContainer>>) {
for entity in query.iter() { for entity in query.iter() {
commands.entity(entity).despawn(); commands.entity(entity).despawn();