- 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>
270 lines
12 KiB
C++
270 lines
12 KiB
C++
// − 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
|