Arts are objects (game::Art) built from the game's definitions: name, launch type, damage source, regeneration group and the skills' art lists. The tooltip gets one line per effect of a skill or attribute, sorted by size; unlearned class skills are greyed out at 0%. Weapon arts list only the damage types the held weapon deals and the held weapon's skill. Tooltip line wrap ignores colour codes; art tooltips wrap wider. Blitzschlag's tooltip rewrite becomes a tooltip filter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
182 lines
5.9 KiB
C++
182 lines
5.9 KiB
C++
#include "game/ui/texts.h"
|
|
|
|
#include <windows.h>
|
|
|
|
#include <cwchar>
|
|
#include <cwctype>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#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<GameString>& Texts() {
|
|
static std::vector<GameString> texts;
|
|
return texts;
|
|
}
|
|
std::unordered_set<uint32_t>& CapitalizedIds() {
|
|
static std::unordered_set<uint32_t> ids;
|
|
return ids;
|
|
}
|
|
std::vector<std::pair<std::wstring, std::wstring>>& Replacements() {
|
|
static std::vector<std::pair<std::wstring, std::wstring>> replacements;
|
|
return replacements;
|
|
}
|
|
// By id; nodes stay put, so the strings handed out stay valid.
|
|
std::unordered_map<uint32_t, Shown> 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<ManagerFn>(0x006728c0)();
|
|
return manager ? static_cast<const GameString*>(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<uint32_t>(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<HashFn>(0x0080e010)(key);
|
|
return hash ? GameText(hash | 0x80000000) : std::wstring();
|
|
}
|
|
|
|
std::wstring WideStringText(const void* string) {
|
|
const auto* s = static_cast<const GameString*>(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<GameString*>(string);
|
|
if (s->begin) {
|
|
s->end = s->begin;
|
|
*const_cast<wchar_t*>(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<AppendFn>(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<const GameString*>(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
|