#include "game/ui/texts.h" #include #include #include #include #include #include #include #include #include #include "re/hook.h" namespace game { // GetText (0x672f70, ret 4): (id) -> string; ours first. const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id); RE_HOOK(0x00672f70, TextManager_GetText); namespace { // Our ids: above the game's (resource ids are small; bit 31 means "by index"). constexpr uint32_t kFirstOwnText = 0x7fff0000; // The string GetText returns: a wide string as {begin, end (at the // terminator), capacity end} (the game's own at cScriptResourceDlg + 8). struct GameString { const wchar_t* begin; const wchar_t* end; const wchar_t* capacity; }; // A game text as we show it: made from `source` (the game's) when that // changes; `changed` false if ours is the same. struct Shown { std::wstring source; std::wstring text; GameString string; bool changed = false; }; std::mutex g_lock; std::vector& Texts() { static std::vector texts; return texts; } std::unordered_set& CapitalizedIds() { static std::unordered_set ids; return ids; } std::vector>& Replacements() { static std::vector> replacements; return replacements; } // By id; nodes stay put, so the strings handed out stay valid. std::unordered_map g_shown; std::wstring Capitalize(std::wstring text) { bool wordStart = true; for (wchar_t& c : text) { if (std::iswalpha(c)) { wchar_t buffer[2] = {c, 0}; if (wordStart) { CharUpperBuffW(buffer, 1); } else { CharLowerBuffW(buffer, 1); } c = buffer[0]; wordStart = false; } else { wordStart = !(c == L'\'' || c == L'-'); } } return text; } std::wstring Transform(uint32_t id, std::wstring text) { for (const auto& [from, to] : Replacements()) { for (size_t at = text.find(from); at != std::wstring::npos; at = text.find(from, at + to.size())) { text.replace(at, from.size(), to); } } return CapitalizedIds().count(id) ? Capitalize(std::move(text)) : text; } const GameString* GameTextString(uint32_t id) { using ManagerFn = void*(__cdecl*)(); void* manager = reinterpret_cast(0x006728c0)(); return manager ? static_cast(TextManager_GetText_original(manager, nullptr, id)) : nullptr; } } // namespace uint32_t RegisterText(const wchar_t* text) { std::lock_guard guard(g_lock); const size_t length = std::wcslen(text); Texts().push_back({text, text + length, text + length}); return kFirstOwnText + static_cast(Texts().size() - 1); } void CapitalizeText(uint32_t id) { std::lock_guard guard(g_lock); CapitalizedIds().insert(id); g_shown.erase(id); } void ReplaceInTexts(const std::wstring& from, const std::wstring& to) { if (from.empty() || from == to) { return; } std::lock_guard guard(g_lock); for (const auto& replacement : Replacements()) { if (replacement.first == from) { return; } } Replacements().emplace_back(from, to); g_shown.clear(); } std::wstring GameText(uint32_t id) { const GameString* string = GameTextString(id); return string && string->begin && string->end > string->begin ? std::wstring(string->begin, string->end) : std::wstring(); } std::wstring GameTextByKey(const char* key) { // The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does. using HashFn = uint32_t(__cdecl*)(const char*); const uint32_t hash = reinterpret_cast(0x0080e010)(key); return hash ? GameText(hash | 0x80000000) : std::wstring(); } std::wstring WideStringText(const void* string) { const auto* s = static_cast(string); return s && s->begin && s->end > s->begin ? std::wstring(s->begin, s->end) : std::wstring(); } void SetWideString(void* string, const std::wstring& value) { auto* s = static_cast(string); if (s->begin) { s->end = s->begin; *const_cast(s->begin) = L'\0'; } // The game's wide string append (0x592ef0, thiscall (first, last, unused), ret 0xc). using AppendFn = void*(__fastcall*)(void*, void*, const wchar_t*, const wchar_t*, uint32_t); reinterpret_cast(0x00592ef0)(string, nullptr, value.data(), value.data() + value.size(), 0); } const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) { if (id >= kFirstOwnText && id < 0x80000000) { std::lock_guard guard(g_lock); const size_t index = id - kFirstOwnText; if (index < Texts().size()) { return &Texts()[index]; } } const void* original = TextManager_GetText_original(self, edx, id); std::lock_guard guard(g_lock); if (!original || (Replacements().empty() && !CapitalizedIds().count(id))) { return original; } const auto* string = static_cast(original); if (!string->begin || string->end <= string->begin) { return original; } Shown& entry = g_shown[id]; if (entry.text.empty() || entry.source.compare(0, std::wstring::npos, string->begin, string->end - string->begin) != 0) { entry.source.assign(string->begin, string->end); entry.text = Transform(id, entry.source); entry.changed = entry.text != entry.source; const wchar_t* text = entry.text.c_str(); entry.string = {text, text + entry.text.size(), text + entry.text.size()}; } return entry.changed ? &entry.string : original; } } // namespace game