Options: tabs Allgemein / Beute / Tastenbelegung with key bindings

- Options window in three tabs; the auto-pickup settings move to Beute;
  sliders move with their page; black sides like the save game screen.
- Tastenbelegung: every binding settable (click, press key; Esc cancels,
  Backspace unbinds), "Alle zurücksetzen" as a game button, saved on OK.
- Potions bindable (default unbound), named by the game's potion texts; the
  game's own potion hotkeys are off; bar tooltips show the bound key.
- Map (M), overview map (Tab, held) and labels (Alt, held) bindable; the
  game's own M / Tab / Alt handling is switched off.
- Save game key calls the game's save window code instead of a posted key.
- Teleportationstrank: fixed text replacement from startup (also the
  Ausdauertrank items, potion kind 5 as well).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-19 00:38:34 +02:00
1 parent 72525cd5ba
commit 838b1aa491
27 files changed
+1423 -184

No files matched your search

+22
View File
@@ -1,6 +1,7 @@
#include "game/engine/items.h"
#include "game/engine/stream.h"
#include "game/ui/texts.h"
namespace game {
namespace {
@@ -48,4 +49,25 @@ uint32_t CloneItem(uint32_t id) {
return item ? CreateItem(ItemType(item), SerializeItem(item), item[kItemOwner]) : 0;
}
std::wstring PotionKindName(int kind) {
// Shown as "\dFFFFFF80Heiltrank \wFFFFFFFF[SPC]" (the resource's
// <color=..> tags become a backslash, a letter and 8 hex digits): without
// the colour codes and the key.
constexpr size_t kColourCodeLength = 10;
const std::wstring text = ShownText(PotionTooltipText(kind));
std::wstring name;
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == L'\\') {
i += kColourCodeLength - 1;
} else if (text[i] == L'[') {
break;
} else {
name += text[i];
}
}
const size_t first = name.find_first_not_of(L' ');
const size_t last = name.find_last_not_of(L' ');
return first == std::wstring::npos ? std::wstring() : name.substr(first, last - first + 1);
}
} // namespace game
+20
View File
@@ -128,3 +128,23 @@ uint32_t CreateItem(uint32_t type, const std::vector<uint8_t>& data, uint32_t ow
uint32_t CloneItem(uint32_t id);
} // namespace game
#include <string>
namespace game {
// 0x43fa90 (ret 4; ecx unused, it asks the text manager): the text id of the
// bottom bar's tooltip for potion kind `kind` (1..kPotionKinds), by text key
// (0x94a8d0 ...), e.g. "<color=FFFFFF80>Heiltrank <colorw=FFFFFFFF>[SPC]";
// 0 for other kinds. Kinds: Heiltrank, Sog der Hölle, Trank des Mentors,
// Antidot der Viper, Trank der Konzentration (Teleportationstrank here).
inline uint32_t PotionTooltipText(int kind) {
using Fn = uint32_t(__fastcall*)(void*, void*, int);
return reinterpret_cast<Fn>(0x0043fa90)(*reinterpret_cast<void**>(kItemTypeTable), nullptr, kind);
}
// The name of potion kind `kind` as the bar's tooltip shows it (with our text
// changes), without its colour codes and key.
std::wstring PotionKindName(int kind);
} // namespace game
+5 -3
View File
@@ -73,11 +73,13 @@ inline void* GetPlayerHero() {
}
// cKernel::PostEvent (0x808930) on Kernel_Instance() (0x808490): the kernel
// copies the event and dispatches it (key events right away).
inline void PostEvent(void* event) {
// copies the event and dispatches it (key events right away). `a`: 1 as the
// window procedure posts key events, 0 as the UI manager's S hotkey posts its
// engine event.
inline void PostEvent(void* event, int a = 1) {
using InstanceFn = void*(__cdecl*)();
using PostFn = void(__fastcall*)(void* kernel, void* edx, void* event, int a, int b);
reinterpret_cast<PostFn>(0x00808930)(reinterpret_cast<InstanceFn>(0x00808490)(), nullptr, event, 1, 0);
reinterpret_cast<PostFn>(0x00808930)(reinterpret_cast<InstanceFn>(0x00808490)(), nullptr, event, a, 0);
}
} // namespace game
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// Untextured quads on the UI's Direct3D 7 device (the argument of the UI's
// render methods, vf14).
#include <cstdint>
#include <windows.h>
namespace game {
constexpr int kDeviceDrawPrimitive = 25; // IDirect3DDevice7 vtable slots
constexpr int kDeviceSetTexture = 35;
constexpr DWORD kTriangleStrip = 5;
constexpr DWORD kFvfScreenColourTexture = 0x1c4; // XYZRHW | DIFFUSE | SPECULAR | TEX1
struct ScreenVertex {
float x, y, z, rhw;
uint32_t diffuse, specular;
float u, v;
};
// Fills (x0, y0)-(x1, y1) with the ARGB colour `color`.
inline void FillRect(void* device, float x0, float y0, float x1, float y1, uint32_t color) {
void** vtable = *static_cast<void***>(device);
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
ScreenVertex quad[4] = {{x0, y0, 0, 1, color, 0, 0, 0},
{x1, y0, 0, 1, color, 0, 1, 0},
{x0, y1, 0, 1, color, 0, 0, 1},
{x1, y1, 0, 1, color, 0, 1, 1}};
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip, kFvfScreenColourTexture,
quad, 4, 0);
}
} // namespace game
+7
View File
@@ -57,6 +57,13 @@ inline uint8_t* TabControl(void* sheet) {
return *reinterpret_cast<uint8_t**>(static_cast<uint8_t*>(sheet) + kTabSheetControl);
}
constexpr size_t kTabControlActive = 0x84; // u16
inline uint16_t ActiveTab(void* sheet) {
const uint8_t* control = TabControl(sheet);
return control ? *reinterpret_cast<const uint16_t*>(control + kTabControlActive) : 0;
}
// The tab entries of a cUI_TabControl.
inline uint8_t* TabEntries(uint8_t* control) {
return *reinterpret_cast<uint8_t**>(control + kTabControlTabs);
+3
View File
@@ -3,6 +3,9 @@
#include <cstdint>
// First, so its DrawText macro renames game::DrawText the same way everywhere.
#include <windows.h>
namespace game {
// Screen rect as the UI uses it (cUI_Control2::GetScreenRect 0x731c70,
+33
View File
@@ -4,6 +4,7 @@
#include <cwchar>
#include <cwctype>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
@@ -57,6 +58,15 @@ std::vector<std::pair<std::wstring, std::wstring>>& Replacements() {
// By id; nodes stay put, so the strings handed out stay valid.
std::unordered_map<uint32_t, Shown> g_shown;
// SetTextOverride: by id. A replaced text is kept (the game may still hold
// the string it got); overrides change rarely (key bindings).
struct Override {
std::wstring text;
GameString string;
};
std::unordered_map<uint32_t, std::unique_ptr<Override>> g_overrides;
std::vector<std::unique_ptr<Override>> g_retiredOverrides;
std::wstring Capitalize(std::wstring text) {
bool wordStart = true;
for (wchar_t& c : text) {
@@ -126,6 +136,26 @@ std::wstring GameText(uint32_t id) {
: std::wstring();
}
std::wstring ShownText(uint32_t id) {
std::lock_guard guard(g_lock);
return Transform(id, GameText(id));
}
void SetTextOverride(uint32_t id, const std::wstring& text) {
std::lock_guard guard(g_lock);
std::unique_ptr<Override>& entry = g_overrides[id];
if (entry && entry->text == text) {
return;
}
if (entry) {
g_retiredOverrides.push_back(std::move(entry));
}
entry = std::make_unique<Override>();
entry->text = text;
const wchar_t* begin = entry->text.c_str();
entry->string = {begin, begin + entry->text.size(), begin + entry->text.size()};
}
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*);
@@ -159,6 +189,9 @@ 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 (const auto found = g_overrides.find(id); found != g_overrides.end()) {
return &found->second->string;
}
if (!original || (Replacements().empty() && !CapitalizedIds().count(id))) {
return original;
}
+7
View File
@@ -23,6 +23,13 @@ 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);
// Text `id` as the game shows it, with our changes (without overrides).
std::wstring ShownText(uint32_t id);
// Shows `text` for the game's text `id` from now on (e.g. a tooltip naming a
// key binding); setting the same text again costs nothing.
void SetTextOverride(uint32_t id, const std::wstring& text);
// The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
// "UI_ATTRIB_STRENGTH"; empty if there is none.
std::wstring GameTextByKey(const char* key);
+51
View File
@@ -0,0 +1,51 @@
#pragma once
// Opening and closing the UI manager's windows by name, as the game's own
// hotkeys and buttons do.
#include <cstddef>
#include <cstdint>
namespace game {
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*
inline uint32_t* UiManager() {
return *reinterpret_cast<uint32_t**>(kUiManager);
}
// cEventUI2 window commands (code at +4, subtype 5 at +8, names at +0x28): the
// UI manager's event handler (vf00, 0x757120) routes them by window name.
// Measured: HandleEsc hides with 0x12 (docs/escape.md); the S hotkey
// (0x757775) shows the save game window with 0x11. (Sent to the window's own
// handler a command was not understood the same way: the trader changed its
// stock instead of closing.)
enum class WindowCommand : uint32_t { Show = 0x11, Hide = 0x12 };
inline void SendWindowCommand(uint32_t* manager, const char* name, WindowCommand command) {
constexpr uint32_t kEventUiVtable = 0x0088edd0;
constexpr uint32_t kWindowSubtype = 5;
constexpr size_t kEventUiSize = 0x80;
// Names, written by 0x5904e0 (sender at +0, window at +0x20 of this).
constexpr size_t kEventUiNames = 0x28;
constexpr size_t kEventUiText = 0x68; // a string, 0x423920 / 0x416660
alignas(4) uint8_t event[kEventUiSize] = {};
*reinterpret_cast<uint32_t*>(event) = kEventUiVtable;
*reinterpret_cast<uint32_t*>(event + 4) = static_cast<uint32_t>(command);
*reinterpret_cast<uint32_t*>(event + 8) = kWindowSubtype;
using SetNamesFn = void(__fastcall*)(void*, void*, const char* window, const char* sender);
reinterpret_cast<SetNamesFn>(0x005904e0)(event + kEventUiNames, nullptr, name, "");
using StringFn = void(__fastcall*)(void*, void*);
reinterpret_cast<StringFn>(0x00423920)(event + kEventUiText, nullptr); // construct
using HandleFn = int(__fastcall*)(void*, void*, void*);
reinterpret_cast<HandleFn>((*reinterpret_cast<void***>(manager))[0])(manager, nullptr, event);
reinterpret_cast<StringFn>(0x00416660)(event + kEventUiText, nullptr); // destroy
}
// cUI_Manager 0x75a690 (thiscall (1), ret 4): false while a window that
// blocks others is open (the S hotkey asks it first).
inline bool UiCanOpenWindow(uint32_t* manager) {
using Fn = bool(__fastcall*)(void*, void*, int);
return reinterpret_cast<Fn>(0x0075a690)(manager, nullptr, 1);
}
} // namespace game