Files
spice2x.github.io/src/spice2x/hooks/lang.cpp
T
bicarus ec3f6639b7 ccj: fix locale issues with carding in (#628)
## Link to GitHub Issue or related Pull Request, if one exists
#624 

## Description of change
See bug. Add locale hooks specifically for CCJ.

also, fix y axis direction for XInput, it was flipped because XInput
uses opposite values from what RawInput does

## Testing
Tested card in / play one round with Windows regional format set to
French (France) and other language set to English.
2026-04-11 14:42:51 -07:00

336 lines
9.3 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/sdvx/sdvx.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(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_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
)
{
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}
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);
}
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");
// 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
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByte",
IsDBCSLeadByte_hook,
&IsDBCSLeadByte_orig);
}
if (games::gitadora::is_arena_model()) {
log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try(
"kernel32.dll",
"WideCharToMultiByte",
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
#endif
}
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;
}