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>
This commit is contained in:
1 parent
6ca96d54b4
commit
46a08e97e9
13 files changed
+368
-22
No files matched your search
+58
-11
@@ -8,11 +8,17 @@
|
||||
#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").
|
||||
@@ -26,10 +32,13 @@ struct GameString {
|
||||
const wchar_t* capacity;
|
||||
};
|
||||
|
||||
struct Capitalized {
|
||||
// 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;
|
||||
@@ -41,8 +50,12 @@ 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, Capitalized> g_capitalized;
|
||||
std::unordered_map<uint32_t, Shown> g_shown;
|
||||
|
||||
std::wstring Capitalize(std::wstring text) {
|
||||
bool wordStart = true;
|
||||
@@ -63,6 +76,21 @@ std::wstring Capitalize(std::wstring text) {
|
||||
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) {
|
||||
@@ -75,11 +103,29 @@ uint32_t RegisterText(const wchar_t* text) {
|
||||
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();
|
||||
}
|
||||
|
||||
// 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);
|
||||
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
||||
if (id >= kFirstOwnText && id < 0x80000000) {
|
||||
std::lock_guard guard(g_lock);
|
||||
@@ -90,22 +136,23 @@ const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
||||
}
|
||||
const void* original = TextManager_GetText_original(self, edx, id);
|
||||
std::lock_guard guard(g_lock);
|
||||
if (!original || !CapitalizedIds().count(id)) {
|
||||
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;
|
||||
}
|
||||
Capitalized& entry = g_capitalized[id];
|
||||
if (entry.source.compare(0, std::wstring::npos, string->begin, string->end - string->begin) != 0 ||
|
||||
entry.text.empty()) {
|
||||
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 = Capitalize(entry.source);
|
||||
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.string;
|
||||
return entry.changed ? &entry.string : original;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user