From 9141ff453b72dbf7cf2aa53ad46612ab67bc8cae Mon Sep 17 00:00:00 2001
From: bicarus <202771338+bicarus-dev@users.noreply.github.com>
Date: Tue, 4 Aug 2026 09:04:29 -0700
Subject: [PATCH] lang: detect forced UTF-8 ACP, try to opt-out (#853)
## Link to GitHub Issue or related Pull Request, if one exists
n/a
## Description of change
Fixes broken Japanese text when Windows `Use Unicode UTF-8 for worldwide
language support` setting is enabled. This setting changes the system
ACP to UTF-8 (65001), causing legacy Shift-JIS lead-byte checks to fail.
Windows 11: requests the legacy process code page through the manifest.
Windows 10: detects ACP 65001 (UTF-8), warns the user via deferred log,
and applies compatibility hook (only for popn pika model for now)
Windows 7 and below - UTF-8 option doesn't exist.
## Testing
---
src/spice2x/build/manifest.manifest | 1 +
src/spice2x/hooks/lang.cpp | 45 +++++++++++++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/src/spice2x/build/manifest.manifest b/src/spice2x/build/manifest.manifest
index f4d072b..d19d2e9 100644
--- a/src/spice2x/build/manifest.manifest
+++ b/src/spice2x/build/manifest.manifest
@@ -6,6 +6,7 @@
PerMonitorV2
true
+ Legacy
diff --git a/src/spice2x/hooks/lang.cpp b/src/spice2x/hooks/lang.cpp
index 99a4f32..553b966 100644
--- a/src/spice2x/hooks/lang.cpp
+++ b/src/spice2x/hooks/lang.cpp
@@ -13,7 +13,9 @@
#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"
@@ -29,6 +31,7 @@ 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(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
@@ -182,9 +185,30 @@ 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);
+}
+
static
int
WINAPI
@@ -250,6 +274,18 @@ GetLocaleInfoA_hook(
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);
@@ -315,6 +351,15 @@ void hooks::lang::early_init() {
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
+
+ if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
+ detour::trampoline_try(
+ "kernel32.dll",
+ "IsDBCSLeadByteEx",
+ IsDBCSLeadByteEx_hook,
+ &IsDBCSLeadByteEx_orig);
+ }
+
#endif
}