1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-03-31 14:56:57 +00:00

Basic i18n support (#82)

This commit is contained in:
József Sallai 2022-03-15 03:54:03 +02:00 committed by GitHub
parent 500f53bebb
commit 1795d71b37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 744 additions and 162 deletions

102
src/builtin/locale/en.json Normal file
View file

@ -0,0 +1,102 @@
{
"common": {
"name": "doukutsu-rs",
"back": "< Back",
"yes": "Yes",
"no": "No",
"on": "ON",
"off": "OFF"
},
"menus": {
"main_menu": {
"start": "Start Game",
"challenges": "Challenges",
"options": "Options",
"editor": "Editor",
"jukebox": "Jukebox",
"quit": "Quit"
},
"pause_menu": {
"resume": "Resume",
"retry": "Retry",
"options": "Options",
"title": "Title",
"title_confirm": "Title?",
"quit": "Quit",
"quit_confirm": "Quit?"
},
"save_menu": {
"new": "New Save",
"delete_info": "Press Right to Delete",
"delete_confirm": "Delete?"
},
"difficulty_menu": {
"title": "Select Difficulty",
"easy": "Easy",
"normal": "Normal",
"hard": "Hard"
},
"challenge_menu": {
"start": "Start",
"no_replay": "No Replay",
"replay_best": "Replay Best"
},
"options_menu": {
"graphics": "Graphics...",
"graphics_menu": {
"lighting_effects": "Lighting effects:",
"weapon_light_cone": "Weapon light cone:",
"motion_interpolation": "Motion interpolation:",
"subpixel_scrolling": "Subpixel scrolling:",
"original_textures": "Original textures:",
"seasonal_textures": "Seasonal textures:",
"renderer": "Renderer:"
},
"sound": "Sound...",
"sound_menu": {
"music_volume": "Music Volume",
"effects_volume": "Effects Volume",
"bgm_interpolation": {
"entry": "BGM Interpolation:",
"linear": "Linear",
"linear_desc": "Fast, similar to freeware on Vista+",
"cosine": "Cosine",
"cosine_desc": "Cosine interpolation",
"cubic": "Cubic",
"cubic_desc": "Cubic interpolation",
"linear_lp": "Linear+LP",
"linear_lp_desc": "Slowest, similar to freeware on XP",
"nearest": "Nearest",
"nearest_desc": "Fastest, lowest quality"
},
"soundtrack": "Soundtrack: {soundtrack}"
},
"language": "Language...",
"game_timing": {
"entry": "Game timing:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
}
}
},
"soundtrack": {
"organya": "Organya",
"remastered": "Remastered",
"new": "New",
"famitracks": "Famitracks"
},
"game": {
"cutscene_skip": "Hold {key} to skip the cutscene"
}
}

102
src/builtin/locale/jp.json Normal file
View file

@ -0,0 +1,102 @@
{
"common": {
"name": "doukutsu-rs",
"back": "< 戻る",
"yes": "はい",
"no": "いいえ",
"on": "オン",
"off": "オフ"
},
"menus": {
"main_menu": {
"start": "ゲームスタート",
"challenges": "チャレンジ",
"options": "設定",
"editor": "レベルエディタ",
"jukebox": "ジュークボックス",
"quit": "辞める"
},
"pause_menu": {
"resume": "再開",
"retry": "リトライ",
"options": "設定",
"title": "メインメニュー",
"title_confirm": "メインメニュー?",
"quit": "辞める",
"quit_confirm": "辞める?"
},
"save_menu": {
"new": "新しいデータ",
"delete_info": "右矢印キーで削除",
"delete_confirm": "消去?"
},
"difficulty_menu": {
"title": "難易度選択",
"easy": "簡単",
"normal": "普通",
"hard": "難しい"
},
"challenge_menu": {
"start": "スタート",
"no_replay": "ノーリプレイ",
"replay_best": "ベストプレイを再生"
},
"options_menu": {
"graphics": "グラフィック",
"graphics_menu": {
"lighting_effects": "ライティング効果:",
"weapon_light_cone": "兵器のライトコーン:",
"motion_interpolation": "モーション補間:",
"subpixel_scrolling": "サブピクセルスクロール:",
"original_textures": "オリジナルテクスチャ:",
"seasonal_textures": "季節ものテクスチャ:",
"renderer": "レンダラ:"
},
"sound": "サウンド",
"sound_menu": {
"music_volume": "BGM音量",
"effects_volume": "サウンド音量",
"bgm_interpolation": {
"entry": "BGM内挿",
"linear": "線形補間",
"linear_desc": "速い、フリーウェア版に近いVista+",
"cosine": "余弦",
"cosine_desc": "余弦補間",
"cubic": "立方体",
"cubic_desc": "立方体補間",
"linear_lp": "線形補間+LP",
"linear_lp_desc": "最も遅い、フリーウェア版に近いXP",
"nearest": "最近傍",
"nearest_desc": "最速、最低品質"
},
"soundtrack": "サウンドトラック: {soundtrack}"
},
"language": "言語",
"game_timing": {
"entry": "ゲームのタイミング:",
"50tps": "50tps (freeware)",
"60tps": "60tps (CS+)"
}
}
},
"soundtrack": {
"organya": "オルガーニャ",
"remastered": "リマスター",
"new": "新",
"famitracks": "ファミトラック"
},
"game": {
"cutscene_skip": "{key} を押し続け、カットシーンをスキップ"
}
}

View file

@ -1,9 +1,9 @@
use std::{fmt, io};
use std::fmt::Debug;
use std::io::Cursor;
use std::io::ErrorKind;
use std::io::SeekFrom;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};
use crate::framework::error::GameError::FilesystemError;
use crate::framework::error::GameResult;
@ -68,32 +68,22 @@ enum FSNode {
impl FSNode {
fn get_name(&self) -> &'static str {
match self {
FSNode::File(name, _) => { name }
FSNode::Directory(name, _) => { name }
FSNode::File(name, _) => name,
FSNode::Directory(name, _) => name,
}
}
fn to_file(&self) -> GameResult<Box<dyn VFile>> {
match self {
FSNode::File(_, buf) => { Ok(BuiltinFile::from(buf)) }
FSNode::Directory(name, _) => { Err(FilesystemError(format!("{} is a directory.", name))) }
FSNode::File(_, buf) => Ok(BuiltinFile::from(buf)),
FSNode::Directory(name, _) => Err(FilesystemError(format!("{} is a directory.", name))),
}
}
fn to_metadata(&self) -> Box<dyn VMetadata> {
match self {
FSNode::File(_, buf) => {
Box::new(BuiltinMetadata {
is_dir: false,
size: buf.len() as u64,
})
}
FSNode::Directory(_, _) => {
Box::new(BuiltinMetadata {
is_dir: true,
size: 0,
})
}
FSNode::File(_, buf) => Box::new(BuiltinMetadata { is_dir: false, size: buf.len() as u64 }),
FSNode::Directory(_, _) => Box::new(BuiltinMetadata { is_dir: true, size: 0 }),
}
}
}
@ -105,24 +95,39 @@ pub struct BuiltinFS {
impl BuiltinFS {
pub fn new() -> Self {
Self {
root: vec![
FSNode::Directory("builtin", vec![
root: vec![FSNode::Directory(
"builtin",
vec![
FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")),
FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")),
FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")),
FSNode::File("organya-wavetable-doukutsu.bin", include_bytes!("builtin/organya-wavetable-doukutsu.bin")),
FSNode::File(
"organya-wavetable-doukutsu.bin",
include_bytes!("builtin/organya-wavetable-doukutsu.bin"),
),
FSNode::File("touch.png", include_bytes!("builtin/touch.png")),
FSNode::Directory("shaders", vec![
// FSNode::File("basic_150.vert.glsl", include_bytes!("builtin/shaders/basic_150.vert.glsl")),
// FSNode::File("water_150.frag.glsl", include_bytes!("builtin/shaders/water_150.frag.glsl")),
// FSNode::File("basic_es300.vert.glsl", include_bytes!("builtin/shaders/basic_es300.vert.glsl")),
// FSNode::File("water_es300.frag.glsl", include_bytes!("builtin/shaders/water_es300.frag.glsl")),
]),
FSNode::Directory("lightmap", vec![
FSNode::File("spot.png", include_bytes!("builtin/lightmap/spot.png")),
]),
])
],
FSNode::Directory(
"shaders",
vec![
// FSNode::File("basic_150.vert.glsl", include_bytes!("builtin/shaders/basic_150.vert.glsl")),
// FSNode::File("water_150.frag.glsl", include_bytes!("builtin/shaders/water_150.frag.glsl")),
// FSNode::File("basic_es300.vert.glsl", include_bytes!("builtin/shaders/basic_es300.vert.glsl")),
// FSNode::File("water_es300.frag.glsl", include_bytes!("builtin/shaders/water_es300.frag.glsl")),
],
),
FSNode::Directory(
"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")),
],
),
],
)],
}
}
@ -177,10 +182,7 @@ impl Debug for BuiltinFS {
impl VFS for BuiltinFS {
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
if open_options.write || open_options.create || open_options.append || open_options.truncate {
let msg = format!(
"Cannot alter file {:?} in root {:?}, filesystem read-only",
path, self
);
let msg = format!("Cannot alter file {:?} in root {:?}, filesystem read-only", path, self);
return Err(FilesystemError(msg));
}
@ -207,7 +209,7 @@ impl VFS for BuiltinFS {
self.get_node(path).map(|v| v.to_metadata())
}
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item = GameResult<PathBuf>>>> {
match self.get_node(path) {
Ok(FSNode::Directory(_, contents)) => {
let mut vec = Vec::new();
@ -217,12 +219,8 @@ impl VFS for BuiltinFS {
Ok(Box::new(vec.into_iter()))
}
Ok(FSNode::File(_, _)) => {
Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path)))
}
Err(e) => {
Err(e)
}
Ok(FSNode::File(_, _)) => Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path))),
Err(e) => Err(e),
}
}
@ -236,12 +234,13 @@ fn test_builtin_fs() {
let fs = BuiltinFS {
root: vec![
FSNode::File("test.txt", &[]),
FSNode::Directory("memes", vec![
FSNode::File("nothing.txt", &[]),
FSNode::Directory("secret stuff", vec![
FSNode::File("passwords.txt", b"12345678"),
]),
]),
FSNode::Directory(
"memes",
vec![
FSNode::File("nothing.txt", &[]),
FSNode::Directory("secret stuff", vec![FSNode::File("passwords.txt", b"12345678")]),
],
),
FSNode::File("test2.txt", &[]),
],
};

View file

@ -7,7 +7,7 @@ use crate::framework::error::GameResult;
use crate::graphics;
use crate::player::Player;
use crate::scripting::tsc::text_script::TextScriptExecutionState;
use crate::shared_game_state::SharedGameState;
use crate::shared_game_state::{Language, SharedGameState};
use crate::stage::Stage;
#[derive(Copy, Clone, Eq, PartialEq)]
@ -80,7 +80,13 @@ impl MapSystem {
Ok(())
}
pub fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context, stage: &Stage, players: [&Player; 2]) -> GameResult {
pub fn tick(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
stage: &Stage,
players: [&Player; 2],
) -> GameResult {
if state.textscript_vm.state == TextScriptExecutionState::MapSystem {
if self.state == MapSystemState::Hidden {
state.control_flags.set_control_enabled(false);
@ -141,13 +147,11 @@ impl MapSystem {
}
MapSystemState::Visible => {
for player in &players {
if player.controller.trigger_jump() || player.controller.trigger_shoot()
{
if player.controller.trigger_jump() || player.controller.trigger_shoot() {
self.state = MapSystemState::FadeOutBox(8);
break;
}
}
}
_ => (),
}
@ -155,7 +159,13 @@ impl MapSystem {
Ok(())
}
pub fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, stage: &Stage, players: [&Player; 2]) -> GameResult {
pub fn draw(
&self,
state: &mut SharedGameState,
ctx: &mut Context,
stage: &Stage,
players: [&Player; 2],
) -> GameResult {
if self.state == MapSystemState::Hidden {
return Ok(());
}
@ -177,17 +187,16 @@ impl MapSystem {
graphics::draw_rect(ctx, rect_black_bar, Color::new(0.0, 0.0, 0.0, 1.0))?;
}
let map_name_width = state.font.text_width(stage.data.name.chars(), &state.constants);
let map_name = if state.settings.locale == Language::Japanese {
stage.data.name_jp.chars()
} else {
stage.data.name.chars()
};
let map_name_width = state.font.text_width(map_name.clone(), &state.constants);
let map_name_off_x = (state.canvas_size.0 - map_name_width) / 2.0;
state.font.draw_text(
stage.data.name.chars(),
map_name_off_x,
9.0,
&state.constants,
&mut state.texture_set,
ctx,
)?;
state.font.draw_text(map_name, map_name_off_x, 9.0, &state.constants, &mut state.texture_set, ctx)?;
let mut map_rect = Rect::new(0.0, 0.0, self.last_size.0 as f32, self.last_size.1 as f32);
@ -232,12 +241,7 @@ impl MapSystem {
tex.clear();
tex.add(SpriteBatchCommand::DrawRect(
map_rect,
Rect::new_size(
(scr_w - width) / 2.0,
(scr_h - height) / 2.0,
map_rect.width(),
map_rect.height(),
),
Rect::new_size((scr_w - width) / 2.0, (scr_h - height) / 2.0, map_rect.width(), map_rect.height()),
));
tex.draw()?;
}
@ -258,10 +262,7 @@ impl MapSystem {
let plr_x = x_offset + (player.x / tile_div) as f32;
let plr_y = y_offset + (player.y / tile_div) as f32;
batch.add_rect(
plr_x, plr_y,
&PLAYER_RECT,
);
batch.add_rect(plr_x, plr_y, &PLAYER_RECT);
}
batch.draw(ctx)?;

View file

@ -11,10 +11,11 @@ use crate::engine_constants::npcs::NPCConsts;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::i18n::Locale;
use crate::player::ControlMode;
use crate::scripting::tsc::text_script::TextScriptEncoding;
use crate::settings::Settings;
use crate::shared_game_state::Season;
use crate::shared_game_state::{Language, Season};
use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform};
use crate::sound::SoundManager;
@ -312,6 +313,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>,
}
impl Clone for EngineConstants {
@ -342,6 +344,7 @@ impl Clone for EngineConstants {
animated_face_table: self.animated_face_table.clone(),
string_table: self.string_table.clone(),
missile_flags: self.missile_flags.clone(),
locales: self.locales.clone(),
}
}
}
@ -1624,6 +1627,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(),
}
}
@ -1650,9 +1654,6 @@ impl EngineConstants {
self.title.menu_left = Rect { left: 0, top: 4, right: 4, bottom: 12 };
self.title.menu_right = Rect { left: 12, top: 4, right: 16, bottom: 12 };
self.font_path = "csfont.fnt".to_owned();
self.font_scale = 0.5;
let typewriter_sample = PixToneParameters {
// fx2 (CS+)
channels: [
@ -1719,6 +1720,14 @@ impl EngineConstants {
_ => {}
}
}
if settings.locale != Language::English {
self.base_paths.insert(0, format!("/base/{}/", settings.locale.to_language_code()));
}
} else {
if settings.locale != Language::English {
self.base_paths.insert(0, format!("/{}/", settings.locale.to_language_code()));
}
}
if let Some(mut mod_path) = mod_path {
@ -1782,6 +1791,15 @@ impl EngineConstants {
Ok(())
}
pub fn load_locales(&mut self, ctx: &mut Context) -> GameResult {
for language in Language::values() {
self.locales.insert(language.to_string(), Locale::new(ctx, language.to_language_code(), language.font()));
log::info!("Loaded locale {} ({}).", language.to_string(), language.to_language_code());
}
Ok(())
}
pub fn apply_constant_json_files(&mut self) {}
pub fn load_texture_size_hints(&mut self, ctx: &mut Context) -> GameResult {

63
src/i18n.rs Normal file
View file

@ -0,0 +1,63 @@
use crate::framework::context::Context;
use crate::framework::filesystem;
use crate::shared_game_state::FontData;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Locale {
strings: HashMap<String, String>,
pub font: FontData,
}
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();
let json: serde_json::Value = serde_json::from_reader(file).unwrap();
let strings = Locale::flatten(&json);
Locale { strings, font }
}
fn flatten(json: &serde_json::Value) -> HashMap<String, String> {
let mut strings = HashMap::new();
for (key, value) in json.as_object().unwrap() {
match value {
serde_json::Value::String(string) => {
strings.insert(key.to_owned(), string.to_owned());
}
serde_json::Value::Object(_) => {
let substrings = Locale::flatten(value);
for (sub_key, sub_value) in substrings.iter() {
strings.insert(format!("{}.{}", key, sub_key), sub_value.to_owned());
}
}
_ => {}
}
}
strings
}
pub fn t(&self, key: &str) -> String {
self.strings.get(key).unwrap_or(&key.to_owned()).to_owned()
}
pub fn tt(&self, key: &str, args: HashMap<String, String>) -> String {
let mut string = self.t(key);
for (key, value) in args.iter() {
string = string.replace(&format!("{{{}}}", key), &value);
}
string
}
}

View file

@ -41,6 +41,7 @@ mod frame;
mod framework;
#[cfg(feature = "hooks")]
mod hooks;
mod i18n;
mod input;
mod inventory;
mod live_debugger;

View file

@ -97,6 +97,69 @@ impl Menu {
self.entries.push(entry);
}
pub fn update_width(&mut self, state: &SharedGameState) {
let mut width = self.width as f32;
for entry in &self.entries {
match entry {
MenuEntry::Hidden => {}
MenuEntry::Active(entry) | MenuEntry::DisabledWhite(entry) | MenuEntry::Disabled(entry) => {
let entry_width = state.font.text_width(entry.chars(), &state.constants) + 32.0;
width = width.max(entry_width);
}
MenuEntry::Toggle(entry, _) => {
let mut entry_with_option = entry.clone();
entry_with_option.push_str(" ");
let longest_option_width = if state.t("common.off").len() > state.t("common.on").len() {
state.font.text_width(state.t("common.off").chars(), &state.constants)
} else {
state.font.text_width(state.t("common.on").chars(), &state.constants)
};
let entry_width = state.font.text_width(entry_with_option.chars(), &state.constants)
+ longest_option_width
+ 32.0;
width = width.max(entry_width);
}
MenuEntry::Options(entry, _, options) => {
let mut entry_with_option = entry.clone();
entry_with_option.push_str(" ");
let longest_option = options.iter().max_by(|&a, &b| a.len().cmp(&b.len())).unwrap();
entry_with_option.push_str(longest_option);
let entry_width = state.font.text_width(entry_with_option.chars(), &state.constants) + 32.0;
width = width.max(entry_width);
}
MenuEntry::DescriptiveOptions(entry, _, options, descriptions) => {
let mut entry_with_option = entry.clone();
entry_with_option.push_str(" ");
let longest_option = options.iter().max_by(|&a, &b| a.len().cmp(&b.len())).unwrap();
entry_with_option.push_str(longest_option);
let entry_width = state.font.text_width(entry_with_option.chars(), &state.constants) + 32.0;
width = width.max(entry_width);
let longest_description = descriptions.iter().max_by(|&a, &b| a.len().cmp(&b.len())).unwrap();
let description_width = state.font.text_width(longest_description.chars(), &state.constants) + 32.0;
width = width.max(description_width);
}
MenuEntry::OptionsBar(entry, _) => {
let bar_width = if state.constants.is_switch { 81.0 } else { 109.0 };
let entry_width = state.font.text_width(entry.chars(), &state.constants) + 32.0 + bar_width;
width = width.max(entry_width);
}
MenuEntry::SaveData(_) => {}
MenuEntry::NewSave => {}
}
}
width = width.max(16.0);
self.width = if (width + 4.0) % 8.0 != 0.0 { (width + 4.0 - width % 8.0) as u16 } else { width as u16 };
}
pub fn update_height(&mut self) {
let mut height = 8.0;
@ -410,7 +473,7 @@ impl Menu {
}
MenuEntry::NewSave => {
state.font.draw_text(
"New Save".chars(),
state.t("menus.save_menu.new").chars(),
self.x as f32 + 20.0,
y,
&state.constants,

View file

@ -48,15 +48,15 @@ impl PauseMenu {
self.controller.add(state.settings.create_player1_controller());
self.controller.add(state.settings.create_player2_controller());
self.pause_menu.push_entry(MenuEntry::Active("Resume".to_owned()));
self.pause_menu.push_entry(MenuEntry::Active("Retry".to_owned()));
self.pause_menu.push_entry(MenuEntry::Active("Options".to_owned()));
self.pause_menu.push_entry(MenuEntry::Active("Title".to_owned()));
self.pause_menu.push_entry(MenuEntry::Active("Quit".to_owned()));
self.pause_menu.push_entry(MenuEntry::Active(state.t("menus.pause_menu.resume")));
self.pause_menu.push_entry(MenuEntry::Active(state.t("menus.pause_menu.retry")));
self.pause_menu.push_entry(MenuEntry::Active(state.t("menus.pause_menu.options")));
self.pause_menu.push_entry(MenuEntry::Active(state.t("menus.pause_menu.title")));
self.pause_menu.push_entry(MenuEntry::Active(state.t("menus.pause_menu.quit")));
self.confirm_menu.push_entry(MenuEntry::Disabled("".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active("Yes".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active("No".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active(state.t("common.yes")));
self.confirm_menu.push_entry(MenuEntry::Active(state.t("common.no")));
self.confirm_menu.selected = 1;
@ -73,9 +73,12 @@ impl PauseMenu {
}
fn update_sizes(&mut self, state: &SharedGameState) {
self.pause_menu.update_width(state);
self.pause_menu.update_height();
self.pause_menu.x = ((state.canvas_size.0 - self.pause_menu.width as f32) / 2.0).floor() as isize;
self.pause_menu.y = ((state.canvas_size.1 - self.pause_menu.height as f32) / 2.0).floor() as isize;
self.confirm_menu.update_width(state);
self.confirm_menu.update_height();
self.confirm_menu.x = ((state.canvas_size.0 - self.confirm_menu.width as f32) / 2.0).floor() as isize;
self.confirm_menu.y = ((state.canvas_size.1 - self.confirm_menu.height as f32) / 2.0).floor() as isize;
@ -125,11 +128,11 @@ impl PauseMenu {
self.current_menu = CurrentMenu::OptionsMenu;
}
MenuSelectionResult::Selected(3, _) => {
self.confirm_menu.entries[0] = MenuEntry::Disabled("Title?".to_owned());
self.confirm_menu.entries[0] = MenuEntry::Disabled(state.t("menus.pause_menu.title_confirm"));
self.current_menu = CurrentMenu::ConfirmMenu;
}
MenuSelectionResult::Selected(4, _) => {
self.confirm_menu.entries[0] = MenuEntry::Disabled("Quit?".to_owned());
self.confirm_menu.entries[0] = MenuEntry::Disabled(state.t("menus.pause_menu.quit_confirm"));
self.current_menu = CurrentMenu::ConfirmMenu;
}
_ => (),

View file

@ -71,20 +71,20 @@ impl SaveSelectMenu {
}
}
self.save_menu.push_entry(MenuEntry::Active("< Back".to_owned()));
self.save_menu.push_entry(MenuEntry::Disabled("Press Right to Delete".to_owned()));
self.save_menu.push_entry(MenuEntry::Active(state.t("common.back")));
self.save_menu.push_entry(MenuEntry::Disabled(state.t("menus.save_menu.delete_info")));
self.difficulty_menu.push_entry(MenuEntry::Disabled("Select Difficulty".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Easy".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Normal".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("Hard".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Active("< Back".to_owned()));
self.difficulty_menu.push_entry(MenuEntry::Disabled(state.t("menus.difficulty_menu.title")));
self.difficulty_menu.push_entry(MenuEntry::Active(state.t("menus.difficulty_menu.easy")));
self.difficulty_menu.push_entry(MenuEntry::Active(state.t("menus.difficulty_menu.normal")));
self.difficulty_menu.push_entry(MenuEntry::Active(state.t("menus.difficulty_menu.hard")));
self.difficulty_menu.push_entry(MenuEntry::Active(state.t("common.back")));
self.difficulty_menu.selected = 2;
self.delete_confirm.push_entry(MenuEntry::Disabled("Delete?".to_owned()));
self.delete_confirm.push_entry(MenuEntry::Active("Yes".to_owned()));
self.delete_confirm.push_entry(MenuEntry::Active("No".to_owned()));
self.delete_confirm.push_entry(MenuEntry::Disabled(state.t("menus.save_menu.delete_confirm")));
self.delete_confirm.push_entry(MenuEntry::Active(state.t("common.yes")));
self.delete_confirm.push_entry(MenuEntry::Active(state.t("common.no")));
self.delete_confirm.selected = 2;
@ -98,13 +98,18 @@ impl SaveSelectMenu {
}
fn update_sizes(&mut self, state: &SharedGameState) {
self.save_menu.update_width(state);
self.save_menu.update_height();
self.save_menu.x = ((state.canvas_size.0 - self.save_menu.width as f32) / 2.0).floor() as isize;
self.save_menu.y = 30 + ((state.canvas_size.1 - self.save_menu.height as f32) / 2.0).floor() as isize;
self.difficulty_menu.update_width(state);
self.difficulty_menu.update_height();
self.difficulty_menu.x = ((state.canvas_size.0 - self.difficulty_menu.width as f32) / 2.0).floor() as isize;
self.difficulty_menu.y =
30 + ((state.canvas_size.1 - self.difficulty_menu.height as f32) / 2.0).floor() as isize;
self.delete_confirm.update_width(state);
self.delete_confirm.update_height();
self.delete_confirm.x = ((state.canvas_size.0 - self.delete_confirm.width as f32) / 2.0).floor() as isize;
self.delete_confirm.y = 30 + ((state.canvas_size.1 - self.delete_confirm.height as f32) / 2.0).floor() as isize

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use itertools::Itertools;
use crate::framework::context::Context;
@ -6,7 +8,8 @@ use crate::framework::filesystem;
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::menu::MenuEntry;
use crate::menu::{Menu, MenuSelectionResult};
use crate::shared_game_state::{SharedGameState, TimingMode};
use crate::scene::title_scene::TitleScene;
use crate::shared_game_state::{Language, SharedGameState, TimingMode};
use crate::sound::InterpolationMode;
#[derive(PartialEq, Eq, Copy, Clone)]
@ -17,6 +20,7 @@ enum CurrentMenu {
GraphicsMenu,
SoundMenu,
SoundtrackMenu,
LanguageMenu,
}
pub struct SettingsMenu {
@ -25,6 +29,7 @@ pub struct SettingsMenu {
graphics: Menu,
sound: Menu,
soundtrack: Menu,
language: Menu,
pub on_title: bool,
}
@ -36,78 +41,119 @@ impl SettingsMenu {
let graphics = Menu::new(0, 0, 180, 0);
let sound = Menu::new(0, 0, 260, 0);
let soundtrack = Menu::new(0, 0, 260, 0);
let language = Menu::new(0, 0, 120, 0);
SettingsMenu { current: CurrentMenu::MainMenu, main, graphics, sound, soundtrack, on_title: false }
SettingsMenu { current: CurrentMenu::MainMenu, main, graphics, sound, soundtrack, language, on_title: false }
}
pub fn init(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
self.graphics.push_entry(MenuEntry::Toggle("Lighting effects:".to_string(), state.settings.shader_effects));
self.graphics.push_entry(MenuEntry::Toggle("Weapon light cone:".to_string(), state.settings.light_cone));
self.graphics
.push_entry(MenuEntry::Toggle("Motion interpolation:".to_string(), state.settings.motion_interpolation));
self.graphics.push_entry(MenuEntry::Toggle("Subpixel scrolling:".to_string(), state.settings.subpixel_coords));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.lighting_effects"),
state.settings.shader_effects,
));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.weapon_light_cone"),
state.settings.light_cone,
));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.motion_interpolation"),
state.settings.motion_interpolation,
));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.subpixel_scrolling"),
state.settings.subpixel_coords,
));
// NS version uses two different maps, therefore we can't dynamically switch between graphics presets.
if state.constants.supports_og_textures {
if !state.constants.is_switch || self.on_title {
self.graphics
.push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.original_textures"),
state.settings.original_textures,
));
} else {
self.graphics.push_entry(MenuEntry::Disabled("Original textures".to_string()));
self.graphics
.push_entry(MenuEntry::Disabled(state.t("menus.options_menu.graphics_menu.original_textures")));
}
} else {
self.graphics.push_entry(MenuEntry::Hidden);
}
if state.constants.is_cs_plus {
self.graphics
.push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures));
self.graphics.push_entry(MenuEntry::Toggle(
state.t("menus.options_menu.graphics_menu.seasonal_textures"),
state.settings.seasonal_textures,
));
} else {
self.graphics.push_entry(MenuEntry::Hidden);
}
self.graphics
.push_entry(MenuEntry::Disabled(format!("Renderer: {}", ctx.renderer.as_ref().unwrap().renderer_name())));
self.graphics.push_entry(MenuEntry::Disabled(format!(
"{} {}",
state.t("menus.options_menu.graphics_menu.renderer"),
ctx.renderer.as_ref().unwrap().renderer_name()
)));
self.graphics.push_entry(MenuEntry::Active("< Back".to_owned()));
self.graphics.push_entry(MenuEntry::Active(state.t("common.back")));
self.main.push_entry(MenuEntry::Active("Graphics...".to_owned()));
self.main.push_entry(MenuEntry::Active("Sound...".to_owned()));
self.main.push_entry(MenuEntry::Active(state.t("menus.options_menu.graphics")));
self.main.push_entry(MenuEntry::Active(state.t("menus.options_menu.sound")));
self.language.push_entry(MenuEntry::Disabled(state.t("menus.options_menu.language")));
for language in Language::values() {
self.language.push_entry(MenuEntry::Active(language.to_string()));
}
self.language.push_entry(MenuEntry::Active(state.t("common.back")));
if self.on_title {
self.main.push_entry(MenuEntry::Active(state.t("menus.options_menu.language")));
} else {
self.main.push_entry(MenuEntry::Disabled(state.t("menus.options_menu.language")));
}
self.main.push_entry(MenuEntry::Options(
"Game timing:".to_owned(),
state.t("menus.options_menu.game_timing.entry"),
if state.settings.timing_mode == TimingMode::_50Hz { 0 } else { 1 },
vec!["50tps (freeware)".to_owned(), "60tps (CS+)".to_owned()],
vec![state.t("menus.options_menu.game_timing.50tps"), state.t("menus.options_menu.game_timing.60tps")],
));
self.main.push_entry(MenuEntry::Active(DISCORD_LINK.to_owned()));
self.main.push_entry(MenuEntry::Active("< Back".to_owned()));
self.main.push_entry(MenuEntry::Active(state.t("common.back")));
self.sound.push_entry(MenuEntry::OptionsBar("Music Volume".to_owned(), state.settings.bgm_volume));
self.sound.push_entry(MenuEntry::OptionsBar("Effects Volume".to_owned(), state.settings.sfx_volume));
self.sound.push_entry(MenuEntry::OptionsBar(
state.t("menus.options_menu.sound_menu.music_volume"),
state.settings.bgm_volume,
));
self.sound.push_entry(MenuEntry::OptionsBar(
state.t("menus.options_menu.sound_menu.effects_volume"),
state.settings.sfx_volume,
));
self.sound.push_entry(MenuEntry::DescriptiveOptions(
"BGM Interpolation:".to_owned(),
state.t("menus.options_menu.sound_menu.bgm_interpolation.entry"),
state.settings.organya_interpolation as usize,
vec![
"Nearest".to_owned(),
"Linear".to_owned(),
"Cosine".to_owned(),
"Cubic".to_owned(),
"Linear+LP".to_owned(),
state.t("menus.options_menu.sound_menu.bgm_interpolation.nearest"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.linear"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.cosine"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.cubic"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.linear_lp"),
],
vec![
"(Fastest, lowest quality)".to_owned(),
"(Fast, similar to freeware on Vista+)".to_owned(),
"(Cosine interpolation)".to_owned(),
"(Cubic interpolation)".to_owned(),
"(Slowest, similar to freeware on XP)".to_owned(),
state.t("menus.options_menu.sound_menu.bgm_interpolation.nearest_desc"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.linear_desc"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.cosine_desc"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.cubic_desc"),
state.t("menus.options_menu.sound_menu.bgm_interpolation.linear_lp_desc"),
],
));
self.sound.push_entry(MenuEntry::DisabledWhite("".to_owned()));
self.sound.push_entry(MenuEntry::Active(format!("Soundtrack: {}", state.settings.soundtrack)));
self.sound.push_entry(MenuEntry::Active("< Back".to_owned()));
self.sound.push_entry(MenuEntry::Active(state.tt(
"menus.options_menu.sound_menu.soundtrack",
HashMap::from([("soundtrack".to_owned(), state.settings.soundtrack.to_owned())]),
)));
self.sound.push_entry(MenuEntry::Active(state.t("common.back")));
let mut soundtrack_entries =
state.constants.soundtracks.iter().filter(|s| s.available).map(|s| s.name.to_owned()).collect_vec();
@ -138,7 +184,7 @@ impl SettingsMenu {
.unwrap_or(self.soundtrack.width as f32) as u16
+ 32;
self.soundtrack.push_entry(MenuEntry::Active("< Back".to_owned()));
self.soundtrack.push_entry(MenuEntry::Active(state.t("common.back")));
self.update_sizes(state);
@ -146,21 +192,30 @@ impl SettingsMenu {
}
fn update_sizes(&mut self, state: &SharedGameState) {
self.main.update_width(state);
self.main.update_height();
self.main.x = ((state.canvas_size.0 - self.main.width as f32) / 2.0).floor() as isize;
self.main.y = 30 + ((state.canvas_size.1 - self.main.height as f32) / 2.0).floor() as isize;
self.graphics.update_width(state);
self.graphics.update_height();
self.graphics.x = ((state.canvas_size.0 - self.graphics.width as f32) / 2.0).floor() as isize;
self.graphics.y = 30 + ((state.canvas_size.1 - self.graphics.height as f32) / 2.0).floor() as isize;
self.sound.update_width(state);
self.sound.update_height();
self.sound.x = ((state.canvas_size.0 - self.sound.width as f32) / 2.0).floor() as isize;
self.sound.y = 30 + ((state.canvas_size.1 - self.sound.height as f32) / 2.0).floor() as isize;
self.soundtrack.update_width(state);
self.soundtrack.update_height();
self.soundtrack.x = ((state.canvas_size.0 - self.soundtrack.width as f32) / 2.0).floor() as isize;
self.soundtrack.y = ((state.canvas_size.1 - self.soundtrack.height as f32) / 2.0).floor() as isize;
self.language.update_width(state);
self.language.update_height();
self.language.x = ((state.canvas_size.0 - self.language.width as f32) / 2.0).floor() as isize;
self.language.y = ((state.canvas_size.1 - self.language.height as f32) / 2.0).floor() as isize;
}
pub fn tick(
@ -180,7 +235,11 @@ impl SettingsMenu {
MenuSelectionResult::Selected(1, _) => {
self.current = CurrentMenu::SoundMenu;
}
MenuSelectionResult::Selected(2, toggle) => {
MenuSelectionResult::Selected(2, _) => {
self.language.selected = (state.settings.locale as usize) + 1;
self.current = CurrentMenu::LanguageMenu;
}
MenuSelectionResult::Selected(3, toggle) => {
if let MenuEntry::Options(_, value, _) = toggle {
match state.settings.timing_mode {
TimingMode::_50Hz => {
@ -196,12 +255,12 @@ impl SettingsMenu {
let _ = state.settings.save(ctx);
}
}
MenuSelectionResult::Selected(3, _) => {
MenuSelectionResult::Selected(4, _) => {
if let Err(e) = webbrowser::open(DISCORD_LINK) {
log::warn!("Error opening web browser: {}", e);
}
}
MenuSelectionResult::Selected(4, _) | MenuSelectionResult::Canceled => exit_action(),
MenuSelectionResult::Selected(5, _) | MenuSelectionResult::Canceled => exit_action(),
_ => (),
},
CurrentMenu::GraphicsMenu => match self.graphics.tick(controller, state) {
@ -320,6 +379,35 @@ impl SettingsMenu {
}
_ => (),
},
CurrentMenu::LanguageMenu => {
let last = self.language.entries.len() - 1;
match self.language.tick(controller, state) {
MenuSelectionResult::Selected(idx, entry) => {
if let (true, MenuEntry::Active(_)) = (idx != last, entry) {
let new_locale = Language::from_primitive(idx.saturating_sub(1));
if new_locale == state.settings.locale {
self.current = CurrentMenu::MainMenu;
} else {
state.settings.locale = new_locale;
state.reload_fonts(ctx);
let _ = state.settings.save(ctx);
let mut new_menu = TitleScene::new();
new_menu.open_settings_menu()?;
state.next_scene = Some(Box::new(new_menu));
}
}
self.current = CurrentMenu::MainMenu;
}
MenuSelectionResult::Canceled => {
self.current = CurrentMenu::MainMenu;
}
_ => {}
}
}
CurrentMenu::SoundtrackMenu => {
let last = self.soundtrack.entries.len() - 1;
match self.soundtrack.tick(controller, state) {
@ -350,6 +438,7 @@ impl SettingsMenu {
CurrentMenu::GraphicsMenu => self.graphics.draw(state, ctx)?,
CurrentMenu::SoundMenu => self.sound.draw(state, ctx)?,
CurrentMenu::SoundtrackMenu => self.soundtrack.draw(state, ctx)?,
CurrentMenu::LanguageMenu => self.language.draw(state, ctx)?,
}
Ok(())

View file

@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::{Deref, Range};
use std::rc::Rc;
@ -45,7 +46,7 @@ use crate::scene::title_scene::TitleScene;
use crate::scene::Scene;
use crate::scripting::tsc::credit_script::CreditScriptVM;
use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM};
use crate::shared_game_state::{ReplayState, SharedGameState, TileSize};
use crate::shared_game_state::{Language, ReplayState, SharedGameState, TileSize};
use crate::stage::{BackgroundType, Stage, StageTexturePaths};
use crate::texture_set::SpriteBatch;
use crate::weapon::bullet::BulletManager;
@ -1949,7 +1950,11 @@ impl Scene for GameScene {
let map_name = if self.stage.data.name == "u" {
state.constants.title.intro_text.chars()
} else {
self.stage.data.name.chars()
if state.settings.locale == Language::Japanese {
self.stage.data.name_jp.chars()
} else {
self.stage.data.name.chars()
}
};
let width = state.font.text_width(map_name.clone(), &state.constants);
@ -1971,7 +1976,10 @@ impl Scene for GameScene {
self.text_boxes.draw(state, ctx, &self.frame)?;
if self.skip_counter > 1 {
let text = format!("Hold {:?} to skip the cutscene", state.settings.player1_key_map.inventory);
let text = state.tt(
"game.cutscene_skip",
HashMap::from([("key".to_owned(), format!("{:?}", state.settings.player1_key_map.inventory))]),
);
let width = state.font.text_width(text.chars(), &state.constants);
let pos_x = state.canvas_size.0 - width - 20.0;
let pos_y = 0.0;

View file

@ -32,6 +32,7 @@ impl JukeboxScene {
map: Map { width: 0, height: 0, tiles: vec![], attrib: [0; 0x100], tile_size: TileSize::Tile16x16 },
data: StageData {
name: "".to_string(),
name_jp: "".to_string(),
map: "".to_string(),
boss_no: 0,
tileset: Tileset { name: "0".to_string() },

View file

@ -49,6 +49,7 @@ impl TitleScene {
map: Map { width: 0, height: 0, tiles: vec![], attrib: [0; 0x100], tile_size: TileSize::Tile16x16 },
data: StageData {
name: "".to_string(),
name_jp: "".to_string(),
map: "".to_string(),
boss_no: 0,
tileset: Tileset { name: "0".to_string() },
@ -127,6 +128,11 @@ impl TitleScene {
}
Ok(())
}
pub fn open_settings_menu(&mut self) -> GameResult {
self.current_menu = CurrentMenu::OptionMenu;
Ok(())
}
}
static COPYRIGHT_PIXEL: &str = "2004.12 Studio Pixel"; // Freeware
@ -142,24 +148,24 @@ impl Scene for TitleScene {
self.controller.add(state.settings.create_player1_controller());
self.controller.add(state.settings.create_player2_controller());
self.main_menu.push_entry(MenuEntry::Active("Start Game".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.start")));
if !state.mod_list.mods.is_empty() {
self.main_menu.push_entry(MenuEntry::Active("Challenges".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.challenges")));
} else {
self.main_menu.push_entry(MenuEntry::Hidden);
}
self.main_menu.push_entry(MenuEntry::Active("Options".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.options")));
if cfg!(feature = "editor") {
self.main_menu.push_entry(MenuEntry::Active("Editor".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.editor")));
} else {
self.main_menu.push_entry(MenuEntry::Hidden);
}
if state.constants.is_switch {
self.main_menu.push_entry(MenuEntry::Active("Jukebox".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.jukebox")));
} else {
self.main_menu.push_entry(MenuEntry::Hidden);
}
self.main_menu.push_entry(MenuEntry::Active("Quit".to_string()));
self.main_menu.push_entry(MenuEntry::Active(state.t("menus.main_menu.quit")));
self.settings_menu.init(state, ctx)?;
@ -168,12 +174,12 @@ impl Scene for TitleScene {
for mod_info in state.mod_list.mods.iter() {
self.challenges_menu.push_entry(MenuEntry::Active(mod_info.name.clone()));
}
self.challenges_menu.push_entry(MenuEntry::Active("< Back".to_string()));
self.challenges_menu.push_entry(MenuEntry::Active(state.t("common.back")));
self.confirm_menu.push_entry(MenuEntry::Disabled("".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active("Start".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Disabled("No Replay".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active("< Back".to_owned()));
self.confirm_menu.push_entry(MenuEntry::Active(state.t("menus.challenge_menu.start")));
self.confirm_menu.push_entry(MenuEntry::Disabled(state.t("menus.challenge_menu.no_replay")));
self.confirm_menu.push_entry(MenuEntry::Active(state.t("common.back")));
self.confirm_menu.selected = 1;
self.controller.update(state, ctx)?;
@ -194,10 +200,12 @@ impl Scene for TitleScene {
self.controller.update(state, ctx)?;
self.controller.update_trigger();
self.main_menu.update_width(state);
self.main_menu.update_height();
self.main_menu.x = ((state.canvas_size.0 - self.main_menu.width as f32) / 2.0).floor() as isize;
self.main_menu.y = ((state.canvas_size.1 + 70.0 - self.main_menu.height as f32) / 2.0).floor() as isize;
self.challenges_menu.update_width(state);
self.challenges_menu.update_height();
self.challenges_menu.x = ((state.canvas_size.0 - self.challenges_menu.width as f32) / 2.0).floor() as isize;
self.challenges_menu.y =
@ -281,9 +289,9 @@ impl Scene for TitleScene {
(state.font.text_width(mod_name.chars(), &state.constants).max(50.0) + 32.0) as u16;
self.confirm_menu.entries[0] = MenuEntry::Disabled(mod_name);
self.confirm_menu.entries[2] = if state.has_replay_data(ctx) {
MenuEntry::Active("Replay Best".to_owned())
MenuEntry::Active(state.t("menus.challenge_menu.replay_best"))
} else {
MenuEntry::Disabled("No Replay".to_owned())
MenuEntry::Disabled(state.t("menus.challenge_menu.no_replay"))
};
self.nikumaru_rec.load_counter(state, ctx)?;
self.current_menu = CurrentMenu::ChallengeConfirmMenu;
@ -318,6 +326,7 @@ impl Scene for TitleScene {
},
}
self.confirm_menu.update_width(state);
self.confirm_menu.update_height();
self.confirm_menu.x = ((state.canvas_size.0 - self.confirm_menu.width as f32) / 2.0).floor() as isize;
self.confirm_menu.y = ((state.canvas_size.1 + 30.0 - self.confirm_menu.height as f32) / 2.0).floor() as isize;

View file

@ -6,7 +6,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::TimingMode;
use crate::shared_game_state::{Language, TimingMode};
use crate::sound::InterpolationMode;
#[derive(serde::Serialize, serde::Deserialize)]
@ -46,6 +46,7 @@ pub struct Settings {
#[serde(skip)]
pub debug_outlines: bool,
pub fps_counter: bool,
pub locale: Language,
}
fn default_true() -> bool {
@ -54,7 +55,7 @@ fn default_true() -> bool {
#[inline(always)]
fn current_version() -> u32 {
6
7
}
#[inline(always)]
@ -77,6 +78,11 @@ fn default_vol() -> f32 {
1.0
}
#[inline(always)]
fn default_locale() -> Language {
Language::English
}
impl Settings {
pub fn load(ctx: &Context) -> GameResult<Settings> {
if let Ok(file) = user_open(ctx, "/settings.json") {
@ -114,6 +120,11 @@ impl Settings {
self.player2_key_map.strafe = ScanCode::RShift;
}
if self.version == 6 {
self.version = 7;
self.locale = default_locale();
}
if self.version != initial_version {
log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version);
}
@ -164,6 +175,7 @@ impl Default for Settings {
infinite_booster: false,
debug_outlines: false,
fps_counter: false,
locale: Language::English,
}
}
}

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::{cmp, ops::Div};
use bitvec::vec::BitVec;
@ -17,6 +18,7 @@ use crate::framework::vfs::OpenOptions;
use crate::framework::{filesystem, graphics};
#[cfg(feature = "hooks")]
use crate::hooks::init_hooks;
use crate::i18n::Locale;
use crate::input::touch_controls::TouchControls;
use crate::mod_list::ModList;
use crate::npc::NPCTable;
@ -80,6 +82,57 @@ impl GameDifficulty {
}
}
#[derive(PartialEq, Eq, Copy, Clone, 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),
// TODO: implement JP font rendering
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(Clone, Debug)]
pub struct FontData {
pub path: String,
pub scale: f32,
pub space_offset: f32,
}
impl FontData {
pub fn new(path: String, scale: f32, space_offset: f32) -> FontData {
FontData { path, scale, space_offset }
}
}
pub struct Fps {
pub frame_count: u32,
pub fps: u32,
@ -221,6 +274,8 @@ impl SharedGameState {
let sound_manager = SoundManager::new(ctx)?;
let settings = Settings::load(ctx)?;
constants.load_locales(ctx)?;
if filesystem::exists(ctx, "/base/lighting.tbl") {
info!("Cave Story+ (Switch) data files detected.");
ctx.size_hint = (854, 480);
@ -257,7 +312,13 @@ impl SharedGameState {
let season = Season::current();
constants.rebuild_path_list(None, season, &settings);
let font = BMFontRenderer::load(&constants.base_paths, &constants.font_path, ctx).or_else(|e| {
let active_locale = constants.locales.get(&settings.locale.to_string()).unwrap();
if constants.is_cs_plus {
constants.font_scale = active_locale.font.scale;
}
let font = BMFontRenderer::load(&constants.base_paths, &active_locale.font.path, ctx).or_else(|e| {
log::warn!("Failed to load font, using built-in: {}", e);
BMFontRenderer::load(&vec!["/".to_owned()], "/builtin/builtin_font.fnt", ctx)
})?;
@ -362,7 +423,7 @@ impl SharedGameState {
self.constants.load_csplus_tables(ctx)?;
self.constants.load_animated_faces(ctx)?;
self.constants.load_texture_size_hints(ctx)?;
let stages = StageData::load_stage_table(ctx, &self.constants.base_paths)?;
let stages = StageData::load_stage_table(ctx, &self.constants.base_paths, self.constants.is_switch)?;
self.stages = stages;
let npc_tbl = filesystem::open_find(ctx, &self.constants.base_paths, "/npc.tbl")?;
@ -397,6 +458,23 @@ impl SharedGameState {
self.texture_set.unload_all();
}
pub fn reload_fonts(&mut self, ctx: &mut Context) {
let active_locale = self.get_active_locale();
let font = BMFontRenderer::load(&self.constants.base_paths, &active_locale.font.path, ctx)
.or_else(|e| {
log::warn!("Failed to load font, using built-in: {}", e);
BMFontRenderer::load(&vec!["/".to_owned()], "/builtin/builtin_font.fnt", ctx)
})
.unwrap();
if self.constants.is_cs_plus {
self.constants.font_scale = active_locale.font.scale;
}
self.font = font;
}
pub fn graphics_reset(&mut self) {
self.texture_set.unload_all();
}
@ -662,4 +740,17 @@ 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;
}
pub fn t(&self, key: &str) -> String {
return self.get_active_locale().t(key);
}
pub fn tt(&self, key: &str, args: HashMap<String, String>) -> String {
return self.get_active_locale().tt(key, args);
}
}

View file

@ -199,6 +199,7 @@ pub struct PxPackStageData {
#[derive(Debug)]
pub struct StageData {
pub name: String,
pub name_jp: String,
pub map: String,
pub boss_no: u8,
pub tileset: Tileset,
@ -214,6 +215,7 @@ impl Clone for StageData {
fn clone(&self) -> Self {
StageData {
name: self.name.clone(),
name_jp: self.name_jp.clone(),
map: self.map.clone(),
boss_no: self.boss_no,
tileset: self.tileset.clone(),
@ -274,8 +276,16 @@ fn from_shift_jis(s: &[u8]) -> String {
chars.iter().collect()
}
fn from_csplus_stagetbl(s: &[u8], is_switch: bool) -> String {
if is_switch {
from_utf8(s).unwrap_or("").trim_matches('\0').to_string()
} else {
from_shift_jis(s)
}
}
impl StageData {
pub fn load_stage_table(ctx: &mut Context, roots: &Vec<String>) -> GameResult<Vec<Self>> {
pub fn load_stage_table(ctx: &mut Context, roots: &Vec<String>, is_switch: bool) -> GameResult<Vec<Self>> {
let stage_tbl_path = "/stage.tbl";
let stage_sect_path = "/stage.sect";
let mrmap_bin_path = "/mrmap.bin";
@ -316,15 +326,17 @@ impl StageData {
f.read_exact(&mut name_jap_buf)?;
f.read_exact(&mut name_buf)?;
let tileset = from_shift_jis(&ts_buf[0..zero_index(&ts_buf)]);
let map = from_shift_jis(&map_buf[0..zero_index(&map_buf)]);
let background = from_shift_jis(&back_buf[0..zero_index(&back_buf)]);
let npc1 = from_shift_jis(&npc1_buf[0..zero_index(&npc1_buf)]);
let npc2 = from_shift_jis(&npc2_buf[0..zero_index(&npc2_buf)]);
let name = from_shift_jis(&name_buf[0..zero_index(&name_buf)]);
let tileset = from_csplus_stagetbl(&ts_buf[0..zero_index(&ts_buf)], is_switch);
let map = from_csplus_stagetbl(&map_buf[0..zero_index(&map_buf)], is_switch);
let background = from_csplus_stagetbl(&back_buf[0..zero_index(&back_buf)], is_switch);
let npc1 = from_csplus_stagetbl(&npc1_buf[0..zero_index(&npc1_buf)], is_switch);
let npc2 = from_csplus_stagetbl(&npc2_buf[0..zero_index(&npc2_buf)], is_switch);
let name = from_csplus_stagetbl(&name_buf[0..zero_index(&name_buf)], is_switch);
let name_jp = from_csplus_stagetbl(&name_jap_buf[0..zero_index(&name_jap_buf)], is_switch);
let stage = StageData {
name: name.clone(),
name_jp: name_jp.clone(),
map: map.clone(),
boss_no,
tileset: Tileset::new(&tileset),
@ -389,6 +401,7 @@ impl StageData {
let stage = StageData {
name: name.clone(),
name_jp: name.clone(),
map: map.clone(),
boss_no,
tileset: Tileset::new(&tileset),
@ -447,6 +460,7 @@ impl StageData {
let stage = StageData {
name: name.clone(),
name_jp: name.clone(),
map: map.clone(),
boss_no,
tileset: Tileset::new(&tileset),
@ -503,6 +517,7 @@ impl StageData {
let stage = StageData {
name: name.clone(),
name_jp: name.clone(),
map: map.clone(),
boss_no,
tileset: Tileset::new(NXENGINE_TILESETS.get(tileset_id).unwrap_or(&"0")),