Files
sacred_k/src/mods/respec.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

270 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// − buttons on the character panel give back single invested points (request
// of 2026-10-16).
//
// The panel (cUI_StatisticsChar, part of the inventory) shows the attributes
// or the skills (Fertigkeiten, panel +0x164 bit 0x10 set: attributes) of the
// player hero with a + button per row while points are free. The hero keeps
// (in cCreatureArts, hero +0x3a8, measured 2026-10-16):
// +0x1c[6] attribute points invested (the shown values add class and level)
// +0x23 free attribute points (index 7 of the same array)
// +0x24[8] skill ids by row, +0x2c[8] their bought levels, +0x34[8] item bonus
// +0x42 free skill points, +0x44 new skills still to choose
// A − button is shown left of the value on each row with points invested.
// Attributes: 0x5a03a0 (index, delta) moves one point back to the free ones, as
// the + does (0x580d50). Skills above level 1: 0x582360 (row, -1, spend) gives
// back a skill point. Level 1 (the level a skill is chosen with): the skill is
// removed and a new skill can be chosen again (+0x44), with the game's own
// choice. Both go through the game's functions, which also update its
// protected copy of these fields (0x182cddc) and recalculate the hero.
//
// The buttons are the panel's second array of 8 buttons (+0x8ac), which the
// game constructs and destroys but never lays out or uses; they show the +
// icon with its vertical arm covered (no minus image exists). See
// docs/respec.md.
#include <cstdint>
#include <cstring>
#include <windows.h>
#include "game/engine/arts.h"
#include "game/engine/objects.h"
#include "game/engine/skills.h"
#include "game/ui/draw.h"
#include "game/ui/events.h"
#include "game/ui/text.h"
#include "mods/hud.h"
#include "re/hook.h"
#include "re/log.h"
namespace mods::respec {
namespace {
// cUI_StatisticsChar.
constexpr size_t kPanelFlags = 0x164;
constexpr uint32_t kPanelShowsAttributes = 0x10;
constexpr size_t kPanelChoosingSkill = 0xf84; // u8: picking a new skill
constexpr size_t kPlusButtons = 0x2cc;
constexpr size_t kMinusButtons = 0x8ac; // unused by the game
constexpr size_t kButtonSize = 0xbc;
constexpr int kRows = 8;
constexpr int kAttributeRows = game::kAttributeCount;
// cUI_Button (vtable 0x895308).
constexpr size_t kControlFlags = 0x10;
constexpr uint32_t kControlVisible = 1;
constexpr size_t kControlRect = 0x24; // x, y (int32), width, height (int16)
constexpr size_t kControlParent = 0x50;
// Images (render 0x72af90): u0, u1, v0, v1 floats and a texture id; the
// normal one, and the one shown while pressed (flags bit 0x10).
constexpr size_t kButtonImage = 0x64;
constexpr size_t kButtonTexture = 0x84;
constexpr size_t kButtonPressedImage = 0x54;
constexpr size_t kButtonPressedTexture = 0x80;
constexpr int kControlRender = 5; // vf14 (device)
constexpr int kControlHitTest = 7; // vf1c (x, y): inside and visible parts
// Image descriptors (static, 0x54 bytes): texture at +0x28, u0, v0, u1, v1 at
// +0x2c..+0x38 (the panel copies them in the order u0, u1, v0, v1, 0x6ac120).
// The + of the rows: 0x9ed2d0, pressed 0x9ed324 (16x16 in the UI atlas). The
// game registers UI_BUTT_MINUS_UP/DW names but has no minus image in its UI
// atlases (all drawn once and looked at, 2026-10-16), so the − is the + with
// the vertical arm of its cross covered by two patches in the disc's grey.
constexpr uint32_t kPlusImage = 0x009ed2d0;
constexpr uint32_t kPlusPressedImage = 0x009ed324;
constexpr size_t kImageTexture = 0x28;
constexpr size_t kImageUvOrder[4] = {0x2c, 0x34, 0x30, 0x38};
// The cross's vertical arm in the 16x16 icon (measured in a screenshot, in
// icon pixels): x 6.5..8.8, above the bar y 4.0..6.9, below it y 9.0..11.5
// (the bar: y 6.9..8.9). The disc around it averages grey 55.
struct Patch {
float x0, y0, x1, y1;
};
constexpr Patch kArmPatches[] = {{6.3f, 3.8f, 9.0f, 6.95f}, {6.3f, 8.95f, 9.0f, 11.8f}};
constexpr uint32_t kDiscGrey = 0xff37373a;
constexpr float kIconSize = 16.0f;
// The − sits left of the value column (values end at the + at x 232).
constexpr int kMinusX = 184;
// cCreatureArts fields (see the top, game/engine/skills.h).
using game::kFreeAttributesIndex;
constexpr size_t kAttributes = game::kArtsInvested;
constexpr size_t kSkillIds = game::kArtsSkillIds;
constexpr size_t kSkillLevels = game::kArtsSkillLevels;
constexpr size_t kSkillBonus = game::kArtsSkillBonus;
constexpr size_t kSkillChoices = game::kArtsSkillChoices;
using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta);
using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints);
using RecalculateFn = void(__fastcall*)(void* arts, void*, int, int); // as the + calls it: (0, 1)
const auto AddAttribute = reinterpret_cast<AddAttributeFn>(0x005a03a0);
const auto AddSkillLevels = reinterpret_cast<AddSkillLevelsFn>(0x00582360);
const auto Recalculate = reinterpret_cast<RecalculateFn>(0x00579560);
uint8_t* Button(uint8_t* panel, size_t array, int row) {
return panel + array + row * kButtonSize;
}
bool ShowsAttributes(uint8_t* panel) {
return (*reinterpret_cast<uint32_t*>(panel + kPanelFlags) & kPanelShowsAttributes) != 0;
}
uint8_t* HeroArts() {
auto* hero = static_cast<uint8_t*>(game::GetPlayerHero());
return hero ? hero + game::kCreatureArts : nullptr;
}
// Whether `row` has a point the player can take back.
bool CanRevert(uint8_t* panel, uint8_t* arts, int row) {
if (panel[kPanelChoosingSkill]) {
return false;
}
if (ShowsAttributes(panel)) {
return row < kAttributeRows && arts[kAttributes + row] > 0;
}
return arts[kSkillIds + row] != 0 && arts[kSkillLevels + row] > 0;
}
void CopyImage(uint8_t* button, size_t uvs, size_t texture, uint32_t image) {
const auto* descriptor = reinterpret_cast<const uint8_t*>(image);
*reinterpret_cast<uint32_t*>(button + texture) = *reinterpret_cast<const uint32_t*>(descriptor + kImageTexture);
for (int i = 0; i < 4; ++i) {
*reinterpret_cast<uint32_t*>(button + uvs + 4 * i) =
*reinterpret_cast<const uint32_t*>(descriptor + kImageUvOrder[i]);
}
}
bool Visible(uint8_t* control) {
return (*reinterpret_cast<uint32_t*>(control + kControlFlags) & kControlVisible) != 0;
}
// Places the − of each row like its + (the panel lays the + out once per
// font), shows it where a point can be taken back.
void Update(uint8_t* panel, uint8_t* arts) {
for (int row = 0; row < kRows; ++row) {
uint8_t* plus = Button(panel, kPlusButtons, row);
uint8_t* minus = Button(panel, kMinusButtons, row);
if (*reinterpret_cast<uint8_t**>(minus + kControlParent) != panel) {
*reinterpret_cast<uint8_t**>(minus + kControlParent) = panel;
CopyImage(minus, kButtonImage, kButtonTexture, kPlusImage);
CopyImage(minus, kButtonPressedImage, kButtonPressedTexture, kPlusPressedImage);
}
std::memcpy(minus + kControlRect, plus + kControlRect, 12);
*reinterpret_cast<int*>(minus + kControlRect) = kMinusX;
auto& flags = *reinterpret_cast<uint32_t*>(minus + kControlFlags);
// The panel renders while hidden too (inventory closed).
const bool shown = arts && Visible(panel) && CanRevert(panel, arts, row);
flags = shown ? flags | kControlVisible : flags & ~kControlVisible;
}
}
void Revert(uint8_t* panel, uint8_t* arts, int row) {
if (ShowsAttributes(panel)) {
AddAttribute(arts, nullptr, row, -1);
AddAttribute(arts, nullptr, kFreeAttributesIndex, 1);
Recalculate(arts, nullptr, 0, 1);
re::Log("respec: attribute %d back to %u, %u free", row, arts[kAttributes + row],
arts[kAttributes + kFreeAttributesIndex]);
return;
}
if (arts[kSkillLevels + row] > 1) {
AddSkillLevels(arts, nullptr, row, -1, 1);
re::Log("respec: skill %x back to level %u", arts[kSkillIds + row], arts[kSkillLevels + row]);
return;
}
// Level 1: the skill goes, a new one can be chosen in its place.
const uint8_t skill = arts[kSkillIds + row];
arts[kSkillIds + row] = 0;
arts[kSkillLevels + row] = 0;
arts[kSkillBonus + row] = 0;
++*reinterpret_cast<uint16_t*>(arts + kSkillChoices);
AddSkillLevels(arts, nullptr, row, 0, 1); // updates the protected copy, recalculates
re::Log("respec: skill %x removed, %u to choose", skill, *reinterpret_cast<uint16_t*>(arts + kSkillChoices));
}
// Turns the + that `minus` drew into a −.
void CoverArm(void* device, uint8_t* minus) {
const game::UiRect rect = game::ControlScreenRect(minus);
const float sx = rect.w / kIconSize;
const float sy = rect.h / kIconSize;
for (const Patch& patch : kArmPatches) {
game::FillRect(device, rect.x + patch.x0 * sx, rect.y + patch.y0 * sy, rect.x + patch.x1 * sx,
rect.y + patch.y1 * sy, kDiscGrey);
}
}
struct RenderCall {
uint8_t* panel;
void* device;
int (__fastcall* original)(void*, void*, void*);
};
int RenderBody(void* context) {
auto* call = static_cast<RenderCall*>(context);
const int result = call->original(call->panel, nullptr, call->device);
Update(call->panel, HeroArts());
for (int row = 0; row < kRows; ++row) {
uint8_t* minus = Button(call->panel, kMinusButtons, row);
if (Visible(minus)) {
using RenderFn = int(__fastcall*)(void*, void*, void*);
reinterpret_cast<RenderFn>((*reinterpret_cast<void***>(minus))[kControlRender])(minus, nullptr,
call->device);
CoverArm(call->device, minus);
}
}
return result;
}
struct EventCall {
uint8_t* panel;
void* event;
int (__fastcall* original)(void*, void*, void*);
};
int EventBody(void* context) {
auto* call = static_cast<EventCall*>(context);
uint8_t* arts = HeroArts();
if (arts && Visible(call->panel) &&
game::MouseEventKind(call->event) == game::MouseEvent::ButtonDown &&
game::EventField(call->event, game::kMouseButton) == game::kLeftButton) {
const int x = game::EventField(call->event, game::kMouseButtonX);
const int y = game::EventField(call->event, game::kMouseButtonY);
for (int row = 0; row < kRows; ++row) {
uint8_t* minus = Button(call->panel, kMinusButtons, row);
using HitTestFn = char(__fastcall*)(void*, void*, int, int);
if (Visible(minus) && CanRevert(call->panel, arts, row) &&
reinterpret_cast<HitTestFn>((*reinterpret_cast<void***>(minus))[kControlHitTest])(minus, nullptr, x,
y)) {
Revert(call->panel, arts, row);
Update(call->panel, arts);
return 1;
}
}
}
return call->original(call->panel, nullptr, call->event);
}
} // namespace
} // namespace mods::respec
namespace game {
// cUI_StatisticsChar vf10 (event) and vf14 (render); centred like the other
// windows (mods/hud.cpp).
int __fastcall UiStatisticsChar_HandleEvent(void* self, void* edx, void* event);
RE_HOOK(0x006a5dd0, UiStatisticsChar_HandleEvent);
int __fastcall UiStatisticsChar_HandleEvent(void* self, void*, void* event) {
mods::respec::EventCall call{static_cast<uint8_t*>(self), event, UiStatisticsChar_HandleEvent_original};
return mods::hud::HandleCentred(event, &mods::respec::EventBody, &call);
}
int __fastcall UiStatisticsChar_Render(void* self, void* edx, void* device);
RE_HOOK(0x006a9be0, UiStatisticsChar_Render);
int __fastcall UiStatisticsChar_Render(void* self, void*, void* device) {
mods::respec::RenderCall call{static_cast<uint8_t*>(self), device, UiStatisticsChar_Render_original};
return mods::hud::RenderCentred(self, &mods::respec::RenderBody, &call);
}
} // namespace game