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
+1422 -183

No files matched your search

+1
View File
@@ -68,6 +68,7 @@ add_library(sacred_re SHARED
src/mods/teleportpotion.cpp src/mods/teleportpotion.cpp
src/mods/guards.cpp src/mods/guards.cpp
src/mods/charexport.cpp src/mods/charexport.cpp
src/mods/options.cpp
) )
target_include_directories(sacred_re PRIVATE src) target_include_directories(sacred_re PRIVATE src)
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
+111
View File
@@ -0,0 +1,111 @@
# Options window: tabs and key bindings
Request (2026-10-18): the settings screen gets three tabs: *Allgemein*,
*Beute* (loot filter; first iteration: the game's loot options moved there
from the common page) and *Tastenbelegung* (key bindings, so far only
settable in `sacred_re.ini`). Code: `src/mods/options.cpp`; bindings in
`src/mods/input.cpp`.
## The window (cUI_Options)
- vtable `0x89505c`, constructor `0x716570` (name, rect, parent, flags;
`ret 0x18`), destructor `0x7165f0`; controls built by `0x7184e0`.
- vf44 `0x716640` (renderer, ?, ?; `ret 0xc`): window render (`0x727040`),
then drives the sliders while the mouse is held. vf10 `0x716b30`: events.
- `0x717ea0` loads the settings into the controls when the window is shown;
`0x717460` (thiscall, plain `ret`) writes them back — called for OK.
- Controls: vector at `+0x90` (begin) / `+0x94` (end); the 16 background tiles
are in the window's own list `+0x78`. Buttons: `+0x158` Cancel, `+0x15c` OK.
Pickup: radio buttons `+0x16c/+0x170/+0x174` (`PICKUPAUTO` 0/1/2),
checkbox `+0x178` (`PICKUPANIM`).
- Layout (1024x768): frames at x `0x40` / `0x220`, y `0x40` / `0x120` /
`0x200`, borders `0x14` outside, labels above (y `0x20` / `0x100` /
`0x1e0`). Left: graphics, auto-pickup (*AUTOCOLLECT*), general; right: sound
volume, 3D sound, internet. OK / Cancel at y `0x284`.
- cUI_Frame (vtable `0x8955ac`): its render `0x73a540` does not test the
visible flag, only `+0xdc` (set to 1 by its constructors).
## How the mod works
- After the constructor: every control in `+0x90` is put on a page by its
position — left column middle row → *Beute*, everything else → *Allgemein*,
OK / Cancel on all pages. The auto-pickup group moves up one row (to the top
of its page), the general group below it moves up into its place.
- A cUI_TabSheet (`game/ui/tabs.h`) at the top frames' border; its headers
(default row layout) sit above the frame labels.
- Each frame (vf44) the controls not on the active page are hidden (visible
flag, frames also `+0xdc`); controls hidden by the mod are shown again on
their page. The game re-shows controls when the window opens, so this runs
every frame.
- *Tastenbelegung* is drawn by the mod: rows per binding, grouped
(Kampfkünste, Waffensets, Bewegung, Aktionen), a key box per row. A click on
a box captures the next key press (`input::SetKeyCapture`, swallowed before
the game sees it): Esc cancels, Backspace unbinds, a bindable key is taken
(and removed from any other action). The page works on a copy loaded when
the window opens; OK (`0x717460`) saves it to `[keys]` in `sacred_re.ini`,
Cancel drops it.
- Movement keys (W/A/S/D) are bindings now (`MoveUp/Left/Down/Right`);
`mods/wasd.cpp` polls the bound keys.
## Follow-ups (2026-10-18)
- Pages moved down 24 px (`kContentShift`) for room below the tab headers.
- *Alle zurücksetzen* (left of OK / Cancel) sets every binding back to its
default; saved with OK like any other change.
- Potion bindings *Trank 1–5* and *Trank 1 für Gefährten* (default: no key).
They call `cEngine::UseActionSlot` (`0x613040`, thiscall (kind, on ally),
`ret 8`) on the engine tick. The game's own potion hotkeys are off: its
engine hotkey switch (`cEngine::ReceiveEvent`, index bytes `0x619adc` per
upper-case character − 0x20, targets `0x619ac0`) had Space → kind 1 (Shift:
5), B → kind 1 on the weakest ally, Q 2, W 3, E 4, R 5; those six entries
now point at index 6 (no hotkey). `ActionSlot3` (W's action on G) is gone.
- The window is drawn like the save game / export screens: black beside the
1024 frame (`hud::RenderFramed`).
- Key bindings page: a backdrop (`0x724ed0`) and a frame (`0x739f90`) per
column, created with the window's controls and shown only on that page.
(Drawing the window's existing ones at other rects did not fit: a frame's
border pieces keep the place they were made at.)
- Pages move down 24 px under the tab headers. A slider (cUI_Scrollbar,
vtable `0x895664`) moves with its parts: minus, plus, track and knob are
embedded controls (+0x94 / +0x110 / +0x18c / +0x208) with rects in window
coordinates, and the knob is placed from +0x78 / +0x7c each frame.
- Key page layout (2026-10-19): the two frames span the other pages' frame
area (top row's top to the bottom of *Internet-Optionen*); the hints sit
in the bottom row next to *Alle zurücksetzen*, since they hold for the
whole page.
- Potion kinds are named by the bottom bar's tooltip texts:
`PotionTooltipText` `0x43fa90` (kind) → text id by key, e.g.
`<color=FFFFFF80>Heiltrank <colorw=FFFFFFFF>[SPC]` (kinds 1–5: Heiltrank,
Sog der Hölle, Trank des Mentors, Antidot der Viper, Trank der
Konzentration / Teleportationstrank; read in `scripts/de/SRglbl.res`).
Shown, the tags are colour codes (backslash, letter, 8 hex digits:
`\dFFFFFF80Heiltrank \wFFFFFFFF[SPC]`); `game::PotionKindName` strips them and the key; the tooltips get the bound key
in the brackets, or none (`game::SetTextOverride`, mods/potioncounts.cpp).
- No posted game keys any more: *Spiel speichern* calls what the UI manager's
S case (`0x757775`) does: save window built (manager slot `0x2b`),
`CanOpenWindow` `0x75a690`, engine event (vtable `0x88eb38`, 0x13, 5, 1),
then window command *show* (0x11) for `UI_WND_SAVEGAME`
(`game/ui/windows.h`, shared with escape.cpp's *hide* 0x12).
## Karte, Minikarte, Beschriftungen (2026-10-19)
Request: M (map), Tab (minimap) and Alt (text overlay) bindable too.
- M: the UI manager's letter switch (`cUI_Manager::ReceiveEvent` 0x757120,
upper-case character − 'C', index bytes 0x75868c, targets 0x758660) shows
`UI_WND_MEGAMAP` (slot 0x25) unless a blocking window is open; while the map
is open its own branch (cmp 'M' at 0x757f86) hides it. Switched off (M →
index 10, the compare → -1); the *Karte* binding does the same checks and
sends the window command.
- Tab: the in-game window procedure 0x8110d0 shows `UI_WND_OVERVIEWMAP` on
key down (switch on vk − 9: index bytes 0x812124, case 0x81146d) and hides
it on key up (0x812270, case 0x8117f8). Switched off (Tab → the "other key"
indices 4 / 3); *Minikarte (halten)* shows / hides it on the bound key's
down / up.
- Alt: no message, the game polls `GetAsyncKeyState(VK_MENU)` for its name
overlay in seven places (DrawObjectLabels 0x627260, PickObjectAt 0x626d30,
cEngine 0x60f1d0 / 0x611f10 / 0x617360, cCreature 0x599880, the map's debug
teleport 0x6c7ec0). Their `call [GetAsyncKeyState]` read our pointer
instead: `LabelsKeyState` returns the bound key's state.
- Alt and F10 come as WM_SYSKEYDOWN: the key capture takes those too.
- Key page: 20 lines per column (line 25 px) to fit the three rows.
+7 -4
View File
@@ -19,10 +19,13 @@ one of them.
## Mod (src/mods/teleportpotion.cpp) ## Mod (src/mods/teleportpotion.cpp)
- Every item type of potion kind 5 counts. Their names (text 0x428700 of each - Every item type of potion kind 5 counts. Their name, "Trank der
type) are replaced in every game text (`game::ReplaceInTexts`): the item Konzentration", is replaced in every game text from the DLL's start
names and the bottom bar's quick-select tooltip ("Trank der Konzentration (`game::ReplaceInTexts`, applied where the game fetches a text): the item
[R]", its own text id). The shortest name is replaced, so stage words stay. names (stage words in front stay) and the bottom bar's quick-select tooltip
("Trank der Konzentration [R]", its own text id). Before (until
2026-10-19) the names were collected from the item types at game start, so
the main menu (options' key bindings) still had the old name.
- cCreature::UseItem (hooked in itemstack.cpp) asks `teleportpotion::UseItem` - cCreature::UseItem (hooked in itemstack.cpp) asks `teleportpotion::UseItem`
first: for the hero's Teleportationstrank it posts the teleporter's event first: for the hero's Teleportationstrank it posts the teleporter's event
with the hero's mask instead of the drink. with the hero's mask instead of the drink.
+1 -1
View File
@@ -118,7 +118,7 @@ in-game texts/tooltips that name the old keys):
| Key | Action | Temporary key (sacred_re.ini) | | Key | Action | Temporary key (sacred_re.ini) |
|---|---|---| |---|---|---|
| W | action slot 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | G (`[keys] ActionSlot3`) | | W | potion kind 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | game potion keys switched off; `[keys] Potion1-5` (docs/options.md) |
| A | pick up items nearby (creature event 0x113) | Q (`[keys] PickUp`, 2026-10-17; was V) | | A | pick up items nearby (creature event 0x113) | Q (`[keys] PickUp`, 2026-10-17; was V) |
| S | save game window (`UI_WND_SAVEGAME`) | K (`[keys] SaveGame`) | | S | save game window (`UI_WND_SAVEGAME`) | K (`[keys] SaveGame`) |
| D | unbound | - | | D | unbound | - |
+22
View File
@@ -1,6 +1,7 @@
#include "game/engine/items.h" #include "game/engine/items.h"
#include "game/engine/stream.h" #include "game/engine/stream.h"
#include "game/ui/texts.h"
namespace game { namespace game {
namespace { namespace {
@@ -48,4 +49,25 @@ uint32_t CloneItem(uint32_t id) {
return item ? CreateItem(ItemType(item), SerializeItem(item), item[kItemOwner]) : 0; 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 } // 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); uint32_t CloneItem(uint32_t id);
} // namespace game } // 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 // cKernel::PostEvent (0x808930) on Kernel_Instance() (0x808490): the kernel
// copies the event and dispatches it (key events right away). // copies the event and dispatches it (key events right away). `a`: 1 as the
inline void PostEvent(void* event) { // 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 InstanceFn = void*(__cdecl*)();
using PostFn = void(__fastcall*)(void* kernel, void* edx, void* event, int a, int b); 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 } // 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); 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. // The tab entries of a cUI_TabControl.
inline uint8_t* TabEntries(uint8_t* control) { inline uint8_t* TabEntries(uint8_t* control) {
return *reinterpret_cast<uint8_t**>(control + kTabControlTabs); return *reinterpret_cast<uint8_t**>(control + kTabControlTabs);
+3
View File
@@ -3,6 +3,9 @@
#include <cstdint> #include <cstdint>
// First, so its DrawText macro renames game::DrawText the same way everywhere.
#include <windows.h>
namespace game { namespace game {
// Screen rect as the UI uses it (cUI_Control2::GetScreenRect 0x731c70, // Screen rect as the UI uses it (cUI_Control2::GetScreenRect 0x731c70,
+33
View File
@@ -4,6 +4,7 @@
#include <cwchar> #include <cwchar>
#include <cwctype> #include <cwctype>
#include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map> #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. // By id; nodes stay put, so the strings handed out stay valid.
std::unordered_map<uint32_t, Shown> g_shown; 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) { std::wstring Capitalize(std::wstring text) {
bool wordStart = true; bool wordStart = true;
for (wchar_t& c : text) { for (wchar_t& c : text) {
@@ -126,6 +136,26 @@ std::wstring GameText(uint32_t id) {
: std::wstring(); : 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) { std::wstring GameTextByKey(const char* key) {
// The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does. // The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does.
using HashFn = uint32_t(__cdecl*)(const char*); 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); const void* original = TextManager_GetText_original(self, edx, id);
std::lock_guard guard(g_lock); 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))) { if (!original || (Replacements().empty() && !CapitalizedIds().count(id))) {
return original; 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). // The game's text `id` without our changes (empty if there is none).
std::wstring GameText(uint32_t id); 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. // The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
// "UI_ATTRIB_STRENGTH"; empty if there is none. // "UI_ATTRIB_STRENGTH"; empty if there is none.
std::wstring GameTextByKey(const char* key); 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
+2 -30
View File
@@ -13,6 +13,7 @@
#include <cstring> #include <cstring>
#include "game/ui/control.h" #include "game/ui/control.h"
#include "game/ui/windows.h"
#include "mods/hud.h" #include "mods/hud.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
@@ -54,39 +55,10 @@ void CloseDialog(uint8_t* tooltip) {
} }
constexpr size_t kControlFlags = 0x10; // bit 0: visible constexpr size_t kControlFlags = 0x10; // bit 0: visible
// cEventUI2 as HandleEsc builds it (event at [esp+10h] there).
constexpr uint32_t kEventUiVtable = 0x0088edd0;
constexpr uint32_t kEventUiWindowCommand = 0x12;
constexpr uint32_t kWindowCommandHide = 5;
constexpr size_t kEventUiSize = 0x80;
// Names, written by 0x5904e0 (sender at +0, window at +0x20 of this): measured
// in the UI commands the game sends when a window is closed with its button or
// hotkey (window name at +0x48).
constexpr size_t kEventUiNames = 0x28;
constexpr size_t kEventUiText = 0x68; // a string, 0x423920 / 0x416660
bool Visible(const uint8_t* window) { bool Visible(const uint8_t* window) {
return window && (window[kControlFlags] & 1) != 0; return window && (window[kControlFlags] & 1) != 0;
} }
// The window command goes to the UI manager's event handler (vf00,
// 0x757120), which routes it by name, as HandleEsc does for the inventory.
// (Sent to the window's own handler it was not understood the same way: the
// trader changed its stock instead of closing.)
void Hide(uint32_t* manager, const char* name) {
alignas(4) uint8_t event[kEventUiSize] = {};
*reinterpret_cast<uint32_t*>(event) = kEventUiVtable;
*reinterpret_cast<uint32_t*>(event + 4) = kEventUiWindowCommand;
*reinterpret_cast<uint32_t*>(event + 8) = kWindowCommandHide;
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
}
} // namespace } // namespace
// True if Esc closed windows (the game's handling is skipped). // True if Esc closed windows (the game's handling is skipped).
@@ -108,7 +80,7 @@ bool CloseWindows(uint32_t* manager) {
for (const ClosableWindow& w : kClosable) { for (const ClosableWindow& w : kClosable) {
auto* window = reinterpret_cast<uint8_t*>(manager[w.slot]); auto* window = reinterpret_cast<uint8_t*>(manager[w.slot]);
if (Visible(window)) { if (Visible(window)) {
Hide(manager, w.name); game::SendWindowCommand(manager, w.name, game::WindowCommand::Hide);
closed = true; closed = true;
} }
} }
+8 -2
View File
@@ -694,8 +694,7 @@ HUD_EVENT(0x006f8960, UiNetworkInfo_HandleEvent, Anchor::Center)
HUD_RENDER(0x006f73f0, UiNetworkInfo_Render, Anchor::Center) HUD_RENDER(0x006f73f0, UiNetworkInfo_Render, Anchor::Center)
HUD_EVENT(0x0070e860, UiCharacter_HandleEvent, Anchor::Center) HUD_EVENT(0x0070e860, UiCharacter_HandleEvent, Anchor::Center)
HUD_RENDER3_FRAMED(0x0070f160, UiCharacter_Render) // export screen in-game HUD_RENDER3_FRAMED(0x0070f160, UiCharacter_Render) // export screen in-game
HUD_EVENT(0x00716b30, UiOptions_HandleEvent, Anchor::Center) // cUI_Options (0x716b30 / 0x716640): hooked by mods/options.cpp.
HUD_RENDER3(0x00716640, UiOptions_Render, Anchor::Center)
HUD_EVENT(0x0071d130, UiSavegame_HandleEvent, Anchor::Center) HUD_EVENT(0x0071d130, UiSavegame_HandleEvent, Anchor::Center)
HUD_RENDER_FRAMED(0x0071dd80, UiSavegame_Render) HUD_RENDER_FRAMED(0x0071dd80, UiSavegame_Render)
HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center) HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center)
@@ -778,6 +777,13 @@ int RenderCentred(void* window, int (*body)(void* context), void* context) {
return body(context); return body(context);
} }
int RenderFramed(void* window, void* device, int (*body)(void* context), void* context) {
const int dx = OffsetFor(Anchor::Center);
FrameScope frame(window, device, dx);
DrawOffset offset(window, dx);
return body(context);
}
int HandleCentred(void* event, int (*body)(void* context), void* context) { int HandleCentred(void* event, int (*body)(void* context), void* context) {
EventOffset offset(event, OffsetFor(Anchor::Center)); EventOffset offset(event, OffsetFor(Anchor::Center));
return body(context); return body(context);
+3
View File
@@ -26,5 +26,8 @@ int VisibleDialogTooltips(void** out, int max);
// shifted) or event handler (mouse position shifted by the window's offset). // shifted) or event handler (mouse position shifted by the window's offset).
int RenderCentred(void* window, int (*body)(void* context), void* context); int RenderCentred(void* window, int (*body)(void* context), void* context);
int HandleCentred(void* event, int (*body)(void* context), void* context); int HandleCentred(void* event, int (*body)(void* context), void* context);
// RenderCentred for full-screen windows: black beside the 1024 frame, drawn
// with a viewport on it (as the save game and export screens).
int RenderFramed(void* window, void* device, int (*body)(void* context), void* context);
} // namespace mods::hud } // namespace mods::hud
+383 -69
View File
@@ -6,22 +6,30 @@
// 0x6e1970). This mod reads the raw key messages instead (virtual keys, // 0x6e1970). This mod reads the raw key messages instead (virtual keys,
// independent of the keyboard layout): a bound key's messages are swallowed // independent of the keyboard layout): a bound key's messages are swallowed
// and its action runs. Skill keys cast their slot (src/mods/skillkeys.cpp), // and its action runs. Skill keys cast their slot (src/mods/skillkeys.cpp),
// weapon set keys select it on the taskbar; keys standing in for an original // weapon set keys select it on the taskbar; the other actions call the game
// hotkey post that hotkey's key event to the game's event kernel, the way the window // function its hotkey ran. The game's own hotkeys for bound actions are
// procedure does (the kernel dispatches it right away, on the window thread). // switched off (potions) or kept from it (the keys are swallowed).
// //
// While a text field has the keyboard, keys pass through unchanged. (Keys // While a text field has the keyboard, keys pass through unchanged. (Keys
// with Ctrl / Alt held used to pass too; GetKeyState reports Alt as held after // with Ctrl / Alt held used to pass too; GetKeyState reports Alt as held after
// Alt+Tab until it is pressed again in the game, which switched the bindings // Alt+Tab until it is pressed again in the game, which switched the bindings
// off. Alt+key arrives as WM_SYSKEYDOWN and is not handled here anyway.) // off. Alt+key arrives as WM_SYSKEYDOWN and is not handled here anyway.)
// //
// sacred_re.ini (key names: A-Z, 0-9, F1-F24, Space, None): // The bindings are set on the options window's key bindings tab
// (mods/options.cpp) and kept in sacred_re.ini (key names: kKeyNames below,
// None):
// [keys] // [keys]
// SkillSlot0=0 ... SkillSlot6=6 ; cast combat-art slot (0 = right mouse button art) // SkillSlot0=0 ... SkillSlot6=6 ; cast combat-art slot (0 = right mouse button art)
// WeaponSet1=F1 ... WeaponSet7=F7 ; select weapon set // WeaponSet1=F1 ... WeaponSet7=F7 ; select weapon set
// ActionSlot3=G ; what W did (W is movement) // MoveUp=W, MoveLeft=A, MoveDown=S, MoveRight=D ; direction movement (mods/wasd.cpp polls them)
// Potion1=None ... Potion5=None ; drink a potion of kind 1-5
// AllyPotion=None ; kind 1 on the weakest ally (the game's own potion keys
// ; Space/B/Q/W/E/R are switched off)
// PickUp=Q ; what A did (pick up items nearby; Q's own action slot is given up) // PickUp=Q ; what A did (pick up items nearby; Q's own action slot is given up)
// SaveGame=K ; what S did (save game window) // SaveGame=K ; what S did (save game window)
// Map=M ; open / close the map (the game's M is switched off)
// OverviewMap=Tab ; the overview map while held (the game's Tab is switched off)
// Labels=Alt ; names of items and creatures while held (the game's Alt is switched off)
#include "mods/input.h" #include "mods/input.h"
@@ -29,14 +37,16 @@
#include <cctype> #include <cctype>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <iterator>
#include <string> #include <string>
#include "game/engine/events.h" #include "game/engine/events.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/engine/items.h"
#include "game/engine/orders.h" #include "game/engine/orders.h"
#include "game/ui/windows.h"
#include "mods/skillbar.h" #include "mods/skillbar.h"
#include "mods/skillkeys.h" #include "mods/skillkeys.h"
#include "mods/wasd.h"
#include "re/config.h" #include "re/config.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
@@ -45,69 +55,109 @@
namespace mods::input { namespace mods::input {
namespace { namespace {
// --- sacred.exe ---------------------------------------------------------------
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*
constexpr uint32_t kKeyModifiers = 0x0182cdec; // u16, as the in-game window procedure keeps it
constexpr uint32_t kEventKeyVtable = 0x00895568;
// cEvent_key: +4 1 = pressed, 0 = released; +6 / +8 the key (character when
// pressed, virtual key when released); +0xa modifiers.
struct KeyEvent {
uint32_t vtable;
uint16_t pressed;
uint16_t raw;
uint16_t key;
uint16_t modifiers;
};
static_assert(sizeof(KeyEvent) == 0xc);
// Posts the key event the window procedure would post for `key` (an original
// hotkey letter or digit).
void PostGameKey(char key, bool pressed) {
KeyEvent event = {};
event.vtable = kEventKeyVtable;
event.pressed = pressed ? 1 : 0;
// Pressed: the character (lower case, as without Shift); released: the
// virtual key (upper case).
const auto code = static_cast<uint16_t>(pressed ? tolower(key) : toupper(key));
event.raw = code;
event.key = code;
event.modifiers = *reinterpret_cast<uint16_t*>(kKeyModifiers);
game::PostEvent(&event);
}
// --- bindings ------------------------------------------------------------------ // --- bindings ------------------------------------------------------------------
enum class Kind { SkillSlot, WeaponSet, GameKey, PickUp }; // Move: a held key polled by mods/wasd.cpp; its messages are only swallowed.
// Labels: a held key the game polls (LabelsKeyState); its messages are only
// swallowed.
enum class Kind { SkillSlot, WeaponSet, Move, Potion, PickUp, SaveGame, Map, OverviewMap, Labels };
// Potion index: the potion kind, with this flag on the weakest ally.
constexpr int kAllyPotion = 0x100;
struct Binding { struct Binding {
const char* name; const char* name;
const char* fallback; const char* fallback;
const wchar_t* label;
Kind kind; Kind kind;
int index; // slot / set (0-based) int index; // slot / set (0-based), Move direction
char gameKey; // GameKey: the original hotkey
int vk = 0; int vk = 0;
}; };
// Display order (the key bindings page shows them in groups by kind).
Binding g_bindings[] = { Binding g_bindings[] = {
{"SkillSlot0", "0", Kind::SkillSlot, 0, 0}, {"SkillSlot0", "0", L"Kampfkunst 0 (Rechtsklick)", Kind::SkillSlot, 0},
{"SkillSlot1", "1", Kind::SkillSlot, 1, 0}, {"SkillSlot1", "1", L"Kampfkunst 1", Kind::SkillSlot, 1},
{"SkillSlot2", "2", Kind::SkillSlot, 2, 0}, {"SkillSlot2", "2", L"Kampfkunst 2", Kind::SkillSlot, 2},
{"SkillSlot3", "3", Kind::SkillSlot, 3, 0}, {"SkillSlot3", "3", L"Kampfkunst 3", Kind::SkillSlot, 3},
{"SkillSlot4", "4", Kind::SkillSlot, 4, 0}, {"SkillSlot4", "4", L"Kampfkunst 4", Kind::SkillSlot, 4},
{"SkillSlot5", "5", Kind::SkillSlot, 5, 0}, {"SkillSlot5", "5", L"Kampfkunst 5", Kind::SkillSlot, 5},
{"SkillSlot6", "6", Kind::SkillSlot, 6, 0}, {"SkillSlot6", "6", L"Kampfkunst 6", Kind::SkillSlot, 6},
{"WeaponSet1", "F1", Kind::WeaponSet, 0, 0}, {"WeaponSet1", "F1", L"Waffenset 1", Kind::WeaponSet, 0},
{"WeaponSet2", "F2", Kind::WeaponSet, 1, 0}, {"WeaponSet2", "F2", L"Waffenset 2", Kind::WeaponSet, 1},
{"WeaponSet3", "F3", Kind::WeaponSet, 2, 0}, {"WeaponSet3", "F3", L"Waffenset 3", Kind::WeaponSet, 2},
{"WeaponSet4", "F4", Kind::WeaponSet, 3, 0}, {"WeaponSet4", "F4", L"Waffenset 4", Kind::WeaponSet, 3},
{"WeaponSet5", "F5", Kind::WeaponSet, 4, 0}, {"WeaponSet5", "F5", L"Waffenset 5", Kind::WeaponSet, 4},
{"WeaponSet6", "F6", Kind::WeaponSet, 5, 0}, {"WeaponSet6", "F6", L"Waffenset 6", Kind::WeaponSet, 5},
{"WeaponSet7", "F7", Kind::WeaponSet, 6, 0}, {"WeaponSet7", "F7", L"Waffenset 7", Kind::WeaponSet, 6},
{"ActionSlot3", "G", Kind::GameKey, 0, 'W'}, {"MoveUp", "W", L"Nach oben", Kind::Move, static_cast<int>(Move::Up)},
{"PickUp", "Q", Kind::PickUp, 0, 0}, {"MoveLeft", "A", L"Nach links", Kind::Move, static_cast<int>(Move::Left)},
{"SaveGame", "K", Kind::GameKey, 0, 'S'}, {"MoveDown", "S", L"Nach unten", Kind::Move, static_cast<int>(Move::Down)},
{"MoveRight", "D", L"Nach rechts", Kind::Move, static_cast<int>(Move::Right)},
{"Potion1", "None", L"Trank 1", Kind::Potion, 1},
{"Potion2", "None", L"Trank 2", Kind::Potion, 2},
{"Potion3", "None", L"Trank 3", Kind::Potion, 3},
{"Potion4", "None", L"Trank 4", Kind::Potion, 4},
{"Potion5", "None", L"Trank 5", Kind::Potion, 5},
{"AllyPotion", "None", L"Trank 1", Kind::Potion, 1 | kAllyPotion},
{"PickUp", "Q", L"Gegenstände aufheben", Kind::PickUp, 0},
{"SaveGame", "K", L"Spiel speichern", Kind::SaveGame, 0},
{"Map", "M", L"Karte", Kind::Map, 0},
{"OverviewMap", "Tab", L"Minikarte (halten)", Kind::OverviewMap, 0},
{"Labels", "Alt", L"Beschriftungen (halten)", Kind::Labels, 0},
}; };
constexpr int kBindingCount = static_cast<int>(std::size(g_bindings));
// Keys that can be bound besides A-Z, 0-9 and F1-F24 (named by their letter /
// number): ini name, shown name. Esc and Backspace stay free for the key
// bindings page (cancel / unbind), Enter for the game's chat.
struct NamedKey {
int vk;
const char* ini;
const wchar_t* label;
};
constexpr NamedKey kKeyNames[] = {
{VK_SPACE, "Space", L"Leertaste"}, {VK_TAB, "Tab", L"Tab"},
{VK_MENU, "Alt", L"Alt"},
{VK_INSERT, "Insert", L"Einfg"}, {VK_DELETE, "Delete", L"Entf"},
{VK_HOME, "Home", L"Pos1"}, {VK_END, "End", L"Ende"},
{VK_PRIOR, "PageUp", L"Bild auf"}, {VK_NEXT, "PageDown", L"Bild ab"},
{VK_UP, "Up", L"Pfeil hoch"}, {VK_DOWN, "Down", L"Pfeil runter"},
{VK_LEFT, "Left", L"Pfeil links"}, {VK_RIGHT, "Right", L"Pfeil rechts"},
{VK_NUMPAD0, "Num0", L"Num 0"}, {VK_NUMPAD1, "Num1", L"Num 1"},
{VK_NUMPAD2, "Num2", L"Num 2"}, {VK_NUMPAD3, "Num3", L"Num 3"},
{VK_NUMPAD4, "Num4", L"Num 4"}, {VK_NUMPAD5, "Num5", L"Num 5"},
{VK_NUMPAD6, "Num6", L"Num 6"}, {VK_NUMPAD7, "Num7", L"Num 7"},
{VK_NUMPAD8, "Num8", L"Num 8"}, {VK_NUMPAD9, "Num9", L"Num 9"},
};
bool LetterOrDigit(int vk) {
return (vk >= 'A' && vk <= 'Z') || (vk >= '0' && vk <= '9');
}
bool FunctionKey(int vk) {
return vk >= VK_F1 && vk <= VK_F24;
}
const NamedKey* NamedKeyFor(int vk) {
for (const NamedKey& key : kKeyNames) {
if (key.vk == vk) {
return &key;
}
}
return nullptr;
}
// The ini name of a bindable key ("None" for 0).
std::string IniName(int vk) {
if (LetterOrDigit(vk)) {
return std::string(1, static_cast<char>(vk));
}
if (FunctionKey(vk)) {
return "F" + std::to_string(vk - VK_F1 + 1);
}
const NamedKey* key = NamedKeyFor(vk);
return key ? key->ini : "None";
}
int ParseKey(const std::string& text) { int ParseKey(const std::string& text) {
if (text.size() == 1 && isalnum(static_cast<unsigned char>(text[0]))) { if (text.size() == 1 && isalnum(static_cast<unsigned char>(text[0]))) {
@@ -119,8 +169,10 @@ int ParseKey(const std::string& text) {
return VK_F1 + n - 1; return VK_F1 + n - 1;
} }
} }
if (_stricmp(text.c_str(), "Space") == 0) { for (const NamedKey& key : kKeyNames) {
return VK_SPACE; if (_stricmp(text.c_str(), key.ini) == 0) {
return key.vk;
}
} }
return 0; // None / unknown return 0; // None / unknown
} }
@@ -145,11 +197,14 @@ const Binding* BindingFor(int vk) {
} }
// Keys taken away from the game although nothing is bound to them: the digit // Keys taken away from the game although nothing is bound to them: the digit
// row (the game's slot keys) and the movement keys. // row (the game's slot keys).
bool Reserved(int vk) { bool Reserved(int vk) {
return (vk >= '0' && vk <= '9') || mods::wasd::IsMovementKey(vk); return vk >= '0' && vk <= '9';
} }
// The key bindings page's key capture.
void (*g_capture)(int vk) = nullptr;
// Pick up items nearby: the order the engine's A hotkey sends (creature order // Pick up items nearby: the order the engine's A hotkey sends (creature order
// 0x113, cEngine::ReceiveEvent 0x618c41; the hero collects, mode +0xfc 0xc), // 0x113, cEngine::ReceiveEvent 0x618c41; the hero collects, mode +0xfc 0xc),
// given to the controlled creature on the next engine tick. (Posting the A // given to the controlled creature on the next engine tick. (Posting the A
@@ -163,8 +218,30 @@ constexpr uint32_t kCollectRingLimit = 0x004ec8c6;
constexpr uint8_t kCollectRingsOriginal = 0xec; // -20 constexpr uint8_t kCollectRingsOriginal = 0xec; // -20
constexpr uint8_t kCollectRings = 0xd8; // -40 constexpr uint8_t kCollectRings = 0xd8; // -40
// Drink a potion: cEngine::UseActionSlot (0x613040, thiscall (potion kind,
// on an ally), ret 8), as the engine's potion hotkeys call it, on the next
// engine tick. Those hotkeys (cEngine::ReceiveEvent, a jump table on the
// upper-case character - 0x20: index bytes at 0x619adc, targets at 0x619ac0)
// are switched off: Space kind 1 (Shift: 5), B kind 1 on the weakest ally,
// Q 2, W 3, E 4, R 5 now go to index 6, the table's "no hotkey".
constexpr uint32_t kHotkeyIndices = 0x00619adc;
constexpr uint8_t kNoHotkey = 6;
constexpr re::BytePatch kPotionHotkeys[] = {
{kHotkeyIndices + ' ' - 0x20, 0, 0}, {kHotkeyIndices + 'B' - 0x20, 0, 1},
{kHotkeyIndices + 'E' - 0x20, 0, 2}, {kHotkeyIndices + 'Q' - 0x20, 0, 3},
{kHotkeyIndices + 'R' - 0x20, 0, 4}, {kHotkeyIndices + 'W' - 0x20, 0, 5},
};
std::atomic<int> g_potion{0};
void UseActionSlot(uint8_t* engine, int kind, bool ally) {
using UseFn = int(__fastcall*)(void*, void*, int, int);
reinterpret_cast<UseFn>(0x00613040)(engine, nullptr, kind, ally ? 1 : 0);
}
void OnTick(uint8_t* engine) { void OnTick(uint8_t* engine) {
if (const int potion = g_potion.exchange(0)) {
UseActionSlot(engine, potion & 0xff, (potion & kAllyPotion) != 0);
}
if (!g_pickUp.exchange(false)) { if (!g_pickUp.exchange(false)) {
return; return;
} }
@@ -175,6 +252,107 @@ void OnTick(uint8_t* engine) {
} }
} }
// The save game window, as the UI manager's S hotkey opens it (0x757775):
// only with the window built (manager slot 0x2b) and no blocking window open;
// it posts an engine event (code 0x13, 5, 1; with 0 for the kernel's second
// argument) and shows UI_WND_SAVEGAME.
constexpr int kSaveGameSlot = 0x2b;
constexpr uint32_t kEngineEventVtable = 0x0088eb38;
constexpr uint32_t kEngineEventSaveGame = 0x13;
void OpenSaveGame() {
uint32_t* ui = game::UiManager();
if (!ui || !ui[kSaveGameSlot] || !game::UiCanOpenWindow(ui)) {
return;
}
uint32_t event[9] = {kEngineEventVtable, kEngineEventSaveGame, 5, 1};
game::PostEvent(event, 0);
game::SendWindowCommand(ui, "UI_WND_SAVEGAME", game::WindowCommand::Show);
}
// The map (cUI_Megamap, manager slot 0x25), as the UI manager's M hotkey
// toggles it (cUI_Manager::ReceiveEvent 0x757120): open, M closes it; else
// nothing while one of the windows it checks first is open (slots 0x2b save
// game, 0x2c, 0x2f options, or the flag 0x17e82c4); it opens unless the flag
// 0x17e8264 is set or slot 0x36 is open. That hotkey is switched off: its
// letter switch (upper-case character - 'C': index bytes at 0x75868c, targets
// at 0x758660) sends M to index 10, "no hotkey", and the closing branch
// compares with -1 instead of 'M' (cmp eax, 4Dh at 0x757f86).
constexpr int kMegamapSlot = 0x25;
constexpr int kMapBlockingSlots[] = {0x2b, 0x2c, 0x2f};
constexpr uint32_t kMapBlockedFlag = 0x017e82c4;
constexpr uint32_t kMapDisabledFlag = 0x017e8264;
constexpr int kMapDisabledSlot = 0x36;
constexpr uint32_t kUiLetterIndices = 0x0075868c;
constexpr uint8_t kUiLetterMap = 5;
constexpr uint8_t kUiNoHotkey = 10;
constexpr uint32_t kUiMapCloseCompare = 0x00757f86;
constexpr size_t kControlFlagsOffset = 0x10; // bit 0: visible
bool WindowShown(const uint32_t* ui, int slot) {
const auto* window = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(ui[slot]));
return window && (*reinterpret_cast<const uint32_t*>(window + kControlFlagsOffset) & 1) != 0;
}
bool Flag(uint32_t address) {
return *reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(address)) != 0;
}
void ToggleMap() {
uint32_t* ui = game::UiManager();
if (!ui || !ui[kMegamapSlot]) {
return;
}
if (WindowShown(ui, kMegamapSlot)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Hide);
return;
}
for (const int slot : kMapBlockingSlots) {
if (WindowShown(ui, slot)) {
return;
}
}
if (!Flag(kMapBlockedFlag) && !Flag(kMapDisabledFlag) && !WindowShown(ui, kMapDisabledSlot)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Show);
}
}
// The overview map (cUI_OverviewMap) while the key is held, as the game's
// window procedure (0x8110d0) shows it on Tab down (0x81146d) and hides it on
// Tab up (0x8117f8). Both are switched off: their switches on the virtual key
// - 9 (down: index bytes at 0x812124, targets at 0x812110; up: 0x812270 /
// 0x812260) send Tab to their "other key" index.
constexpr uint32_t kKeyDownIndices = 0x00812124;
constexpr uint32_t kKeyUpIndices = 0x00812270;
constexpr uint8_t kKeyDownOther = 4;
constexpr uint8_t kKeyUpOther = 3;
void ShowOverviewMap(bool show) {
if (uint32_t* ui = game::UiManager()) {
game::SendWindowCommand(ui, "UI_WND_OVERVIEWMAP",
show ? game::WindowCommand::Show : game::WindowCommand::Hide);
}
}
// Labels: the game asks GetAsyncKeyState(VK_MENU) for its name overlay
// (cWorldView0::DrawObjectLabels 0x627260 and PickObjectAt 0x626d30, cEngine
// 0x60f1d0 / 0x611f10 / 0x617360, cCreature 0x599880; and the map's debug
// teleport, cUI_Megamap 0x6c7ec0); those calls (call [import]) ask
// LabelsKeyState instead.
constexpr uint32_t kGetAsyncKeyStateImport = 0x0088e2b0;
constexpr uint32_t kLabelsKeyCalls[] = {0x00599ebf, 0x0060f227, 0x00612015, 0x006176ec,
0x00626ffe, 0x00627299, 0x006c80b0};
SHORT WINAPI LabelsKeyState(int) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Labels) {
return binding.vk ? GetAsyncKeyState(binding.vk) : 0;
}
}
return 0;
}
SHORT(WINAPI* const g_labelsKeyState)(int) = &LabelsKeyState;
struct Registration { struct Registration {
Registration() { Registration() {
game::OnEngineTick(&OnTick); game::OnEngineTick(&OnTick);
@@ -190,12 +368,24 @@ void Press(const Binding& binding, int vk) {
case Kind::WeaponSet: case Kind::WeaponSet:
mods::skillbar::SelectWeaponSet(slot); mods::skillbar::SelectWeaponSet(slot);
break; break;
case Kind::GameKey: case Kind::SaveGame:
PostGameKey(binding.gameKey, true); OpenSaveGame();
break; break;
case Kind::PickUp: case Kind::PickUp:
g_pickUp = true; g_pickUp = true;
break; break;
case Kind::Potion:
g_potion = binding.index;
break;
case Kind::Map:
ToggleMap();
break;
case Kind::OverviewMap:
ShowOverviewMap(true);
break;
case Kind::Move:
case Kind::Labels:
break;
} }
} }
@@ -204,11 +394,16 @@ void Release(const Binding& binding, int vk) {
case Kind::SkillSlot: case Kind::SkillSlot:
mods::skillkeys::KeyReleased(vk); mods::skillkeys::KeyReleased(vk);
break; break;
case Kind::WeaponSet: case Kind::OverviewMap:
case Kind::PickUp: ShowOverviewMap(false);
break; break;
case Kind::GameKey: case Kind::WeaponSet:
PostGameKey(binding.gameKey, false); case Kind::Move:
case Kind::Potion:
case Kind::PickUp:
case Kind::SaveGame:
case Kind::Map:
case Kind::Labels:
break; break;
} }
} }
@@ -222,6 +417,17 @@ bool g_swallowChar = false;
void Startup() { void Startup() {
LoadBindings(); LoadBindings();
if (re::PatchAll(kPotionHotkeys, kNoHotkey) != 0) {
re::Log("input: potion hotkey patch FAILED");
}
if (!re::PatchInstructionByte(kUiLetterIndices + 'M' - 'C', 0, kUiLetterMap, kUiNoHotkey) ||
!re::PatchInstructionByte(kUiMapCloseCompare, 2, 'M', 0xff) ||
!re::PatchInstructionByte(kKeyDownIndices + VK_TAB - 9, 0, 0, kKeyDownOther) ||
!re::PatchInstructionByte(kKeyUpIndices + VK_TAB - 9, 0, 0, kKeyUpOther) ||
re::PatchAll(kLabelsKeyCalls, kGetAsyncKeyStateImport,
static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&g_labelsKeyState))) != 0) {
re::Log("input: map / overview map / labels key patch FAILED");
}
if (!re::PatchInstructionByte(kCollectRingLimit, 2, kCollectRingsOriginal, kCollectRings)) { if (!re::PatchInstructionByte(kCollectRingLimit, 2, kCollectRingsOriginal, kCollectRings)) {
re::Log("input: pickup range patch FAILED"); re::Log("input: pickup range patch FAILED");
} }
@@ -252,7 +458,7 @@ uint32_t ControlFlags(const uint8_t* control) {
} }
uint8_t* FocusedTextField() { uint8_t* FocusedTextField() {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager); auto* ui = reinterpret_cast<uint8_t*>(game::UiManager());
void* focus = ui ? *reinterpret_cast<void**>(ui + kUiFocus) : nullptr; void* focus = ui ? *reinterpret_cast<void**>(ui + kUiFocus) : nullptr;
if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) { if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) {
return nullptr; return nullptr;
@@ -269,7 +475,7 @@ uint8_t* FocusedTextField() {
// Only the focused control counts: a window draws several edit controls per // Only the focused control counts: a window draws several edit controls per
// frame (measured: the save window), the last one drawn is not the focus. // frame (measured: the save window), the last one drawn is not the focus.
void NoteEditDrawn(void* edit) { void NoteEditDrawn(void* edit) {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager); auto* ui = reinterpret_cast<uint8_t*>(game::UiManager());
if (ui && edit == *reinterpret_cast<void**>(ui + kUiFocus) && if (ui && edit == *reinterpret_cast<void**>(ui + kUiFocus) &&
(ControlFlags(static_cast<uint8_t*>(edit)) & kControlVisible) != 0) { (ControlFlags(static_cast<uint8_t*>(edit)) & kControlVisible) != 0) {
g_drawnEdit = edit; g_drawnEdit = edit;
@@ -281,11 +487,119 @@ bool TypingIntoTextField() {
return FocusedTextField() != nullptr; return FocusedTextField() != nullptr;
} }
int MovementKey(Move direction) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Move && binding.index == static_cast<int>(direction)) {
return binding.vk;
}
}
return 0;
}
int BindingCount() {
return kBindingCount;
}
std::wstring BindingLabel(int index) {
const Binding& binding = g_bindings[index];
if (binding.kind != Kind::Potion) {
return binding.label;
}
// The potion kind's name as the game shows it ("Trank N" until known).
std::wstring name = game::PotionKindName(binding.index & 0xff);
if (name.empty()) {
name = binding.label;
}
return (binding.index & kAllyPotion) ? name + L" für Gefährten" : name;
}
int PotionKey(int kind) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Potion && binding.index == kind) {
return binding.vk;
}
}
return 0;
}
const wchar_t* BindingGroup(int index) {
switch (g_bindings[index].kind) {
case Kind::SkillSlot:
return L"Kampfkünste";
case Kind::WeaponSet:
return L"Waffensets";
case Kind::Move:
return L"Bewegung";
case Kind::Potion:
return L"Tränke";
case Kind::PickUp:
case Kind::SaveGame:
case Kind::Map:
case Kind::OverviewMap:
case Kind::Labels:
break;
}
return L"Aktionen";
}
int BindingKey(int index) {
return g_bindings[index].vk;
}
int DefaultBindingKey(int index) {
return ParseKey(g_bindings[index].fallback);
}
void SetBindingKeys(const int* keys) {
for (int i = 0; i < kBindingCount; ++i) {
Binding& binding = g_bindings[i];
binding.vk = Bindable(keys[i]) ? keys[i] : 0;
re::SetConfigString("keys", binding.name, IniName(binding.vk).c_str());
}
re::Log("input: key bindings saved");
}
bool Bindable(int vk) {
return LetterOrDigit(vk) || FunctionKey(vk) || NamedKeyFor(vk);
}
std::wstring KeyLabel(int vk) {
if (LetterOrDigit(vk)) {
return std::wstring(1, static_cast<wchar_t>(vk));
}
if (FunctionKey(vk)) {
return L"F" + std::to_wstring(vk - VK_F1 + 1);
}
const NamedKey* key = NamedKeyFor(vk);
return key ? key->label : L"";
}
void SetKeyCapture(void (*handler)(int vk)) {
g_capture = handler;
}
bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) { bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) { switch (message) {
case WM_SYSKEYDOWN:
// Alt and F10 arrive as system keys: only the key capture takes them.
if (g_capture) {
if ((lParam & (1 << 30)) == 0) {
g_capture(static_cast<int>(wParam & 0xff));
}
return true;
}
return false;
case WM_KEYDOWN: { case WM_KEYDOWN: {
const int vk = static_cast<int>(wParam & 0xff); const int vk = static_cast<int>(wParam & 0xff);
const bool repeat = (lParam & (1 << 30)) != 0; const bool repeat = (lParam & (1 << 30)) != 0;
if (g_capture) {
g_taken[vk] = true;
g_swallowChar = true;
if (!repeat) {
g_capture(vk);
}
return true;
}
if (!repeat) { if (!repeat) {
g_taken[vk] = false; g_taken[vk] = false;
if (FocusedTextField()) { if (FocusedTextField()) {
+30
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <string>
#include <windows.h> #include <windows.h>
namespace mods::input { namespace mods::input {
@@ -14,4 +16,32 @@ void NoteEditDrawn(void* edit);
// handled and must not reach the game. // handled and must not reach the game.
bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam); bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
// The movement keys (polled by mods/wasd.cpp); 0 when unbound.
enum class Move { Up, Left, Down, Right };
int MovementKey(Move direction);
// The key bound to drinking potion kind `kind` (1..5); 0 when unbound.
int PotionKey(int kind);
// --- key bindings page (mods/options.cpp) --------------------------------------
// The bindable actions in display order, grouped (a group's actions follow
// each other).
int BindingCount();
std::wstring BindingLabel(int index); // potions: the game's name of the kind
const wchar_t* BindingGroup(int index);
int BindingKey(int index); // virtual key, 0 = none
int DefaultBindingKey(int index);
// Sets all bindings (`keys[BindingCount()]`) and writes them to sacred_re.ini.
void SetBindingKeys(const int* keys);
// Whether `vk` can be bound, and its name as shown ("Leertaste", "F1", ...).
bool Bindable(int vk);
std::wstring KeyLabel(int vk);
// While set, every key press goes to `handler` (on the window thread) instead
// of the game or the bindings; its key messages are swallowed.
void SetKeyCapture(void (*handler)(int vk));
} // namespace mods::input } // namespace mods::input
+616
View File
@@ -0,0 +1,616 @@
// Options window in three tabs: Allgemein, Beute, Tastenbelegung (request of
// 2026-10-18). Beute holds the game's auto-pickup settings (first iteration:
// moved there from the common page); Tastenbelegung sets the key bindings of
// mods/input.cpp, which had only been settable in sacred_re.ini.
//
// The window (cUI_Options, vtable 0x89505c, constructor 0x716570 building its
// controls in 0x7184e0) is laid out for 1024x768: six frames in two columns
// (frame x 0x40 / 0x220, rows y 0x40 / 0x120 / 0x200, a frame label above
// each), OK / Cancel at the bottom right. All its controls are in the vector
// at +0x90. A cUI_TabSheet of the game's (the inventory's header look) above
// the frames switches between pages: each control belongs to the page of the
// frame it sits in (by position), hidden controls draw nothing and take no
// events (frames have their own draw flag, see kFrameDrawn). The auto-pickup
// frame (left column, middle row) moves to the top row of the Beute page, the
// general frame below it moves up into its place.
//
// The key bindings page is drawn here: a row per binding with its key in a
// box; a click on the box waits for a key (Esc: cancel, Backspace: no key),
// a key bound to another action is taken from it. OK saves the bindings
// (cUI_Options apply 0x717460), Cancel drops the changes. See docs/options.md.
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
#include <windows.h>
#include "game/ui/control.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/tabs.h"
#include "game/ui/text.h"
#include "game/ui/texts.h"
#include "mods/hud.h"
#include "mods/input.h"
#include "re/hook.h"
#include "re/log.h"
namespace mods::options {
namespace {
// --- cUI_Options -----------------------------------------------------------------
constexpr size_t kWindowControls = 0x90; // vector of its controls: begin, +4 end
constexpr size_t kCancelButton = 0x158;
constexpr size_t kOkButton = 0x15c;
// cUI_Frame (vtable 0x8955ac): its render (0x73a540) doesn't test the visible
// flag but +0xdc (set to 1 by its constructors, read nowhere else).
constexpr uint32_t kFrameVtable = 0x008955ac;
constexpr size_t kFrameDrawn = 0xdc;
// The key bindings page gets a translucent backdrop and a frame per column,
// made as 0x7184e0 makes the others: cUI_Window2 0x724ed0 (rect*, colour;
// ret 8) adds a backdrop (cUI_Static) to +0x90; frames are cUI_Frame
// (0xe0 bytes, constructor 0x739f90 (name, type 1, rect*, 1), ret 0x10)
// pushed onto +0x90 (vector push_back 0x6bcf50, (&pointer), ret 4).
// (Drawing the existing ones at other rects did not fit: a frame's border
// pieces keep the place they got when it was made.)
constexpr uint32_t kBackdropColour = 0x9f000000;
constexpr int kFrameBorder = 0x14; // backdrop beyond the frame on each side
constexpr size_t kFrameSize = 0xe0;
constexpr uint32_t kMainFrame = 1;
// Layout of 0x7184e0 (window coordinates): the frames' borders begin 0x14
// left of / above the frame positions, their labels at y 0x20 / 0x100 / 0x1e0.
constexpr int kColumnSplit = 0x200; // left column frames end at x 0x1d4
constexpr int kMiddleRowTop = 0xf8; // auto-pickup label y 0x100, frame from y 0x10c
constexpr int kBottomRowTop = 0x1d8; // general label y 0x1e0, frame from y 0x1ec
constexpr int kRowStep = 0x120 - 0x40; // middle row frame y - top row frame y
// The tab headers sit 0x14 + their height above the sheet (cUI_TabSheet::Layout
// 0x729210); with the sheet at the top frames' border they end at y 0x18,
// 8 pixels above the frame labels. The pages move down for some room.
constexpr game::UiRect kSheetRect = {0x2c, 0x2c, 0, 0};
constexpr int kContentShift = 24;
enum Page : uint16_t { kPageCommon, kPageLoot, kPageKeys };
enum class Group { Always, Common, Loot, Keys };
struct Control {
uint8_t* control;
Group group;
bool hiddenByUs = false;
};
struct Window {
uint8_t* window = nullptr;
void* sheet = nullptr;
uint8_t* resetButton = nullptr;
std::vector<Control> controls;
bool wasVisible = false;
};
std::vector<Window> g_windows;
Window* Find(void* window) {
for (Window& w : g_windows) {
if (w.window == window) {
return &w;
}
}
return nullptr;
}
int32_t& ControlX(uint8_t* control) {
return *reinterpret_cast<int32_t*>(control + game::kControlRect);
}
int32_t& ControlY(uint8_t* control) {
return *reinterpret_cast<int32_t*>(control + game::kControlRect + 4);
}
bool IsFrame(const uint8_t* control) {
return *reinterpret_cast<const uint32_t*>(control) == kFrameVtable;
}
// cUI_Scrollbar (vtable 0x895664, the volume and map transparency sliders):
// its parts are embedded controls with rects in the window's coordinates,
// placed once when it is made; its render (0x7527c0) puts the knob at
// +0x78 / +0x7c (track start) plus the value each frame.
constexpr uint32_t kScrollbarVtable = 0x00895664;
constexpr size_t kScrollbarParts[] = {0x94, 0x110, 0x18c, 0x208}; // minus, plus, track, knob
constexpr size_t kScrollbarKnobY = 0x7c;
void MoveDown(uint8_t* control, int dy) {
ControlY(control) += dy;
if (*reinterpret_cast<const uint32_t*>(control) == kScrollbarVtable) {
for (const size_t part : kScrollbarParts) {
ControlY(control + part) += dy;
}
*reinterpret_cast<int32_t*>(control + kScrollbarKnobY) += dy;
}
}
uint8_t* Field(uint8_t* window, size_t offset) {
return *reinterpret_cast<uint8_t**>(window + offset);
}
// Sorts the controls into pages, moves the auto-pickup and general frames.
void Classify(Window& w) {
auto** begin = *reinterpret_cast<uint8_t***>(w.window + kWindowControls);
auto** end = *reinterpret_cast<uint8_t***>(w.window + kWindowControls + 4);
const uint8_t* ok = Field(w.window, kOkButton);
const uint8_t* cancel = Field(w.window, kCancelButton);
for (auto** it = begin; it && it != end; ++it) {
uint8_t* control = *it;
if (!control) {
continue;
}
const int x = ControlX(control);
const int y = ControlY(control);
Group group = Group::Common;
if (control == ok || control == cancel) {
group = Group::Always;
} else if (x < kColumnSplit && y >= kMiddleRowTop && y < kBottomRowTop) {
group = Group::Loot;
MoveDown(control, -kRowStep);
} else if (x < kColumnSplit && y >= kBottomRowTop) {
MoveDown(control, -kRowStep);
}
if (group != Group::Always) {
MoveDown(control, kContentShift);
}
w.controls.push_back({control, group});
}
}
void CreateTabs(Window& w) {
static const uint32_t kNames[3] = {game::RegisterText(L"Allgemein"), game::RegisterText(L"Beute"),
game::RegisterText(L"Tastenbelegung")};
static const char* const kPageNames[3] = {"UI_TABSHEET_OPTIONS_COMMON", "UI_TABSHEET_OPTIONS_LOOT",
"UI_TABSHEET_OPTIONS_KEYS"};
w.sheet = game::NewTabSheet("UI_TABSHEET_OPTIONS", kSheetRect, w.window);
if (!w.sheet) {
return;
}
// Pages: empty controls; the window's own controls are shown and hidden.
for (int page = 0; page < 3; ++page) {
game::AddTab(w.sheet, kNames[page], game::NewTabSheet(kPageNames[page], {0, 0, 0, 0}, w.sheet));
}
game::SetActiveTab(w.sheet, kPageCommon);
game::AddWindowControl(w.window, w.sheet);
}
bool OnPage(Group group, uint16_t page) {
return group == Group::Always || (group == Group::Common && page == kPageCommon) ||
(group == Group::Loot && page == kPageLoot) || (group == Group::Keys && page == kPageKeys);
}
// Every frame: the game shows its controls again when the window opens.
void ShowPage(Window& w, uint16_t page) {
for (Control& c : w.controls) {
const bool show = OnPage(c.group, page);
if (!show && game::ControlShown(c.control)) {
game::SetControlShown(c.control, false);
c.hiddenByUs = true;
} else if (show && c.hiddenByUs) {
game::SetControlShown(c.control, true);
c.hiddenByUs = false;
}
if (IsFrame(c.control)) {
c.control[kFrameDrawn] = show ? 1 : 0;
}
}
}
// --- key bindings page ---------------------------------------------------------
constexpr int kMaxBindings = 64;
int g_pending[kMaxBindings] = {}; // the page's keys until OK
bool g_pendingLoaded = false;
std::atomic<int> g_capturing{-1}; // binding waiting for a key
int g_hover = -1; // binding whose box is under the mouse
constexpr uint16_t kFont = 1;
constexpr uint32_t kTextColour = 0xffe6d6aa; // as the tab headers' normal text (cUI_TabControl::Render)
constexpr uint32_t kHeadingColour = 0xfff0c060;
constexpr uint32_t kUnboundColour = 0xffb4b4b4;
constexpr uint32_t kBoxColour = 0xc0141414;
constexpr uint32_t kBoxHoverColour = 0xc0343434;
constexpr uint32_t kBoxCaptureColour = 0xc0604818;
// Two framed columns, groups kept together (window coordinates). The frames
// line up with the other pages': from the top row's frame top to the bottom
// of the lowest frame (Internet-Optionen, y 0x200, height 0x40).
constexpr int kFrameX[2] = {0x40, 0x220};
constexpr int kFrameWidth = 0x180;
constexpr int kFrameTop = 0x40 + kContentShift;
constexpr int kFrameHeight = 0x200 + 0x40 + kContentShift - kFrameTop;
constexpr int kTextInset = 0x10; // as the game's labels in its frames
constexpr int kLine = 25;
constexpr int kLinesPerColumn = 20;
constexpr int kBoxHeight = 22;
constexpr int kTop = kFrameTop + (kFrameHeight - (kLinesPerColumn * kLine - (kLine - kBoxHeight))) / 2;
constexpr int kBoxWidth = 0x80;
constexpr int kBoxX = kFrameWidth - 2 * kTextInset - kBoxWidth; // from the column
constexpr int kLabelWidth = kBoxX - 8;
// The bottom row: the hints (they hold for the whole page), then "Alle
// zurücksetzen", a button like Cancel (x 0x25c, y 0x284) and OK next to them.
// The buttons' ornaments reach kButtonOrnament beyond their rects (measured
// in a screenshot).
constexpr int kBottomY = 0x284;
constexpr int kBottomHeight = 37;
constexpr int kCancelX = 0x25c;
constexpr int kButtonOrnament = 0x21;
constexpr int kResetWidth = 140; // fits its caption
constexpr int kResetX = kCancelX - 2 * kButtonOrnament - 12 - kResetWidth;
// The hints box: from the frames backdrop edge, a little taller than the row.
constexpr int kHintX = 0x40 - kFrameBorder;
constexpr int kHintMargin = 4;
constexpr game::UiRect kHintRect = {kHintX, kBottomY - kHintMargin, kResetX - kButtonOrnament - 8 - kHintX,
kBottomHeight + 2 * kHintMargin};
const wchar_t* const kHints[2] = {L"Feld anklicken, dann Taste drücken.", L"Esc: abbrechen, Rücktaste: leeren."};
enum class LineKind { Heading, Binding };
struct Line {
LineKind kind;
int binding; // Binding: its index
std::wstring text;
int column;
int line;
};
std::vector<Line> Layout() {
std::vector<Line> lines;
const int count = std::min(input::BindingCount(), kMaxBindings);
int column = 0;
int line = 0;
for (int i = 0; i < count;) {
const wchar_t* group = input::BindingGroup(i);
int size = 0;
while (i + size < count && input::BindingGroup(i + size) == group) {
++size;
}
if (column == 0 && line > 0 && line + 1 + size > kLinesPerColumn) {
column = 1;
line = 0;
}
lines.push_back({LineKind::Heading, -1, group, column, line++});
for (int k = 0; k < size; ++k) {
lines.push_back({LineKind::Binding, i + k, input::BindingLabel(i + k), column, line++});
}
++line; // gap between groups
i += size;
}
return lines;
}
game::UiRect LineRect(void* window, const Line& line, int dx, int width) {
const game::UiRect origin = game::ControlScreenRect(window);
return {origin.x + kFrameX[line.column] + kTextInset + dx, origin.y + kTop + line.line * kLine,
static_cast<int16_t>(width), static_cast<int16_t>(kBoxHeight)};
}
game::UiRect BoxRect(void* window, const Line& line) {
return LineRect(window, line, kBoxX, kBoxWidth);
}
game::UiRect OnWindow(void* window, const game::UiRect& r) {
const game::UiRect origin = game::ControlScreenRect(window);
return {origin.x + r.x, origin.y + r.y, r.w, r.h};
}
bool Inside(const game::UiRect& r, int x, int y) {
return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
}
void FillBox(void* device, const game::UiRect& r, uint32_t colour) {
game::FillRect(device, static_cast<float>(r.x), static_cast<float>(r.y), static_cast<float>(r.x + r.w),
static_cast<float>(r.y + r.h), colour);
}
void StopCapture() {
g_capturing = -1;
input::SetKeyCapture(nullptr);
}
// The key pressed while a box waits (window thread).
void OnCapturedKey(int vk) {
const int row = g_capturing;
if (row < 0) {
StopCapture();
return;
}
if (vk == VK_ESCAPE) {
StopCapture();
} else if (vk == VK_BACK) {
g_pending[row] = 0;
StopCapture();
} else if (input::Bindable(vk)) {
for (int& key : g_pending) {
if (key == vk) {
key = 0;
}
}
g_pending[row] = vk;
StopCapture();
}
}
void LoadPending() {
const int count = std::min(input::BindingCount(), kMaxBindings);
for (int i = 0; i < count; ++i) {
g_pending[i] = input::BindingKey(i);
}
g_pendingLoaded = true;
}
void DrawKeysPage(const Window& w, void* device) {
void* window = w.window;
for (const Line& line : Layout()) {
if (line.kind != LineKind::Binding) {
const game::UiRect rect = LineRect(window, line, 0, kBoxX + kBoxWidth);
game::DrawText(device, kFont, game::CenterLineVertically(kFont, rect), line.text.c_str(), kHeadingColour,
0);
continue;
}
const game::UiRect label = LineRect(window, line, 0, kLabelWidth);
game::DrawText(device, kFont, game::CenterLineVertically(kFont, label), line.text.c_str(), kTextColour, 0);
const game::UiRect box = BoxRect(window, line);
const bool capturing = g_capturing == line.binding;
const uint32_t fill = capturing ? kBoxCaptureColour : g_hover == line.binding ? kBoxHoverColour : kBoxColour;
FillBox(device, box, fill);
const int vk = g_pending[line.binding];
const std::wstring key = capturing ? L"Taste drücken" : vk ? input::KeyLabel(vk) : L"–";
game::DrawText(device, kFont, game::CenterLineVertically(kFont, box), key.c_str(),
vk || capturing ? kTextColour : kUnboundColour, game::kTextCenter);
}
const game::UiRect hints = OnWindow(window, kHintRect);
FillBox(device, hints, kBoxColour);
const int lineHeight = game::FontLineHeight(kFont);
game::UiRect hint = {hints.x, hints.y + (hints.h - 2 * lineHeight) / 2, hints.w, static_cast<int16_t>(lineHeight)};
for (const wchar_t* text : kHints) {
game::DrawText(device, kFont, hint, text, kTextColour, game::kTextCenter);
hint.y += lineHeight;
}
}
// "Alle zurücksetzen": every binding back to its default (saved with OK).
void ResetPending() {
const int count = std::min(input::BindingCount(), kMaxBindings);
for (int i = 0; i < count; ++i) {
g_pending[i] = input::DefaultBindingKey(i);
}
}
// The binding whose box is at (x, y), -1 if none.
int BoxAt(void* window, int x, int y) {
for (const Line& line : Layout()) {
if (line.kind == LineKind::Binding && Inside(BoxRect(window, line), x, y)) {
return line.binding;
}
}
return -1;
}
// cUI_Button2 (vtable 0x895308, 0xbc bytes, constructor 0x72a180, plain
// ret). 0x7184e0 sets up Cancel and OK the same way: images (+0x54..+0x87),
// sounds (0x72acf0 -> +0x90..+0x9f), caption (0x72ad60 (text id, font 1) ->
// +0xa4 / +0xa0), caption offset (0x72ad40 -> +0xb0), name (0x732090).
constexpr size_t kButtonSize = 0xbc;
constexpr size_t kButtonLook = 0x54;
constexpr size_t kButtonLookEnd = 0x88;
constexpr size_t kButtonSounds = 0x90;
constexpr size_t kButtonSoundsEnd = 0xa0;
constexpr size_t kButtonCaptionOffset = 0xb0; // x, y (int16)
constexpr uint32_t kButtonFlags = 5; // 0x731e70, as 0x7184e0 passes
void PushControl(uint8_t* window, void* control) {
using PushFn = void(__fastcall*)(void*, void*, void**);
reinterpret_cast<PushFn>(0x006bcf50)(window + kWindowControls, nullptr, &control);
}
// "Alle zurücksetzen": a button with Cancel's look.
uint8_t* CreateResetButton(uint8_t* window) {
static const uint32_t kCaption = game::RegisterText(L"Alle zurücksetzen");
uint8_t* cancel = Field(window, kCancelButton);
void* memory = cancel ? game::GameNew(kButtonSize) : nullptr;
if (!memory) {
return nullptr;
}
using ConstructFn = uint8_t*(__fastcall*)(void*, void*);
uint8_t* button = reinterpret_cast<ConstructFn>(0x0072a180)(memory, nullptr);
using FlagsFn = void(__fastcall*)(void*, void*, uint32_t);
reinterpret_cast<FlagsFn>(0x00731e70)(button, nullptr, kButtonFlags);
std::copy(cancel + kButtonLook, cancel + kButtonLookEnd, button + kButtonLook);
std::copy(cancel + kButtonSounds, cancel + kButtonSoundsEnd, button + kButtonSounds);
std::copy(cancel + kButtonCaptionOffset, cancel + kButtonCaptionOffset + 4, button + kButtonCaptionOffset);
auto* rect = reinterpret_cast<game::UiRect*>(button + game::kControlRect);
*rect = *reinterpret_cast<const game::UiRect*>(cancel + game::kControlRect);
rect->x = kResetX;
rect->w = kResetWidth;
using CaptionFn = void(__fastcall*)(void*, void*, uint32_t, uint32_t);
reinterpret_cast<CaptionFn>(0x0072ad60)(button, nullptr, kCaption, 1);
using NameFn = void(__fastcall*)(void*, void*, const char*);
reinterpret_cast<NameFn>(0x00732090)(button, nullptr, "UI_BTN_KEYS_RESET");
PushControl(window, button);
return button;
}
// The key bindings page's own controls, shown only there: a backdrop and
// frame around each column, the reset button.
void CreateKeyControls(Window& w) {
static const char* const kFrameNames[2] = {"UI_FRAME_KEYS_LEFT", "UI_FRAME_KEYS_RIGHT"};
auto* controls = w.window + kWindowControls;
const auto existing = static_cast<size_t>(*reinterpret_cast<uint8_t***>(controls + 4) -
*reinterpret_cast<uint8_t***>(controls));
for (int column = 0; column < 2; ++column) {
game::UiRect frame = {kFrameX[column], kFrameTop, kFrameWidth, kFrameHeight};
game::UiRect backdrop = {frame.x - kFrameBorder, frame.y - kFrameBorder,
static_cast<int16_t>(frame.w + 2 * kFrameBorder),
static_cast<int16_t>(frame.h + 2 * kFrameBorder)};
using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
reinterpret_cast<AddBackdropFn>(0x00724ed0)(w.window, nullptr, &backdrop, kBackdropColour);
if (void* memory = game::GameNew(kFrameSize)) {
using FrameFn = void*(__fastcall*)(void*, void*, const char*, uint32_t, game::UiRect*, uint32_t);
PushControl(w.window, reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, kFrameNames[column],
kMainFrame, &frame, 1));
}
}
w.resetButton = CreateResetButton(w.window);
// Everything after the window's own controls is ours.
auto** begin = *reinterpret_cast<uint8_t***>(controls);
auto** end = *reinterpret_cast<uint8_t***>(controls + 4);
for (auto** it = begin + existing; it < end; ++it) {
if (*it) {
w.controls.push_back({*it, Group::Keys});
}
}
}
// --- hooks ---------------------------------------------------------------------
struct RenderCall {
uint8_t* window;
void* device;
int a;
int b;
int(__fastcall* original)(void*, void*, void*, int, int);
};
int RenderBody(void* context) {
auto* call = static_cast<RenderCall*>(context);
const int result = call->original(call->window, nullptr, call->device, call->a, call->b);
Window* w = Find(call->window);
if (!w || !w->sheet) {
return result;
}
const bool visible = game::ControlShown(call->window);
if (visible && !w->wasVisible) {
LoadPending();
}
w->wasVisible = visible;
const uint16_t page = game::ActiveTab(w->sheet);
if (!visible || page != kPageKeys) {
if (g_capturing >= 0) {
StopCapture();
}
g_hover = -1;
}
if (!visible) {
return result;
}
ShowPage(*w, page);
if (page == kPageKeys && g_pendingLoaded) {
DrawKeysPage(*w, call->device);
}
return result;
}
struct EventCall {
uint8_t* window;
void* event;
int(__fastcall* original)(void*, void*, void*);
};
int EventBody(void* context) {
auto* call = static_cast<EventCall*>(context);
Window* w = Find(call->window);
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
g_pendingLoaded) {
switch (game::MouseEventKind(call->event)) {
case game::MouseEvent::Move: {
const int x = game::EventField(call->event, 0xc);
const int y = game::EventField(call->event, 0x10);
g_hover = BoxAt(call->window, x, y);
break;
}
case game::MouseEvent::ButtonDown: {
const int x = game::EventField(call->event, game::kMouseButtonX);
const int y = game::EventField(call->event, game::kMouseButtonY);
const bool left = game::EventField(call->event, game::kMouseButton) == game::kLeftButton;
if (g_capturing >= 0) {
StopCapture();
}
if (left && w->resetButton && Inside(game::ControlScreenRect(w->resetButton), x, y)) {
ResetPending(); // the button still gets the click (pressed look, sound)
break;
}
const int box = BoxAt(call->window, x, y);
if (left && box >= 0) {
g_capturing = box;
input::SetKeyCapture(&OnCapturedKey);
return 1;
}
break;
}
default:
break;
}
}
return call->original(call->window, nullptr, call->event);
}
} // namespace
} // namespace mods::options
namespace game {
using namespace mods::options;
// cUI_Options::cUI_Options (0x716570, ret 0x18): (name, rect, parent, flags).
void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, UiRect rect, void* parent,
uint32_t flags);
RE_HOOK(0x00716570, UiOptions_Construct);
void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, UiRect rect, void* parent,
uint32_t flags) {
void* window = UiOptions_Construct_original(self, edx, name, rect, parent, flags);
if (window) {
g_windows.push_back({static_cast<uint8_t*>(window)});
Window& w = g_windows.back();
Classify(w);
CreateKeyControls(w);
CreateTabs(w);
re::Log("options: tabs created (%u controls)", static_cast<unsigned>(w.controls.size()));
}
return window;
}
// cUI_Options destructor (0x7165f0, thiscall, plain ret).
void __fastcall UiOptions_Destruct(void* self, void* edx);
RE_HOOK(0x007165f0, UiOptions_Destruct);
void __fastcall UiOptions_Destruct(void* self, void* edx) {
std::erase_if(g_windows, [self](const Window& w) { return w.window == self; });
UiOptions_Destruct_original(self, edx);
}
// cUI_Options::vf44 (0x716640, ret 0xc): renders the window (cUI_Window2
// render first), drives the sliders; black beside the frame like the
// save game window.
int __fastcall UiOptions_Render(void* self, void* edx, void* device, int a, int b);
RE_HOOK(0x00716640, UiOptions_Render);
int __fastcall UiOptions_Render(void* self, void*, void* device, int a, int b) {
RenderCall call{static_cast<uint8_t*>(self), device, a, b, UiOptions_Render_original};
return mods::hud::RenderFramed(self, device, &RenderBody, &call);
}
// cUI_Options::HandleEvent (0x716b30, ret 4); centred like the other windows.
int __fastcall UiOptions_HandleEvent(void* self, void* edx, void* event);
RE_HOOK(0x00716b30, UiOptions_HandleEvent);
int __fastcall UiOptions_HandleEvent(void* self, void*, void* event) {
EventCall call{static_cast<uint8_t*>(self), event, UiOptions_HandleEvent_original};
return mods::hud::HandleCentred(event, &EventBody, &call);
}
// cUI_Options apply (0x717460, thiscall, plain ret): OK writes the settings.
void __fastcall UiOptions_Apply(void* self, void* edx);
RE_HOOK(0x00717460, UiOptions_Apply);
void __fastcall UiOptions_Apply(void* self, void* edx) {
UiOptions_Apply_original(self, edx);
if (g_pendingLoaded) {
mods::input::SetBindingKeys(g_pending);
}
}
} // namespace game
+29
View File
@@ -9,13 +9,21 @@
// control behind it (+0x3ac). GetPotionRef walks the inventory grid and asks // control behind it (+0x3ac). GetPotionRef walks the inventory grid and asks
// ItemPotionKind for each item's kind; the count here does the same and adds // ItemPotionKind for each item's kind; the count here does the same and adds
// up the potion stacks (itemstack.cpp). // up the potion stacks (itemstack.cpp).
//
// Their tooltips (game::PotionTooltipText, set on the slots by the taskbar,
// 0x6e6060; shown as "\dFFFFFF80Heiltrank \wFFFFFFFF[SPC]") name the key
// bound to the kind (mods/input.cpp) instead of the game's own, which is
// switched off; no key part while unbound.
#include <cstdint> #include <cstdint>
#include <cwchar> #include <cwchar>
#include <string>
#include "game/engine/items.h" #include "game/engine/items.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/ui/text.h" #include "game/ui/text.h"
#include "game/ui/texts.h"
#include "mods/input.h"
#include "mods/itemstack.h" #include "mods/itemstack.h"
#include "mods/skillslots.h" #include "mods/skillslots.h"
#include "re/hook.h" #include "re/hook.h"
@@ -59,6 +67,26 @@ void RenderCounts(uint8_t* taskbar, void* renderer) {
} }
} }
// The game's tooltip with its "[key]" end replaced by the bound key's.
std::wstring TooltipWithKey(uint32_t text, int vk) {
std::wstring tooltip = game::ShownText(text);
if (const size_t key = tooltip.rfind(L'['); key != std::wstring::npos && tooltip.back() == L']') {
tooltip.erase(key);
}
if (vk) {
tooltip += L"[" + input::KeyLabel(vk) + L"]";
}
return tooltip;
}
void UpdateTooltips() {
for (int kind = 1; kind <= game::kPotionKinds; ++kind) {
if (const uint32_t text = game::PotionTooltipText(kind)) {
game::SetTextOverride(text, TooltipWithKey(text, input::PotionKey(kind)));
}
}
}
} // namespace } // namespace
} // namespace mods::potioncounts } // namespace mods::potioncounts
@@ -69,5 +97,6 @@ void __fastcall UiTaskbar_RenderPotionSlots(void* self, void* edx, void* rendere
UiTaskbar_RenderPotionSlots_original(self, edx, renderer); UiTaskbar_RenderPotionSlots_original(self, edx, renderer);
if (self) { if (self) {
mods::potioncounts::RenderCounts(static_cast<uint8_t*>(self), renderer); mods::potioncounts::RenderCounts(static_cast<uint8_t*>(self), renderer);
mods::potioncounts::UpdateTooltips();
} }
} }
+3 -25
View File
@@ -30,6 +30,7 @@
#include "game/engine/arts.h" #include "game/engine/arts.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/engine/skills.h" #include "game/engine/skills.h"
#include "game/ui/draw.h"
#include "game/ui/events.h" #include "game/ui/events.h"
#include "game/ui/text.h" #include "game/ui/text.h"
#include "mods/hud.h" #include "mods/hud.h"
@@ -182,37 +183,14 @@ void Revert(uint8_t* panel, uint8_t* arts, int row) {
re::Log("respec: skill %x removed, %u to choose", skill, *reinterpret_cast<uint16_t*>(arts + kSkillChoices)); re::Log("respec: skill %x removed, %u to choose", skill, *reinterpret_cast<uint16_t*>(arts + kSkillChoices));
} }
// IDirect3DDevice7 as the UI's render methods get it (their vf14 argument).
constexpr int kDeviceDrawPrimitive = 25;
constexpr int kDeviceSetTexture = 35;
constexpr DWORD kTriangleStrip = 5;
constexpr DWORD kFvfScreenColourTexture = 0x1c4; // XYZRHW | DIFFUSE | SPECULAR | TEX1
struct Vertex {
float x, y, z, rhw;
uint32_t diffuse, specular;
float u, v;
};
// Turns the + that `minus` drew into a −. // Turns the + that `minus` drew into a −.
void CoverArm(void* device, uint8_t* minus) { void CoverArm(void* device, uint8_t* minus) {
const game::UiRect rect = game::ControlScreenRect(minus); const game::UiRect rect = game::ControlScreenRect(minus);
const float sx = rect.w / kIconSize; const float sx = rect.w / kIconSize;
const float sy = rect.h / kIconSize; const float sy = rect.h / kIconSize;
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);
for (const Patch& patch : kArmPatches) { for (const Patch& patch : kArmPatches) {
const float x0 = rect.x + patch.x0 * sx; game::FillRect(device, rect.x + patch.x0 * sx, rect.y + patch.y0 * sy, rect.x + patch.x1 * sx,
const float x1 = rect.x + patch.x1 * sx; rect.y + patch.y1 * sy, kDiscGrey);
const float y0 = rect.y + patch.y0 * sy;
const float y1 = rect.y + patch.y1 * sy;
Vertex quad[4] = {{x0, y0, 0, 1, kDiscGrey, 0, 0, 0},
{x1, y0, 0, 1, kDiscGrey, 0, 1, 0},
{x0, y1, 0, 1, kDiscGrey, 0, 0, 1},
{x1, y1, 0, 1, kDiscGrey, 0, 1, 1}};
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip,
kFvfScreenColourTexture, quad, 4, 0);
} }
} }
+10 -28
View File
@@ -34,6 +34,7 @@
#include "mods/skillslots.h" #include "mods/skillslots.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
#include "re/patch.h"
namespace mods::teleportpotion { namespace mods::teleportpotion {
namespace { namespace {
@@ -109,36 +110,18 @@ void OnTick(uint8_t*) {
} }
} }
// The yellow potions' names become kName in every text (names, the bottom // Every kind 5 potion is a Teleportationstrank in every text, from the start:
// bar's tooltip): the shortest name, so stage words in front ("Großer ...") // the yellow ones ("Trank der Konzentration", also the bottom bar's tooltip)
// stay, and any name that does not contain it. // and the stamina ones, kind 5 as well ("Schwacher / Starker / Kompletter
void OnGameStart() { // Ausdauertrank", measured 2026-10-19); stage words in front stay.
static bool renamed = false; constexpr const wchar_t* kOriginalNames[] = {L"Trank der Konzentration", L"Ausdauertrank"};
if (renamed) {
return; void Startup() {
} for (const wchar_t* name : kOriginalNames) {
std::vector<std::wstring> names;
for (uint32_t type = 1; type < game::kItemTypeCount; ++type) {
if (IsTeleportPotion(type)) {
std::wstring name = game::GameText(game::ItemNameText(type));
if (!name.empty() && std::find(names.begin(), names.end(), name) == names.end()) {
names.push_back(std::move(name));
}
}
}
std::sort(names.begin(), names.end(), [](const auto& a, const auto& b) { return a.size() < b.size(); });
std::vector<std::wstring> replaced;
for (const std::wstring& name : names) {
const bool covered = std::any_of(replaced.begin(), replaced.end(),
[&](const std::wstring& r) { return name.find(r) != std::wstring::npos; });
if (!covered) {
game::ReplaceInTexts(name, kName); game::ReplaceInTexts(name, kName);
replaced.push_back(name);
} }
}
renamed = !names.empty();
re::Log("teleportpotion: %zu potion names, %zu replaced", names.size(), replaced.size());
} }
RE_STARTUP(TeleportPotionStartup, Startup);
void OnGameEnd() { void OnGameEnd() {
g_pending = 0; g_pending = 0;
@@ -147,7 +130,6 @@ void OnGameEnd() {
struct Registration { struct Registration {
Registration() { Registration() {
game::OnGameStart(&OnGameStart);
game::OnGameEnd(&OnGameEnd); game::OnGameEnd(&OnGameEnd);
game::OnEngineTick(&OnTick); game::OnEngineTick(&OnTick);
} }
+12 -13
View File
@@ -25,11 +25,10 @@
// Releasing the keys sends the target-less walk order a released mouse button // Releasing the keys sends the target-less walk order a released mouse button
// sends, which stops it in place. // sends, which stops it in place.
// //
// W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep // The movement keys are key bindings (default W/A/S/D, src/mods/input.cpp,
// W/A/S/D away from the game and move those actions to other keys. Typing into // which keeps them away from the game; the game's W/A/S actions moved to
// a text field (chat, console, save names) is left alone. See docs/wasd.md. // other keys). Typing into a text field (chat, console, save names) is left
// alone. See docs/wasd.md.
#include "mods/wasd.h"
#include <atomic> #include <atomic>
#include <cmath> #include <cmath>
@@ -70,8 +69,11 @@ constexpr double kSlideAngles[] = {30.0, 60.0, 90.0};
constexpr uint32_t kEngineMouseHeld = 0x10; constexpr uint32_t kEngineMouseHeld = 0x10;
constexpr uint32_t kEngineInputBlocked = 0x4000; constexpr uint32_t kEngineInputBlocked = 0x4000;
using mods::input::Move;
using mods::input::MovementKey;
bool KeyDown(int key) { bool KeyDown(int key) {
return (GetAsyncKeyState(key) & 0x8000) != 0; return key != 0 && (GetAsyncKeyState(key) & 0x8000) != 0;
} }
void SendWalk(uint8_t* engine, void* creature, int x, int y) { void SendWalk(uint8_t* engine, void* creature, int x, int y) {
@@ -110,10 +112,10 @@ bool HeldDirection(const uint8_t* engine, Direction& dir) {
int y = 0; int y = 0;
const uint32_t flags = *reinterpret_cast<const uint32_t*>(engine + kEngineFlags); const uint32_t flags = *reinterpret_cast<const uint32_t*>(engine + kEngineFlags);
if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) { if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown('W')) { x -= 1; y -= 1; } if (KeyDown(MovementKey(Move::Up))) { x -= 1; y -= 1; }
if (KeyDown('S')) { x += 1; y += 1; } if (KeyDown(MovementKey(Move::Down))) { x += 1; y += 1; }
if (KeyDown('A')) { x -= 1; y += 1; } if (KeyDown(MovementKey(Move::Left))) { x -= 1; y += 1; }
if (KeyDown('D')) { x += 1; y -= 1; } if (KeyDown(MovementKey(Move::Right))) { x += 1; y -= 1; }
} }
if (x == 0 && y == 0) { if (x == 0 && y == 0) {
return false; return false;
@@ -213,9 +215,6 @@ struct Registration {
} // namespace } // namespace
bool IsMovementKey(int vk) {
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
}
// Before each update of a creature: steer the controlled one. // Before each update of a creature: steer the controlled one.
void BeforeCreatureUpdate(uint8_t* creature) { void BeforeCreatureUpdate(uint8_t* creature) {
-8
View File
@@ -1,8 +0,0 @@
#pragma once
namespace mods::wasd {
// Whether `vk` is one of the movement keys W/A/S/D.
bool IsMovementKey(int vk);
} // namespace mods::wasd
+9
View File
@@ -47,4 +47,13 @@ std::string ConfigString(const char* section, const char* key, const char* fallb
return narrow; return narrow;
} }
void SetConfigString(const char* section, const char* key, const char* value) {
if (g_path.empty()) {
return;
}
wchar_t wide[256];
MultiByteToWideChar(CP_UTF8, 0, value, -1, wide, static_cast<int>(std::size(wide)));
WritePrivateProfileStringW(Widen(section).c_str(), Widen(key).c_str(), wide, g_path.c_str());
}
} // namespace re } // namespace re
+2
View File
@@ -11,5 +11,7 @@ const std::wstring& ConfigPath();
std::wstring ModuleDirectory(); std::wstring ModuleDirectory();
int ConfigInt(const char* section, const char* key, int fallback); int ConfigInt(const char* section, const char* key, int fallback);
std::string ConfigString(const char* section, const char* key, const char* fallback); std::string ConfigString(const char* section, const char* key, const char* fallback);
// Writes `key=value` into the ini (value as UTF-8).
void SetConfigString(const char* section, const char* key, const char* value);
} // namespace re } // namespace re
+11
View File
@@ -309,3 +309,14 @@
0x0070e650 cUI_Character::OnConfirmButton # the right-hand button (Exportieren, ...) 0x0070e650 cUI_Character::OnConfirmButton # the right-hand button (Exportieren, ...)
0x007f01a0 HeroSlots_DeleteHero # (slot + 1), thiscall on 0x7d7c40(), ret 4: deletes Save\Hero%02d.pax 0x007f01a0 HeroSlots_DeleteHero # (slot + 1), thiscall on 0x7d7c40(), ret 4: deletes Save\Hero%02d.pax
0x00731e70 cUI_Control2::SetFlags # (flags), ret 4: bit 0 = visible (show) 0x00731e70 cUI_Control2::SetFlags # (flags), ret 4: bit 0 = visible (show)
0x00716570 cUI_Options::cUI_Options # (name, rect, parent, flags) ret 0x18
0x007165f0 cUI_Options::~cUI_Options
0x007184e0 cUI_Options::CreateControls # frames, labels, buttons, sliders (vector +0x90)
0x00717ea0 cUI_Options::LoadSettings # settings -> controls, on show
0x00717460 cUI_Options::Apply # controls -> settings (OK)
0x00739f90 cUI_Frame::cUI_Frame
0x0043fa90 PotionTooltipText # (kind 1..5), ret 4: bar tooltip text id by key ("<color>Heiltrank <colorw>[SPC]")
0x00724ed0 cUI_Window2::AddBackdrop # (rect*, ARGB), ret 8: translucent cUI_Static into +0x90
0x0075a690 cUI_Manager::CanOpenWindow # (1), ret 4: false while a blocking window is open
0x007320c0 cUI_Control2::SetTooltipText # (text id) -> +0x20, ret 4
0x007320d0 cUI_Control2::TooltipText # +0x20 once hovered long enough, else 0