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:
parent
500f53bebb
commit
1795d71b37
102
src/builtin/locale/en.json
Normal file
102
src/builtin/locale/en.json
Normal 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
102
src/builtin/locale/jp.json
Normal 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} を押し続け、カットシーンをスキップ"
|
||||
}
|
||||
}
|
|
@ -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", &[]),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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
63
src/i18n.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ mod frame;
|
|||
mod framework;
|
||||
#[cfg(feature = "hooks")]
|
||||
mod hooks;
|
||||
mod i18n;
|
||||
mod input;
|
||||
mod inventory;
|
||||
mod live_debugger;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
29
src/stage.rs
29
src/stage.rs
|
@ -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")),
|
||||
|
|
Loading…
Reference in a new issue