Shared chest tabs, rune stacks and rune master, Q pickup, Kettenblitz tooltip and range

- Shared chest: tabs "Truhe"/"Gemeinsam" as a column left of the grid
  (game tab sheet), grids placed every tick, persistent across heroes
- Inventory tab texts capitalized (RegisterText / CapitalizeText)
- Combat-art runes stack like potions (itemstack, renamed from potionstack)
- Rune master: any rune for any number of runes, price by rune count
  (4 runes = 1,000, x10 per missing rune), bought rune goes into the
  backpack, Buy disabled without runes, gold or room, last rune remembered
  per hero (saved), the chosen rune's name below its icon, dropping a
  stack gives up one rune
- Keys: the old key handling no longer takes over after the save screen
  (text field focus only counts while its dialog is shown)
- Esc crash: tooltips forgotten when the game destroys them
- Pick up (Q) sends the pickup order directly; search reach doubled
- Kettenblitz: tooltip shows hits per chain, chains, cast and jump range;
  cast range 900 (longer ray segments), jumps half of that

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-17 03:40:42 +02:00
1 parent 3d9d864c25
commit 6ca96d54b4
29 files changed
+2006 -171

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
#include "game/ui/texts.h"
#include <windows.h>
#include <cwchar>
#include <cwctype>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "re/hook.h"
namespace game {
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;
};
struct Capitalized {
std::wstring source;
std::wstring text;
GameString string;
};
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;
}
// By id; nodes stay put, so the strings handed out stay valid.
std::unordered_map<uint32_t, Capitalized> g_capitalized;
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;
}
} // 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);
}
// 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);
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 || !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()) {
entry.source.assign(string->begin, string->end);
entry.text = Capitalize(entry.source);
const wchar_t* text = entry.text.c_str();
entry.string = {text, text + entry.text.size(), text + entry.text.size()};
}
return &entry.string;
}
} // namespace game