diff --git a/src/builtin/locale/en.json b/src/builtin/builtin_data/locale/en.json similarity index 98% rename from src/builtin/locale/en.json rename to src/builtin/builtin_data/locale/en.json index 30c2796..3af3c50 100644 --- a/src/builtin/locale/en.json +++ b/src/builtin/builtin_data/locale/en.json @@ -1,4 +1,8 @@ { + "name": "English", + "font": "csfont.fnt", + "font_scale": "0.5", + "common": { "name": "doukutsu-rs", "back": "< Back", diff --git a/src/builtin/locale/jp.json b/src/builtin/builtin_data/locale/jp.json similarity index 98% rename from src/builtin/locale/jp.json rename to src/builtin/builtin_data/locale/jp.json index 79b2a00..20fdeb6 100644 --- a/src/builtin/locale/jp.json +++ b/src/builtin/builtin_data/locale/jp.json @@ -1,4 +1,8 @@ { + "name": "Japanese", + "font": "csfontjp.fnt", + "font_scale": "0.5", + "common": { "name": "doukutsu-rs", "back": "< 戻る", diff --git a/src/builtin_fs.rs b/src/builtin_fs.rs index b19f239..133963f 100644 --- a/src/builtin_fs.rs +++ b/src/builtin_fs.rs @@ -181,6 +181,13 @@ impl BuiltinFS { ), ], ), + FSNode::Directory( + "locale", + vec![ + FSNode::File("en.json", include_bytes!("builtin/builtin_data/locale/en.json")), + FSNode::File("jp.json", include_bytes!("builtin/builtin_data/locale/jp.json")), + ], + ), ], ), FSNode::Directory( @@ -196,13 +203,6 @@ impl BuiltinFS { "lightmap", vec![FSNode::File("spot.png", include_bytes!("builtin/lightmap/spot.png"))], ), - FSNode::Directory( - "locale", - vec![ - FSNode::File("en.json", include_bytes!("builtin/locale/en.json")), - FSNode::File("jp.json", include_bytes!("builtin/locale/jp.json")), - ], - ), ], )], } diff --git a/src/components/map_system.rs b/src/components/map_system.rs index 4b0ace4..ef53ef2 100644 --- a/src/components/map_system.rs +++ b/src/components/map_system.rs @@ -8,7 +8,7 @@ use crate::graphics; use crate::input::touch_controls::TouchControlType; use crate::player::Player; use crate::scripting::tsc::text_script::TextScriptExecutionState; -use crate::shared_game_state::{Language, SharedGameState}; +use crate::shared_game_state::SharedGameState; use crate::stage::Stage; #[derive(Copy, Clone, Eq, PartialEq)] @@ -197,7 +197,7 @@ impl MapSystem { graphics::draw_rect(ctx, rect_black_bar, Color::new(0.0, 0.0, 0.0, 1.0))?; } - let map_name = if state.settings.locale == Language::Japanese { + let map_name = if state.constants.is_cs_plus && state.settings.locale == "jp" { stage.data.name_jp.chars() } else { stage.data.name.chars() diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index 1707748..2b1085b 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -16,7 +16,7 @@ use crate::i18n::Locale; use crate::player::ControlMode; use crate::scripting::tsc::text_script::TextScriptEncoding; use crate::settings::Settings; -use crate::shared_game_state::{FontData, Language, Season}; +use crate::shared_game_state::{FontData, Season}; use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform}; use crate::sound::SoundManager; @@ -341,7 +341,7 @@ pub struct EngineConstants { pub animated_face_table: Vec, pub string_table: HashMap, pub missile_flags: Vec, - pub locales: HashMap, + pub locales: Vec, pub gamepad: GamepadConsts, } @@ -1671,7 +1671,7 @@ impl EngineConstants { animated_face_table: vec![AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] }], string_table: HashMap::new(), missile_flags: vec![200, 201, 202, 218, 550, 766, 880, 920, 1551], - locales: HashMap::new(), + locales: Vec::new(), gamepad: GamepadConsts { button_rects: HashMap::from([ (Button::North, GamepadConsts::rects(Rect::new(0, 0, 32, 16))), @@ -1787,8 +1787,8 @@ impl EngineConstants { pub fn rebuild_path_list(&mut self, mod_path: Option, season: Season, settings: &Settings) { self.base_paths.clear(); - self.base_paths.push("/".to_owned()); self.base_paths.push("/builtin/builtin_data/".to_owned()); + self.base_paths.push("/".to_owned()); if self.is_cs_plus { self.base_paths.insert(0, "/base/".to_owned()); @@ -1803,12 +1803,12 @@ impl EngineConstants { } } - if settings.locale != Language::English { - self.base_paths.insert(0, format!("/base/{}/", settings.locale.to_language_code())); + if settings.locale != "en".to_string() { + self.base_paths.insert(0, format!("/base/{}/", settings.locale)); } } else { - if settings.locale != Language::English { - self.base_paths.insert(0, format!("/{}/", settings.locale.to_language_code())); + if settings.locale != "en".to_string() { + self.base_paths.insert(0, format!("/{}/", settings.locale)); } } @@ -1876,15 +1876,27 @@ impl EngineConstants { pub fn load_locales(&mut self, ctx: &mut Context) -> GameResult { self.locales.clear(); - for language in Language::values() { - // Only Switch 1.3+ data contains an entirely valid JP font - let font = if language == Language::Japanese && filesystem::exists(ctx, "/base/credit_jp.tsc") { - FontData::new("csfontjp.fnt".to_owned(), 0.5, 0.0) - } else { - language.font() + let locale_files = filesystem::read_dir_find(ctx, &self.base_paths, "locale/"); + + for locale_file in locale_files.unwrap() { + if locale_file.extension().unwrap() != "json" { + continue; + } + + let locale_code = { + let filename = locale_file.file_name().unwrap().to_string_lossy(); + let mut parts = filename.split('.'); + parts.next().unwrap().to_string() }; - self.locales.insert(language.to_string(), Locale::new(ctx, language.to_language_code(), font)); - log::info!("Loaded locale {} ({}).", language.to_string(), language.to_language_code()); + + let mut locale = Locale::new(ctx, &self.base_paths, &locale_code); + + if locale_code == "jp" && filesystem::exists(ctx, "/base/credit_jp.tsc") { + locale.set_font(FontData::new("csfontjp.fnt".to_owned(), 0.5, 0.0)); + } + + self.locales.push(locale.clone()); + log::info!("Loaded locale {} ({})", locale_code, locale.name.clone()); } Ok(()) diff --git a/src/framework/filesystem.rs b/src/framework/filesystem.rs index 924be7b..544d358 100644 --- a/src/framework/filesystem.rs +++ b/src/framework/filesystem.rs @@ -325,7 +325,6 @@ pub fn exists_find>(ctx: &Context, roots: &Vec, pat false } - /// Check whether a path points at a file. pub fn is_file>(ctx: &Context, path: P) -> bool { ctx.filesystem.is_file(path) @@ -344,6 +343,26 @@ pub fn read_dir>(ctx: &Context, path: P) -> GameResult>( + ctx: &Context, + roots: &Vec, + path: P, +) -> GameResult>> { + let mut files = Vec::new(); + + for root in roots { + let mut full_path = root.to_string(); + full_path.push_str(path.as_ref().to_string_lossy().as_ref()); + + let result = ctx.filesystem.read_dir(full_path); + if result.is_ok() { + files.push(result.unwrap()); + } + } + + Ok(Box::new(files.into_iter().flatten())) +} + /// Adds the given (absolute) path to the list of directories /// it will search to look for resources. /// diff --git a/src/i18n.rs b/src/i18n.rs index 5d35f0f..d43b403 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -5,24 +5,26 @@ use std::collections::HashMap; #[derive(Debug, Clone)] pub struct Locale { - strings: HashMap, + pub code: String, + pub name: String, pub font: FontData, + strings: HashMap, } impl Locale { - pub fn new(ctx: &mut Context, code: &str, font: FontData) -> Locale { - let mut filename = "en.json".to_owned(); - - if code != "en" && filesystem::exists(ctx, &format!("/builtin/locale/{}.json", code)) { - filename = format!("{}.json", code); - } - - let file = filesystem::open(ctx, &format!("/builtin/locale/{}", filename)).unwrap(); + pub fn new(ctx: &mut Context, base_paths: &Vec, code: &str) -> Locale { + let file = filesystem::open_find(ctx, base_paths, &format!("locale/{}.json", code)).unwrap(); let json: serde_json::Value = serde_json::from_reader(file).unwrap(); let strings = Locale::flatten(&json); - Locale { strings, font } + let name = strings["name"].clone(); + + let font_name = strings["font"].clone(); + let font_scale = strings["font_scale"].parse::().unwrap_or(1.0); + let font = FontData::new(font_name, font_scale, 0.0); + + Locale { code: code.to_string(), name, font, strings } } fn flatten(json: &serde_json::Value) -> HashMap { @@ -60,4 +62,8 @@ impl Locale { string } + + pub fn set_font(&mut self, font: FontData) { + self.font = font; + } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 085f0fe..d6dfb4a 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -101,7 +101,7 @@ pub struct Menu { pub non_interactive: bool, } -impl Menu { +impl Menu { pub fn new(x: isize, y: isize, width: u16, height: u16) -> Menu { Menu { x, @@ -716,7 +716,7 @@ impl Menu { if let Some((id, entry)) = self.entries.get(selected) { if entry.selectable() { - self.selected = *id; + self.selected = id.clone(); break; } } else { @@ -727,7 +727,7 @@ impl Menu { let mut y = self.y as f32 + 8.0; for (id, entry) in self.entries.iter_mut() { - let idx = *id; + let idx = id.clone(); let entry_bounds = Rect::new_size(self.x, y as isize, self.width as isize, entry.height() as isize); let right_entry_bounds = Rect::new_size(self.x + self.width as isize, y as isize, self.width as isize, entry.height() as isize); @@ -747,7 +747,7 @@ impl Menu { || state.touch_controls.consume_click_in(entry_bounds) => { state.sound_manager.play_sfx(18); - self.selected = idx; + self.selected = idx.clone(); return MenuSelectionResult::Selected(idx, entry); } MenuEntry::Options(_, _, _) | MenuEntry::OptionsBar(_, _) @@ -755,35 +755,35 @@ impl Menu { || state.touch_controls.consume_click_in(left_entry_bounds) => { state.sound_manager.play_sfx(1); - return MenuSelectionResult::Left(self.selected, entry, -1); + return MenuSelectionResult::Left(self.selected.clone(), entry, -1); } MenuEntry::Options(_, _, _) | MenuEntry::OptionsBar(_, _) if (self.selected == idx && controller.trigger_right()) || state.touch_controls.consume_click_in(right_entry_bounds) => { state.sound_manager.play_sfx(1); - return MenuSelectionResult::Right(self.selected, entry, 1); + return MenuSelectionResult::Right(self.selected.clone(), entry, 1); } MenuEntry::DescriptiveOptions(_, _, _, _) if (self.selected == idx && controller.trigger_left()) || state.touch_controls.consume_click_in(left_entry_bounds) => { state.sound_manager.play_sfx(1); - return MenuSelectionResult::Left(self.selected, entry, -1); + return MenuSelectionResult::Left(self.selected.clone(), entry, -1); } MenuEntry::DescriptiveOptions(_, _, _, _) | MenuEntry::SaveData(_) if (self.selected == idx && controller.trigger_right()) || state.touch_controls.consume_click_in(right_entry_bounds) => { state.sound_manager.play_sfx(1); - return MenuSelectionResult::Right(self.selected, entry, 1); + return MenuSelectionResult::Right(self.selected.clone(), entry, 1); } MenuEntry::Control(_, _) => { if self.selected == idx && controller.trigger_ok() || state.touch_controls.consume_click_in(entry_bounds) { state.sound_manager.play_sfx(18); - self.selected = idx; + self.selected = idx.clone(); return MenuSelectionResult::Selected(idx, entry); } } diff --git a/src/menu/settings_menu.rs b/src/menu/settings_menu.rs index 670fe2e..580545b 100644 --- a/src/menu/settings_menu.rs +++ b/src/menu/settings_menu.rs @@ -9,9 +9,7 @@ use crate::input::combined_menu_controller::CombinedMenuController; use crate::menu::MenuEntry; use crate::menu::{Menu, MenuSelectionResult}; use crate::scene::title_scene::TitleScene; -use crate::shared_game_state::{ - CutsceneSkipMode, Language, ScreenShakeIntensity, SharedGameState, TimingMode, WindowMode, -}; +use crate::shared_game_state::{CutsceneSkipMode, ScreenShakeIntensity, SharedGameState, TimingMode, WindowMode}; use crate::sound::InterpolationMode; use crate::{graphics, VSyncMode}; @@ -95,16 +93,16 @@ impl Default for SoundtrackMenuEntry { } } -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] enum LanguageMenuEntry { Title, - Language(Language), + Language(String), Back, } impl Default for LanguageMenuEntry { fn default() -> Self { - LanguageMenuEntry::Language(Language::English) + LanguageMenuEntry::Back } } @@ -280,8 +278,9 @@ impl SettingsMenu { self.language.push_entry(LanguageMenuEntry::Title, MenuEntry::Disabled(state.t("menus.options_menu.language"))); - for language in Language::values() { - self.language.push_entry(LanguageMenuEntry::Language(language), MenuEntry::Active(language.to_string())); + for locale in &state.constants.locales { + self.language + .push_entry(LanguageMenuEntry::Language(locale.code.clone()), MenuEntry::Active(locale.name.clone())); } self.language.push_entry(LanguageMenuEntry::Back, MenuEntry::Active(state.t("common.back"))); @@ -460,7 +459,6 @@ impl SettingsMenu { self.current = CurrentMenu::ControlsMenu; } MenuSelectionResult::Selected(MainMenuEntry::Language, _) => { - self.language.selected = LanguageMenuEntry::Language(state.settings.locale); self.current = CurrentMenu::LanguageMenu; } MenuSelectionResult::Selected(MainMenuEntry::Behavior, _) => { diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 9d138d0..a3b7f62 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -48,7 +48,7 @@ use crate::scene::Scene; use crate::scripting::tsc::credit_script::CreditScriptVM; use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::settings::ControllerType; -use crate::shared_game_state::{CutsceneSkipMode, Language, PlayerCount, ReplayState, SharedGameState, TileSize}; +use crate::shared_game_state::{CutsceneSkipMode, PlayerCount, ReplayState, SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage, StageTexturePaths}; use crate::texture_set::SpriteBatch; use crate::weapon::bullet::BulletManager; @@ -2182,7 +2182,7 @@ impl Scene for GameScene { let map_name = if self.stage.data.name == "u" { state.constants.title.intro_text.chars() } else { - if state.settings.locale == Language::Japanese { + if state.constants.is_cs_plus && state.settings.locale == "jp" { self.stage.data.name_jp.chars() } else { self.stage.data.name.chars() diff --git a/src/settings.rs b/src/settings.rs index 14974e7..2c5017c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,7 +10,7 @@ use crate::input::keyboard_player_controller::KeyboardController; use crate::input::player_controller::PlayerController; use crate::input::touch_player_controller::TouchPlayerController; use crate::player::TargetPlayer; -use crate::shared_game_state::{CutsceneSkipMode, Language, ScreenShakeIntensity, TimingMode, WindowMode}; +use crate::shared_game_state::{CutsceneSkipMode, ScreenShakeIntensity, TimingMode, WindowMode}; use crate::sound::InterpolationMode; #[derive(serde::Serialize, serde::Deserialize)] @@ -68,7 +68,7 @@ pub struct Settings { #[serde(skip)] pub debug_outlines: bool, pub fps_counter: bool, - pub locale: Language, + pub locale: String, #[serde(default = "default_window_mode")] pub window_mode: WindowMode, #[serde(default = "default_vsync")] @@ -89,7 +89,7 @@ fn default_true() -> bool { #[inline(always)] fn current_version() -> u32 { - 19 + 21 } #[inline(always)] @@ -118,8 +118,8 @@ fn default_vol() -> f32 { } #[inline(always)] -fn default_locale() -> Language { - Language::English +fn default_locale() -> String { + "en".to_string() } #[inline(always)] @@ -303,6 +303,17 @@ impl Settings { self.cutscene_skip_mode = CutsceneSkipMode::Hold; } + if self.version == 20 { + self.version = 21; + + self.locale = match self.locale.as_str() { + "English" => "en".to_string(), + "Japanese" => "jp".to_string(), + + _ => default_locale(), + }; + } + if self.version != initial_version { log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version); } @@ -400,7 +411,7 @@ impl Default for Settings { infinite_booster: false, debug_outlines: false, fps_counter: false, - locale: Language::English, + locale: default_locale(), window_mode: WindowMode::Windowed, vsync_mode: VSyncMode::VSync, screen_shake_intensity: ScreenShakeIntensity::Full, diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index aecac87..384e3f0 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -118,44 +118,6 @@ impl GameDifficulty { } } -#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, num_derive::FromPrimitive, serde::Serialize, serde::Deserialize)] -pub enum Language { - English, - Japanese, -} - -impl Language { - pub fn to_language_code(self) -> &'static str { - match self { - Language::English => "en", - Language::Japanese => "jp", - } - } - - pub fn to_string(self) -> String { - match self { - Language::English => "English".to_string(), - Language::Japanese => "Japanese".to_string(), - } - } - - pub fn font(self) -> FontData { - match self { - Language::English => FontData::new("csfont.fnt".to_owned(), 0.5, 0.0), - // Use default as fallback if no proper JP font is found - Language::Japanese => FontData::new("0.fnt".to_owned(), 1.0, 0.0), - } - } - - pub fn from_primitive(val: usize) -> Language { - return num_traits::FromPrimitive::from_usize(val).unwrap_or(Language::English); - } - - pub fn values() -> Vec { - vec![Language::English, Language::Japanese] - } -} - #[derive(PartialEq, Eq, Copy, Clone, num_derive::FromPrimitive, serde::Serialize, serde::Deserialize)] pub enum ScreenShakeIntensity { Full, @@ -399,12 +361,12 @@ impl SharedGameState { } } - constants.load_locales(ctx)?; - let season = Season::current(); constants.rebuild_path_list(None, season, &settings); - let active_locale = constants.locales.get(&settings.locale.to_string()).unwrap(); + constants.load_locales(ctx)?; + + let active_locale = SharedGameState::active_locale(settings.locale.clone(), constants.clone()); if constants.is_cs_plus { constants.font_scale = active_locale.font.scale; @@ -843,9 +805,30 @@ impl SharedGameState { return self.difficulty as u16; } - pub fn get_active_locale(&self) -> &Locale { - let active_locale = self.constants.locales.get(&self.settings.locale.to_string()).unwrap(); - return active_locale; + fn active_locale(user_locale: String, constants: EngineConstants) -> Locale { + let mut active_locale: Option = None; + let mut en_locale: Option = None; + + for locale in &constants.locales { + if locale.code == "en" { + en_locale = Some(locale.clone()); + } + + if locale.code == user_locale { + active_locale = Some(locale.clone()); + break; + } + } + + match active_locale { + Some(locale) => locale, + None => en_locale.unwrap(), + } + } + + pub fn get_active_locale(&self) -> Locale { + let locale = SharedGameState::active_locale(self.settings.locale.clone(), self.constants.clone()); + locale.clone() } pub fn t(&self, key: &str) -> String {