Files
spice2x-r3d/src/spice2x/hooks/lang.cpp
T
James LiuandGitHub 3b29227dbc gitadora: fix XG2 gibberish text showing (#863)
> [!NOTE]
> Before submitting code changes... 
> * Please do note that this is a GPL v3.0 open source project.
> * Please read the
[CONTRIBUTING](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md)
guide.
> * Maintainers reserve the right to reject or modify your submission
without reason.
> * No new compiler warnings must be introduced. Check the CI build
results.
> 
> Feel free to remove this section after you have read it.

## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Fix gibberish in dynamic Japanese text in GuitarFreaks/DrumMania XG2
(K32/K33) when Windows uses a non-Japanese system code page.

XG2 converts UTF-8 property strings through WideCharToMultiByte(CP_ACP)
before rendering. On systems where the active code page is not
Shift-JIS, these strings are converted using the system code page and
become corrupted or turn into question marks.

Spice2x already hooks WideCharToMultiByte and redirects these
conversions to CP932 for newer 64-bit Gitadora models. This change makes
the existing hook available to 32-bit builds and enables it specifically
for XG2 models K32 and K33.

The existing 64-bit Gitadora Arena and T44 conditions remain unchanged.

## Testing
Before
<img width="1280" height="745" alt="image"
src="https://github.com/user-attachments/assets/34aa2c93-89e8-4c4f-b267-6cbc236f19de"
/>
<img width="2054" height="1188" alt="image"
src="https://github.com/user-attachments/assets/d88b5770-5545-4402-b9e7-3126fe048fb9"
/>

After
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/d43b4e47-b6af-4b97-8898-53849c9a0ca4"
/>
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/884301e2-1edd-4ad2-92ac-ece656853fe7"
/>

- Tested GuitarFreaks XG2 on a non-Japanese (English US) Windows.
- Verified that Community Log preset comments render correctly in
Japanese.
- Verified that Cooperation Challenge descriptions, rewards, and
progress text render correctly.
- Confirmed that the previous mojibake and question marks no longer
appear.
- GitHub Actions build completed successfully for both architectures.
2026-08-12 00:08:05 -07:00

394 lines
11 KiB
C++

// GetLocaleInfoEx
#define _WIN32_WINNT 0x0600
#include "lang.h"
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <winternl.h>
#include <ntstatus.h>
#include "avs/game.h"
#include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h"
#include "games/popn/popn.h"
#include "games/sdvx/sdvx.h"
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
// ANSI/OEM Japanese; Japanese (Shift-JIS)
constexpr UINT CODEPAGE_SHIFT_JIS = 932;
static decltype(GetACP) *GetACP_orig = nullptr;
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
#endif
static NTSTATUS NTAPI RtlMultiByteToUnicodeN_hook(
PWCH UnicodeString,
ULONG MaxBytesInUnicodeString,
PULONG BytesInUnicodeString,
const CHAR *MultiByteString,
ULONG BytesInMultiByteString)
{
// try to convert
auto wc_num = MultiByteToWideChar(
CODEPAGE_SHIFT_JIS,
0,
MultiByteString,
static_cast<int>(BytesInMultiByteString),
UnicodeString,
static_cast<int>(MaxBytesInUnicodeString)
);
// error handling
if (!wc_num) {
auto error = GetLastError();
switch (error) {
case ERROR_INSUFFICIENT_BUFFER:
return STATUS_BUFFER_TOO_SMALL;
case ERROR_INVALID_PARAMETER:
case ERROR_INVALID_FLAGS:
return STATUS_INVALID_PARAMETER;
case ERROR_NO_UNICODE_TRANSLATION:
return STATUS_UNMAPPABLE_CHARACTER;
default:
return STATUS_UNSUCCESSFUL;
}
}
// set byte count
if (BytesInUnicodeString) {
*BytesInUnicodeString = 2 * static_cast<UINT>(wc_num);
}
// return success
return STATUS_SUCCESS;
}
static UINT WINAPI GetACP_hook() {
return CODEPAGE_SHIFT_JIS;
}
static UINT WINAPI GetOEMCP_hook() {
return CODEPAGE_SHIFT_JIS;
}
#ifdef SPICE64
static LCID WINAPI GetSystemDefaultLCID_hook() {
// ja-JP per https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
// this is passed to LCMapStringA for kana conversions (e.g., for subscreen song search)
return 0x411;
}
static LCID WINAPI GetThreadLocale_hook() {
return 0x411;
}
#endif
static int WINAPI MultiByteToWideChar_hook(
UINT CodePage,
DWORD dwFlags,
LPCCH lpMultiByteStr,
int cbMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
// this fixes pop'n music's mojibake issue with the system locale not set to Japanese
SetThreadLocale(MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN));
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return MultiByteToWideChar_orig(
CodePage,
dwFlags,
lpMultiByteStr,
cbMultiByte,
lpWideCharStr,
cchWideChar);
}
static int WINAPI GetLocaleInfoEx_hook (
LPCWSTR lpLocaleName,
LCTYPE LCType,
LPWSTR lpLCData,
int cchData)
{
#if 0
if (lpLocaleName == LOCALE_NAME_INVARIANT) {
log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_INVARIANT), {}, {}", LCType, cchData);
} else if (lpLocaleName == LOCALE_NAME_SYSTEM_DEFAULT) {
log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_SYSTEM_DEFAULT), {}, {}", LCType, cchData);
} else if (lpLocaleName == LOCALE_NAME_USER_DEFAULT) {
log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT), {}, {}", LCType, cchData);
} else {
log_misc("hooks::lang", "GetLocaleInfoEx_hook hit ({}), {}, {}", ws2s(lpLocaleName), LCType, cchData);
}
#endif
if (lpLocaleName == LOCALE_NAME_USER_DEFAULT &&
LCType == LOCALE_SISO639LANGNAME &&
lpLCData != NULL &&
cchData >= 3) {
log_misc("hooks::lang",
"GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT / LOCALE_SISO639LANGNAME), return `ja`");
wcscpy(lpLCData, L"ja");
return 3;
}
if (lpLocaleName == LOCALE_NAME_USER_DEFAULT &&
LCType == LOCALE_SISO3166CTRYNAME &&
lpLCData != NULL &&
cchData >= 3) {
log_misc("hooks::lang",
"GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT / LOCALE_SISO3166CTRYNAME), return `JP`");
wcscpy(lpLCData, L"JP");
return 3;
}
return GetLocaleInfoEx_orig(lpLocaleName, LCType, lpLCData, cchData);
}
#ifdef SPICE64
static BOOL WINAPI IsDBCSLeadByte_hook (
BYTE TestChar
)
{
if (IsDBCSLeadByteEx_orig) {
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
}
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}
static BOOL WINAPI IsDBCSLeadByteEx_hook(
UINT CodePage,
BYTE TestChar)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
}
#endif
static
int
WINAPI
WideCharToMultiByte_hook(
UINT CodePage,
DWORD dwFlags,
LPCWCH lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCCH lpDefaultChar,
LPBOOL lpUsedDefaultChar
)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
SetThreadLocale(MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN));
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return WideCharToMultiByte_orig(
CodePage,
dwFlags,
lpWideCharStr,
cchWideChar,
lpMultiByteStr,
cbMultiByte,
lpDefaultChar,
lpUsedDefaultChar);
}
#ifdef SPICE64
int
WINAPI
GetLocaleInfoA_hook(
LCID Locale,
LCTYPE LCType,
LPSTR lpLCData,
int cchData) {
if (LCType == LOCALE_SISO639LANGNAME && lpLCData != NULL && cchData >= 3) {
log_misc("hooks::lang", "GetLocaleInfoA_hook hit ({:#x}, LOCALE_SISO639LANGNAME), return `ja`", Locale);
strcpy(lpLCData, "ja");
return 3;
}
if (LCType == LOCALE_SISO3166CTRYNAME && lpLCData != NULL && cchData >= 3) {
log_misc("hooks::lang",
"GetLocaleInfoA_hook hit ({:#x}, LOCALE_SISO3166CTRYNAME), return `JP`", Locale);
strcpy(lpLCData, "JP");
return 3;
}
log_misc("hooks::lang", "GetLocaleInfoA_hook hit, {:#x}, {:#x}", Locale, LCType);
return GetLocaleInfoA_orig(Locale, LCType, lpLCData, cchData);
}
#endif
void hooks::lang::early_init() {
log_info("hooks::lang", "early initialization");
const auto native_code_page = GetACP();
if (native_code_page == CP_UTF8) {
log_warning(
"hooks::lang",
"Windows is using UTF-8 as the system code page; "
"some games may render text incorrectly or behave unexpectedly");
deferredlogs::defer_error_messages({
"Windows is using UTF-8 as the system code page",
" some games may render text incorrectly or behave unexpectedly"});
}
// hooking these two functions fixes the jubeat mojibake
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
#ifdef SPICE64 // SDVX5+ specific code
if (games::sdvx::is_valkyrie_model()) {
log_info("hooks::lang", "hooking GetSystemDefaultLCID");
detour::trampoline_try(
"kernel32.dll",
"GetSystemDefaultLCID",
GetSystemDefaultLCID_hook,
&GetSystemDefaultLCID_orig);
}
#endif
if (avs::game::is_model("XIF")) {
log_info("hooks::lang", "hooking GetLocaleInfoEx");
detour::trampoline_try(
"kernel32.dll",
"GetLocaleInfoEx",
GetLocaleInfoEx_hook,
&GetLocaleInfoEx_orig);
}
#ifdef SPICE64
if (avs::game::is_model("UJK")) {
// CCJ build of Unity calls GetThreadLocale,
// and then GetLocaleInfoA to figure out the locale; eventually this is
// used to figure out the decimal separator, which is then used in parsing
// Double values - which fails carding in if it isn't "."
log_info("hooks::lang", "hooking GetThreadLocale");
detour::trampoline_try(
"kernel32.dll",
"GetThreadLocale",
GetThreadLocale_hook,
&GetThreadLocale_orig);
log_info("hooks::lang", "hooking GetLocaleInfoA");
detour::trampoline_try(
"kernel32.dll",
"GetLocaleInfoA",
GetLocaleInfoA_hook,
&GetLocaleInfoA_orig);
}
// for TDJ subscreen search keyboard and T44 narrow-string handling
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByte",
IsDBCSLeadByte_hook,
&IsDBCSLeadByte_orig);
}
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByteEx",
IsDBCSLeadByteEx_hook,
&IsDBCSLeadByteEx_orig);
}
#endif
#ifdef SPICE64
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model("T44");
#else
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
#endif
if (hook_wide_char_to_multi_byte) {
log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try(
"kernel32.dll",
"WideCharToMultiByte",
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
}
void hooks::lang::init() {
log_info("hooks::lang", "initializing");
detour::iat_try("RtlMultiByteToUnicodeN", RtlMultiByteToUnicodeN_hook, nullptr, "ntdll.dll");
MultiByteToWideChar_orig = detour::iat_try(
"MultiByteToWideChar",
MultiByteToWideChar_hook,
nullptr,
"kernel32.dll");
}
bool hooks::lang::is_native_shiftjis() {
return GetACP() == CODEPAGE_SHIFT_JIS;
}