// GetLocaleInfoEx #define _WIN32_WINNT 0x0600 #include "lang.h" #define WIN32_NO_STATUS #include #undef WIN32_NO_STATUS #include #include #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(BytesInMultiByteString), UnicodeString, static_cast(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(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; }