- 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>
288 lines
11 KiB
C++
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
|