> [!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.
394 lines
11 KiB
C++
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;
|
|
}
|