Separate game data parameters from locale files

This commit is contained in:
biroder
2026-06-28 01:36:08 +03:00
parent cc3c1b579d
commit f7ba0049db
13 changed files with 717 additions and 257 deletions
Generated
+223 -8
View File
@@ -60,6 +60,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
version = "0.62.0"
@@ -124,6 +130,15 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -219,6 +234,7 @@ checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"num-traits",
"serde",
"windows-link",
]
@@ -542,8 +558,18 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.13.4",
"darling_macro 0.13.4",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core 0.23.0",
"darling_macro 0.23.0",
]
[[package]]
@@ -556,21 +582,45 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"strsim 0.10.0",
"syn 1.0.109",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim 0.11.1",
"syn 2.0.117",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"darling_core 0.13.4",
"quote",
"syn 1.0.109",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
"syn 2.0.117",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
@@ -600,6 +650,15 @@ dependencies = [
"bindgen 0.62.0",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"serde_core",
]
[[package]]
name = "directories"
version = "3.0.2"
@@ -693,6 +752,7 @@ dependencies = [
"serde_cbor",
"serde_derive",
"serde_json",
"serde_with",
"strum",
"strum_macros",
"vec_mut_scan",
@@ -709,6 +769,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.16.0"
@@ -948,6 +1014,12 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -966,6 +1038,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.12"
@@ -1144,6 +1222,17 @@ dependencies = [
"mint",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1151,7 +1240,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
@@ -1494,7 +1585,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
dependencies = [
"darling",
"darling 0.13.4",
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
@@ -1544,6 +1635,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-derive"
version = "0.3.3"
@@ -1805,6 +1902,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -1911,6 +2014,26 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -1980,6 +2103,30 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2072,6 +2219,38 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "serde_with"
version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
dependencies = [
"base64",
"bs58",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.14.0",
"schemars 0.9.0",
"schemars 1.2.1",
"serde_core",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [
"darling 0.23.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "shared_library"
version = "0.1.9"
@@ -2124,6 +2303,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.24.1"
@@ -2216,6 +2401,36 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "time"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -2271,7 +2486,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"indexmap 2.14.0",
"toml_datetime 0.6.11",
"winnow 0.5.40",
]
@@ -2282,7 +2497,7 @@ version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [
"indexmap",
"indexmap 2.14.0",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.3",
+1
View File
@@ -87,6 +87,7 @@ serde = { version = "1", features = ["derive"] }
serde_derive = "1"
serde_cbor = { version = "0.11", optional = true }
serde_json = "1.0"
serde_with = "3.8.1"
strum = "0.24"
strum_macros = "0.24"
# remove and replace with extract_if, when our MSRV is 1.87
+148
View File
@@ -1,4 +1,5 @@
use std::fmt;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use bitfield::bitfield;
@@ -8,6 +9,7 @@ use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeTupleStruct;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::framework::error::GameError;
use crate::graphics::texture_set::G_MAG;
pub const ORG_NAME: &str = "io.github";
@@ -579,3 +581,149 @@ impl<T> SliceExt for [T] {
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[allow(non_camel_case_types)]
#[repr(u8)]
pub enum Encoding {
UTF8 = 0,
ShiftJIS,
UTF16BE,
UTF16LE,
ISO_2022_JP,
ISO_8859_2,
ISO_8859_3,
ISO_8859_4,
ISO_8859_5,
ISO_8859_6,
ISO_8859_7,
ISO_8859_8,
ISO_8859_8_I,
ISO_8859_10,
ISO_8859_13,
ISO_8859_14,
ISO_8859_15,
ISO_8859_16,
KOI8_R,
KOI8_U,
MACINTOSH,
EUC_JP,
EUC_KR,
GB18030,
GBK,
BIG5,
Win1250,
Win1251,
Win1252,
Win1253,
Win1254,
Win1255,
Win1256,
Win1257,
Win1258,
}
impl Default for Encoding {
fn default() -> Self {
Self::ShiftJIS
}
}
impl From<&str> for Encoding {
fn from(s: &str) -> Self {
s.parse().unwrap_or_default()
}
}
impl FromStr for Encoding {
type Err = GameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"utf-8" => Ok(Self::UTF8),
"utf-16be" => Ok(Self::UTF16BE),
"utf-16le" => Ok(Self::UTF16LE),
"iso-2022-jp" => Ok(Self::ISO_2022_JP),
"iso-8859-2" => Ok(Self::ISO_8859_2),
"iso-8859-3" => Ok(Self::ISO_8859_3),
"iso-8859-4" => Ok(Self::ISO_8859_4),
"iso-8859-5" => Ok(Self::ISO_8859_5),
"iso-8859-6" => Ok(Self::ISO_8859_6),
"iso-8859-7" => Ok(Self::ISO_8859_7),
"iso-8859-8" => Ok(Self::ISO_8859_8),
"iso-8859-8-i" => Ok(Self::ISO_8859_8_I),
"iso-8859-10" => Ok(Self::ISO_8859_10),
"iso-8859-13" => Ok(Self::ISO_8859_13),
"iso-8859-14" => Ok(Self::ISO_8859_14),
"iso-8859-15" => Ok(Self::ISO_8859_15),
"iso-8859-16" => Ok(Self::ISO_8859_16),
"koi8-r" => Ok(Self::KOI8_R),
"koi8-u" => Ok(Self::KOI8_U),
"macintosh" => Ok(Self::MACINTOSH),
"euc-jp" => Ok(Self::EUC_JP),
"euc-kr" => Ok(Self::EUC_KR),
"gb18030" => Ok(Self::GB18030),
"gbk" => Ok(Self::GBK),
"big5" => Ok(Self::BIG5),
"windows-1250" => Ok(Self::Win1250),
"windows-1251" => Ok(Self::Win1251),
"windows-1252" => Ok(Self::Win1252),
"windows-1253" => Ok(Self::Win1253),
"windows-1254" => Ok(Self::Win1254),
"windows-1255" => Ok(Self::Win1255),
"windows-1256" => Ok(Self::Win1256),
"windows-1257" => Ok(Self::Win1257),
"windows-1258" => Ok(Self::Win1258),
_ => Err(Self::Err::ParseError(format!("Invalid or unsupported encoding: {}", s))),
}
}
}
impl From<Encoding> for &'static encoding_rs::Encoding {
fn from(value: Encoding) -> Self {
match value {
Encoding::ShiftJIS => encoding_rs::SHIFT_JIS,
Encoding::UTF8 => encoding_rs::UTF_8,
Encoding::UTF16BE => encoding_rs::UTF_16BE,
Encoding::UTF16LE => encoding_rs::UTF_16LE,
Encoding::ISO_2022_JP => encoding_rs::ISO_2022_JP,
Encoding::ISO_8859_2 => encoding_rs::ISO_8859_2,
Encoding::ISO_8859_3 => encoding_rs::ISO_8859_3,
Encoding::ISO_8859_4 => encoding_rs::ISO_8859_4,
Encoding::ISO_8859_5 => encoding_rs::ISO_8859_5,
Encoding::ISO_8859_6 => encoding_rs::ISO_8859_6,
Encoding::ISO_8859_7 => encoding_rs::ISO_8859_7,
Encoding::ISO_8859_8 => encoding_rs::ISO_8859_8,
Encoding::ISO_8859_8_I => encoding_rs::ISO_8859_8_I,
Encoding::ISO_8859_10 => encoding_rs::ISO_8859_10,
Encoding::ISO_8859_13 => encoding_rs::ISO_8859_13,
Encoding::ISO_8859_14 => encoding_rs::ISO_8859_14,
Encoding::ISO_8859_15 => encoding_rs::ISO_8859_15,
Encoding::ISO_8859_16 => encoding_rs::ISO_8859_16,
Encoding::KOI8_R => encoding_rs::KOI8_R,
Encoding::KOI8_U => encoding_rs::KOI8_U,
Encoding::MACINTOSH => encoding_rs::MACINTOSH,
Encoding::EUC_JP => encoding_rs::EUC_JP,
Encoding::EUC_KR => encoding_rs::EUC_KR,
Encoding::GB18030 => encoding_rs::GB18030,
Encoding::GBK => encoding_rs::GBK,
Encoding::BIG5 => encoding_rs::BIG5,
Encoding::Win1250 => encoding_rs::WINDOWS_1250,
Encoding::Win1251 => encoding_rs::WINDOWS_1251,
Encoding::Win1252 => encoding_rs::WINDOWS_1252,
Encoding::Win1253 => encoding_rs::WINDOWS_1253,
Encoding::Win1254 => encoding_rs::WINDOWS_1254,
Encoding::Win1255 => encoding_rs::WINDOWS_1255,
Encoding::Win1256 => encoding_rs::WINDOWS_1256,
Encoding::Win1257 => encoding_rs::WINDOWS_1257,
Encoding::Win1258 => encoding_rs::WINDOWS_1258,
}
}
}
+118 -55
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io::{BufRead, BufReader, Cursor, Read};
use byteorder::{ReadBytesExt, LE};
@@ -6,19 +7,18 @@ use case_insensitive_hashmap::CaseInsensitiveHashMap;
use xmltree::Element;
use crate::case_insensitive_hashmap;
use crate::common::{BulletFlag, Color, Rect};
use crate::common::{BulletFlag, Color, Encoding, Rect};
use crate::engine_constants::npcs::NPCConsts;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::gamepad::{Axis, Button};
use crate::game::player::ControlMode;
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
use crate::game::settings::Settings;
use crate::game::shared_game_state::{FontData, Season};
use crate::game::stage::StageData;
use crate::graphics::texture_set::TextureSet;
use crate::i18n::Locale;
use crate::i18n::{Locale, Translation};
use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform};
use crate::sound::SoundManager;
@@ -178,7 +178,7 @@ pub struct ExtraSoundtrack {
#[derive(Debug, Copy, Clone)]
pub struct TextScriptConsts {
pub encoding: TextScriptEncoding,
pub encoding: Encoding,
pub encrypted: bool,
pub reset_invicibility_on_any_script: bool,
pub animated_face_pics: bool,
@@ -346,20 +346,45 @@ impl DataType {
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RootType {
Base, // "/" path. Supports mods and locales.
Mod, // Mod root (not CS+ challenge). Doesn't support mods, but supports locales.
Translation // Translation of the main game. Doesn't support mods and locales.
/// The root of the main game, located in the "data" folder. Supports mods, locales, and translation variants.
Base,
/// The root of the mod (not CS+ challenge). Doesn't support mods and translation variants, but supports locales.
Mod,
// Translation of the main game. Doesn't support mods, locales, and translation variants.
Translation,
}
impl RootType {
/// Whether the root supports locales.
///
/// This determines whether the list of locales will be reloaded when switching to the specified root.
pub fn support_locales(&self) -> bool {
*self != Self::Translation
}
/// Whether the root supports translation variants.
///
/// Translation variants are only intended for the base (main game) root, since historically, only
/// the base game has has multiple translation variants for each language (locale). Mods, on the other hand,
/// often have almost no translations into other languages, and even if they do, they typically have only one
/// translation variant for a given language.
pub fn support_translations(&self) -> bool {
*self == Self::Base
}
}
#[derive(Clone, Debug)]
pub struct DataRoot {
// Type of the game data in the root
/// The type of game data in the root. It's `None` if the data type cannot be determined, or if the root is
/// an incomplete translation, since incomplete (overlaid) translations don't contain any files specific to any data type,
/// as all such files are untranslatable.
pub data_type: Option<DataType>,
pub root_type: RootType,
// Full path to the root dir
/// Full path to the root dir.
pub path: String,
pub support_locales: bool,
pub support_translations: bool,
}
impl Default for DataRoot {
@@ -368,7 +393,8 @@ impl Default for DataRoot {
data_type: None,
root_type: RootType::Base,
path: "/".to_string(),
support_locales: false
support_locales: false,
support_translations: false,
}
}
}
@@ -386,12 +412,13 @@ impl DataRoot {
root_type,
path,
support_locales: root_type != RootType::Translation,
support_locales: root_type.support_locales(),
support_translations: root_type.support_translations(),
}
}
// Path to the directory with a base game data.
// For CS+ it's "/base", for WiiWare "/root" etc.
/// Returns the path to the directory with the base game data.
/// For CS+ it's "/base", for WiiWare "/root", for CS 3D it's "/data".
pub fn base_path(&self) -> String {
let mut base_path = self.path.clone();
@@ -403,6 +430,10 @@ impl DataRoot {
base_path
}
/// Checks whether the translation root is complete.
///
/// Incomplete translations (also called overlaid translations) often lack necessary files — such as the stage table
/// and non-translatable textures — since those files are expected to be loaded from the base root.
pub fn is_complete(&self, ctx: &Context) -> bool {
// Only overlaid translations may be incomplete.
// As a result, incomplete translations don't have any specific files to determine their data type.
@@ -426,6 +457,12 @@ impl DataRoot {
true
}
pub fn translation_path(&self, _ctx: &Context, translation: &Translation) -> String {
assert!(self.support_locales, "Translation path requested for the root ({}), which doesn't support locales", &self.path);
[&self.base_path(), &translation.full_code(), "/"].join("")
}
}
@@ -467,8 +504,9 @@ pub struct EngineConstants {
pub missile_flags: Vec<u16>,
pub base_locale: String,
pub locales: Vec<Locale>,
pub translations: Vec<Translation>,
pub gamepad: GamepadConsts,
pub stage_encoding: Option<TextScriptEncoding>,
pub stage_encoding: Option<Encoding>,
}
impl EngineConstants {
@@ -1621,7 +1659,7 @@ impl EngineConstants {
"title".to_owned(),
],
textscript: TextScriptConsts {
encoding: TextScriptEncoding::ShiftJIS,
encoding: Encoding::ShiftJIS,
encrypted: true,
reset_invicibility_on_any_script: true,
animated_face_pics: false,
@@ -1777,6 +1815,7 @@ impl EngineConstants {
missile_flags: vec![200, 201, 202, 218, 550, 766, 880, 920, 1551],
base_locale: "en".to_owned(),
locales: Vec::new(),
translations: Vec::new(),
gamepad: {
let mut holder = GamepadConsts {
button_rects: HashMap::from([
@@ -1818,6 +1857,10 @@ impl EngineConstants {
}
}
pub fn is_base(&self) -> bool {
!self.is_switch && !self.is_cs_plus && !self.is_demo
}
pub fn apply_csplus_patches(&mut self, sound_manager: &mut SoundManager) {
log::info!("Applying Cave Story+ constants patches...");
@@ -1869,10 +1912,6 @@ impl EngineConstants {
let _ = sound_manager.set_sample_params(2, typewriter_sample);
}
pub fn is_base(&self) -> bool {
!self.is_switch && !self.is_cs_plus && !self.is_demo
}
pub fn apply_csplus_nx_patches(&mut self) {
log::info!("Applying Switch-specific Cave Story+ constants patches...");
@@ -1885,7 +1924,7 @@ impl EngineConstants {
self.title.logo_rect = Rect { left: 0, top: 0, right: 214, bottom: 62 };
self.inventory_dim_color = Color::from_rgba(0, 0, 32, 150);
self.intro_background_color = Color::from_rgb(0, 0, 32);
self.textscript.encoding = TextScriptEncoding::UTF8;
self.textscript.encoding = Encoding::UTF8;
self.textscript.encrypted = false;
self.textscript.animated_face_pics = true;
self.textscript.text_shadow = true;
@@ -1913,6 +1952,7 @@ impl EngineConstants {
let mut consts = Self::defaults();
consts.roots.clone_from(&self.roots);
consts.locales.clone_from(&self.locales);
consts.translations.clone_from(&self.translations);
consts.active_root.clone_from(&self.active_root);
*self = consts;
@@ -1937,22 +1977,20 @@ impl EngineConstants {
consts.active_root = consts.roots.get(&data_root).cloned().unwrap();
consts.rebuild_path_list(None, Season::current(), settings);
let _ = consts.load_locales(ctx);
let _ = consts.load_translations(ctx, true);
log::debug!("Temporary instance of EngineConstants has been initialized");
// The following roots are locales with data files whose type differs from that of the main root.
// Switching to a such root requires reloading the engine constants.
for loc in &consts.locales {
let mut full_path = if consts.is_cs_plus { "/base/" } else { "/" }.to_string();
full_path.push_str(loc.code.as_str());
full_path.push('/');
for tran in &consts.translations {
let tran_path = consts.active_root.translation_path(ctx, tran);
if filesystem::exists(ctx, full_path.as_str()) {
let data_type = DataType::from_path(ctx, full_path.as_str());
if filesystem::exists(ctx, tran_path.as_str()) {
let data_type = DataType::from_path(ctx, tran_path.as_str());
if data_type != consts.data_type {
let root = DataRoot::load(ctx, full_path.clone(), RootType::Translation);
let root = DataRoot::load(ctx, tran_path.clone(), RootType::Translation);
if root.is_complete(ctx) {
self.roots.insert(full_path.clone(), root);
self.roots.insert(tran_path, root);
}
}
}
@@ -1983,7 +2021,7 @@ impl EngineConstants {
self.rebuild_path_list(None, Season::current(), settings);
if data_root.support_locales {
let _ = self.load_locales(ctx);
let _ = self.load_translations(ctx, true);
}
log::debug!("Path list after switching the root: {:?}", &self.base_paths);
@@ -2083,13 +2121,14 @@ impl EngineConstants {
Ok(())
}
pub fn load_locales(&mut self, ctx: &mut Context) -> GameResult {
pub fn load_locales(&mut self, ctx: &Context) -> GameResult {
self.locales.clear();
let locale_files = filesystem::read_dir_find(ctx, &self.base_paths, "locale/");
for locale_file in locale_files.unwrap() {
if locale_file.extension().unwrap() != "json" {
for locale_file in locale_files? {
let file_ext = locale_file.extension().and_then(OsStr::to_str);
if file_ext != Some("json") {
continue;
}
@@ -2100,38 +2139,62 @@ impl EngineConstants {
};
let mut locale = Locale::new(ctx, &self.base_paths, &locale_code);
// Roots that support locales may have their own default locale, which differs from English
if locale.code == self.base_locale {
locale.is_present = true;
locale.is_complete = true;
locale.data_type = self.active_root.data_type;
} else {
let translation_path = [self.active_root.base_path().as_str(), locale.code.as_str(), "/"].join("");
locale.is_present = filesystem::exists(ctx, &translation_path);
if locale.is_present {
locale.data_type = DataType::from_path(ctx, translation_path.as_str());
if locale.data_type.is_none() {
let translation_root = self.roots.get(&translation_path);
locale.is_complete = translation_root.is_some(); // incomplete translation roots aren't added to the list
locale.data_type = translation_root.unwrap_or(&self.active_root).data_type;
}
}
}
if locale_code == "jp" && filesystem::exists(ctx, "/base/credit_jp.tsc") {
locale.set_font(FontData::new("csfontjp.fnt".to_owned(), 0.5, 0.0));
}
self.locales.push(locale.clone());
log::info!("Loaded locale {} ({})", locale_code, locale.name.clone());
self.locales.push(locale);
}
Ok(())
}
pub fn load_translations(&mut self, ctx: &Context, load_locales: bool) -> GameResult {
self.translations.clear();
if self.locales.is_empty() || load_locales {
self.load_locales(ctx)?;
}
// If the root doesn't support locales, only the default translation variants will be generated from loaded locales
if self.active_root.support_translations {
let translation_files = filesystem::read_dir_find(ctx, &self.base_paths, "translation/")?;
for tran_file in translation_files {
let file_ext = tran_file.extension().and_then(OsStr::to_str);
if file_ext != Some("json") {
continue;
}
let file = filesystem::open(ctx, tran_file)?;
if let Ok(mut tran) = Translation::from_file(file) {
if let Some(locale) = self.locales.iter().find(|loc| loc.code == tran.locale) {
tran.analyze_data(ctx, &self);
log::info!("Loaded {} translation for {}", &tran.name, locale.name);
self.translations.push(tran);
} else {
log::debug!("Translation {} ({:?}) omitted because locale {} is missing", &tran.name, &tran.code, tran.locale);
}
}
}
}
// Add default translations for locales that don't have one
for locale in &self.locales {
if !self.translations.iter().any(|t| t.locale == locale.code && t.is_default()) {
let mut tran: Translation = locale.into();
tran.analyze_data(ctx, &self);
log::debug!("Default translation variant created for {}", &locale.name);
self.translations.push(tran);
}
}
Ok(())
}
pub fn apply_constant_json_files(&mut self) {}
// TODO: load soundtrack metadata from a json file
+2 -2
View File
@@ -1,8 +1,8 @@
use std::io::{Cursor, Read};
use crate::common::Encoding;
use crate::framework::error::GameError::ParseError;
use crate::framework::error::GameResult;
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
pub fn put_varint(val: i32, out: &mut Vec<u8>) {
let mut x = ((val as u32) >> 31) ^ ((val as u32) << 1);
@@ -57,7 +57,7 @@ pub fn read_varint<I: Iterator<Item = u8>>(iter: &mut I) -> GameResult<i32> {
Ok(((result << 31) ^ (result >> 1)) as i32)
}
pub fn put_string(buffer: &mut Vec<u8>, out: &mut Vec<u8>, encoding: TextScriptEncoding) {
pub fn put_string(buffer: &mut Vec<u8>, out: &mut Vec<u8>, encoding: Encoding) {
if buffer.is_empty() {
return;
}
+5 -4
View File
@@ -4,17 +4,18 @@ use std::str::FromStr;
use itertools::Itertools;
use crate::common::Encoding;
use crate::framework::error::GameError::ParseError;
use crate::framework::error::GameResult;
use crate::game::scripting::tsc::bytecode_utils::{put_string, put_varint};
use crate::game::scripting::tsc::credit_script::CreditScript;
use crate::game::scripting::tsc::opcodes::{CreditOpCode, TSCOpCode};
use crate::game::scripting::tsc::parse_utils::{expect_char, read_number, skip_until};
use crate::game::scripting::tsc::text_script::{TextScript, TextScriptEncoding};
use crate::game::scripting::tsc::text_script::TextScript;
impl TextScript {
/// Compiles a decrypted text script data into internal bytecode.
pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult<TextScript> {
pub fn compile(data: &[u8], strict: bool, encoding: Encoding) -> GameResult<TextScript> {
let mut event_map = HashMap::new();
let mut iter = data.iter().copied().peekable();
let mut last_event = 0;
@@ -70,7 +71,7 @@ impl TextScript {
fn compile_event<I: Iterator<Item=u8>>(
iter: &mut Peekable<I>,
strict: bool,
encoding: TextScriptEncoding,
encoding: Encoding,
) -> GameResult<Vec<u8>> {
let mut bytecode = Vec::new();
let mut char_buf = Vec::with_capacity(16);
@@ -313,7 +314,7 @@ impl TextScript {
}
impl CreditScript {
pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult<CreditScript> {
pub fn compile(data: &[u8], strict: bool, encoding: Encoding) -> GameResult<CreditScript> {
let mut labels = HashMap::new();
let mut bytecode = Vec::new();
let mut iter = data.iter().copied().peekable();
+2 -135
View File
@@ -8,6 +8,7 @@ use std::io::SeekFrom;
use std::ops::ControlFlow;
use std::ops::Not;
use std::rc::Rc;
use std::str::FromStr;
use bitfield::bitfield;
use num_traits::{clamp, FromPrimitive};
@@ -17,8 +18,7 @@ use crate::common::{Direction, FadeDirection, FadeState, Rect};
use crate::engine_constants::EngineConstants;
use crate::entity::GameEntity;
use crate::framework::context::Context;
use crate::framework::error::map_err_to_break;
use crate::framework::error::GameResult;
use crate::framework::error::{GameError, GameResult, map_err_to_break};
use crate::game::frame::UpdateTarget;
use crate::game::npc::NPCContext;
use crate::game::npc::NPC;
@@ -47,139 +47,6 @@ bitfield! {
pub cutscene_skip, set_cutscene_skip: 7;
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[allow(non_camel_case_types)]
#[repr(u8)]
pub enum TextScriptEncoding {
UTF8 = 0,
ShiftJIS,
UTF16BE,
UTF16LE,
ISO_2022_JP,
ISO_8859_2,
ISO_8859_3,
ISO_8859_4,
ISO_8859_5,
ISO_8859_6,
ISO_8859_7,
ISO_8859_8,
ISO_8859_8_I,
ISO_8859_10,
ISO_8859_13,
ISO_8859_14,
ISO_8859_15,
ISO_8859_16,
KOI8_R,
KOI8_U,
MACINTOSH,
EUC_JP,
EUC_KR,
GB18030,
GBK,
BIG5,
Win1250,
Win1251,
Win1252,
Win1253,
Win1254,
Win1255,
Win1256,
Win1257,
Win1258,
}
impl From<&str> for TextScriptEncoding {
fn from(s: &str) -> Self {
match s {
"utf-8" => Self::UTF8,
"iso-2022-jp" => Self::ISO_2022_JP,
"iso-8859-2" => Self::ISO_8859_2,
"iso-8859-3" => Self::ISO_8859_3,
"iso-8859-4" => Self::ISO_8859_4,
"iso-8859-5" => Self::ISO_8859_5,
"iso-8859-6" => Self::ISO_8859_6,
"iso-8859-7" => Self::ISO_8859_7,
"iso-8859-8" => Self::ISO_8859_8,
"iso-8859-8-i" => Self::ISO_8859_8_I,
"iso-8859-10" => Self::ISO_8859_10,
"iso-8859-13" => Self::ISO_8859_13,
"iso-8859-14" => Self::ISO_8859_14,
"iso-8859-15" => Self::ISO_8859_15,
"iso-8859-16" => Self::ISO_8859_16,
"koi8-r" => Self::KOI8_R,
"koi8-u" => Self::KOI8_U,
"macintosh" => Self::MACINTOSH,
"euc-jp" => Self::EUC_JP,
"euc-kr" => Self::EUC_KR,
"gb18030" => Self::GB18030,
"gbk" => Self::GBK,
"big5" => Self::BIG5,
"windows-1250" => Self::Win1250,
"windows-1251" => Self::Win1251,
"windows-1252" => Self::Win1252,
"windows-1253" => Self::Win1253,
"windows-1254" => Self::Win1254,
"windows-1255" => Self::Win1255,
"windows-1256" => Self::Win1256,
"windows-1257" => Self::Win1257,
"windows-1258" => Self::Win1258,
"utf-16be" => Self::UTF16BE,
"utf-16le" => Self::UTF16LE,
_ => Self::ShiftJIS,
}
}
}
impl From<TextScriptEncoding> for &'static encoding_rs::Encoding {
fn from(value: TextScriptEncoding) -> Self {
match value {
TextScriptEncoding::ShiftJIS => encoding_rs::SHIFT_JIS,
TextScriptEncoding::UTF8 => encoding_rs::UTF_8,
TextScriptEncoding::UTF16BE => encoding_rs::UTF_16BE,
TextScriptEncoding::UTF16LE => encoding_rs::UTF_16LE,
TextScriptEncoding::ISO_2022_JP => encoding_rs::ISO_2022_JP,
TextScriptEncoding::ISO_8859_2 => encoding_rs::ISO_8859_2,
TextScriptEncoding::ISO_8859_3 => encoding_rs::ISO_8859_3,
TextScriptEncoding::ISO_8859_4 => encoding_rs::ISO_8859_4,
TextScriptEncoding::ISO_8859_5 => encoding_rs::ISO_8859_5,
TextScriptEncoding::ISO_8859_6 => encoding_rs::ISO_8859_6,
TextScriptEncoding::ISO_8859_7 => encoding_rs::ISO_8859_7,
TextScriptEncoding::ISO_8859_8 => encoding_rs::ISO_8859_8,
TextScriptEncoding::ISO_8859_8_I => encoding_rs::ISO_8859_8_I,
TextScriptEncoding::ISO_8859_10 => encoding_rs::ISO_8859_10,
TextScriptEncoding::ISO_8859_13 => encoding_rs::ISO_8859_13,
TextScriptEncoding::ISO_8859_14 => encoding_rs::ISO_8859_14,
TextScriptEncoding::ISO_8859_15 => encoding_rs::ISO_8859_15,
TextScriptEncoding::ISO_8859_16 => encoding_rs::ISO_8859_16,
TextScriptEncoding::KOI8_R => encoding_rs::KOI8_R,
TextScriptEncoding::KOI8_U => encoding_rs::KOI8_U,
TextScriptEncoding::MACINTOSH => encoding_rs::MACINTOSH,
TextScriptEncoding::EUC_JP => encoding_rs::EUC_JP,
TextScriptEncoding::EUC_KR => encoding_rs::EUC_KR,
TextScriptEncoding::GB18030 => encoding_rs::GB18030,
TextScriptEncoding::GBK => encoding_rs::GBK,
TextScriptEncoding::BIG5 => encoding_rs::BIG5,
TextScriptEncoding::Win1250 => encoding_rs::WINDOWS_1250,
TextScriptEncoding::Win1251 => encoding_rs::WINDOWS_1251,
TextScriptEncoding::Win1252 => encoding_rs::WINDOWS_1252,
TextScriptEncoding::Win1253 => encoding_rs::WINDOWS_1253,
TextScriptEncoding::Win1254 => encoding_rs::WINDOWS_1254,
TextScriptEncoding::Win1255 => encoding_rs::WINDOWS_1255,
TextScriptEncoding::Win1256 => encoding_rs::WINDOWS_1256,
TextScriptEncoding::Win1257 => encoding_rs::WINDOWS_1257,
TextScriptEncoding::Win1258 => encoding_rs::WINDOWS_1258,
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[repr(u8)]
pub enum TextScriptLine {
+8 -1
View File
@@ -87,6 +87,7 @@ pub struct Settings {
pub discord_rpc: bool,
#[serde(default = "default_true")]
pub allow_strafe: bool,
pub translation: Option<String>,
}
fn default_true() -> bool {
@@ -95,7 +96,7 @@ fn default_true() -> bool {
#[inline(always)]
fn current_version() -> u32 {
25
26
}
#[inline(always)]
@@ -359,6 +360,11 @@ impl Settings {
}
}
if self.version == 25 {
self.version = 26;
self.translation = None;
}
if self.version != initial_version {
log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version);
}
@@ -477,6 +483,7 @@ impl Default for Settings {
cutscene_skip_mode: CutsceneSkipMode::Hold,
discord_rpc: true,
allow_strafe: true,
translation: None,
}
}
}
+35 -29
View File
@@ -2,7 +2,7 @@ use std::{cmp, ops::Div};
use chrono::{Datelike, Local};
use crate::common::{Color, ControlFlags, Direction, FadeState};
use crate::common::{Color, ControlFlags, Direction, Encoding, FadeState};
use crate::components::draw_common::{draw_number, Alignment};
use crate::data::vanilla::VanillaExtractor;
#[cfg(feature = "discord-rpc")]
@@ -20,13 +20,13 @@ use crate::game::player::TargetPlayer;
use crate::game::profile::GameProfile;
use crate::game::scripting::tsc::credit_script::{CreditScript, CreditScriptVM};
use crate::game::scripting::tsc::text_script::{
ScriptMode, TextScript, TextScriptEncoding, TextScriptExecutionState, TextScriptVM,
ScriptMode, TextScript, TextScriptExecutionState, TextScriptVM,
};
use crate::game::settings::Settings;
use crate::game::stage::StageData;
use crate::graphics::bmfont::BMFont;
use crate::graphics::texture_set::TextureSet;
use crate::i18n::Locale;
use crate::i18n::{Locale, Translation};
use crate::input::touch_controls::TouchControls;
use crate::mod_list::ModList;
use crate::mod_requirements::ModRequirements;
@@ -346,6 +346,7 @@ pub struct SharedGameState {
pub replay_state: ReplayState,
pub mod_requirements: ModRequirements,
pub loc: Locale,
pub tran: Translation,
pub tutorial_counter: u16,
pub more_rust: bool,
#[cfg(feature = "discord-rpc")]
@@ -398,11 +399,11 @@ impl SharedGameState {
let season = Season::current();
constants.rebuild_path_list(None, season, &settings);
constants.load_locales(ctx)?;
constants.load_translations(ctx, true)?;
constants.rebuild_roots_list(ctx, &settings, &mut sound_manager);
let locale = SharedGameState::get_locale(&constants, &settings.locale).unwrap_or_default();
let font = Self::try_update_locale(ctx, &mut constants, &settings, &mut sound_manager, &locale).unwrap();
let (locale, translation) = SharedGameState::get_translation(&constants, &settings.locale, settings.translation.as_deref()).unwrap_or_default();
let font = Self::try_update_locale(ctx, &mut constants, &settings, &mut sound_manager, &locale, &translation).unwrap();
let mod_list = ModList::load(ctx, &constants)?;
@@ -484,6 +485,7 @@ impl SharedGameState {
replay_state: ReplayState::None,
mod_requirements,
loc: locale,
tran: translation,
tutorial_counter: 0,
more_rust,
#[cfg(feature = "discord-rpc")]
@@ -563,8 +565,10 @@ impl SharedGameState {
settings: &Settings,
sound_manager: &mut SoundManager,
locale: &Locale,
translation: &Translation
) -> GameResult<BMFont> {
log::info!("Updating locale to {} ({})", &locale.code, &locale.name);
assert!(locale.code == translation.locale);
// If the active root doesn't support locales (we're probably on a Translation root)
// or the target locale isn't the default one (its data dir could be a root), we need to switch the root
@@ -573,14 +577,14 @@ impl SharedGameState {
let path = if constants.active_root.support_locales {
// If the active root support locales, we'll try to set active root to the translation data dir.
format!("{}{}/", constants.active_root.base_path(), &locale.code)
} else if constants.active_root.root_type == RootType::Translation && locale.code != constants.base_locale && locale.is_complete {
constants.active_root.translation_path(ctx, &translation)
} else if constants.active_root.root_type == RootType::Translation && locale.code != constants.base_locale && translation.is_complete {
// Only the main game can have translations of different data types,
// so we'll try to find the translation data dir in the main root.
// We only need to do it if the translation is complete; otherwise it could be missing,
// which results in applying the new locale to the menu entries, while the game data is
// used from the previous locale.
format!("{}{}/", main_root.base_path(), &locale.code)
main_root.translation_path(ctx, &translation)
} else {
// The active root doesn't support translations and isn't a translation itself,
// or the data files in the target lcoale data dir are incomplete (they're missing or the translation isn't a root).
@@ -591,32 +595,30 @@ impl SharedGameState {
constants.set_active_root(ctx, path, settings, sound_manager);
}
constants.textscript.encoding = if let Some(encoding) = locale.encoding {
encoding
} else {
constants.textscript.encoding = translation.encoding.unwrap_or_else(|| {
// In freeware, Japanese and English text scripts use ShiftJIS.
// In Cave Story+, Japanese scripts use ShiftJIS and English scripts use UTF-8.
// In the Switch edition, UTF-8 is used for both English and Japanese scripts.
match locale.code.as_str() {
"jp" => {
if constants.is_switch {
TextScriptEncoding::UTF8
Encoding::UTF8
} else {
TextScriptEncoding::ShiftJIS
Encoding::ShiftJIS
}
}
"en" => {
if constants.is_base() {
TextScriptEncoding::ShiftJIS
Encoding::ShiftJIS
} else {
TextScriptEncoding::UTF8
Encoding::UTF8
}
}
_ => TextScriptEncoding::UTF8,
_ => Encoding::UTF8,
}
};
});
constants.stage_encoding = locale.stage_encoding;
constants.stage_encoding = translation.stage_encoding;
let font = BMFont::load(&constants.base_paths, &locale.font.path, ctx, locale.font.scale).or_else(|e| {
log::warn!("Failed to load font, using built-in: {}", e);
@@ -627,14 +629,16 @@ impl SharedGameState {
}
pub fn update_locale(&mut self, ctx: &mut Context) {
let Some(locale) = SharedGameState::get_locale(&self.constants, &self.settings.locale) else {
let loc_res = SharedGameState::get_translation(&self.constants, &self.settings.locale, self.settings.translation.as_deref());
let Some((locale, translation)) = loc_res else {
return;
};
let prev_root = self.constants.active_root.path.clone();
let font = Self::try_update_locale(ctx, &mut self.constants, &self.settings, &mut self.sound_manager, &locale).unwrap();
let font = Self::try_update_locale(ctx, &mut self.constants, &self.settings, &mut self.sound_manager, &locale, &translation).unwrap();
self.loc = locale;
self.tran = translation;
self.font = font;
let _ = if prev_root != self.constants.active_root.path {
@@ -934,21 +938,23 @@ impl SharedGameState {
return self.difficulty as u16;
}
fn get_locale(constants: &EngineConstants, user_locale: &str) -> Option<Locale> {
let mut out_locale = None;
fn get_translation(constants: &EngineConstants, locale: &str, translation: Option<&str>) -> Option<(Locale, Translation)> {
let mut out = None;
for locale in &constants.locales {
if locale.code == constants.base_locale {
out_locale = Some(locale.clone());
for tran in &constants.translations {
if tran.locale == constants.base_locale {
let loc = constants.locales.iter().find(|l| l.code == locale)?;
out = Some((loc.clone(), tran.clone()));
}
if locale.code == user_locale {
out_locale = Some(locale.clone());
if tran.locale == locale && tran.code.as_deref() == translation {
let loc = constants.locales.iter().find(|l| l.code == locale)?;
out = Some((loc.clone(), tran.clone()));
break;
}
}
out_locale
out
}
pub fn get_localized_soundtrack_name(&self, id: &str) -> String {
+4 -4
View File
@@ -5,14 +5,14 @@ use byteorder::ReadBytesExt;
use byteorder::LE;
use strum::IntoEnumIterator;
use crate::common::Color;
use crate::common::{Color, Encoding};
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem;
use crate::game::map::{Map, NPCData};
use crate::game::scripting::tsc::text_script::{TextScript, TextScriptEncoding};
use crate::game::scripting::tsc::text_script::TextScript;
use crate::game::SharedGameState;
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
@@ -225,7 +225,7 @@ fn zero_index(s: &[u8]) -> usize {
s.iter().position(|&c| c == b'\0').unwrap_or(s.len())
}
fn from_encoding(s: &[u8], encoding: Option<TextScriptEncoding>) -> String {
fn from_encoding(s: &[u8], encoding: Option<Encoding>) -> String {
if let Some(encoding) = encoding {
let encoding: &encoding_rs::Encoding = encoding.into();
return encoding.decode_without_bom_handling(s).0.into_owned();
@@ -238,7 +238,7 @@ fn from_shift_jis(s: &[u8]) -> String {
encoding_rs::SHIFT_JIS.decode_without_bom_handling(s).0.into_owned()
}
fn from_csplus_stagetbl(s: &[u8], is_switch: bool, encoding: Option<TextScriptEncoding>) -> String {
fn from_csplus_stagetbl(s: &[u8], is_switch: bool, encoding: Option<Encoding>) -> String {
if let Some(encoding) = encoding {
let encoding: &encoding_rs::Encoding = encoding.into();
return encoding.decode_without_bom_handling(s).0.into_owned();
+129 -7
View File
@@ -1,9 +1,13 @@
use std::collections::HashMap;
use std::io::Read;
use crate::engine_constants::DataType;
use serde_with::{DisplayFromStr, serde_as};
use crate::common::Encoding;
use crate::engine_constants::{DataType, EngineConstants};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
use crate::game::shared_game_state::FontData;
#[derive(Debug, Clone)]
@@ -11,8 +15,8 @@ pub struct Locale {
pub code: String,
pub name: String,
pub font: FontData,
pub encoding: Option<TextScriptEncoding>,
pub stage_encoding: Option<TextScriptEncoding>,
pub encoding: Option<Encoding>,
pub stage_encoding: Option<Encoding>,
strings: HashMap<String, String>,
// Properties of the translation data "root"
@@ -39,7 +43,7 @@ impl Default for Locale {
}
impl Locale {
pub fn new(ctx: &mut Context, base_paths: &Vec<String>, code: &str) -> Locale {
pub fn new(ctx: &Context, base_paths: &Vec<String>, code: &str) -> Locale {
let file = filesystem::open_find(ctx, base_paths, &format!("locale/{code}.json")).unwrap();
let json: serde_json::Value = serde_json::from_reader(file).unwrap();
@@ -52,12 +56,12 @@ impl Locale {
let font = FontData::new(font_name, font_scale, 0.0);
let encoding = if let Some(enc) = strings.get("encoding").clone() {
Some(TextScriptEncoding::from(enc.as_str()))
Some(Encoding::from(enc.as_str()))
} else {
None
};
let stage_encoding = if let Some(enc) = strings.get("stage_encoding").clone() {
Some(TextScriptEncoding::from(enc.as_str()))
Some(Encoding::from(enc.as_str()))
} else {
None
};
@@ -115,3 +119,121 @@ impl Locale {
self.font = font;
}
}
#[serde_as]
#[derive(Clone, Debug, serde::Deserialize)]
/// Info about a translation variant.
///
/// A translation variant contains information about a specific translation of game files
/// into a particular language (its name, code and game data parameters), while the [`Locale`] contains data
/// common to all translation variants (font parameters and UI strings).
pub struct Translation {
/// The locale code for which the translation variant is intended.
pub locale: String,
/// Code of the translation variant.
/// `None` represents the default translation variant for the specified locale.
pub code: Option<String>,
/// Name of the the translation variant (typically the name of the translator or translation team).
pub name: String,
#[serde_as(as = "Option<DisplayFromStr>")]
/// Encoding of text scripts.
pub encoding: Option<Encoding>,
#[serde_as(as = "Option<DisplayFromStr>")]
/// Encoding of strings in the stage table.
pub stage_encoding: Option<Encoding>,
// Properties of the translation data "root"
#[serde(default, skip)]
/// Whether the translation game data directory exists.
pub available: bool,
#[serde(default, skip)]
/// Whether the translation data directory is complete.
///
/// Incomplete translations don't contain all necessary files,
/// as they are intended to contain only translatable files (scripts, textures, the stage table);
/// therefore, a type of a type of game data cannot be determined for them.
pub is_complete: bool,
#[serde(default, skip)]
pub data_type: Option<DataType>,
}
impl Default for Translation {
fn default() -> Self {
let locale = Locale::default();
Self {
locale: locale.code,
code: None,
name: "".to_string(),
encoding: None,
stage_encoding: None,
available: false,
is_complete: false,
data_type: None,
}
}
}
impl Translation {
pub fn new(ctx: &mut Context, base_paths: &Vec<String>, code: &str) -> GameResult<Self> {
let file = filesystem::open_find(ctx, base_paths, &format!("translation/{code}.json"))?;
Self::from_file(file)
}
pub fn from_file<R: std::io::Read>(mut file: R) -> GameResult<Self> {
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(serde_json::from_str(&content)?)
}
pub fn full_code(&self) -> String {
let mut full_code = self.locale.clone();
if let Some(code) = &self.code {
full_code.push('_');
full_code.push_str(code);
}
full_code
}
/// Whether this translation is the default for its locale.
pub fn is_default(&self) -> bool {
self.code.is_none()
}
pub fn analyze_data(&mut self, ctx: &Context, constants: &EngineConstants) {
if self.locale == constants.base_locale && self.is_default() {
self.available = true;
self.is_complete = true;
self.data_type = constants.active_root.data_type;
} else {
let tran_path = constants.active_root.translation_path(ctx, self);
self.available = filesystem::exists(ctx, &tran_path);
if self.available {
self.data_type = DataType::from_path(ctx, tran_path.as_str());
if self.data_type.is_none() {
let translation_root = constants.roots.get(&tran_path);
self.is_complete = translation_root.is_some(); // incomplete translation roots aren't added to the list
self.data_type = translation_root.unwrap_or(&constants.active_root).data_type;
}
}
}
}
}
impl From<&Locale> for Translation {
fn from(value: &Locale) -> Self {
Self {
locale: value.code.clone(),
name: "".to_string(),
encoding: value.encoding,
stage_encoding: value.stage_encoding,
..Default::default()
}
}
}
+3 -2
View File
@@ -1,8 +1,9 @@
use num_traits::FromPrimitive;
use crate::common::Encoding;
use crate::framework::error::{GameError::CommandLineError, GameResult};
use crate::game::npc::NPC;
use crate::game::scripting::tsc::text_script::{ScriptMode, TextScript, TextScriptEncoding};
use crate::game::scripting::tsc::text_script::{ScriptMode, TextScript};
use crate::game::shared_game_state::SharedGameState;
use crate::game::weapon::WeaponType;
use crate::scene::game_scene::GameScene;
@@ -249,7 +250,7 @@ impl CommandLineCommand {
}
CommandLineCommand::TSC(script) => {
log::info!("Executing TSC script: {}", format!("#9999\n{}", script));
match TextScript::compile(format!("#9999\n{}", script).as_bytes(), true, TextScriptEncoding::UTF8) {
match TextScript::compile(format!("#9999\n{}", script).as_bytes(), true, Encoding::UTF8) {
Ok(text_script) => {
state.textscript_vm.set_debug_script(text_script);
state.textscript_vm.set_mode(ScriptMode::Debug);
+39 -10
View File
@@ -1,6 +1,7 @@
use itertools::Itertools;
use crate::common::Rect;
use crate::engine_constants::RootType;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::VSyncMode;
@@ -101,7 +102,7 @@ impl Default for SoundtrackMenuEntry {
#[derive(Debug, Clone, Eq, PartialEq)]
enum LanguageMenuEntry {
Title,
Language(String),
Language(String, Option<String>), // locale code, translation code (None represents a default translation variant)
Spacer,
Warning,
Back,
@@ -379,8 +380,13 @@ impl SettingsMenu {
let mut has_diff_data_locales = false;
let mut has_no_data_locales = false;
for locale in &state.constants.locales {
if !locale.is_present {
for tran in &state.constants.translations {
if !tran.available {
if !tran.is_default() {
log::debug!("Skipped locale: {:?}", tran);
continue;
}
has_no_data_locales = true;
}
@@ -388,15 +394,37 @@ impl SettingsMenu {
let main_root = state.constants.roots.get("/").unwrap();
let active_root = &state.constants.active_root;
let mut entry = locale.name.clone();
if !locale.is_present {
entry.push(no_data_char.0)
} else if locale.code != state.constants.base_locale && active_root.support_locales && active_root.data_type != locale.data_type || main_root.data_type != locale.data_type {
let loc = state.constants.locales.iter().find(|l| l.code == tran.locale).unwrap();
let mut entry = loc.name.clone();
// Workaround for locales like "Chinese (Simplified)"
if !tran.is_default() {
if entry.ends_with(')') {
let _ = entry.pop();
entry.push_str(", ");
} else {
entry.push_str(" (");
}
entry.push_str(tran.name.as_str());
entry.push(')');
}
if !tran.available {
entry.push(no_data_char.0);
} else if
tran.locale != state.constants.base_locale &&
active_root.support_locales && active_root.data_type != tran.data_type ||
(main_root.data_type != tran.data_type && active_root.root_type == RootType::Translation)
{
// We compare the data type of the translation with the data type of the active root,
// if it supports locales and isn't a translation root (in case it's a mod root that, for some reason,
// doesn't support locales), or with the data type of the main root otherwise.
has_diff_data_locales = true;
entry.push(save_warn_char.0);
}
self.language.push_entry(LanguageMenuEntry::Language(locale.code.clone()), MenuEntry::Active(entry));
self.language.push_entry(LanguageMenuEntry::Language(tran.locale.clone(), tran.code.clone()), MenuEntry::Active(entry));
}
self.language.push_entry(LanguageMenuEntry::Spacer, MenuEntry::Spacer(8.0));
@@ -982,12 +1010,13 @@ impl SettingsMenu {
)?;
}
CurrentMenu::LanguageMenu => match self.language.tick(controller, state) {
MenuSelectionResult::Selected(LanguageMenuEntry::Language(new_locale), entry) => {
MenuSelectionResult::Selected(LanguageMenuEntry::Language(new_locale, new_translation), entry) => {
if let MenuEntry::Active(_) = entry {
if new_locale == state.settings.locale {
if new_locale == state.settings.locale && new_translation == state.settings.translation {
self.current = CurrentMenu::MainMenu;
} else {
state.settings.locale = new_locale;
state.settings.translation = new_translation;
state.update_locale(ctx);
let _ = state.settings.save(ctx);