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.
This commit is contained in:
2026-07-12 09:23:40 +02:00
parent 246289e386
commit 1eee482165
7 changed files with 54 additions and 81 deletions

View File

@@ -9,6 +9,7 @@
#include <mmreg.h> // WAVE_FORMAT_* (needs windows.h first) #include <mmreg.h> // WAVE_FORMAT_* (needs windows.h first)
#include "coop/tool_paths.hpp" #include "coop/tool_paths.hpp"
#include "util/utf8.hpp"
namespace coop namespace coop
{ {
@@ -22,32 +23,8 @@ std::wstring to_lower(std::wstring s)
} }
return s; return s;
} }
// A non-ASCII image name (e.g. a CJK game exe) round-trips through UTF-8 so it survives
// UTF-8 round-trip so a non-ASCII image name (e.g. a CJK game exe) survives persist/reload and // persist/reload; ASCII names (the common case) are byte-identical. See util/utf8.hpp.
// 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<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string s(static_cast<std::size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(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<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
} // namespace } // namespace
AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path)) AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path))

View File

@@ -8,6 +8,7 @@
#include <mmreg.h> #include <mmreg.h>
#include "ui/app_chrome.hpp" #include "ui/app_chrome.hpp"
#include "util/utf8.hpp"
namespace coop 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) std::string image_basename(const std::wstring& image_path)
{ {
const std::size_t slash = image_path.find_last_of(L"\\/"); 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); return narrow(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<char>(c & 0x7F));
}
return out;
} }
} // namespace } // namespace

View File

@@ -9,27 +9,11 @@
#include "coop/dpi.hpp" #include "coop/dpi.hpp"
#include "coop/tool_paths.hpp" #include "coop/tool_paths.hpp"
#include "ui/app_chrome.hpp" #include "ui/app_chrome.hpp"
#include "util/utf8.hpp"
namespace coop 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<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string s(static_cast<std::size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
return s;
}
} // namespace
ImGuiLayer::~ImGuiLayer() ImGuiLayer::~ImGuiLayer()
{ {
if (initialized_) 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). // computed default on startup (which would clobber the restored positions).
const std::wstring ini_w = exe_directory() + L"coop_layout.ini"; const std::wstring ini_w = exe_directory() + L"coop_layout.ini";
const bool had_layout = GetFileAttributesW(ini_w.c_str()) != INVALID_FILE_ATTRIBUTES; 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(); io.IniFilename = ini_path_.c_str();
set_layout_persisted(had_layout); set_layout_persisted(had_layout);

View File

@@ -10,6 +10,7 @@
#include "inject/injector.hpp" #include "inject/injector.hpp"
#include "ui/app_chrome.hpp" #include "ui/app_chrome.hpp"
#include "ui/text_match.hpp" #include "ui/text_match.hpp"
#include "util/utf8.hpp"
namespace coop 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 kRed(1.0f, 0.45f, 0.4f, 1.0f);
const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 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<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string out(static_cast<std::size_t>(len), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), out.data(), len, nullptr, nullptr);
return out;
}
// Case-insensitive equality of two image names (e.g. "game.exe"). // Case-insensitive equality of two image names (e.g. "game.exe").
bool iequals_name(const std::wstring& a, const std::wstring& b) bool iequals_name(const std::wstring& a, const std::wstring& b)
{ {

View File

@@ -35,6 +35,7 @@
#include "log_panel.hpp" #include "log_panel.hpp"
#include "test_harness.hpp" #include "test_harness.hpp"
#include "ui/app_chrome.hpp" #include "ui/app_chrome.hpp"
#include "util/utf8.hpp"
#include "vk_layer_setup.hpp" #include "vk_layer_setup.hpp"
namespace namespace
@@ -91,17 +92,7 @@ void draw_screenshot_toast(double seconds_since, const std::string& name)
} }
#ifdef COOP_TEST_HARNESS #ifdef COOP_TEST_HARNESS
std::wstring widen(const std::string& s) using coop::widen;
{
if (s.empty())
{
return {};
}
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
// Run a test-harness command on the main thread, hitting the same code the UI buttons do. // 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. // Returns a one-line response the driver reads back.

39
host/src/util/utf8.hpp Normal file
View File

@@ -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 <string>
#include <windows.h>
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<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string s(static_cast<std::size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(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<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), w.data(), n);
return w;
}
} // namespace coop

View File

@@ -3,6 +3,7 @@
#include <windows.h> #include <windows.h>
#include "coop/tool_paths.hpp" #include "coop/tool_paths.hpp"
#include "util/utf8.hpp"
namespace coop 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::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 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<int>(base.size()), nullptr, 0, const std::string utf8 = narrow(base);
nullptr, nullptr);
std::string utf8(static_cast<std::size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, base.c_str(), static_cast<int>(base.size()), utf8.data(), n, nullptr,
nullptr);
HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (f != INVALID_HANDLE_VALUE) if (f != INVALID_HANDLE_VALUE)
{ {