mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-09-20 11:13:41 +00:00
Compare commits
7 commits
1042d9e57d
...
7081b5616f
Author | SHA1 | Date | |
---|---|---|---|
|
7081b5616f | ||
|
e58b1af514 | ||
|
fc902d71a8 | ||
|
02a87bbe9e | ||
|
91c35a25f9 | ||
|
21c8e92501 | ||
|
d5f6089cdc |
958
Cargo.lock
generated
958
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -84,10 +84,12 @@ pelite = { version = ">=0.9.2", default-features = false, features = ["std"] }
|
|||
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "244ae85833cff4f97ab4b58331741be20e422bd7", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||
sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "244ae85833cff4f97ab4b58331741be20e422bd7", optional = true, features = ["bundled", "static-link"] }
|
||||
rc-box = "1.2.0"
|
||||
rfd = "0.15.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
serde_cbor = { version = "0.11", optional = true }
|
||||
serde_json = "1.0"
|
||||
serde_json = { version = "1.0", features = ["alloc", "unbounded_depth"] }
|
||||
serde_with = { version = "3.11", default_features = false, features = ["macros"] }
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
# remove and replace with extract_if, when our MSRV is 1.87
|
||||
|
|
75
drsandroid/Cargo.lock
generated
75
drsandroid/Cargo.lock
generated
|
@ -533,8 +533,18 @@ version = "0.13.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.13.4",
|
||||
"darling_macro 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -547,21 +557,46 @@ dependencies = [
|
|||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.10.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.13.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
|
@ -663,6 +698,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"vec_mut_scan",
|
||||
|
@ -1452,7 +1488,7 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.13.4",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1962,6 +1998,29 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_with_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
|
@ -2002,6 +2061,12 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
|
|
|
@ -194,7 +194,7 @@ pub enum FadeState {
|
|||
FadeOut(i8, FadeDirection),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Direction {
|
||||
Left = 0,
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"on": "ON",
|
||||
"off": "OFF"
|
||||
"off": "OFF",
|
||||
"choose_file": "No file selected…"
|
||||
},
|
||||
"menus": {
|
||||
"main_menu": {
|
||||
|
@ -60,6 +61,32 @@
|
|||
"replay_last": "Replay Last",
|
||||
"delete_replay": "Delete Best Replay"
|
||||
},
|
||||
"save_manage_menu": {
|
||||
"import_export_save": "Import/Export Save...",
|
||||
"import_format": "Import format",
|
||||
"export_format": "Export format",
|
||||
"action_type": {
|
||||
"entry": "Action:",
|
||||
"import": "Import",
|
||||
"export": "Export"
|
||||
},
|
||||
"save_location": {
|
||||
"import": "Import location",
|
||||
"export": "Export location"
|
||||
},
|
||||
"save_format": {
|
||||
"entry": "Save format:",
|
||||
"freeware": "Freeware",
|
||||
"plus": "Cave Story+ (PC)",
|
||||
"switch": "Cave Story+ (Switch)",
|
||||
"auto": "Auto-detect"
|
||||
},
|
||||
"file_filters": {
|
||||
"freeware": "Cave Story save",
|
||||
"plus": "Cave Story+ save",
|
||||
"switch": "Cave Story+ save"
|
||||
}
|
||||
},
|
||||
"options_menu": {
|
||||
"graphics": "Graphics...",
|
||||
"graphics_menu": {
|
||||
|
@ -134,7 +161,13 @@
|
|||
"auto": "Auto"
|
||||
},
|
||||
"discord_rpc": "Discord Rich Presence:",
|
||||
"allow_strafe": "Allow strafe:"
|
||||
"allow_strafe": "Allow strafe:",
|
||||
"save_format": {
|
||||
"entry": "Save format:",
|
||||
"freeware": "Freeware",
|
||||
"plus": "Cave Story+ (PC)",
|
||||
"switch": "Cave Story+ (Switch)"
|
||||
}
|
||||
},
|
||||
"links": "Links...",
|
||||
"advanced": "Advanced...",
|
||||
|
|
|
@ -1,17 +1,35 @@
|
|||
use std::collections::{HashMap, hash_map::Entry};
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::Copy;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use byteorder::{BE, LE, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::{clamp, FromPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::common::{Direction, FadeState, get_timestamp};
|
||||
use crate::components::nikumaru::NikumaruCounter;
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameError::ResourceLoadError;
|
||||
use crate::framework::error::GameError::{self, ResourceLoadError};
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::framework::filesystem::{user_create, user_delete, user_exists, user_open};
|
||||
use crate::game::player::{ControlMode, TargetPlayer};
|
||||
use crate::game::shared_game_state::{GameDifficulty, SharedGameState};
|
||||
use crate::game::shared_game_state::{GameDifficulty, PlayerCount, SharedGameState};
|
||||
use crate::game::weapon::{WeaponLevel, WeaponType};
|
||||
use crate::game::inventory::Inventory;
|
||||
use crate::scene::game_scene::GameScene;
|
||||
|
||||
const SIG_Do041115: u64 = 0x446f303431313135;
|
||||
const SIG_Do041220: u64 = 0x446f303431323230;
|
||||
const SIG_FLAG: u32 = 0x464c4147;
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct WeaponData {
|
||||
pub weapon_id: u32,
|
||||
pub level: u32,
|
||||
|
@ -20,11 +38,23 @@ pub struct WeaponData {
|
|||
pub ammo: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct TeleporterSlotData {
|
||||
pub index: u32,
|
||||
pub event_num: u32,
|
||||
}
|
||||
|
||||
|
||||
trait SaveProfile {
|
||||
fn apply(&self, state: &mut SharedGameState, game_scene: &mut GameScene, ctx: &mut Context);
|
||||
fn dump(state: &mut SharedGameState, game_scene: &mut GameScene, target_player: Option<TargetPlayer>) -> Self;
|
||||
fn write_save<W: io::Write>(&self, data: W, format: SaveFormat) -> GameResult;
|
||||
fn load_from_save<R: io::Read>(data: R, format: SaveFormat) -> GameResult<GameProfile>;
|
||||
fn is_empty(&self) -> bool;
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
pub struct GameProfile {
|
||||
pub current_map: u32,
|
||||
pub current_song: u32,
|
||||
|
@ -40,12 +70,48 @@ pub struct GameProfile {
|
|||
pub control_mode: u32,
|
||||
pub counter: u32,
|
||||
pub weapon_data: [WeaponData; 8],
|
||||
pub weapon_data_p2: Option<[WeaponData; 8]>,
|
||||
pub items: [u32; 32],
|
||||
pub teleporter_slots: [TeleporterSlotData; 8],
|
||||
#[serde_as(as = "[_; 128]")]
|
||||
pub map_flags: [u8; 128],
|
||||
#[serde_as(as = "[_; 1000]")]
|
||||
pub flags: [u8; 1000],
|
||||
pub timestamp: u64,
|
||||
pub difficulty: u8,
|
||||
|
||||
// CS+ fields
|
||||
pub eggfish_killed: bool,
|
||||
}
|
||||
|
||||
impl Default for GameProfile {
|
||||
fn default() -> GameProfile {
|
||||
GameProfile {
|
||||
current_map: 0,
|
||||
current_song: 0,
|
||||
pos_x: 0,
|
||||
pos_y: 0,
|
||||
direction: Direction::Left,
|
||||
max_life: 0,
|
||||
stars: 0,
|
||||
life: 0,
|
||||
current_weapon: 0,
|
||||
current_item: 0,
|
||||
equipment: 0,
|
||||
control_mode: 0,
|
||||
counter: 0,
|
||||
weapon_data: [WeaponData::default(); 8],
|
||||
weapon_data_p2: None,
|
||||
items: [0u32; 32],
|
||||
teleporter_slots: [TeleporterSlotData::default(); 8],
|
||||
map_flags: [0u8; 128],
|
||||
flags: [0u8; 1000],
|
||||
timestamp: 0,
|
||||
difficulty: 0,
|
||||
|
||||
eggfish_killed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameProfile {
|
||||
|
@ -58,29 +124,37 @@ impl GameProfile {
|
|||
|
||||
game_scene.inventory_player1.current_weapon = self.current_weapon as u16;
|
||||
game_scene.inventory_player1.current_item = self.current_item as u16;
|
||||
for weapon in &self.weapon_data {
|
||||
if weapon.weapon_id == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = state.mod_requirements.append_weapon(ctx, weapon.weapon_id as u16);
|
||||
let weapon_type: Option<WeaponType> = FromPrimitive::from_u8(weapon.weapon_id as u8);
|
||||
fn apply_weapons(ctx: &mut Context, state: &mut SharedGameState, weapons: &[WeaponData], inventory: &mut Inventory) {
|
||||
for weapon in weapons {
|
||||
if weapon.weapon_id == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(wtype) = weapon_type {
|
||||
game_scene.inventory_player1.add_weapon_data(
|
||||
wtype,
|
||||
weapon.ammo as u16,
|
||||
weapon.max_ammo as u16,
|
||||
weapon.exp as u16,
|
||||
match weapon.level {
|
||||
2 => WeaponLevel::Level2,
|
||||
3 => WeaponLevel::Level3,
|
||||
_ => WeaponLevel::Level1,
|
||||
},
|
||||
);
|
||||
let _ = state.mod_requirements.append_weapon(ctx, weapon.weapon_id as u16);
|
||||
let weapon_type: Option<WeaponType> = FromPrimitive::from_u8(weapon.weapon_id as u8);
|
||||
|
||||
if let Some(wtype) = weapon_type {
|
||||
inventory.add_weapon_data(
|
||||
wtype,
|
||||
weapon.ammo as u16,
|
||||
weapon.max_ammo as u16,
|
||||
weapon.exp as u16,
|
||||
match weapon.level {
|
||||
2 => WeaponLevel::Level2,
|
||||
3 => WeaponLevel::Level3,
|
||||
_ => WeaponLevel::Level1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply_weapons(ctx, state, &self.weapon_data, &mut game_scene.inventory_player1);
|
||||
if let Some(weapons) = self.weapon_data_p2.as_ref() {
|
||||
apply_weapons(ctx, state, weapons, &mut game_scene.inventory_player2);
|
||||
}
|
||||
|
||||
for item in self.items.iter().copied() {
|
||||
let item_id = item as u16;
|
||||
let _ = state.mod_requirements.append_item(ctx, item_id);
|
||||
|
@ -90,7 +164,10 @@ impl GameProfile {
|
|||
break;
|
||||
}
|
||||
|
||||
// TODO: original save formats can't store inventory for player 2, but we can, so should we save it,
|
||||
// even if it's equal to the inventroy of player 1?
|
||||
game_scene.inventory_player1.add_item_amount(item_id, amount + 1);
|
||||
game_scene.inventory_player2.add_item_amount(item_id, amount + 1);
|
||||
}
|
||||
|
||||
for slot in &self.teleporter_slots {
|
||||
|
@ -147,7 +224,7 @@ impl GameProfile {
|
|||
game_scene.player1.stars = clamp(self.stars, 0, 3) as u8;
|
||||
|
||||
game_scene.player2 = game_scene.player1.clone();
|
||||
game_scene.inventory_player2 = game_scene.inventory_player1.clone();
|
||||
// game_scene.inventory_player2 = game_scene.inventory_player1.clone();
|
||||
|
||||
game_scene.player1.cond.0 = 0x80;
|
||||
|
||||
|
@ -163,6 +240,7 @@ impl GameProfile {
|
|||
TargetPlayer::Player2 => &game_scene.player2,
|
||||
};
|
||||
|
||||
// TODO: should we store inventory of player 2?
|
||||
let inventory_player = match target_player.unwrap_or(TargetPlayer::Player1) {
|
||||
TargetPlayer::Player1 => &game_scene.inventory_player1,
|
||||
TargetPlayer::Player2 => &game_scene.inventory_player2,
|
||||
|
@ -181,38 +259,31 @@ impl GameProfile {
|
|||
let equipment = player.equip.0 as u32;
|
||||
let control_mode = 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 weapon_data = [WeaponData::default(); 8];
|
||||
let mut weapon_data_p2 = None;
|
||||
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 },
|
||||
];
|
||||
let mut teleporter_slots = [TeleporterSlotData::default(); 8];
|
||||
|
||||
for (idx, weap) in weapon_data.iter_mut().enumerate() {
|
||||
if let Some(weapon) = inventory_player.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;
|
||||
fn dump_weapons(weapons: &mut [WeaponData], inventory: &Inventory) {
|
||||
for (idx, weap) in weapons.iter_mut().enumerate() {
|
||||
if let Some(weapon) = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dump_weapons(&mut weapon_data, &game_scene.inventory_player1);
|
||||
if state.player_count == PlayerCount::Two {
|
||||
let mut weapons_p2 = [WeaponData::default(); 8];
|
||||
dump_weapons(&mut weapons_p2, &game_scene.inventory_player2);
|
||||
|
||||
weapon_data_p2 = Some(weapons_p2);
|
||||
}
|
||||
|
||||
for (idx, item) in items.iter_mut().enumerate() {
|
||||
if let Some(sitem) = inventory_player.get_item_idx(idx) {
|
||||
*item = sitem.0 as u32 + (((sitem.1 - 1) as u32) << 16);
|
||||
|
@ -240,6 +311,7 @@ impl GameProfile {
|
|||
|
||||
let timestamp = get_timestamp();
|
||||
let difficulty = state.difficulty as u8;
|
||||
let eggfish_killed = false; // TODO
|
||||
|
||||
GameProfile {
|
||||
current_map,
|
||||
|
@ -256,17 +328,19 @@ impl GameProfile {
|
|||
control_mode,
|
||||
counter,
|
||||
weapon_data,
|
||||
weapon_data_p2,
|
||||
items,
|
||||
teleporter_slots,
|
||||
map_flags,
|
||||
flags,
|
||||
timestamp,
|
||||
difficulty,
|
||||
eggfish_killed
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_save<W: io::Write>(&self, mut data: W) -> GameResult {
|
||||
data.write_u64::<BE>(0x446f303431323230)?;
|
||||
pub fn write_save<W: io::Write>(&self, data: &mut W, format: &SaveFormat) -> GameResult {
|
||||
data.write_u64::<BE>(SIG_Do041220)?;
|
||||
|
||||
data.write_u32::<LE>(self.current_map)?;
|
||||
data.write_u32::<LE>(self.current_song)?;
|
||||
|
@ -274,8 +348,20 @@ impl GameProfile {
|
|||
data.write_i32::<LE>(self.pos_y)?;
|
||||
data.write_u32::<LE>(self.direction as u32)?;
|
||||
data.write_u16::<LE>(self.max_life)?;
|
||||
|
||||
// TODO: P2 values
|
||||
if *format == SaveFormat::Switch {
|
||||
data.write_u16::<LE>(self.max_life)?;
|
||||
}
|
||||
|
||||
data.write_u16::<LE>(self.stars)?;
|
||||
data.write_u16::<LE>(self.life)?;
|
||||
|
||||
// TODO: P2 values
|
||||
if *format == SaveFormat::Switch {
|
||||
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)?;
|
||||
|
@ -290,6 +376,15 @@ impl GameProfile {
|
|||
data.write_u32::<LE>(weapon.max_ammo)?;
|
||||
data.write_u32::<LE>(weapon.ammo)?;
|
||||
}
|
||||
if *format == SaveFormat::Switch {
|
||||
for weapon in &self.weapon_data_p2.unwrap_or([WeaponData::default(); 8]) {
|
||||
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)?;
|
||||
|
@ -300,27 +395,36 @@ impl GameProfile {
|
|||
data.write_u32::<LE>(slot.event_num)?;
|
||||
}
|
||||
|
||||
// Probably map flags, but unused anyway
|
||||
let something = [0u8; 0x80];
|
||||
data.write(&something)?;
|
||||
|
||||
data.write_u32::<BE>(0x464c4147)?;
|
||||
data.write_u32::<BE>(SIG_FLAG)?;
|
||||
data.write(&self.flags)?;
|
||||
|
||||
data.write_u32::<LE>(0)?; // unused(?) CS+ space
|
||||
if *format == SaveFormat::Plus {
|
||||
data.write_u32::<LE>(0)?; // unused(?) CS+ space
|
||||
}
|
||||
|
||||
data.write_u64::<LE>(self.timestamp)?;
|
||||
data.write_u8(self.difficulty)?;
|
||||
|
||||
if format.is_csp() {
|
||||
// unused(?) CS+ space
|
||||
let zeros = [0u8; 15];
|
||||
data.write(&zeros);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_save<R: io::Read>(mut data: R) -> GameResult<GameProfile> {
|
||||
pub fn load_from_save<R: io::Read>(data: &mut R, format: SaveFormat) -> GameResult<GameProfile> {
|
||||
/*
|
||||
let magic = data.read_u64::<BE>()?;
|
||||
// Do041220, Do041115
|
||||
if magic != 0x446f303431323230 && magic != 0x446f303431313135 {
|
||||
if magic != SIG_Do041220 && magic != SIG_Do041115 {
|
||||
return Err(ResourceLoadError("Invalid magic".to_owned()));
|
||||
}
|
||||
|
||||
*/
|
||||
let current_map = data.read_u32::<LE>()?;
|
||||
let current_song = data.read_u32::<LE>()?;
|
||||
let pos_x = data.read_i32::<LE>()?;
|
||||
|
@ -335,34 +439,27 @@ impl GameProfile {
|
|||
let equipment = 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 },
|
||||
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 weapon_data = [WeaponData::default(); 8];
|
||||
let mut weapon_data_p2 = None;
|
||||
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 },
|
||||
];
|
||||
let mut teleporter_slots = [TeleporterSlotData::default(); 8];
|
||||
|
||||
for WeaponData { weapon_id, level, exp, max_ammo, ammo } in &mut weapon_data {
|
||||
*weapon_id = data.read_u32::<LE>()?;
|
||||
*level = data.read_u32::<LE>()?;
|
||||
*exp = data.read_u32::<LE>()?;
|
||||
*max_ammo = data.read_u32::<LE>()?;
|
||||
*ammo = data.read_u32::<LE>()?;
|
||||
fn load_weapons<R: io::Read>(data: &mut R, weapons: &mut [WeaponData]) -> GameResult {
|
||||
for WeaponData { weapon_id, level, exp, max_ammo, ammo } in weapons {
|
||||
*weapon_id = data.read_u32::<LE>()?;
|
||||
*level = data.read_u32::<LE>()?;
|
||||
*exp = data.read_u32::<LE>()?;
|
||||
*max_ammo = data.read_u32::<LE>()?;
|
||||
*ammo = data.read_u32::<LE>()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
load_weapons(data, &mut weapon_data)?;
|
||||
if format == SaveFormat::Switch {
|
||||
weapon_data_p2 = Some([WeaponData::default(); 8]);
|
||||
load_weapons(data, weapon_data_p2.as_mut().unwrap());
|
||||
}
|
||||
|
||||
for item in &mut items {
|
||||
|
@ -377,7 +474,7 @@ impl GameProfile {
|
|||
let mut map_flags = [0u8; 0x80];
|
||||
data.read_exact(&mut map_flags)?;
|
||||
|
||||
if data.read_u32::<BE>()? != 0x464c4147 {
|
||||
if data.read_u32::<BE>()? != SIG_FLAG {
|
||||
return Err(ResourceLoadError("Invalid FLAG signature".to_owned()));
|
||||
}
|
||||
|
||||
|
@ -388,6 +485,7 @@ impl GameProfile {
|
|||
|
||||
let timestamp = data.read_u64::<LE>().unwrap_or(0);
|
||||
let difficulty = data.read_u8().unwrap_or(0);
|
||||
let eggfish_killed = false; // TODO
|
||||
|
||||
Ok(GameProfile {
|
||||
current_map,
|
||||
|
@ -404,12 +502,533 @@ impl GameProfile {
|
|||
control_mode,
|
||||
counter,
|
||||
weapon_data,
|
||||
weapon_data_p2,
|
||||
items,
|
||||
teleporter_slots,
|
||||
map_flags,
|
||||
flags,
|
||||
timestamp,
|
||||
difficulty,
|
||||
eggfish_killed
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.timestamp == 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SaveSlot {
|
||||
MainGame(usize), // (save slot)
|
||||
CSPMod(u8, usize), // (mod save set, save_slot)
|
||||
Mod(String, usize), // (mod id, save slot)
|
||||
}
|
||||
|
||||
impl SaveSlot {
|
||||
pub fn into_idx(self) -> Self {
|
||||
match self {
|
||||
Self::MainGame(save_slot) => if save_slot == 0 {
|
||||
Self::MainGame(save_slot)
|
||||
} else {
|
||||
Self::MainGame(save_slot - 1)
|
||||
},
|
||||
Self::CSPMod(save_set, save_slot) => { Self::CSPMod(save_set - 1, save_slot - 1) },
|
||||
Self::Mod(mod_id, save_slot) => if save_slot == 0 {
|
||||
Self::Mod(mod_id, save_slot)
|
||||
} else {
|
||||
Self::Mod(mod_id, save_slot - 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub enum SaveFormat {
|
||||
Freeware,
|
||||
Plus,
|
||||
|
||||
// TODO: add version (v1.2 or v1.3)
|
||||
Switch,
|
||||
Generic,
|
||||
}
|
||||
|
||||
impl SaveFormat {
|
||||
pub fn recognise(data: &[u8]) -> GameResult<SaveFormat> {
|
||||
let mut cur = std::io::Cursor::new(data);
|
||||
let magic = cur.read_u64::<BE>()?;
|
||||
|
||||
let original_format = match magic {
|
||||
// In CS+ a profile signature at the start of the save file is present only
|
||||
// if the first game slot profile exists. Otherwise it will be filled with zeros.
|
||||
// TODO: should we handle
|
||||
SIG_Do041220 =>
|
||||
match data.len() {
|
||||
0x604..=0x620 => Some(SaveFormat::Freeware),
|
||||
0x20020 => Some(SaveFormat::Plus),
|
||||
|
||||
// 0x20fb0 — v1.2
|
||||
// 0x20fb4 — v1.3
|
||||
0x20fb0 | 0x20fb4 => Some(SaveFormat::Switch),
|
||||
_ => None
|
||||
},
|
||||
|
||||
SIG_Do041115 => Some(SaveFormat::Freeware),
|
||||
_ => None
|
||||
};
|
||||
|
||||
if let Some(format) = original_format {
|
||||
return Ok(format);
|
||||
}
|
||||
|
||||
// Generic save is stored in JSON format, so it must start with '{' character
|
||||
if data[0] == '{' as u8 {
|
||||
cur.set_position(0);
|
||||
if serde_json::from_reader::<_, SaveContainer>(cur).is_ok() {
|
||||
return Ok(SaveFormat::Generic);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ResourceLoadError("Unsupported or invalid save file".to_owned()))
|
||||
}
|
||||
|
||||
pub fn is_csp(&self) -> bool {
|
||||
match self {
|
||||
SaveFormat::Plus | SaveFormat::Switch => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: compatibility warnings
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct CSPModProfile {
|
||||
pub profiles: HashMap<usize, GameProfile>,
|
||||
// TODO: add TimingMode for best times
|
||||
pub time: usize, // Best time for challenges
|
||||
}
|
||||
|
||||
impl CSPModProfile {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.profiles.is_empty() && self.time == 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum SavePatch {
|
||||
Added,
|
||||
Modified,
|
||||
Deleted
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SaveParams {
|
||||
pub slots: Vec<SaveSlot>,
|
||||
pub settings: bool,
|
||||
}
|
||||
|
||||
impl Default for SaveParams {
|
||||
fn default() -> Self {
|
||||
// Import/export all slots by default
|
||||
Self {
|
||||
slots: vec![],
|
||||
settings: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic container to store all possible info from original game saves
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SaveContainer {
|
||||
pub version: usize,
|
||||
pub game_profiles: HashMap<usize, GameProfile>,
|
||||
pub csp_mods: HashMap<u8, CSPModProfile>, // save_set number -> saves & time
|
||||
|
||||
#[serde(skip)]
|
||||
patchset: HashMap<SaveSlot, SavePatch>,
|
||||
// TODO: engine and mods specific fields
|
||||
}
|
||||
|
||||
impl Default for SaveContainer {
|
||||
fn default() -> SaveContainer {
|
||||
SaveContainer {
|
||||
version: 1,
|
||||
|
||||
game_profiles: HashMap::new(),
|
||||
csp_mods: HashMap::new(),
|
||||
|
||||
patchset: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SaveContainer {
|
||||
pub fn load(ctx: &mut Context, state: &mut SharedGameState) -> GameResult<SaveContainer> {
|
||||
log::debug!("DEBUG LOAD SAVE");
|
||||
if let Ok(mut file) = user_open(ctx, "/save.json") {
|
||||
log::debug!("DEBUG LOAD SAVE - FILE EXISTS");
|
||||
// Using of buf significantly speed up the deserialization.
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
|
||||
log::debug!("DEBUG LOAD SAVE - PREDESERIALIZE. Len: {}", buf.len());
|
||||
let now = std::time::Instant::now();
|
||||
match serde_json::from_slice::<SaveContainer>(buf.as_mut_slice()) {
|
||||
Ok(mut save) => {
|
||||
log::debug!("DEBUG LOAD SAVE - PREUPGRADE");
|
||||
save.upgrade();
|
||||
log::debug!("DEBUG LOAD SAVE - UPGRADED");
|
||||
log::info!("DEBUG LOAD - {} SECS ELAPSED", now.elapsed().as_secs_f64());
|
||||
//log::debug!("{:?}", save);
|
||||
return Ok(save);
|
||||
},
|
||||
Err(err) => log::warn!("Failed to deserialize a generic save: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("DEBUG LOAD SAVE - DEFAULT CREATED");
|
||||
|
||||
let mut container = SaveContainer::default();
|
||||
//container.write_save(ctx, state, SaveFormat::Generic, None, None, &SaveParams::default())?;
|
||||
Ok(container)
|
||||
}
|
||||
|
||||
pub fn save(&mut self, ctx: &mut Context, state: &mut SharedGameState, params: SaveParams) -> GameResult {
|
||||
self.write_save(ctx, state, SaveFormat::Generic, None, None, ¶ms)?;
|
||||
self.write_save(ctx, state, state.settings.save_format, None, None, ¶ms)?;
|
||||
self.patchset.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_save(&mut self, ctx: &mut Context, state: &mut SharedGameState, format: SaveFormat, slot: Option<SaveSlot>, mut out_path: Option<PathBuf>, params: &SaveParams) -> GameResult {
|
||||
log::debug!("DEBUG WRITE SAVE");
|
||||
|
||||
let save_path = Self::get_save_filename(&format, slot.clone());
|
||||
|
||||
match format {
|
||||
SaveFormat::Generic => {
|
||||
let exists = user_exists(ctx, &save_path);
|
||||
let mut file = user_create(ctx, &save_path)?;
|
||||
|
||||
// Using of buf significantly speed up the serializing.
|
||||
let buf = serde_json::to_vec(&self)?;
|
||||
file.write_all(&buf)?;
|
||||
file.flush()?
|
||||
},
|
||||
SaveFormat::Freeware => {
|
||||
for (save_slot, profile) in &self.game_profiles {
|
||||
if params.slots.is_empty() || params.slots.contains(&SaveSlot::MainGame(*save_slot)) {
|
||||
let mut buf = Vec::new();
|
||||
let mut cur = std::io::Cursor::new(&mut buf);
|
||||
profile.write_save(&mut cur, &format)?;
|
||||
|
||||
let mut filename = Self::get_save_filename(&format, Some(SaveSlot::MainGame(*save_slot)));
|
||||
if let Some(path) = &mut out_path {
|
||||
let os_filename = OsString::from_str(filename.split_off(1).as_str()).unwrap();
|
||||
let _ = path.set_file_name(os_filename);
|
||||
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&buf)?;
|
||||
} else {
|
||||
let mut file = user_create(ctx, filename)?;
|
||||
file.write_all(&buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (save_set, csp_mod) in &self.csp_mods {
|
||||
for (slot, profile) in &csp_mod.profiles {
|
||||
if params.slots.is_empty() || params.slots.contains(&SaveSlot::CSPMod(*save_set, *slot)) {
|
||||
let mut buf = Vec::new();
|
||||
let mut cur = std::io::Cursor::new(&mut buf);
|
||||
profile.write_save(&mut cur, &format)?;
|
||||
|
||||
let mut filename = Self::get_save_filename(&format, Some(SaveSlot::CSPMod(*save_set, *slot)));
|
||||
if let Some(path) = &mut out_path {
|
||||
let os_filename = OsString::from_str(filename.split_off(1).as_str()).unwrap();
|
||||
let _ = path.set_file_name(os_filename);
|
||||
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&buf)?;
|
||||
} else {
|
||||
let mut file = user_create(ctx, filename)?;
|
||||
file.write_all(&buf)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (patch_slot, patch_state) in self.patchset.iter() {
|
||||
if *patch_state == SavePatch::Deleted {
|
||||
// TODO: should we unwrap the result?
|
||||
user_delete(ctx, Self::get_save_filename(&format, Some(patch_slot.clone())))?;
|
||||
}
|
||||
}
|
||||
},
|
||||
SaveFormat::Plus | SaveFormat::Switch => {
|
||||
let mut active_slots = [0u8; 32];
|
||||
|
||||
// Setting
|
||||
let bgm_volume = ((state.settings.bgm_volume * 10.0) as u32).min(10);
|
||||
let sfx_volume = ((state.settings.bgm_volume * 10.0) as u32).min(10);
|
||||
let seasonal_textures = state.settings.seasonal_textures as u8;
|
||||
let soundtrack: u8 = match state.settings.soundtrack.as_str() {
|
||||
"organya" => 2,
|
||||
"new" => 3,
|
||||
"remastered" => 4,
|
||||
"famitracks" => 5,
|
||||
"ridiculon" => 6,
|
||||
_ => 2 // Fallback to Organya
|
||||
};
|
||||
let graphics = state.settings.original_textures as u8;
|
||||
let language = (state.settings.locale == "jp") as u8;
|
||||
let beaten_hell = state.mod_requirements.beat_hell as u8;
|
||||
let unused = [0u8; 14];
|
||||
let mut jukebox: [u8; 48] = [
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80,
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80,
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80,
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80,
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80,
|
||||
0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80
|
||||
]; // TODO
|
||||
let mut eggfish_killed = [0u8; 3];
|
||||
let mut nikumaru = NikumaruCounter::new();
|
||||
nikumaru.load_counter(state, ctx);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut cur = std::io::Cursor::new(&mut buf);
|
||||
|
||||
// TODO
|
||||
// In CS+, only slots up to the last non-empty one are written to the save file.
|
||||
// E.g., if the user saved only the profile in slot 3, then profiles from slots 1, 2 and 3 will be written to the file.
|
||||
let default_profile = GameProfile::default();
|
||||
for save_slot in 1..=3 {
|
||||
let profile = if params.slots.is_empty() || params.slots.contains(&SaveSlot::MainGame(save_slot)) {
|
||||
log::debug!("Writing Game profile: {}", save_slot);
|
||||
if let Some(game_profile) = self.game_profiles.get(&save_slot) {
|
||||
active_slots[0] |= 1u8 << (save_slot - 1);
|
||||
game_profile
|
||||
} else {
|
||||
&default_profile
|
||||
}
|
||||
} else {
|
||||
&default_profile
|
||||
};
|
||||
|
||||
profile.write_save(&mut cur, &format)?;
|
||||
|
||||
eggfish_killed[save_slot - 1] = profile.eggfish_killed as u8;
|
||||
}
|
||||
|
||||
let default_csp_profile = CSPModProfile::default();
|
||||
for save_set in 1..=26 {
|
||||
let csp_mod = self.csp_mods.get(&save_set).unwrap_or(&default_csp_profile);
|
||||
for save_slot in 1..=3 {
|
||||
let profile = if params.slots.is_empty() || params.slots.contains(&SaveSlot::CSPMod(save_set, save_slot)) {
|
||||
log::debug!("Writing CSP profile: {} - {}", save_set, save_slot);
|
||||
if let Some(game_profile) = csp_mod.profiles.get(&save_slot) {
|
||||
active_slots[save_set as usize] |= 1u8 << (save_slot - 1);
|
||||
game_profile
|
||||
} else {
|
||||
&default_profile
|
||||
}
|
||||
} else {
|
||||
&default_profile
|
||||
};
|
||||
|
||||
profile.write_save(&mut cur, &format)?;
|
||||
}
|
||||
}
|
||||
|
||||
cur.write(&active_slots)?;
|
||||
|
||||
// Settings
|
||||
cur.write_u32::<LE>(bgm_volume)?;
|
||||
cur.write_u32::<LE>(sfx_volume)?;
|
||||
cur.write_u8(seasonal_textures)?;
|
||||
cur.write_u8(soundtrack)?;
|
||||
cur.write_u8(graphics)?;
|
||||
cur.write_u8(language)?;
|
||||
cur.write_u8(beaten_hell)?;
|
||||
|
||||
// TODO
|
||||
// TODO: write some fields only in the v1.3 version mode
|
||||
if format == SaveFormat::Switch {
|
||||
cur.write(&jukebox)?;
|
||||
cur.write_u8(0)?; // Unlock notifications.
|
||||
cur.write_u8(0)?; // Shared health bar.
|
||||
|
||||
// Something
|
||||
cur.write_u16::<LE>(0)?;
|
||||
cur.write_u8(0)?;
|
||||
} else {
|
||||
let zeros = [0u8; 7];
|
||||
cur.write(&zeros)?;
|
||||
}
|
||||
|
||||
// Challange best times
|
||||
cur.write_u32::<LE>(nikumaru.tick.try_into().unwrap_or(u32::MAX))?;
|
||||
for save_set in 1..=26 {
|
||||
let csp_mod = self.csp_mods.get(&save_set).unwrap_or(&default_csp_profile);
|
||||
cur.write_u32::<LE>(csp_mod.time.try_into().unwrap_or(u32::MAX))?;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// TODO: write some fields only in the v1.3 version mode
|
||||
if format == SaveFormat::Switch {
|
||||
cur.write_u8(0)?;
|
||||
|
||||
let something = [0u8; 0x77];
|
||||
let something2 = [0u8; 6];
|
||||
|
||||
cur.write(&something)?;
|
||||
cur.write_u16::<LE>(0)?; // P2 character unlocks
|
||||
cur.write(&something2)?;
|
||||
} else {
|
||||
cur.write(&eggfish_killed)?;
|
||||
|
||||
let something = [0u8; 0x3d];
|
||||
cur.write(&something)?;
|
||||
|
||||
let something2 = [0u8; 0xf20];
|
||||
cur.write(&something2)?;
|
||||
}
|
||||
|
||||
if let Some(path) = out_path {
|
||||
// TODO
|
||||
File::create(path)?.write(&buf)?;
|
||||
} else {
|
||||
user_create(ctx, save_path)?.write(&buf)?;
|
||||
}
|
||||
},
|
||||
_ => todo!()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_save_filename(format: &SaveFormat, slot: Option<SaveSlot>) -> String {
|
||||
match format {
|
||||
SaveFormat::Generic => "/save.json".to_owned(),
|
||||
SaveFormat::Plus | SaveFormat::Switch => "/Profile.dat".to_owned(),
|
||||
SaveFormat::Freeware => {
|
||||
match slot {
|
||||
Some(SaveSlot::MainGame(save_slot)) => if save_slot == 1 {
|
||||
"/Profile.dat".to_owned()
|
||||
} else {
|
||||
format!("/Profile{}.dat", save_slot)
|
||||
},
|
||||
Some(SaveSlot::CSPMod(save_set, save_slot)) => format!("/Mod{}_Profile{}.dat", save_set, save_slot),
|
||||
Some(SaveSlot::Mod(mod_id, save_slot)) => unimplemented!(),
|
||||
_ => "/Profile.dat".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upgrade(&mut self) {
|
||||
log::debug!("DEBUG UPGRADE SAVE");
|
||||
let initial_version = self.version;
|
||||
|
||||
if self.version != initial_version {
|
||||
log::info!("Upgraded generic save from version {} to {}.", initial_version, self.version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn set_profile(&mut self, slot: SaveSlot, profile: GameProfile) {
|
||||
log::debug!("Debug profile set: {:?}; {}", slot, profile.timestamp);
|
||||
|
||||
let prev_save: Option<GameProfile>;
|
||||
match slot {
|
||||
SaveSlot::MainGame(save_slot) => {
|
||||
prev_save = self.game_profiles.insert(save_slot, profile);
|
||||
},
|
||||
SaveSlot::CSPMod(save_set, save_slot) => {
|
||||
prev_save = self.csp_mods.entry(save_set)
|
||||
.or_insert(CSPModProfile::default())
|
||||
.profiles
|
||||
.insert(save_slot, profile);
|
||||
},
|
||||
SaveSlot::Mod(ref mod_id, save_slot) => {
|
||||
// TODO
|
||||
prev_save = None;
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
if prev_save.is_none() {
|
||||
self.patchset.insert(slot, SavePatch::Added);
|
||||
} else {
|
||||
self.patchset.insert(slot, SavePatch::Modified);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_profile(&self, slot: SaveSlot) -> Option<&GameProfile> {
|
||||
log::debug!("Debug profile get: {:?}", slot);
|
||||
match slot {
|
||||
SaveSlot::MainGame(save_slot) => self.game_profiles.get(&save_slot),
|
||||
SaveSlot::CSPMod(save_set, save_slot) => {
|
||||
if let Some(csp_mod) = self.csp_mods.get(&save_set) {
|
||||
return csp_mod.profiles.get(&save_slot);
|
||||
}
|
||||
|
||||
None
|
||||
},
|
||||
SaveSlot::Mod(mod_id, save_slot) => unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_profile(&mut self, ctx: &Context, slot: SaveSlot) {
|
||||
log::debug!("Debug profile delete: {:?}", slot);
|
||||
match slot {
|
||||
SaveSlot::MainGame(save_slot) => {
|
||||
let _ = self.game_profiles.remove(&save_slot);
|
||||
},
|
||||
SaveSlot::CSPMod(save_set, save_slot) => {
|
||||
if let Some(csp_mod) = self.csp_mods.get_mut(&save_set) {
|
||||
csp_mod.profiles.remove(&save_slot);
|
||||
|
||||
if csp_mod.is_empty() {
|
||||
self.csp_mods.remove(&save_set);
|
||||
}
|
||||
}
|
||||
},
|
||||
SaveSlot::Mod(mod_id, save_slot) => unimplemented!()
|
||||
}
|
||||
|
||||
self.patchset.insert(slot, SavePatch::Deleted);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
if !self.game_profiles.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (_, csp_mod) in self.csp_mods.iter() {
|
||||
if csp_mod.is_empty() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
pub fn export(&mut self, state: &mut SharedGameState, ctx: &mut Context, format: SaveFormat, params: SaveParams, out_path: PathBuf) -> GameResult {
|
||||
self.write_save(ctx, state, format, None, Some(out_path.clone()), ¶ms)?;
|
||||
|
||||
log::trace!("Export format: {:?}.", format);
|
||||
log::trace!("Export params: {:?}.", params);
|
||||
log::trace!("Export path: {:?}.", out_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::framework::filesystem::{user_create, user_open};
|
|||
use crate::framework::gamepad::{Axis, AxisDirection, Button, PlayerControllerInputType};
|
||||
use crate::framework::graphics::VSyncMode;
|
||||
use crate::framework::keyboard::ScanCode;
|
||||
use crate::game::profile::SaveFormat;
|
||||
use crate::game::player::TargetPlayer;
|
||||
use crate::game::shared_game_state::{CutsceneSkipMode, ScreenShakeIntensity, TimingMode, WindowMode};
|
||||
use crate::input::combined_player_controller::CombinedPlayerController;
|
||||
|
@ -87,6 +88,8 @@ pub struct Settings {
|
|||
pub discord_rpc: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_strafe: bool,
|
||||
#[serde(default = "default_save_format")]
|
||||
pub save_format: SaveFormat,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
|
@ -95,7 +98,7 @@ fn default_true() -> bool {
|
|||
|
||||
#[inline(always)]
|
||||
fn current_version() -> u32 {
|
||||
25
|
||||
26
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -171,6 +174,11 @@ fn default_cutscene_skip_mode() -> CutsceneSkipMode {
|
|||
CutsceneSkipMode::Hold
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn default_save_format() -> SaveFormat {
|
||||
SaveFormat::Freeware
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load(ctx: &Context) -> GameResult<Settings> {
|
||||
if let Ok(file) = user_open(ctx, "/settings.json") {
|
||||
|
@ -359,6 +367,11 @@ impl Settings {
|
|||
}
|
||||
}
|
||||
|
||||
if self.version == 25 {
|
||||
self.version = 26;
|
||||
self.save_format = default_save_format();
|
||||
}
|
||||
|
||||
if self.version != initial_version {
|
||||
log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version);
|
||||
}
|
||||
|
@ -467,6 +480,7 @@ impl Default for Settings {
|
|||
cutscene_skip_mode: CutsceneSkipMode::Hold,
|
||||
discord_rpc: true,
|
||||
allow_strafe: true,
|
||||
save_format: default_save_format()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::framework::{filesystem, graphics};
|
|||
use crate::game::caret::{Caret, CaretType};
|
||||
use crate::game::npc::NPCTable;
|
||||
use crate::game::player::TargetPlayer;
|
||||
use crate::game::profile::GameProfile;
|
||||
use crate::game::profile::{GameProfile, SaveContainer, SaveFormat, SaveParams, SaveSlot};
|
||||
use crate::game::scripting::tsc::credit_script::{CreditScript, CreditScriptVM};
|
||||
use crate::game::scripting::tsc::text_script::{
|
||||
ScriptMode, TextScript, TextScriptEncoding, TextScriptExecutionState, TextScriptVM,
|
||||
|
@ -678,48 +678,60 @@ impl SharedGameState {
|
|||
ctx: &mut Context,
|
||||
target_player: Option<TargetPlayer>,
|
||||
) -> GameResult {
|
||||
|
||||
/*
|
||||
if let Some(save_path) = self.get_save_filename(self.save_slot) {
|
||||
if let Ok(data) = filesystem::open_options(ctx, save_path, OpenOptions::new().write(true).create(true)) {
|
||||
let profile = GameProfile::dump(self, game_scene, target_player);
|
||||
profile.write_save(data)?;
|
||||
//profile.write_save(data)?;
|
||||
|
||||
let mut save_container = SaveContainer::load(ctx)?;
|
||||
save_container.set_profile(None, self.save_slot, profile);
|
||||
save_container.write_save(ctx, SaveFormat::Generic, None, None);
|
||||
} else {
|
||||
log::warn!("Cannot open save file.");
|
||||
}
|
||||
} else {
|
||||
log::info!("Mod has saves disabled.");
|
||||
}
|
||||
*/
|
||||
if let Some(slot) = self.get_save_slot(self.save_slot) {
|
||||
let profile = GameProfile::dump(self, game_scene, target_player);
|
||||
|
||||
let mut save_container = SaveContainer::load(ctx, self)?;
|
||||
save_container.set_profile(slot, profile);
|
||||
save_container.save(ctx, self, SaveParams::default())?;
|
||||
} else {
|
||||
log::info!("Mod has saves disabled.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_or_start_game(&mut self, ctx: &mut Context) -> GameResult {
|
||||
if let Some(save_path) = self.get_save_filename(self.save_slot) {
|
||||
if let Ok(data) = filesystem::user_open(ctx, save_path) {
|
||||
match GameProfile::load_from_save(data) {
|
||||
Ok(profile) => {
|
||||
self.reset();
|
||||
let mut next_scene = GameScene::new(self, ctx, profile.current_map as usize)?;
|
||||
if let Some(slot) = self.get_save_slot(self.save_slot) {
|
||||
if let Ok(save) = SaveContainer::load(ctx, self) {
|
||||
if let Some(profile) = save.get_profile(slot) {
|
||||
self.reset();
|
||||
let mut next_scene = GameScene::new(self, ctx, profile.current_map as usize)?;
|
||||
|
||||
profile.apply(self, &mut next_scene, ctx);
|
||||
profile.apply(self, &mut next_scene, ctx);
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
self.discord_rpc.update_difficulty(self.difficulty)?;
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
self.discord_rpc.update_difficulty(self.difficulty)?;
|
||||
|
||||
self.next_scene = Some(Box::new(next_scene));
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load save game, starting new one: {}", e);
|
||||
}
|
||||
self.next_scene = Some(Box::new(next_scene));
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
log::warn!("No save game found, starting new one...");
|
||||
}
|
||||
} else {
|
||||
log::info!("Mod has saves disabled.");
|
||||
log::info!("Mod has saves disabled, starting new game...");
|
||||
}
|
||||
|
||||
self.start_new_game(ctx)
|
||||
self.start_new_game(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
|
@ -859,6 +871,30 @@ impl SharedGameState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_save_slot(&mut self, slot: usize) -> Option<SaveSlot> {
|
||||
if let Some(mod_path) = &self.mod_path {
|
||||
if let Some(mod_info) = self.mod_list.get_mod_info_from_path(mod_path.clone()) {
|
||||
log::debug!("Mod info get save slot: {:?}", mod_info);
|
||||
if mod_info.id.starts_with(&"csmod_".to_string()) {
|
||||
if mod_info.save_slot > 0 {
|
||||
return Some(SaveSlot::CSPMod(mod_info.save_slot.try_into().unwrap(), slot));
|
||||
} else if mod_info.save_slot < 0 {
|
||||
// Mods with a negative save set(slot) has saves disabled.
|
||||
return None;
|
||||
}
|
||||
|
||||
// If mod uses save set 0, saves are stored in the main game set
|
||||
} else {
|
||||
return Some(SaveSlot::Mod(mod_info.id.clone(), slot));
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(SaveSlot::MainGame(slot))
|
||||
}
|
||||
|
||||
pub fn get_rec_filename(&self) -> String {
|
||||
if let Some(mod_path) = &self.mod_path {
|
||||
let name = self.mod_list.get_name_from_path(mod_path.to_string());
|
||||
|
|
|
@ -183,8 +183,6 @@ impl Weapon {
|
|||
self.refire_timer = 4;
|
||||
}
|
||||
|
||||
// todo lua hook
|
||||
|
||||
match self.wtype {
|
||||
WeaponType::None => {}
|
||||
WeaponType::Snake => self.tick_snake(player, player_id, bullet_manager, state),
|
||||
|
|
|
@ -86,8 +86,12 @@ impl Locale {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn ts(&self, key: &str) -> String {
|
||||
self.t(key).to_owned()
|
||||
}
|
||||
|
||||
pub fn tt(&self, key: &str, args: &[(&str, &str)]) -> String {
|
||||
let mut string = self.t(key).to_owned();
|
||||
let mut string = self.ts(key);
|
||||
|
||||
for (key, value) in args.iter() {
|
||||
string = string.replace(&format!("{{{}}}", key), &value);
|
||||
|
|
119
src/menu/mod.rs
119
src/menu/mod.rs
|
@ -1,4 +1,6 @@
|
|||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::common::{Color, Rect};
|
||||
use crate::components::draw_common::{draw_number, Alignment};
|
||||
|
@ -9,6 +11,7 @@ use crate::game::shared_game_state::{GameDifficulty, MenuCharacter, SharedGameSt
|
|||
use crate::graphics::font::Font;
|
||||
use crate::input::combined_menu_controller::CombinedMenuController;
|
||||
use crate::menu::save_select_menu::MenuSaveInfo;
|
||||
use crate::util::file_picker::{open_file_picker, FilePickerParams};
|
||||
|
||||
pub mod controls_menu;
|
||||
pub mod coop_menu;
|
||||
|
@ -17,6 +20,7 @@ pub mod save_select_menu;
|
|||
pub mod settings_menu;
|
||||
|
||||
const MENU_MIN_PADDING: f32 = 30.0;
|
||||
const MENU_DISABLED_COLOR: (u8, u8, u8, u8) = (0xa0, 0xa0, 0xff, 0xff);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ControlMenuData {
|
||||
|
@ -43,6 +47,7 @@ pub enum MenuEntry {
|
|||
PlayerSkin,
|
||||
Control(String, ControlMenuData),
|
||||
Spacer(f64),
|
||||
FilePicker(String, bool, FilePickerParams, Option<Vec<PathBuf>>), // text, display selected file names, params, selected files/dirs
|
||||
}
|
||||
|
||||
impl MenuEntry {
|
||||
|
@ -64,6 +69,7 @@ impl MenuEntry {
|
|||
MenuEntry::PlayerSkin => 24.0,
|
||||
MenuEntry::Control(_, _) => 16.0,
|
||||
MenuEntry::Spacer(height) => *height,
|
||||
MenuEntry::FilePicker(_, _, _, _) => 16.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,6 +91,7 @@ impl MenuEntry {
|
|||
MenuEntry::PlayerSkin => true,
|
||||
MenuEntry::Control(_, _) => true,
|
||||
MenuEntry::Spacer(_) => false,
|
||||
MenuEntry::FilePicker(_, _, _, _) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +104,7 @@ pub enum MenuSelectionResult<'a, T: std::cmp::PartialEq> {
|
|||
Right(T, &'a mut MenuEntry, i16),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Menu<T: std::cmp::PartialEq> {
|
||||
pub x: isize,
|
||||
pub y: isize,
|
||||
|
@ -154,6 +162,26 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_entry(&self, id: T) -> Option<&MenuEntry> {
|
||||
for i in 0..self.entries.len() {
|
||||
if self.entries[i].0 == id {
|
||||
return Some(&self.entries[i].1);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_entry_mut(&mut self, id: T) -> Option<&mut MenuEntry> {
|
||||
for i in 0..self.entries.len() {
|
||||
if self.entries[i].0 == id {
|
||||
return Some(&mut self.entries[i].1);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn update_width(&mut self, state: &SharedGameState) {
|
||||
let mut width = self.width as f32;
|
||||
|
||||
|
@ -216,7 +244,25 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
MenuEntry::NewSave => {}
|
||||
MenuEntry::PlayerSkin => {}
|
||||
MenuEntry::Control(_, _) => {}
|
||||
MenuEntry::Spacer(_) => {}
|
||||
MenuEntry::Spacer(_) => {},
|
||||
MenuEntry::FilePicker(entry, display, _, selection) => {
|
||||
let mut entry_with_selection = entry.clone();
|
||||
|
||||
if *display {
|
||||
entry_with_selection.push_str(" ");
|
||||
|
||||
let filename = selection.as_ref()
|
||||
.and_then(|files| files.first())
|
||||
.and_then(|file| file.file_name())
|
||||
.and_then(|filename| filename.to_str())
|
||||
.unwrap_or(state.loc.t("common.choose_file"));
|
||||
|
||||
entry_with_selection.push_str(filename);
|
||||
}
|
||||
|
||||
let entry_width = state.font.builder().compute_width(&entry_with_selection) + 32.0;
|
||||
width = width.max(entry_width);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,21 +399,22 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
batch.draw(ctx)?;
|
||||
|
||||
let options_x = if self.center_options {
|
||||
let mut longest_option_width = 20.0;
|
||||
let mut longest_entry_width = 20.0;
|
||||
|
||||
for (_, entry) in &self.entries {
|
||||
match entry {
|
||||
let text_width = match entry {
|
||||
MenuEntry::Options(text, _, _) | MenuEntry::Active(text) => {
|
||||
let text_width = state.font.builder().compute_width(text) + 32.0;
|
||||
if text_width > longest_option_width {
|
||||
longest_option_width = text_width;
|
||||
}
|
||||
state.font.builder().compute_width(text) + 32.0
|
||||
}
|
||||
_ => {}
|
||||
_ => 0.0
|
||||
};
|
||||
|
||||
if text_width > longest_entry_width {
|
||||
longest_entry_width = text_width;
|
||||
}
|
||||
}
|
||||
|
||||
(state.canvas_size.0 / 2.0) - (longest_option_width / 2.0)
|
||||
(state.canvas_size.0 / 2.0) - (longest_entry_width / 2.0)
|
||||
} else {
|
||||
self.x as f32
|
||||
};
|
||||
|
@ -471,7 +518,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
let mut builder = state.font.builder().position(x, local_y);
|
||||
|
||||
if !*is_white {
|
||||
builder = builder.color((0xa0, 0xa0, 0xff, 0xff));
|
||||
builder = builder.color(MENU_DISABLED_COLOR);
|
||||
}
|
||||
|
||||
builder.draw(&line, ctx, &state.constants, &mut state.texture_set)?;
|
||||
|
@ -482,7 +529,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
y += entry.height() as f32 * (lines.len() - 1) as f32;
|
||||
}
|
||||
MenuEntry::Disabled(name) => {
|
||||
state.font.builder().position(self.x as f32 + 20.0, y).color((0xa0, 0xa0, 0xff, 0xff)).draw(
|
||||
state.font.builder().position(self.x as f32 + 20.0, y).color(MENU_DISABLED_COLOR).draw(
|
||||
name,
|
||||
ctx,
|
||||
&state.constants,
|
||||
|
@ -548,7 +595,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
.font
|
||||
.builder()
|
||||
.position(self.x as f32 + 20.0, y + 16.0)
|
||||
.color((0xc0, 0xc0, 0xff, 0xff))
|
||||
.color(MENU_DISABLED_COLOR)
|
||||
.draw(description_text, ctx, &state.constants, &mut state.texture_set)?;
|
||||
}
|
||||
MenuEntry::OptionsBar(name, percent) => {
|
||||
|
@ -739,6 +786,42 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
MenuEntry::FilePicker(name, display, _, selection) => {
|
||||
// TODO: fix text is rendered out of the text box bounds if the filename is too long.
|
||||
|
||||
let name_text_len = state.font.builder().compute_width(name);
|
||||
let value_text = selection.as_ref()
|
||||
.and_then(|files| files.first())
|
||||
.and_then(|file| file.file_name())
|
||||
.and_then(|filename| filename.to_str())
|
||||
.unwrap_or(state.loc.t("common.choose_file"));
|
||||
|
||||
// Draw the entry as disabled on unsupported platforms
|
||||
let text_color = if cfg!(not(any(target_os = "android", target_os = "horizon"))) {
|
||||
state.font.builder().get_color()
|
||||
} else {
|
||||
MENU_DISABLED_COLOR
|
||||
};
|
||||
|
||||
state.font.builder().position(self.x as f32 + 20.0, y)
|
||||
.draw(
|
||||
name,
|
||||
ctx,
|
||||
&state.constants,
|
||||
&mut state.texture_set,
|
||||
)?;
|
||||
|
||||
if *display {
|
||||
state.font.builder()
|
||||
.position(self.x as f32 + 25.0 + name_text_len, y)
|
||||
.draw(
|
||||
value_text,
|
||||
ctx,
|
||||
&state.constants,
|
||||
&mut state.texture_set,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -882,6 +965,18 @@ impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
|
|||
return MenuSelectionResult::Selected(idx, entry);
|
||||
}
|
||||
}
|
||||
MenuEntry::FilePicker(_, _, params, selection) => {
|
||||
if self.selected == idx && controller.trigger_ok()
|
||||
|| state.touch_controls.consume_click_in(entry_bounds)
|
||||
{
|
||||
state.sound_manager.play_sfx(18);
|
||||
self.selected = idx.clone();
|
||||
|
||||
*selection = open_file_picker(params);
|
||||
|
||||
return MenuSelectionResult::Selected(idx, entry);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
use pelite::pe::imports::Import;
|
||||
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::framework::filesystem;
|
||||
use crate::game::profile::GameProfile;
|
||||
use crate::game::profile::{GameProfile, SaveContainer, SaveFormat, SaveParams, SaveSlot};
|
||||
use crate::game::shared_game_state::{GameDifficulty, SharedGameState};
|
||||
use crate::input::combined_menu_controller::CombinedMenuController;
|
||||
use crate::menu::coop_menu::PlayerCountMenu;
|
||||
use crate::menu::MenuEntry;
|
||||
use crate::menu::{Menu, MenuSelectionResult};
|
||||
use crate::util::file_picker::FilePickerParams;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MenuSaveInfo {
|
||||
|
@ -33,12 +36,14 @@ pub enum CurrentMenu {
|
|||
PlayerCountMenu,
|
||||
DeleteConfirm,
|
||||
LoadConfirm,
|
||||
ImportExport,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SaveMenuEntry {
|
||||
Load(usize),
|
||||
New(usize),
|
||||
ImportExport,
|
||||
Back,
|
||||
}
|
||||
|
||||
|
@ -87,8 +92,143 @@ impl Default for LoadConfirmMenuEntry {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ImportExportMenuEntry {
|
||||
Format,
|
||||
Import,
|
||||
Export,
|
||||
Back,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ImportExportLocation {
|
||||
Filesystem,
|
||||
// TODO: add some Switch emulators
|
||||
}
|
||||
|
||||
impl Default for ImportExportLocation {
|
||||
fn default() -> Self {
|
||||
Self::Filesystem
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MenuExportInfo {
|
||||
pub location: ImportExportLocation,
|
||||
pub format: SaveFormat,
|
||||
pub picker_params: FilePickerParams,
|
||||
pub save_params: SaveParams,
|
||||
}
|
||||
|
||||
impl Default for MenuExportInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
location: ImportExportLocation::Filesystem,
|
||||
format: SaveFormat::Freeware,
|
||||
picker_params: FilePickerParams::new(),
|
||||
save_params: SaveParams::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuExportInfo {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn set_picker_params(&mut self, state: &SharedGameState, is_export: bool) {
|
||||
let filename = SaveContainer::get_save_filename(&self.format, None).split_off(1);
|
||||
|
||||
let mut params = match self.format {
|
||||
SaveFormat::Freeware => {
|
||||
FilePickerParams::new()
|
||||
.pick_dirs(true)
|
||||
}
|
||||
SaveFormat::Plus | SaveFormat::Switch => {
|
||||
FilePickerParams::new()
|
||||
.file_name(Some(filename))
|
||||
.filter(state.loc.ts("menus.save_manage_menu.file_filters.plus"), vec![
|
||||
"dat".to_owned()
|
||||
])
|
||||
}
|
||||
SaveFormat::Generic => {
|
||||
FilePickerParams::new()
|
||||
.file_name(Some(filename))
|
||||
.filter(state.loc.ts("menus.save_manage_menu.file_filters.generic"), vec![
|
||||
"json".to_owned()
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
if is_export {
|
||||
params = params.save(true);
|
||||
};
|
||||
|
||||
if let Some(fs_container) = &state.fs_container {
|
||||
params = params.starting_dir(Some(fs_container.user_path.clone()));
|
||||
}
|
||||
|
||||
self.picker_params = params;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ImportExportMenuEntry {
|
||||
fn format_from_value(val: usize) -> Option<SaveFormat> {
|
||||
match val {
|
||||
0 => None, // Auto
|
||||
1 => Some(SaveFormat::Freeware),
|
||||
2 => Some(SaveFormat::Plus),
|
||||
3 => Some(SaveFormat::Switch),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn fpicker_from_format(state: &SharedGameState, format: SaveFormat, is_export: bool) -> FilePickerParams {
|
||||
let filename = SaveContainer::get_save_filename(&format, None).split_off(1);
|
||||
|
||||
let mut params = match format {
|
||||
SaveFormat::Freeware => {
|
||||
FilePickerParams::new()
|
||||
.pick_dirs(true)
|
||||
}
|
||||
SaveFormat::Plus | SaveFormat::Switch => {
|
||||
FilePickerParams::new()
|
||||
.file_name(Some(filename))
|
||||
.filter(state.loc.ts("menus.save_manage_menu.file_filters.plus"), vec![
|
||||
"dat".to_owned()
|
||||
])
|
||||
}
|
||||
SaveFormat::Generic => {
|
||||
FilePickerParams::new()
|
||||
.file_name(Some(filename))
|
||||
.filter(state.loc.ts("menus.save_manage_menu.file_filters.generic"), vec![
|
||||
"json".to_owned()
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
if is_export && format != SaveFormat::Freeware {
|
||||
params = params.save(true);
|
||||
};
|
||||
|
||||
if let Some(fs_container) = &state.fs_container {
|
||||
params = params.starting_dir(Some(fs_container.user_path.clone()));
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ImportExportMenuEntry {
|
||||
fn default() -> Self {
|
||||
ImportExportMenuEntry::Format
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaveSelectMenu {
|
||||
pub saves: [MenuSaveInfo; 3],
|
||||
pub export_info: MenuExportInfo,
|
||||
current_menu: CurrentMenu,
|
||||
save_menu: Menu<SaveMenuEntry>,
|
||||
save_detailed: Menu<usize>,
|
||||
|
@ -97,12 +237,14 @@ pub struct SaveSelectMenu {
|
|||
delete_confirm: Menu<DeleteConfirmMenuEntry>,
|
||||
load_confirm: Menu<LoadConfirmMenuEntry>,
|
||||
skip_difficulty_menu: bool,
|
||||
import_export_menu: Menu<ImportExportMenuEntry>,
|
||||
}
|
||||
|
||||
impl SaveSelectMenu {
|
||||
pub fn new() -> SaveSelectMenu {
|
||||
SaveSelectMenu {
|
||||
saves: [MenuSaveInfo::default(); 3],
|
||||
export_info: MenuExportInfo::default(),
|
||||
current_menu: CurrentMenu::SaveMenu,
|
||||
save_menu: Menu::new(0, 0, 230, 0),
|
||||
coop_menu: PlayerCountMenu::new(),
|
||||
|
@ -111,10 +253,11 @@ impl SaveSelectMenu {
|
|||
delete_confirm: Menu::new(0, 0, 75, 0),
|
||||
load_confirm: Menu::new(0, 0, 75, 0),
|
||||
skip_difficulty_menu: false,
|
||||
import_export_menu: Menu::new(0, 0, 75, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self, state: &mut SharedGameState, ctx: &Context) -> GameResult {
|
||||
pub fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
self.save_menu = Menu::new(0, 0, 230, 0);
|
||||
self.save_detailed = Menu::new(0, 0, 230, 0);
|
||||
self.coop_menu.on_title = true;
|
||||
|
@ -122,26 +265,36 @@ impl SaveSelectMenu {
|
|||
self.difficulty_menu = Menu::new(0, 0, 130, 0);
|
||||
self.delete_confirm = Menu::new(0, 0, 75, 0);
|
||||
self.load_confirm = Menu::new(0, 0, 75, 0);
|
||||
self.import_export_menu = Menu::new(0, 0, 75, 0);
|
||||
self.skip_difficulty_menu = false;
|
||||
|
||||
let mut should_mutate_selection = true;
|
||||
|
||||
let save_container = SaveContainer::load(ctx, state)?;
|
||||
for (iter, save) in self.saves.iter_mut().enumerate() {
|
||||
if let Ok(data) = filesystem::user_open(ctx, state.get_save_filename(iter + 1).unwrap_or(String::new())) {
|
||||
let loaded_save = GameProfile::load_from_save(data)?;
|
||||
if let Some(slot) = state.get_save_slot(iter + 1) {
|
||||
if let Some(loaded_profile) = save_container.get_profile(slot) {
|
||||
log::trace!("Loading save select menu. Iter - {}. {}", iter, loaded_profile.is_empty());
|
||||
save.current_map = loaded_profile.current_map;
|
||||
save.max_life = loaded_profile.max_life;
|
||||
save.life = loaded_profile.life;
|
||||
save.weapon_count = loaded_profile.weapon_data.iter().filter(|weapon| weapon.weapon_id != 0).count();
|
||||
save.weapon_id = loaded_profile.weapon_data.map(|weapon| weapon.weapon_id);
|
||||
save.difficulty = loaded_profile.difficulty;
|
||||
|
||||
save.current_map = loaded_save.current_map;
|
||||
save.max_life = loaded_save.max_life;
|
||||
save.life = loaded_save.life;
|
||||
save.weapon_count = loaded_save.weapon_data.iter().filter(|weapon| weapon.weapon_id != 0).count();
|
||||
save.weapon_id = loaded_save.weapon_data.map(|weapon| weapon.weapon_id);
|
||||
save.difficulty = loaded_save.difficulty;
|
||||
self.save_menu.push_entry(SaveMenuEntry::Load(iter), MenuEntry::SaveData(*save));
|
||||
|
||||
self.save_menu.push_entry(SaveMenuEntry::Load(iter), MenuEntry::SaveData(*save));
|
||||
if should_mutate_selection {
|
||||
should_mutate_selection = false;
|
||||
self.save_menu.selected = SaveMenuEntry::Load(iter);
|
||||
}
|
||||
} else {
|
||||
self.save_menu.push_entry(SaveMenuEntry::New(iter), MenuEntry::NewSave);
|
||||
|
||||
if should_mutate_selection {
|
||||
should_mutate_selection = false;
|
||||
self.save_menu.selected = SaveMenuEntry::Load(iter);
|
||||
if should_mutate_selection {
|
||||
should_mutate_selection = false;
|
||||
self.save_menu.selected = SaveMenuEntry::New(iter);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.save_menu.push_entry(SaveMenuEntry::New(iter), MenuEntry::NewSave);
|
||||
|
@ -153,6 +306,7 @@ impl SaveSelectMenu {
|
|||
}
|
||||
}
|
||||
|
||||
self.save_menu.push_entry(SaveMenuEntry::ImportExport, MenuEntry::Active(state.loc.ts("menus.save_manage_menu.import_export_save")));
|
||||
self.save_menu.push_entry(SaveMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
|
||||
|
||||
self.difficulty_menu.push_entry(
|
||||
|
@ -176,8 +330,6 @@ impl SaveSelectMenu {
|
|||
|
||||
self.difficulty_menu.selected = DifficultyMenuEntry::Difficulty(GameDifficulty::Normal);
|
||||
|
||||
//self.coop_menu.init(state, ctx);
|
||||
|
||||
self.delete_confirm.push_entry(
|
||||
DeleteConfirmMenuEntry::Title,
|
||||
MenuEntry::Disabled(state.loc.t("menus.save_menu.delete_confirm").to_owned()),
|
||||
|
@ -206,6 +358,37 @@ impl SaveSelectMenu {
|
|||
self.save_detailed.push_entry(0, MenuEntry::SaveDataSingle(save));
|
||||
}
|
||||
|
||||
self.export_info.format = state.settings.save_format;
|
||||
//self.export_info.set_picker_params(state, true);
|
||||
|
||||
self.import_export_menu.push_entry(
|
||||
ImportExportMenuEntry::Format,
|
||||
MenuEntry::Options(
|
||||
state.loc.ts("menus.save_manage_menu.save_format.entry"),
|
||||
0,
|
||||
vec![
|
||||
state.loc.ts("menus.save_manage_menu.save_format.auto"),
|
||||
state.loc.ts("menus.save_manage_menu.save_format.freeware"),
|
||||
state.loc.ts("menus.save_manage_menu.save_format.plus"),
|
||||
state.loc.ts("menus.save_manage_menu.save_format.switch"),
|
||||
]
|
||||
)
|
||||
);
|
||||
self.import_export_menu.push_entry(
|
||||
ImportExportMenuEntry::Import,
|
||||
MenuEntry::Disabled(state.loc.ts("menus.save_manage_menu.action_type.import"))
|
||||
);
|
||||
self.import_export_menu.push_entry(
|
||||
ImportExportMenuEntry::Export,
|
||||
MenuEntry::FilePicker(
|
||||
state.loc.ts("menus.save_manage_menu.action_type.export"),
|
||||
false,
|
||||
ImportExportMenuEntry::fpicker_from_format(state, self.export_info.format, true),
|
||||
None
|
||||
)
|
||||
);
|
||||
self.import_export_menu.push_entry(ImportExportMenuEntry::Back, MenuEntry::Active(state.loc.ts("common.back")));
|
||||
|
||||
self.update_sizes(state);
|
||||
|
||||
Ok(())
|
||||
|
@ -241,6 +424,11 @@ impl SaveSelectMenu {
|
|||
self.save_detailed.update_height(state);
|
||||
self.save_detailed.x = ((state.canvas_size.0 - self.save_detailed.width as f32) / 2.0).floor() as isize;
|
||||
self.save_detailed.y = -40 + ((state.canvas_size.1 - self.save_detailed.height as f32) / 2.0).floor() as isize;
|
||||
|
||||
self.import_export_menu.update_width(state);
|
||||
self.import_export_menu.update_height(state);
|
||||
self.import_export_menu.x = ((state.canvas_size.0 - self.import_export_menu.width as f32) / 2.0).floor() as isize;
|
||||
self.import_export_menu.y = ((state.canvas_size.1 - self.import_export_menu.height as f32) / 2.0).floor() as isize;
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
|
@ -267,17 +455,17 @@ impl SaveSelectMenu {
|
|||
MenuSelectionResult::Selected(SaveMenuEntry::Load(slot), _) => {
|
||||
state.save_slot = slot + 1;
|
||||
|
||||
if let Ok(_) =
|
||||
filesystem::user_open(ctx, state.get_save_filename(state.save_slot).unwrap_or(String::new()))
|
||||
{
|
||||
if let (_, MenuEntry::SaveData(save)) = self.save_menu.entries[slot] {
|
||||
self.save_detailed.entries.clear();
|
||||
self.save_detailed.push_entry(0, MenuEntry::SaveDataSingle(save));
|
||||
}
|
||||
|
||||
self.current_menu = CurrentMenu::LoadConfirm;
|
||||
self.load_confirm.selected = LoadConfirmMenuEntry::Start;
|
||||
if let (_, MenuEntry::SaveData(save)) = self.save_menu.entries[slot] {
|
||||
self.save_detailed.entries.clear();
|
||||
self.save_detailed.push_entry(0, MenuEntry::SaveDataSingle(save));
|
||||
}
|
||||
|
||||
self.current_menu = CurrentMenu::LoadConfirm;
|
||||
self.load_confirm.selected = LoadConfirmMenuEntry::Start;
|
||||
}
|
||||
MenuSelectionResult::Selected(SaveMenuEntry::ImportExport, _) => {
|
||||
self.current_menu = CurrentMenu::ImportExport;
|
||||
self.import_export_menu.selected = ImportExportMenuEntry::Back;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
@ -308,7 +496,9 @@ impl SaveSelectMenu {
|
|||
match self.save_menu.selected {
|
||||
SaveMenuEntry::Load(slot) => {
|
||||
state.sound_manager.play_sfx(17); // Player Death sfx
|
||||
filesystem::user_delete(ctx, state.get_save_filename(slot + 1).unwrap_or(String::new()))?;
|
||||
let mut save = SaveContainer::load(ctx, state)?;
|
||||
save.delete_profile(&ctx, state.get_save_slot(slot + 1).unwrap());
|
||||
save.save(ctx, state, SaveParams::default())?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -340,6 +530,67 @@ impl SaveSelectMenu {
|
|||
}
|
||||
_ => (),
|
||||
},
|
||||
CurrentMenu::ImportExport => match self.import_export_menu.tick(controller, state) {
|
||||
MenuSelectionResult::Selected(ImportExportMenuEntry::Format, toggle)
|
||||
| MenuSelectionResult::Right(ImportExportMenuEntry::Format, toggle, _) => {
|
||||
if let MenuEntry::Options(_, value, _) = toggle {
|
||||
*value = match *value {
|
||||
0..3 => *value + 1,
|
||||
3 => 0,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let format = ImportExportMenuEntry::format_from_value(*value).unwrap_or(state.settings.save_format);
|
||||
self.export_info.format = format;
|
||||
self.export_info.picker_params = ImportExportMenuEntry::fpicker_from_format(state, format, true);
|
||||
|
||||
let export_file_picker = self.import_export_menu.get_entry_mut(ImportExportMenuEntry::Export).unwrap();
|
||||
if let MenuEntry::FilePicker(_, _, params, _) = export_file_picker {
|
||||
*params = self.export_info.picker_params.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuSelectionResult::Left(ImportExportMenuEntry::Format, toggle, _) => {
|
||||
if let MenuEntry::Options(_, value, _) = toggle {
|
||||
*value = match *value {
|
||||
1..=3 => *value - 1,
|
||||
0 => 3,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let format = ImportExportMenuEntry::format_from_value(*value).unwrap_or(state.settings.save_format);
|
||||
self.export_info.format = format;
|
||||
self.export_info.picker_params = ImportExportMenuEntry::fpicker_from_format(state, format, true);
|
||||
|
||||
let export_file_picker = self.import_export_menu.get_entry_mut(ImportExportMenuEntry::Export).unwrap();
|
||||
if let MenuEntry::FilePicker(_, _, params, _) = export_file_picker {
|
||||
*params = self.export_info.picker_params.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuSelectionResult::Selected(ImportExportMenuEntry::Export, entry) => {
|
||||
let out_path = if let MenuEntry::FilePicker(_, _, _, selection) = entry {
|
||||
if let Some(location) = selection {
|
||||
location.first()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else { None };
|
||||
|
||||
if out_path.is_none() {
|
||||
// Export path is not selected, so we break export operation
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
let mut save_container = SaveContainer::load(ctx, state)?;
|
||||
save_container.export(state, ctx, self.export_info.format, self.export_info.save_params.clone(), out_path.unwrap().clone())?;
|
||||
}
|
||||
MenuSelectionResult::Selected(ImportExportMenuEntry::Back, _) | MenuSelectionResult::Canceled => {
|
||||
self.current_menu = CurrentMenu::SaveMenu;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -364,6 +615,9 @@ impl SaveSelectMenu {
|
|||
self.save_detailed.draw(state, ctx)?;
|
||||
self.load_confirm.draw(state, ctx)?;
|
||||
}
|
||||
CurrentMenu::ImportExport => {
|
||||
self.import_export_menu.draw(state, ctx)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::framework::context::Context;
|
|||
use crate::framework::error::GameResult;
|
||||
use crate::framework::graphics::VSyncMode;
|
||||
use crate::framework::{filesystem, graphics};
|
||||
use crate::game::profile::{SaveFormat};
|
||||
use crate::game::shared_game_state::{CutsceneSkipMode, ScreenShakeIntensity, SharedGameState, TimingMode, WindowMode};
|
||||
use crate::graphics::font::Font;
|
||||
use crate::input::combined_menu_controller::CombinedMenuController;
|
||||
|
@ -118,6 +119,7 @@ enum BehaviorMenuEntry {
|
|||
CutsceneSkipMode,
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
DiscordRPC,
|
||||
SaveFormat,
|
||||
Back,
|
||||
}
|
||||
|
||||
|
@ -595,6 +597,19 @@ impl SettingsMenu {
|
|||
),
|
||||
);
|
||||
|
||||
self.behavior.push_entry(
|
||||
BehaviorMenuEntry::SaveFormat,
|
||||
MenuEntry::Options(
|
||||
state.loc.t("menus.options_menu.behavior_menu.save_format.entry").to_owned(),
|
||||
state.settings.save_format as usize,
|
||||
vec![
|
||||
state.loc.t("menus.options_menu.behavior_menu.save_format.freeware").to_owned(),
|
||||
state.loc.t("menus.options_menu.behavior_menu.save_format.plus").to_owned(),
|
||||
state.loc.t("menus.options_menu.behavior_menu.save_format.switch").to_owned(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
self.behavior.push_entry(BehaviorMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
|
||||
|
||||
self.links.push_entry(LinksMenuEntry::Back, MenuEntry::Active(state.loc.t("common.back").to_owned()));
|
||||
|
@ -1056,6 +1071,35 @@ impl SettingsMenu {
|
|||
}
|
||||
}
|
||||
}
|
||||
MenuSelectionResult::Selected(BehaviorMenuEntry::SaveFormat, toggle)
|
||||
| MenuSelectionResult::Right(BehaviorMenuEntry::SaveFormat, toggle, _) => {
|
||||
if let MenuEntry::Options(_, value, _) = toggle {
|
||||
let (new_mode, new_value) = match state.settings.save_format {
|
||||
SaveFormat::Freeware => (SaveFormat::Plus, 1),
|
||||
SaveFormat::Plus => (SaveFormat::Switch, 2),
|
||||
SaveFormat::Switch => (SaveFormat::Freeware, 0),
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
state.settings.save_format = new_mode;
|
||||
*value = new_value;
|
||||
let _ = state.settings.save(ctx);
|
||||
}
|
||||
}
|
||||
MenuSelectionResult::Left(BehaviorMenuEntry::SaveFormat, toggle, _) => {
|
||||
if let MenuEntry::Options(_, value, _) = toggle {
|
||||
let (new_mode, new_value) = match state.settings.save_format {
|
||||
SaveFormat::Freeware => (SaveFormat::Switch, 2),
|
||||
SaveFormat::Plus => (SaveFormat::Freeware, 0),
|
||||
SaveFormat::Switch => (SaveFormat::Plus, 1),
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
state.settings.save_format = new_mode;
|
||||
*value = new_value;
|
||||
let _ = state.settings.save(ctx);
|
||||
}
|
||||
}
|
||||
MenuSelectionResult::Selected(BehaviorMenuEntry::Back, _) | MenuSelectionResult::Canceled => {
|
||||
self.current = CurrentMenu::MainMenu;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::framework::error::GameResult;
|
|||
use crate::framework::filesystem;
|
||||
use crate::mod_requirements::ModRequirements;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModInfo {
|
||||
pub id: String,
|
||||
pub requirement: Requirement,
|
||||
|
@ -173,6 +173,7 @@ impl ModList {
|
|||
description = "mod.txt not found".to_string();
|
||||
}
|
||||
|
||||
log::debug!("CSP Mod Loaded: {:?}", ModInfo { id: id.clone(), requirement, priority, save_slot, path: path.clone(), name: name.clone(), description: description.clone(), valid });
|
||||
mods.push(ModInfo { id, requirement, priority, save_slot, path, name, description, valid })
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +183,14 @@ impl ModList {
|
|||
Ok(ModList { mods })
|
||||
}
|
||||
|
||||
pub fn get_mod_info_from_path(&self, mod_path: String) -> Option<ModInfo> {
|
||||
if let Some(mod_sel) = self.mods.iter().find(|x| x.path == mod_path) {
|
||||
Some(mod_sel.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_save_from_path(&self, mod_path: String) -> i32 {
|
||||
if let Some(mod_sel) = self.mods.iter().find(|x| x.path == mod_path) {
|
||||
mod_sel.save_slot
|
||||
|
|
86
src/util/file_picker.rs
Normal file
86
src/util/file_picker.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
use rfd::FileDialog;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FilePickerParams {
|
||||
pub save: bool, /// Opens save file dialog
|
||||
pub multiple: bool,
|
||||
pub pick_dirs: bool,
|
||||
pub file_name: Option<String>, /// Starting file name.
|
||||
pub starting_dir: Option<PathBuf>,
|
||||
pub filters: HashMap<String, Vec<String>>, // filter name -> file extensions
|
||||
}
|
||||
|
||||
impl FilePickerParams {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn save(mut self, save: bool) -> Self {
|
||||
self.save = save;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn multiple(mut self, multiple: bool) -> Self {
|
||||
self.multiple = multiple;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pick_dirs(mut self, pick_dirs: bool) -> Self {
|
||||
self.pick_dirs = pick_dirs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file_name(mut self, file_name: Option<String>) -> Self {
|
||||
self.file_name = file_name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn starting_dir(mut self, starting_dir: Option<PathBuf>) -> Self {
|
||||
self.starting_dir = starting_dir;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter(mut self, name: String, ext: Vec<String>) -> Self {
|
||||
self.filters.insert(name, ext);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
pub fn open_file_picker(params: &FilePickerParams) -> Option<Vec<PathBuf>> {
|
||||
log::trace!("Call a file picker dialog with params: {:?}", params.clone());
|
||||
let mut dialog = FileDialog::new();
|
||||
|
||||
if let Some(filename) = params.file_name.clone() {
|
||||
dialog = dialog.set_file_name(filename);
|
||||
}
|
||||
|
||||
if let Some(dir) = params.starting_dir.clone() {
|
||||
dialog = dialog.set_directory(dir);
|
||||
}
|
||||
|
||||
for filter in params.filters.iter() {
|
||||
dialog = dialog.add_filter(filter.0, filter.1);
|
||||
}
|
||||
|
||||
let selected_files = match (params.pick_dirs, params.multiple, params.save) {
|
||||
(_, _, true) => dialog.save_file().map(|path| vec![path]),
|
||||
(true, false, _) => dialog.pick_folder().map(|path| vec![path]),
|
||||
(true, true, _) => dialog.pick_folders(),
|
||||
(false, false, _) => dialog.pick_file().map(|path| vec![path]),
|
||||
(false, true, _) => dialog.pick_files(),
|
||||
};
|
||||
|
||||
log::trace!("Selected file entries: {:?}", selected_files.clone());
|
||||
|
||||
return selected_files;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "horizon"))]
|
||||
pub fn open_file_picker(params: &FileChooserParams) -> Option<Vec<PathBuf>> {
|
||||
None
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bitvec;
|
||||
pub mod browser;
|
||||
pub mod file_picker;
|
||||
pub mod rng;
|
||||
|
|
Loading…
Reference in a new issue