Add game saving support

This commit is contained in:
Alula 2020-10-30 02:29:53 +01:00
parent eb94343df3
commit 451c3671d7
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
8 changed files with 280 additions and 82 deletions

View File

@ -44,8 +44,9 @@ const CONFIG_NAME: &str = "/conf.toml";
#[derive(Debug)]
pub struct Filesystem {
vfs: vfs::OverlayFS,
user_vfs: vfs::OverlayFS,
//resources_path: path::PathBuf,
user_config_path: path::PathBuf,
//user_config_path: path::PathBuf,
user_data_path: path::PathBuf,
}
@ -112,9 +113,11 @@ impl Filesystem {
// Set up VFS to merge resource path, root path, and zip path.
let mut overlay = vfs::OverlayFS::new();
// User data VFS.
let mut user_overlay = vfs::OverlayFS::new();
let user_data_path;
let user_config_path;
//let user_config_path;
// let mut resources_path;
// let mut resources_zip_path;
@ -155,22 +158,23 @@ impl Filesystem {
{
user_data_path = project_dirs.data_local_dir();
log::trace!("User-local data path: {:?}", user_data_path);
let physfs = vfs::PhysicalFS::new(&user_data_path, true);
overlay.push_back(Box::new(physfs));
let physfs = vfs::PhysicalFS::new(&user_data_path, false);
user_overlay.push_back(Box::new(physfs));
}
// Writeable local dir, ~/.config/whatever/
// Save game dir is read-write
{
/*{
user_config_path = project_dirs.config_dir();
log::trace!("User-local configuration path: {:?}", user_config_path);
let physfs = vfs::PhysicalFS::new(&user_config_path, false);
overlay.push_back(Box::new(physfs));
}
}*/
let fs = Filesystem {
vfs: overlay,
user_config_path: user_config_path.to_path_buf(),
user_vfs: user_overlay,
//user_config_path: user_config_path.to_path_buf(),
user_data_path: user_data_path.to_path_buf(),
};
@ -183,6 +187,12 @@ impl Filesystem {
self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Opens the given `path` from user directory and returns the resulting `File`
/// in read-only mode.
pub(crate) fn user_open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.user_vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Opens a file in the user directory with the given
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
/// Note that even if you open a file read-write, it can only
@ -192,7 +202,7 @@ impl Filesystem {
path: P,
options: OpenOptions,
) -> GameResult<File> {
self.vfs
self.user_vfs
.open_options(path.as_ref(), options)
.map(|f| File::VfsFile(f))
.map_err(|e| {
@ -206,26 +216,31 @@ impl Filesystem {
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub(crate) fn create<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
pub(crate) fn user_create<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.user_vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub(crate) fn create_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.mkdir(path.as_ref())
pub(crate) fn user_create_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.mkdir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub(crate) fn delete<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.rm(path.as_ref())
pub(crate) fn user_delete<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.rm(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub(crate) fn delete_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.rmrf(path.as_ref())
pub(crate) fn user_delete_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.rmrf(path.as_ref())
}
/// Check whether a file or directory in the user directory exists.
pub(crate) fn user_exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs.exists(path.as_ref())
}
/// Check whether a file or directory exists.
@ -233,6 +248,14 @@ impl Filesystem {
self.vfs.exists(path.as_ref())
}
/// Check whether a path points at a file.
pub(crate) fn user_is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs
.metadata(path.as_ref())
.map(|m| m.is_file())
.unwrap_or(false)
}
/// Check whether a path points at a file.
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
@ -241,6 +264,14 @@ impl Filesystem {
.unwrap_or(false)
}
/// Check whether a path points at a directory.
pub(crate) fn user_is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs
.metadata(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false)
}
/// Check whether a path points at a directory.
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
@ -249,6 +280,20 @@ impl Filesystem {
.unwrap_or(false)
}
/// Returns a list of all files and directories in the user directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
&mut self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
let itr = self.user_vfs.read_dir(path.as_ref())?.map(|fname| {
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
});
Ok(Box::new(itr))
}
/// Returns a list of all files and directories in the resource directory,
/// in no particular order.
///
@ -311,38 +356,6 @@ impl Filesystem {
pub(crate) fn mount_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
self.vfs.push_back(vfs);
}
/// Looks for a file named `/conf.toml` in any resource directory and
/// loads it if it finds it.
/// If it can't read it for some reason, returns an error.
pub(crate) fn read_config(&mut self) -> GameResult<conf::Conf> {
let conf_path = path::Path::new(CONFIG_NAME);
if self.is_file(conf_path) {
let mut file = self.open(conf_path)?;
let c = conf::Conf::from_toml_file(&mut file)?;
Ok(c)
} else {
Err(GameError::ConfigError(String::from(
"Config file not found",
)))
}
}
/// Takes a `Conf` object and saves it to the user directory,
/// overwriting any file already there.
pub(crate) fn write_config(&mut self, conf: &conf::Conf) -> GameResult<()> {
let conf_path = path::Path::new(CONFIG_NAME);
let mut file = self.create(conf_path)?;
conf.to_toml_file(&mut file)?;
if self.is_file(conf_path) {
Ok(())
} else {
Err(GameError::ConfigError(format!(
"Failed to write config file at {}",
conf_path.to_string_lossy()
)))
}
}
}
/// Opens the given path and returns the resulting `File`
@ -351,9 +364,13 @@ pub fn open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File
ctx.filesystem.open(path)
}
/// Opens the given path in the user directory and returns the resulting `File`
/// in read-only mode.
pub fn user_open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.user_open(path)
}
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
/// Note that even if you open a file read-only, it can only access
/// files in the user directory.
pub fn open_options<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
@ -364,26 +381,52 @@ pub fn open_options<P: AsRef<path::Path>>(
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub fn create<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.create(path)
pub fn user_create<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.user_create(path)
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub fn create_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.create_dir(path.as_ref())
pub fn user_create_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_create_dir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub fn delete<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.delete(path.as_ref())
pub fn user_delete<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_delete(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub fn delete_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.delete_dir(path.as_ref())
pub fn user_delete_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_delete_dir(path.as_ref())
}
/// Check whether a file or directory exists.
pub fn user_exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_exists(path.as_ref())
}
/// Check whether a path points at a file.
pub fn user_is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_is_file(path)
}
/// Check whether a path points at a directory.
pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_is_dir(path)
}
/// Returns a list of all files and directories in the user directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub fn user_read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
ctx.filesystem.user_read_dir(path)
}
/// Check whether a file or directory exists.
@ -443,19 +486,6 @@ pub fn mount_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
ctx.filesystem.mount_vfs(vfs)
}
/// Looks for a file named `/conf.toml` in any resource directory and
/// loads it if it finds it.
/// If it can't read it for some reason, returns an error.
pub fn read_config(ctx: &mut Context) -> GameResult<conf::Conf> {
ctx.filesystem.read_config()
}
/// Takes a `Conf` object and saves it to the user directory,
/// overwriting any file already there.
pub fn write_config(ctx: &mut Context, conf: &conf::Conf) -> GameResult {
ctx.filesystem.write_config(conf)
}
#[cfg(test)]
mod tests {
use std::io::{Read, Write};
@ -474,7 +504,8 @@ mod tests {
ofs.push_front(Box::new(physfs));
Filesystem {
vfs: ofs,
user_config_path: path,
user_vfs: ofs,
//user_config_path: path,
user_data_path: path
}
}

View File

@ -241,7 +241,7 @@ impl Image {
) -> GameResult {
use std::io;
let data = self.to_rgba8(ctx)?;
let f = filesystem::create(ctx, path)?;
let f = filesystem::user_create(ctx, path)?;
let writer = &mut io::BufWriter::new(f);
let color_format = image::ColorType::RGBA(8);
match format {

View File

@ -6,7 +6,7 @@ use crate::weapon::{Weapon, WeaponLevel, WeaponType};
#[derive(Clone, Copy)]
/// (id, amount)
pub struct Item(u16, u16);
pub struct Item(pub u16, pub u16);
#[derive(Clone)]
pub struct Inventory {
@ -70,6 +70,10 @@ impl Inventory {
self.items.iter_mut().by_ref().find(|item| item.0 == item_id)
}
pub fn get_item_idx(&self, idx: usize) -> Option<&Item> {
self.items.get(idx)
}
pub fn has_item(&self, item_id: u16) -> bool {
self.items.iter().any(|item| item.0 == item_id)
}

View File

@ -96,7 +96,7 @@ impl Game {
if let Some(scene) = self.scene.as_mut() {
match self.state.timing_mode {
TimingMode::_50Hz | TimingMode::_60Hz => {
while self.start_time.elapsed().as_nanos() >= self.next_tick && self.loops < 3 {
while self.start_time.elapsed().as_nanos() >= self.next_tick && self.loops < 10 {
if (self.state.settings.speed - 1.0).abs() < 0.01 {
self.next_tick += self.state.timing_mode.get_delta() as u128;
} else {
@ -255,13 +255,12 @@ static BACKENDS: [Backend; 4] = [
fn init_ctx<P: Into<path::PathBuf> + Clone>(event_loop: &winit::event_loop::EventLoopWindowTarget<()>, resource_dir: P) -> GameResult<Context> {
for backend in BACKENDS.iter() {
let mut ctx = ContextBuilder::new("doukutsu-rs")
.window_setup(WindowSetup::default().title("Cave Story (doukutsu-rs)"))
.window_setup(WindowSetup::default().title("Cave Story ~ Doukutsu Monogatari (doukutsu-rs)"))
.window_mode(WindowMode::default()
.resizable(true)
.min_dimensions(320.0, 240.0)
.dimensions(854.0, 480.0))
.add_resource_path(resource_dir.clone())
.add_resource_path(path::PathBuf::from(str!("./")))
.backend(*backend)
.build(event_loop);

View File

@ -1,6 +1,6 @@
use std::io;
use byteorder::{BE, LE, ReadBytesExt};
use byteorder::{BE, LE, ReadBytesExt, WriteBytesExt};
use num_traits::{clamp, FromPrimitive};
use crate::common::{Direction, FadeState};
@ -107,6 +107,142 @@ impl GameProfile {
game_scene.player.stars = clamp(self.stars, 0, 3) as u8;
}
pub fn dump(state: &mut SharedGameState, game_scene: &mut GameScene) -> GameProfile {
let current_map = game_scene.stage_id as u32;
let current_song = state.sound_manager.current_song() as u32;
let pos_x = game_scene.player.x as i32;
let pos_y = game_scene.player.y as i32;
let direction = game_scene.player.direction;
let max_life = game_scene.player.max_life;
let stars = game_scene.player.stars as u16;
let life = game_scene.player.life;
let current_weapon = game_scene.inventory.current_weapon as u32;
let current_item = game_scene.inventory.current_item as u32;
let equipment = game_scene.player.equip.0 as u32;
let control_mode = game_scene.player.control_mode as u32;
let counter = 0; // TODO
let mut weapon_data = [
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
];
let mut items = [0u32; 32];
let mut teleporter_slots = [
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
TeleporterSlotData { index: 0, event_num: 0 },
];
for (idx, weap) in weapon_data.iter_mut().enumerate() {
if let Some(weapon) = game_scene.inventory.get_weapon(idx) {
weap.weapon_id = weapon.wtype as u32;
weap.level = weapon.level as u32;
weap.exp = weapon.experience as u32;
weap.max_ammo = weapon.max_ammo as u32;
weap.ammo = weapon.ammo as u32;
}
}
for (idx, item) in items.iter_mut().enumerate() {
if let Some(sitem) = game_scene.inventory.get_item_idx(idx) {
*item = sitem.0 as u32;
}
}
for (idx, slot) in teleporter_slots.iter_mut().enumerate() {
if let Some(&(index, event_num)) = state.teleporter_slots.get(idx) {
slot.index = index as u32;
slot.event_num = event_num as u32;
}
}
let mut bidx = 0;
let mut flags = [0u8; 1000];
for bits in state.game_flags.as_slice() {
let bytes = bits.to_le_bytes();
for b in bytes.iter() {
if let Some(out) = flags.get_mut(bidx) {
*out = *b;
}
bidx += 1;
}
}
GameProfile {
current_map,
current_song,
pos_x,
pos_y,
direction,
max_life,
stars,
life,
current_weapon,
current_item,
equipment,
control_mode,
counter,
weapon_data,
items,
teleporter_slots,
flags,
}
}
pub fn write_save<W: io::Write>(&self, mut data: W) -> GameResult {
data.write_u64::<BE>(0x446f303431323230)?;
data.write_u32::<LE>(self.current_map)?;
data.write_u32::<LE>(self.current_song)?;
data.write_i32::<LE>(self.pos_x)?;
data.write_i32::<LE>(self.pos_y)?;
data.write_u32::<LE>(self.direction as u32)?;
data.write_u16::<LE>(self.max_life)?;
data.write_u16::<LE>(self.stars)?;
data.write_u16::<LE>(self.life)?;
data.write_u16::<LE>(0)?;
data.write_u32::<LE>(self.current_weapon)?;
data.write_u32::<LE>(self.current_item)?;
data.write_u32::<LE>(self.equipment)?;
data.write_u32::<LE>(self.control_mode)?;
data.write_u32::<LE>(self.counter)?;
for weapon in self.weapon_data.iter() {
data.write_u32::<LE>(weapon.weapon_id)?;
data.write_u32::<LE>(weapon.level)?;
data.write_u32::<LE>(weapon.exp)?;
data.write_u32::<LE>(weapon.max_ammo)?;
data.write_u32::<LE>(weapon.ammo)?;
}
for item in self.items.iter().copied() {
data.write_u32::<LE>(item)?;
}
for slot in self.teleporter_slots.iter() {
data.write_u32::<LE>(slot.index)?;
data.write_u32::<LE>(slot.event_num)?;
}
let mut something = [0u8; 0x80];
data.write(&something);
data.write_u32::<BE>(0x464c4147)?;
data.write(&self.flags)?;
Ok(())
}
pub fn load_from_save<R: io::Read>(mut data: R) -> GameResult<GameProfile> {
// Do041220
if data.read_u64::<BE>()? != 0x446f303431323230 {
@ -125,7 +261,7 @@ impl GameProfile {
let current_weapon = data.read_u32::<LE>()?;
let current_item = data.read_u32::<LE>()?;
let equipment = data.read_u32::<LE>()?;
let move_mode = data.read_u32::<LE>()?;
let control_mode = data.read_u32::<LE>()?;
let counter = data.read_u32::<LE>()?;
let mut weapon_data = [
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
@ -188,7 +324,7 @@ impl GameProfile {
current_weapon,
current_item,
equipment,
control_mode: move_mode,
control_mode,
counter,
weapon_data,
items,

View File

@ -20,6 +20,8 @@ use crate::str;
use crate::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM};
use crate::texture_set::TextureSet;
use crate::touch_controls::TouchControls;
use crate::ggez::filesystem::OpenOptions;
use std::io::Seek;
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum TimingMode {
@ -162,8 +164,19 @@ impl SharedGameState {
Ok(())
}
pub fn save_game(&mut self, game_scene: &mut GameScene, ctx: &mut Context) -> GameResult {
if let Ok(data) = filesystem::open_options(ctx, "/Profile.dat", OpenOptions::new().write(true).create(true)) {
let profile = GameProfile::dump(self, game_scene);
profile.write_save(data)?;
} else {
log::warn!("Cannot open save file.");
}
Ok(())
}
pub fn load_or_start_game(&mut self, ctx: &mut Context) -> GameResult {
if let Ok(data) = filesystem::open(ctx, "/Profile.dat") {
if let Ok(data) = filesystem::user_open(ctx, "/Profile.dat") {
match GameProfile::load_from_save(data) {
Ok(profile) => {
self.reset();

View File

@ -159,6 +159,10 @@ impl SoundManager {
Ok(())
}
pub fn current_song(&self) -> usize {
self.current_song_id
}
}
enum PlaybackMessage {

View File

@ -27,6 +27,7 @@ use crate::scene::title_scene::TitleScene;
use crate::shared_game_state::SharedGameState;
use crate::str;
use crate::weapon::WeaponType;
use crate::profile::GameProfile;
/// Engine's text script VM operation codes.
#[derive(EnumString, Debug, FromPrimitive, PartialEq)]
@ -351,6 +352,7 @@ pub enum TextScriptExecutionState {
WaitStanding(u16, u32),
WaitConfirmation(u16, u32, u16, u8, ConfirmSelection),
WaitFade(u16, u32),
SaveProfile(u16, u32),
LoadProfile,
}
@ -627,6 +629,12 @@ impl TextScriptVM {
}
break;
}
TextScriptExecutionState::SaveProfile(event, ip) => {
state.save_game(game_scene, ctx)?;
state.textscript_vm.state = TextScriptExecutionState::Running(event, ip);
break;
}
TextScriptExecutionState::LoadProfile => {
state.load_or_start_game(ctx)?;
break;
@ -1304,6 +1312,9 @@ impl TextScriptVM {
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
}
OpCode::SVP => {
exec_state = TextScriptExecutionState::SaveProfile(event, cursor.position() as u32);
}
OpCode::LDP => {
state.control_flags.set_tick_world(false);
state.control_flags.set_control_enabled(false);
@ -1316,7 +1327,7 @@ impl TextScriptVM {
OpCode::CIL | OpCode::CPS | OpCode::KE2 |
OpCode::CRE | OpCode::CSS | OpCode::FLA | OpCode::MLP |
OpCode::SPS | OpCode::FR2 |
OpCode::STC | OpCode::SVP | OpCode::HM2 => {
OpCode::STC | OpCode::HM2 => {
log::warn!("unimplemented opcode: {:?}", op);
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);