// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). // // Settings > Game > Language: the game's text in any of its ten languages, // from the USA disc's own files. From wendersonrodrigues's GitHub pull // request #26, which found the two constants below. // // EA built one executable per SKU. The USA one (MWRelease) and Spain's // (MWEuropeSpanRelease, default.xex SHA-256 684ceef9...) share the same game // code and the same .data; matched function by function, the only // differences that touch the disc's files are two constants: // // - The boot (sub_823B4740) sets the game's language with // `li r3, ; bl sub_822BCBC8` at 0x823B48D8: 0 (English) in the // USA build, 4 (Spanish) in Spain's. That index picks the text // (LANGUAGES\.BIN, which the USA disc's archives hold for all ten // languages), the police radio's speech bank // (Sound\Speech\copspeech_.big for French, German, Italian, Spanish: // the USA disc has only English's) and the movies' language. // - The movie name (sub_82282E80) is "%sMOVIES\%s%s%s%s": prefix, name, // "_", the TV system, ".wmv". The USA build passes "_ntsc" // (0x82078AA8) in r9; Spain's passes "_pal", which the USA build also // holds, unused, at 0x82078AB0. When that language's movie is missing, // the game tries English's with the same suffix. // // (The rest differs only online: SKU codes, region "NA"/"EU", date format.) // // Two overrides of the generated, weak functions (no ppc/ regeneration); // game/language.h has the decisions: // - sub_822BCBC8, SetCurrentLanguage(r3): on the boot's call r3 becomes // the setting's language (BootLanguage). The debug language cycling's // calls are left alone. // - sub_82282A58, the movie name's sprintf: on sub_82282E80's call r9 // becomes "_pal" only when Movies/ holds the chosen language's PAL // movies (MovieSuffix), so with the USA disc's files the game's own // fallback plays the English movies. // - sub_82148818, the attribute lookup: sub_821B82E8's speech record // becomes English's when the chosen language's bank is missing // (SpeechIndex), so the police radio and the in-game scenes' voices play // in English instead of not at all. // // settings::Id::GameLanguage ([game] language in settings.toml, // NFSMW_LANGUAGE=spanish for one run); read at boot, so a change applies at // the next launch. English, the default, opens the same files as before. #include #include "language.h" #include #include #include #include #include #include #include #include extern "C" PPC_FUNC(__imp__sub_822BCBC8); extern "C" PPC_FUNC(__imp__sub_82282A58); extern "C" PPC_FUNC(__imp__sub_82148818); namespace { using namespace game::language; int32_t Chosen() { return settings::GetInt(settings::Id::GameLanguage); } // `name` in `dir`, in any case (the disc's folders are "Movies" and // "NFS"; an install on a case-sensitive file system keeps the disc's). std::filesystem::path Child(const std::filesystem::path& dir, std::string_view name) { std::error_code ec; for (const auto& e : std::filesystem::directory_iterator(dir, ec)) { const std::string n = e.path().filename().string(); if (n.size() == name.size() && EndsWithNoCase(n, name)) return e.path(); } return {}; } struct Installed { std::array files; }; // Read once: Movies/'s names and NFS/ZDIR.BIN (33 KB), never the game's // own file opens. const Installed& InstalledFiles() { static const Installed installed = [] { Installed out; const std::filesystem::path game = GetGamePath(); std::vector movies; std::error_code ec; if (std::filesystem::path dir = Child(game, "movies"); !dir.empty()) for (const auto& e : std::filesystem::directory_iterator(dir, ec)) movies.push_back(e.path().filename().string()); std::vector zdir; if (std::filesystem::path nfs = Child(game, "nfs"); !nfs.empty()) if (std::filesystem::path index = Child(nfs, "zdir.bin"); !index.empty()) { std::ifstream f(index, std::ios::binary); zdir.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); } for (size_t i = 0; i < std::size(kLanguages); i++) out.files[i] = Scan(kLanguages[i], movies, zdir); return out; }(); return installed; } const Files* FilesOf(int32_t id) { for (size_t i = 0; i < std::size(kLanguages); i++) if (kLanguages[i].id == id) return &InstalledFiles().files[i]; return nullptr; } } namespace game::language { std::string MenuNote(int32_t id) { const Language* l = Find(id); const Files* f = FilesOf(id); return l && f ? Note(*l, *f) : std::string(); } } PPC_FUNC(sub_822BCBC8) { if (static_cast(ctx.lr) == kBootCall) { const int32_t chosen = Chosen(); if (const Language* l = Find(chosen)) fprintf(stderr, "%s\n", LogLine(*l, *FilesOf(chosen)).c_str()); if (const int32_t r3 = BootLanguage(kBootCall, ctx.r3.s32, chosen); r3 != ctx.r3.s32) ctx.r3.s64 = r3; } __imp__sub_822BCBC8(ctx, base); } PPC_FUNC(sub_82282A58) { if (static_cast(ctx.lr) == kMovieNameCall) { const Files* f = FilesOf(Chosen()); if (const uint32_t r9 = MovieSuffix(kMovieNameCall, ctx.r9.u32, f && f->pal); r9 != ctx.r9.u32) ctx.r9.u64 = r9; } __imp__sub_82282A58(ctx, base); } // The attribute lookup (852 callers): only sub_821B82E8's speech-record // lookups change, and only when the chosen language's bank is missing. PPC_FUNC(sub_82148818) { const uint32_t lr = static_cast(ctx.lr); if (lr >= kSpeechPickBegin && lr < kSpeechPickEnd) { const Files* f = FilesOf(Chosen()); if (const uint32_t r5 = SpeechIndex(lr, ctx.r4.u32, ctx.r5.u32, f && f->speechToEnglish); r5 != ctx.r5.u32) ctx.r5.u64 = r5; } __imp__sub_82148818(ctx, base); }