Files
sacred_k/src/mods/wasd.cpp
T
khallaandClaude Opus 5.5 838b1aa491 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>
2026-10-19 00:38:34 +02:00

288 lines
11 KiB
C++

// WASD movement for the player character: the keys steer the hero directly
// (request of 2026-10-18: direction movement instead of walk orders).
//
// The game walks a creature along a path of waypoints in cCreature::Update
// (0x594110, about 40 times per second; path layout in game/engine/orders.h).
// While W, A, S or D is held, each update of the controlled creature first
// replaces its path with one straight segment from where it stands, far into
// the pressed screen direction: the game's own step then moves it at its own
// speed, with its walk animation, and its step test refuses blocked cells. No
// pathfinding, no arrival, and a new direction counts from the next update.
//
// Obstacles: the cell one step ahead is probed with cCreature::IsCellWalkable
// (measured: walking straight into a fence, 27 of 28 steps were refused).
// When it is blocked, the direction turned by up to 90 degrees is tried,
// the side the hero last slid to first, so it slides along walls instead of
// running on the spot; with everything blocked it stands.
//
// The facing is set to the direction too: the game turns it towards the path
// over about 300 ms, which made reversals look like walking backwards.
//
// The path only drives a creature that walks. Standing idle (at the start, or
// after an attack, a cast, a weapon switch, which resets the hero to idle) it
// gets a walk order, and the two steps of cCreature::Think that would start it
// up to 110 ms later run at once: the walk goes on in the same update.
// Releasing the keys sends the target-less walk order a released mouse button
// sends, which stops it in place.
//
// The movement keys are key bindings (default W/A/S/D, src/mods/input.cpp,
// which keeps them away from the game; the game's W/A/S actions moved to
// other keys). Typing into a text field (chat, console, save names) is left
// alone. See docs/wasd.md.
#include <atomic>
#include <cmath>
#include <cstdint>
#include <windows.h>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "mods/input.h"
#include "re/hook.h"
namespace mods::wasd {
namespace {
using game::ControlledCreature;
using game::CreatureOrder;
using game::kEngineFlags;
// The walk order that starts walking aims this far (world units).
constexpr double kStartDistance = 300.0;
// Between two start orders while the walk has not begun yet.
constexpr DWORD kStartOrderMs = 150;
// Keys released this long before the hero stops: changing keys often leaves
// a moment without any (measured: a stop order there cost about 125 ms
// standing).
constexpr DWORD kStopGraceMs = 50;
// The steered segment's length: farther than any step, so it never arrives.
constexpr double kSegmentLength = 1000.0;
// How far ahead a direction is probed: a bit more than one map cell.
constexpr double kProbeDistance = game::kMapCellSize * 1.25;
constexpr double kProbeStep = game::kMapCellSize * 0.25;
// Directions tried around a blocked one (degrees, both sides).
constexpr double kSlideAngles[] = {30.0, 60.0, 90.0};
// Engine flags (+0x54): a mouse button is held / input is blocked.
constexpr uint32_t kEngineMouseHeld = 0x10;
constexpr uint32_t kEngineInputBlocked = 0x4000;
using mods::input::Move;
using mods::input::MovementKey;
bool KeyDown(int key) {
return key != 0 && (GetAsyncKeyState(key) & 0x8000) != 0;
}
void SendWalk(uint8_t* engine, void* creature, int x, int y) {
CreatureOrder order;
order.command = game::kOrderWalk;
order.groundTarget = 2;
order.a = x;
order.b = y;
game::SendCreatureOrder(engine, creature, order);
}
// A walk order without a target stops the creature where it is (what
// releasing a held mouse button sends, cEngine::HandleMouseButton).
void SendStop(uint8_t* engine, void* creature) {
CreatureOrder order;
order.command = game::kOrderWalk;
game::SendCreatureOrder(engine, creature, order);
}
std::atomic<uint8_t*> g_engine{nullptr};
bool g_moving = false;
DWORD g_releasedAt = 0; // when the keys were released (0: held)
DWORD g_startOrderAt = 0;
int g_slideSide = 1; // +1 / -1: the side the hero last turned to
struct Direction {
double x;
double y;
};
// The held movement keys as a unit screen direction in world axes: (-1,-1)
// up, (1,1) down, (-1,1) left, (1,-1) right. False when none (or the keys
// belong to the mouse, blocked input or a text field).
bool HeldDirection(const uint8_t* engine, Direction& dir) {
int x = 0;
int y = 0;
const uint32_t flags = *reinterpret_cast<const uint32_t*>(engine + kEngineFlags);
if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown(MovementKey(Move::Up))) { x -= 1; y -= 1; }
if (KeyDown(MovementKey(Move::Down))) { x += 1; y += 1; }
if (KeyDown(MovementKey(Move::Left))) { x -= 1; y += 1; }
if (KeyDown(MovementKey(Move::Right))) { x += 1; y -= 1; }
}
if (x == 0 && y == 0) {
return false;
}
const double length = std::sqrt(static_cast<double>(x * x + y * y));
dir = {x / length, y / length};
return true;
}
Direction Rotated(Direction dir, double degrees) {
const double a = degrees * 3.14159265358979 / 180.0;
return {dir.x * std::cos(a) - dir.y * std::sin(a), dir.x * std::sin(a) + dir.y * std::cos(a)};
}
// Whether the creature can go a probe distance along `dir`: every cell the
// way crosses (sampled at quarter cells, so none is skipped) is walkable.
bool Open(uint8_t* creature, Direction dir) {
const int x = game::CreatureField(creature, game::kCreatureX);
const int y = game::CreatureField(creature, game::kCreatureY);
game::MapPosition last = game::CellAt(creature, x, y);
for (double d = kProbeStep; d <= kProbeDistance; d += kProbeStep) {
game::MapPosition cell =
game::CellAt(creature, x + static_cast<int>(dir.x * d), y + static_cast<int>(dir.y * d));
if (cell.x == last.x && cell.y == last.y) {
continue;
}
if (!game::IsCellWalkable(creature, cell)) {
return false;
}
last = cell;
}
return true;
}
// The direction to go for keys pointing along `dir`: itself, or the nearest
// open one within 90 degrees (the last slide side first). False if blocked.
bool OpenDirection(uint8_t* creature, Direction dir, Direction& out) {
if (Open(creature, dir)) {
out = dir;
return true;
}
const int sides[] = {g_slideSide, -g_slideSide};
for (const double angle : kSlideAngles) {
for (const int side : sides) {
const Direction turned = Rotated(dir, side * angle);
if (Open(creature, turned)) {
g_slideSide = side;
out = turned;
return true;
}
}
}
return false;
}
// One straight segment from the creature's position along `dir`.
void Steer(uint8_t* creature, Direction dir) {
const int x = game::CreatureField(creature, game::kCreatureX);
const int y = game::CreatureField(creature, game::kCreatureY);
auto* path = reinterpret_cast<int32_t*>(creature + game::kCreaturePath);
path[0] = x;
path[1] = y;
path[2] = x + static_cast<int>(dir.x * kSegmentLength);
path[3] = y + static_cast<int>(dir.y * kSegmentLength);
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathIndex) = 0;
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathCount) = 2;
auto* facing = reinterpret_cast<float*>(creature + game::kCreatureFacing);
facing[0] = static_cast<float>(dir.x);
facing[1] = static_cast<float>(dir.y);
}
// Starts walking along `dir` now: the walk order, then the two Think steps
// that would otherwise follow up to 110 ms later (start the pending walk, the
// walk action's first phase). True if the creature walks.
bool StartWalk(uint8_t* engine, uint8_t* creature, Direction dir) {
SendWalk(engine, creature, game::CreatureField(creature, game::kCreatureX) + static_cast<int>(dir.x * kStartDistance),
game::CreatureField(creature, game::kCreatureY) + static_cast<int>(dir.y * kStartDistance));
if (*reinterpret_cast<uint16_t*>(creature + game::kCreaturePendingAction) == game::kPendingWalk) {
game::StartPendingWalk(creature);
}
if (game::CurrentAction(creature) != game::kActionWalk) {
return false; // held by a status effect: Think decides later
}
if (game::CreatureField(creature, game::kCreatureAction) >> 16 == 0) {
game::WalkAction(creature);
}
return true;
}
void OnTick(uint8_t* engine) {
g_engine = engine;
}
struct Registration {
Registration() { game::OnEngineTick(&OnTick); }
} g_registration;
} // namespace
// Before each update of a creature: steer the controlled one.
void BeforeCreatureUpdate(uint8_t* creature) {
uint8_t* engine = g_engine.load();
if (!engine || creature != ControlledCreature(engine)) {
return;
}
Direction keys;
if (!HeldDirection(engine, keys)) {
const DWORD now = GetTickCount();
if (g_moving && !g_releasedAt) {
g_releasedAt = now;
}
if (g_moving && now - g_releasedAt >= kStopGraceMs) {
if ((*reinterpret_cast<uint32_t*>(engine + kEngineFlags) & kEngineMouseHeld) == 0) {
SendStop(engine, creature);
}
g_moving = false;
}
return;
}
g_moving = true;
g_releasedAt = 0;
int32_t action = game::CurrentAction(creature);
if (action == game::kActionStop) {
action = game::kActionIdle; // stopping: start again like from standing
}
if (action != game::kActionWalk && action != game::kActionIdle) {
return; // attacking, casting, dying: the action finishes first
}
Direction dir;
const bool open = OpenDirection(creature, keys, dir);
if (action == game::kActionWalk) {
auto* phase = reinterpret_cast<uint16_t*>(creature + game::kCreatureActionPhase);
if (open) {
if (*phase == game::kPhaseStopping) {
*phase = 1; // a stop requested but not begun: walk on
}
Steer(creature, dir);
} else if (*phase != game::kPhaseStopping) {
SendStop(engine, creature); // stand instead of running on the spot
}
return;
}
const DWORD now = GetTickCount();
if (open && now - g_startOrderAt >= kStartOrderMs) {
g_startOrderAt = now;
if (StartWalk(engine, creature, dir)) {
g_startOrderAt = 0;
Steer(creature, dir);
}
}
}
} // namespace mods::wasd
namespace game {
// cCreature::Update (0x594110, thiscall (u8), ret 4): moves the creature
// along its path, among others.
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg);
RE_HOOK(0x00594110, Creature_Update);
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg) {
if (self) {
mods::wasd::BeforeCreatureUpdate(static_cast<uint8_t*>(self));
}
Creature_Update_original(self, edx, arg);
}
} // namespace game