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:
1 parent
72525cd5ba
commit
838b1aa491
27 files changed
+1422
-183
No files matched your search
@@ -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
@@ -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.
|
||||||
@@ -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
@@ -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 | - |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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,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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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_STARTUP(TeleportPotionStartup, Startup);
|
||||||
re::Log("teleportpotion: %zu potion names, %zu replaced", names.size(), replaced.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
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
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user