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
@@ -14,6 +14,7 @@ constexpr uint32_t kObjectUsedUp = 0x8000000; // kObjectFlags: set by cPotion3D
|
||||
|
||||
// The item type table at *0xaa9564 (ecx of ItemPotionKind).
|
||||
constexpr uint32_t kItemTypeTable = 0x00aa9564;
|
||||
constexpr uint32_t kItemTypeCount = 0x7e60; // types 1..0x7e5f (bounds check in 0x428700)
|
||||
|
||||
// ItemPotionKind (0x43f630): the potion kind (1..5, the bottom bar's potion
|
||||
// slots; 1 = healing) of item type `type`, 0 if it is no potion.
|
||||
@@ -46,6 +47,15 @@ inline bool ItemIsConsumable(uint32_t type) {
|
||||
return reinterpret_cast<bool(__stdcall*)(uint32_t)>(0x0043f5f0)(type);
|
||||
}
|
||||
|
||||
// 0x428700 (thiscall on the type table, (type), ret 4): the text id of the
|
||||
// item type's name (entry +0x20), which cItemBase::GetName (0x44a420) shows
|
||||
// unless the object has its own (+0x3c).
|
||||
inline uint32_t ItemNameText(uint32_t type) {
|
||||
using Fn = uint32_t(__fastcall*)(void* table, void* edx, uint32_t type);
|
||||
void* table = *reinterpret_cast<void**>(kItemTypeTable);
|
||||
return table ? reinterpret_cast<Fn>(0x00428700)(table, nullptr, type) : 0;
|
||||
}
|
||||
|
||||
inline uint32_t ItemType(const void* item) {
|
||||
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
|
||||
}
|
||||
|
||||
+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
|
||||
@@ -4,6 +4,7 @@
|
||||
// tab headers), and changes to the game's own texts.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace game {
|
||||
|
||||
@@ -15,4 +16,11 @@ uint32_t RegisterText(const wchar_t* text);
|
||||
// "Rucksack"). Call at startup.
|
||||
void CapitalizeText(uint32_t id);
|
||||
|
||||
// Shows `to` in place of `from` wherever a game text contains it (item names,
|
||||
// tooltips, ...).
|
||||
void ReplaceInTexts(const std::wstring& from, const std::wstring& to);
|
||||
|
||||
// The game's text `id` without our changes (empty if there is none).
|
||||
std::wstring GameText(uint32_t id);
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user