make localization system dynamic

This commit is contained in:
Sallai József 2022-08-26 03:17:45 +03:00
parent 3d86995feb
commit 028c60157d
12 changed files with 143 additions and 106 deletions

View File

@ -1,4 +1,8 @@
{
"name": "English",
"font": "csfont.fnt",
"font_scale": "0.5",
"common": {
"name": "doukutsu-rs",
"back": "< Back",

View File

@ -1,4 +1,8 @@
{
"name": "Japanese",
"font": "csfontjp.fnt",
"font_scale": "0.5",
"common": {
"name": "doukutsu-rs",
"back": "< 戻る",

View File

@ -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")),
],
),
],
)],
}

View File

@ -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()

View File

@ -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<AnimatedFace>,
pub string_table: HashMap<String, String>,
pub missile_flags: Vec<u16>,
pub locales: HashMap<String, Locale>,
pub locales: Vec<Locale>,
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<String>, 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(())

View File

@ -325,7 +325,6 @@ pub fn exists_find<P: AsRef<path::Path>>(ctx: &Context, roots: &Vec<String>, pat
false
}
/// Check whether a path points at a file.
pub fn is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.is_file(path)
@ -344,6 +343,26 @@ pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<
ctx.filesystem.read_dir(path)
}
pub fn read_dir_find<P: AsRef<path::Path>>(
ctx: &Context,
roots: &Vec<String>,
path: P,
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
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.
///

View File

@ -5,24 +5,26 @@ use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Locale {
strings: HashMap<String, String>,
pub code: String,
pub name: String,
pub font: FontData,
strings: HashMap<String, String>,
}
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<String>, 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::<f32>().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<String, String> {
@ -60,4 +62,8 @@ impl Locale {
string
}
pub fn set_font(&mut self, font: FontData) {
self.font = font;
}
}

View File

@ -101,7 +101,7 @@ pub struct Menu<T: std::cmp::PartialEq> {
pub non_interactive: bool,
}
impl<T: std::cmp::PartialEq + std::default::Default + Copy> Menu<T> {
impl<T: std::cmp::PartialEq + std::default::Default + Clone> Menu<T> {
pub fn new(x: isize, y: isize, width: u16, height: u16) -> Menu<T> {
Menu {
x,
@ -716,7 +716,7 @@ impl<T: std::cmp::PartialEq + std::default::Default + Copy> Menu<T> {
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<T: std::cmp::PartialEq + std::default::Default + Copy> Menu<T> {
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<T: std::cmp::PartialEq + std::default::Default + Copy> Menu<T> {
|| 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<T: std::cmp::PartialEq + std::default::Default + Copy> Menu<T> {
|| 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);
}
}

View File

@ -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, _) => {

View File

@ -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()

View File

@ -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,

View File

@ -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<Language> {
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<Locale> = None;
let mut en_locale: Option<Locale> = 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 {