mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-28 20:02:54 +01:00
Co-authored-by: wendersonrodrigues <wendersonrodrigues@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
171 lines
6.4 KiB
C++
171 lines
6.4 KiB
C++
// 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, <language>; bl sub_822BCBC8` at 0x823B48D8: 0 (English) in the
|
|
// USA build, 4 (Spanish) in Spain's. That index picks the text
|
|
// (LANGUAGES\<NAME>.BIN, which the USA disc's archives hold for all ten
|
|
// languages), the police radio's speech bank
|
|
// (Sound\Speech\copspeech_<xx>.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,
|
|
// "_<language>", 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 <stdafx.h>
|
|
#include "language.h"
|
|
|
|
#include <kernel/function.h>
|
|
#include <user/paths.h>
|
|
#include <user/settings.h>
|
|
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <system_error>
|
|
|
|
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, std::size(kLanguages)> 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<std::string> 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<uint8_t> 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<char>(f), std::istreambuf_iterator<char>());
|
|
}
|
|
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<uint32_t>(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<uint32_t>(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<uint32_t>(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);
|
|
}
|