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

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

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

No files matched your search

+75 -15
View File
@@ -20,17 +20,20 @@
// SkillSlot0=0 ... SkillSlot6=6 ; cast combat-art slot (0 = right mouse button art)
// WeaponSet1=F1 ... WeaponSet7=F7 ; select weapon set
// ActionSlot3=G ; what W did (W is movement)
// PickUp=V ; what A did (pick up items nearby)
// 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)
#include "mods/input.h"
#include <atomic>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <string>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "mods/skillbar.h"
#include "mods/skillkeys.h"
#include "mods/wasd.h"
@@ -43,7 +46,7 @@ namespace mods::input {
namespace {
// --- sacred.exe ---------------------------------------------------------------
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*: focused control at +0x148
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;
@@ -75,7 +78,7 @@ void PostGameKey(char key, bool pressed) {
// --- bindings ------------------------------------------------------------------
enum class Kind { SkillSlot, WeaponSet, GameKey };
enum class Kind { SkillSlot, WeaponSet, GameKey, PickUp };
struct Binding {
const char* name;
@@ -102,7 +105,7 @@ Binding g_bindings[] = {
{"WeaponSet6", "F6", Kind::WeaponSet, 5, 0},
{"WeaponSet7", "F7", Kind::WeaponSet, 6, 0},
{"ActionSlot3", "G", Kind::GameKey, 0, 'W'},
{"PickUp", "V", Kind::GameKey, 0, 'A'},
{"PickUp", "Q", Kind::PickUp, 0, 0},
{"SaveGame", "K", Kind::GameKey, 0, 'S'},
};
@@ -147,6 +150,37 @@ bool Reserved(int vk) {
return (vk >= '0' && vk <= '9') || mods::wasd::IsMovementKey(vk);
}
// Pick up items nearby: the order the engine's A hotkey sends (creature order
// 0x113, cEngine::ReceiveEvent 0x618c41; the hero collects, mode +0xfc 0xc),
// given to the controlled creature on the next engine tick. (Posting the A
// key event did not reach it: no pickup was logged.)
constexpr uint32_t kOrderPickUp = 0x113;
std::atomic<bool> g_pickUp{false};
// The collect mode (0x4ec3a0, substate 0 without a target) looks for an item
// in square rings of map cells around the hero, nearest ring first, while the
// ring's low offset is above -20 (cmp edi,-20 at 0x4ec8c6). Twice as far.
constexpr uint32_t kCollectRingLimit = 0x004ec8c6;
constexpr uint8_t kCollectRingsOriginal = 0xec; // -20
constexpr uint8_t kCollectRings = 0xd8; // -40
void OnTick(uint8_t* engine) {
if (!g_pickUp.exchange(false)) {
return;
}
if (void* hero = game::ControlledCreature(engine)) {
game::CreatureOrder order;
order.command = kOrderPickUp;
game::SendCreatureOrder(engine, hero, order);
}
}
struct Registration {
Registration() {
game::OnEngineTick(&OnTick);
}
} g_registration;
void Press(const Binding& binding, int vk) {
const auto slot = static_cast<unsigned>(binding.index);
switch (binding.kind) {
@@ -159,6 +193,9 @@ void Press(const Binding& binding, int vk) {
case Kind::GameKey:
PostGameKey(binding.gameKey, true);
break;
case Kind::PickUp:
g_pickUp = true;
break;
}
}
@@ -168,6 +205,7 @@ void Release(const Binding& binding, int vk) {
mods::skillkeys::KeyReleased(vk);
break;
case Kind::WeaponSet:
case Kind::PickUp:
break;
case Kind::GameKey:
PostGameKey(binding.gameKey, false);
@@ -184,23 +222,45 @@ bool g_swallowChar = false;
void Startup() {
LoadBindings();
if (!re::PatchInstructionByte(kCollectRingLimit, 2, kCollectRingsOriginal, kCollectRings)) {
re::Log("input: pickup range patch FAILED");
}
re::Log("input: key bindings active");
}
RE_STARTUP(InputStartup, Startup);
// The text field that has the keyboard, as the game decides it: the UI
// manager hands key events to its dialog (+0x144) while that is shown
// (+0x10 bit 0; cUI_Manager::ReceiveEvent 0x7573ba), and the dialog's focused
// control (+0x148) is an edit control without the read-only bit (+0x10 bit
// 16). The focus pointer alone stays set after the dialog closed (measured:
// the save window's 'DEFAULT_EDIT' after closing it), which switched the
// bindings off.
constexpr size_t kUiDialog = 0x144;
constexpr size_t kUiFocus = 0x148;
constexpr size_t kControlFlags = 0x10;
constexpr uint32_t kControlVisible = 1;
constexpr uint32_t kEditReadOnly = 1u << 16;
uint8_t* FocusedTextField() {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
auto* dialog = ui ? *reinterpret_cast<uint8_t**>(ui + kUiDialog) : nullptr;
if (!dialog || (*reinterpret_cast<uint32_t*>(dialog + kControlFlags) & kControlVisible) == 0) {
return nullptr;
}
void* focus = *reinterpret_cast<void**>(ui + kUiFocus);
auto* edit = static_cast<uint8_t*>(focus ? game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl)
: nullptr);
if (!edit || (*reinterpret_cast<uint32_t*>(edit + kControlFlags) & kEditReadOnly) != 0) {
return nullptr;
}
return edit;
}
} // namespace
bool TypingIntoTextField() {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
if (!ui) {
return false;
}
void* focus = *reinterpret_cast<void**>(ui + 0x148);
if (!focus) {
return false;
}
auto* edit = static_cast<uint8_t*>(game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl));
return edit && ((*reinterpret_cast<uint32_t*>(edit + 0x10) >> 16) & 1) == 0;
return FocusedTextField() != nullptr;
}
bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) {
@@ -210,7 +270,7 @@ bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) {
const bool repeat = (lParam & (1 << 30)) != 0;
if (!repeat) {
g_taken[vk] = false;
if (TypingIntoTextField()) {
if (FocusedTextField()) {
return false;
}
const Binding* binding = BindingFor(vk);