Files
sacred_k/src/game/ui/texts.cpp
T
khallaandClaude Opus 5.5 46a08e97e9 Teleportationstrank: yellow potions open the portal window
- Every yellow potion (potion kind 5, all stages) is a Teleportationstrank:
  drinking it posts the teleporter's event to the busy dialog with the
  hero's known portals; the potion is used up only when the hero teleports
  from that window (closing it keeps the potion)
- Names replaced in every game text (ReplaceInTexts), so item names and the
  bottom bar's quick-select tooltip read Teleportationstrank; GameText reads
  the game's own text
- HeroIndex / HeroInventory moved to skillslots for shared use
- docs/teleport-potion.md; walking animation after loading noted in todo

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 04:04:21 +02:00

159 lines
4.8 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();
}
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