From 1eee48216563c594644730cc4c0e406d5872e442 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 12 Jul 2026 09:23:40 +0200 Subject: [PATCH] Consolidate the host's UTF-8/wide conversions into util/utf8.hpp Five files each carried their own copy of the WideCharToMultiByte / MultiByteToWideChar UTF-8 conversion (injection_panel, audio_overrides, imgui_layer's to_utf8, main's harness widen, and vk_layer_setup's inline form). Replace them all with coop::narrow / coop::widen from one header. audio_panel's image_basename dropped its lossy `c & 0x7F` ASCII mask for the proper narrow(), so a non-ASCII game exe name is no longer mangled in log lines. --- host/src/audio/audio_overrides.cpp | 29 +++------------------- host/src/audio_panel.cpp | 12 +++------ host/src/imgui_layer.cpp | 22 +++-------------- host/src/injection_panel.cpp | 13 +--------- host/src/main.cpp | 13 ++-------- host/src/util/utf8.hpp | 39 ++++++++++++++++++++++++++++++ host/src/vk_layer_setup.cpp | 7 ++---- 7 files changed, 54 insertions(+), 81 deletions(-) create mode 100644 host/src/util/utf8.hpp diff --git a/host/src/audio/audio_overrides.cpp b/host/src/audio/audio_overrides.cpp index 2ededf2..04b2c43 100644 --- a/host/src/audio/audio_overrides.cpp +++ b/host/src/audio/audio_overrides.cpp @@ -9,6 +9,7 @@ #include // WAVE_FORMAT_* (needs windows.h first) #include "coop/tool_paths.hpp" +#include "util/utf8.hpp" namespace coop { @@ -22,32 +23,8 @@ std::wstring to_lower(std::wstring s) } return s; } - -// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and -// can't collide with another name; for ASCII names (the common case) UTF-8 is byte-identical. -std::string narrow(const std::wstring& w) -{ - if (w.empty()) - { - return {}; - } - const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); - std::string s(static_cast(n), '\0'); - WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), s.data(), n, nullptr, nullptr); - return s; -} - -std::wstring widen(const std::string& s) -{ - if (s.empty()) - { - return {}; - } - const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); - std::wstring w(static_cast(n), L'\0'); - MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), w.data(), n); - return w; -} +// A non-ASCII image name (e.g. a CJK game exe) round-trips through UTF-8 so it survives +// persist/reload; ASCII names (the common case) are byte-identical. See util/utf8.hpp. } // namespace AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path)) diff --git a/host/src/audio_panel.cpp b/host/src/audio_panel.cpp index f21a1c9..b662e72 100644 --- a/host/src/audio_panel.cpp +++ b/host/src/audio_panel.cpp @@ -8,6 +8,7 @@ #include #include "ui/app_chrome.hpp" +#include "util/utf8.hpp" namespace coop { @@ -69,18 +70,11 @@ ImVec4 audio_format_state_color(std::uint32_t state) } } -// Basename of an image path, narrowed to ASCII for a log line. +// Basename of an image path (UTF-8), for a log line. std::string image_basename(const std::wstring& image_path) { const std::size_t slash = image_path.find_last_of(L"\\/"); - const std::wstring w = slash == std::wstring::npos ? image_path : image_path.substr(slash + 1); - std::string out; - out.reserve(w.size()); - for (wchar_t c : w) - { - out.push_back(static_cast(c & 0x7F)); - } - return out; + return narrow(slash == std::wstring::npos ? image_path : image_path.substr(slash + 1)); } } // namespace diff --git a/host/src/imgui_layer.cpp b/host/src/imgui_layer.cpp index b1452a8..6eaf4f5 100644 --- a/host/src/imgui_layer.cpp +++ b/host/src/imgui_layer.cpp @@ -9,27 +9,11 @@ #include "coop/dpi.hpp" #include "coop/tool_paths.hpp" #include "ui/app_chrome.hpp" +#include "util/utf8.hpp" namespace coop { -namespace -{ -// ImGui's file IO treats IniFilename as UTF-8 (it converts to wide for _wfopen), so -// the path must be UTF-8 rather than the system ANSI code page. -std::string to_utf8(const std::wstring& w) -{ - if (w.empty()) - { - return {}; - } - const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); - std::string s(static_cast(n), '\0'); - WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), s.data(), n, nullptr, nullptr); - return s; -} -} // namespace - ImGuiLayer::~ImGuiLayer() { if (initialized_) @@ -52,7 +36,9 @@ bool ImGuiLayer::init(HWND hwnd, ID3D11Device* device, ID3D11DeviceContext* cont // computed default on startup (which would clobber the restored positions). const std::wstring ini_w = exe_directory() + L"coop_layout.ini"; const bool had_layout = GetFileAttributesW(ini_w.c_str()) != INVALID_FILE_ATTRIBUTES; - ini_path_ = to_utf8(ini_w); + // ImGui treats IniFilename as UTF-8 (it converts to wide for _wfopen), so narrow to UTF-8, not + // the system ANSI code page. + ini_path_ = narrow(ini_w); io.IniFilename = ini_path_.c_str(); set_layout_persisted(had_layout); diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 272d8f7..50d6723 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -10,6 +10,7 @@ #include "inject/injector.hpp" #include "ui/app_chrome.hpp" #include "ui/text_match.hpp" +#include "util/utf8.hpp" namespace coop { @@ -21,18 +22,6 @@ const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f); const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f); const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 1.0f); -std::string narrow(const std::wstring& w) -{ - if (w.empty()) - { - return {}; - } - const int len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); - std::string out(static_cast(len), '\0'); - WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), out.data(), len, nullptr, nullptr); - return out; -} - // Case-insensitive equality of two image names (e.g. "game.exe"). bool iequals_name(const std::wstring& a, const std::wstring& b) { diff --git a/host/src/main.cpp b/host/src/main.cpp index b0fa9da..4d4cf20 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -35,6 +35,7 @@ #include "log_panel.hpp" #include "test_harness.hpp" #include "ui/app_chrome.hpp" +#include "util/utf8.hpp" #include "vk_layer_setup.hpp" namespace @@ -91,17 +92,7 @@ void draw_screenshot_toast(double seconds_since, const std::string& name) } #ifdef COOP_TEST_HARNESS -std::wstring widen(const std::string& s) -{ - if (s.empty()) - { - return {}; - } - const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); - std::wstring w(static_cast(n), L'\0'); - MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), w.data(), n); - return w; -} +using coop::widen; // Run a test-harness command on the main thread, hitting the same code the UI buttons do. // Returns a one-line response the driver reads back. diff --git a/host/src/util/utf8.hpp b/host/src/util/utf8.hpp new file mode 100644 index 0000000..98a7445 --- /dev/null +++ b/host/src/util/utf8.hpp @@ -0,0 +1,39 @@ +// UTF-8 <-> wide-string conversion, shared by the host code that bridges Win32's UTF-16 +// (image names, window titles, paths) and the UTF-8 the ImGui overlay / .ini files use. +#pragma once + +#include + +#include + +namespace coop +{ + +// UTF-16 -> UTF-8. +inline std::string narrow(const std::wstring& w) +{ + if (w.empty()) + { + return {}; + } + const int n = + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); + std::string s(static_cast(n), '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), s.data(), n, nullptr, nullptr); + return s; +} + +// UTF-8 -> UTF-16. +inline std::wstring widen(const std::string& s) +{ + if (s.empty()) + { + return {}; + } + const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); + std::wstring w(static_cast(n), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), w.data(), n); + return w; +} + +} // namespace coop diff --git a/host/src/vk_layer_setup.cpp b/host/src/vk_layer_setup.cpp index caea6f3..bf9491f 100644 --- a/host/src/vk_layer_setup.cpp +++ b/host/src/vk_layer_setup.cpp @@ -3,6 +3,7 @@ #include #include "coop/tool_paths.hpp" +#include "util/utf8.hpp" namespace coop { @@ -33,11 +34,7 @@ bool register_vk_layer(const std::wstring& target_image) { const std::size_t slash = target_image.find_last_of(L"\\/"); const std::wstring base = slash == std::wstring::npos ? target_image : target_image.substr(slash + 1); - const int n = WideCharToMultiByte(CP_UTF8, 0, base.c_str(), static_cast(base.size()), nullptr, 0, - nullptr, nullptr); - std::string utf8(static_cast(n), '\0'); - WideCharToMultiByte(CP_UTF8, 0, base.c_str(), static_cast(base.size()), utf8.data(), n, nullptr, - nullptr); + const std::string utf8 = narrow(base); HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (f != INVALID_HANDLE_VALUE) {