Character panel: minus buttons take back attribute and skill points
- respec: the panel's unused second button array becomes − buttons between name and value; attributes give back invested points, skills give back skill points, a level-1 skill is removed and can be chosen anew; all through the game's own point functions (protected copy stays in sync) - the − is the + icon with its vertical arm covered (no minus image exists) - hud: RenderCentred / HandleCentred for mods hooking centred windows; mouse event helpers shared in game/ui/events.h Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c6c5530709
commit
037fc73158
7 files changed
+413
-14
No files matched your search
@@ -53,6 +53,7 @@ add_library(sacred_re SHARED
|
|||||||
src/mods/escape.cpp
|
src/mods/escape.cpp
|
||||||
src/mods/recordcache.cpp
|
src/mods/recordcache.cpp
|
||||||
src/mods/megamap.cpp
|
src/mods/megamap.cpp
|
||||||
|
src/mods/respec.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacred_re PRIVATE src)
|
target_include_directories(sacred_re PRIVATE src)
|
||||||
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Taking back attribute and skill points (mods/respec.cpp)
|
||||||
|
|
||||||
|
Request of 2026-10-16: a − button on each row of the character panel
|
||||||
|
(inventory, Attribute / Fertigkeiten) gives back one invested point. A skill
|
||||||
|
at level 1 is removed and a new one can be chosen in its place.
|
||||||
|
|
||||||
|
## Data (cCreatureArts, hero +0x3a8, measured 2026-10-16)
|
||||||
|
|
||||||
|
| Offset | What |
|
||||||
|
|---|---|
|
||||||
|
| +0x1c[6] | attribute points invested, in panel order (Stärke, Widerstand, Geschick, Physische Reg., Mentale Reg., Charisma); the shown values add class and level growth |
|
||||||
|
| +0x23 | free attribute points (index 7 of the same array) |
|
||||||
|
| +0x24[8] | skill id per row (0: empty) |
|
||||||
|
| +0x2c[8] | bought skill level per row (a chosen skill starts at 1) |
|
||||||
|
| +0x34[8] | skill levels from items |
|
||||||
|
| +0x42 | free skill points |
|
||||||
|
| +0x44 | new skills still to choose |
|
||||||
|
| +0x56 | highest skill level allowed |
|
||||||
|
|
||||||
|
Game functions (all on cCreatureArts):
|
||||||
|
|
||||||
|
- `0x5a03a0 (index, delta)` adds to an attribute byte; the + does (row, 1)
|
||||||
|
then (7, −1), then `0x579560 (0, 1)` recalculates the hero (0x580d50).
|
||||||
|
- `0x582360 (row, delta, spend)` adds skill levels; with `spend` it takes them
|
||||||
|
from the free skill points (a negative delta gives them back).
|
||||||
|
- `0x580fe0 (skill)` puts a chosen skill into the first empty row at level 1
|
||||||
|
and uses up one choice; `0x580f30` lists the skills still choosable.
|
||||||
|
|
||||||
|
Each of them also writes the hero's +0x3b0.. block, xor-ed, into a protected
|
||||||
|
page (0x182cddc / 0x182cde0). Changing the fields directly without that made
|
||||||
|
the hero's life show 0. The mod therefore only changes fields through these
|
||||||
|
functions; after removing a skill it calls `0x582360 (row, 0, 1)` to refresh
|
||||||
|
the copy.
|
||||||
|
|
||||||
|
## Panel (cUI_StatisticsChar, vtable 0x8942d8)
|
||||||
|
|
||||||
|
- +0x164 bit 0x10: attributes shown (else skills); +0xf84: choosing a new
|
||||||
|
skill.
|
||||||
|
- +0x2cc: the 8 + buttons (cUI_Button, 0xbc bytes each, laid out by 0x6ac120:
|
||||||
|
x 232, rows 19 px apart from y 72, 16×16).
|
||||||
|
- +0x8ac: a second array of 8 buttons that the game constructs and destroys
|
||||||
|
but never lays out or uses. These are the − buttons now.
|
||||||
|
- The panel renders while hidden (inventory closed); the − buttons check its
|
||||||
|
visible flag.
|
||||||
|
|
||||||
|
cUI_Button images (render 0x72af90): normal at +0x64 (u0, u1, v0, v1) with
|
||||||
|
texture +0x84, pressed at +0x54 / +0x80. Image descriptors are static, 0x54
|
||||||
|
bytes each (texture +0x28, pixel rect +0x2c..+0x38, converted to UVs at
|
||||||
|
start-up). The + uses 0x9ed2d0 (pressed 0x9ed324) in UI atlas 0x26a. The
|
||||||
|
names UI_BUTT_MINUS_UP/DW are registered, but neither UI atlas (0x26a, 0x289;
|
||||||
|
both drawn whole once and looked at) has a minus image. The − is therefore
|
||||||
|
the + with the vertical arm of its cross covered by two grey patches,
|
||||||
|
measured from a screenshot: arm x 6.5..8.8, y 4.0..6.9 and 9.0..11.5 in icon
|
||||||
|
pixels.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
// UI events (cEvent subclasses handed to the windows' vf10).
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace game {
|
||||||
|
|
||||||
|
// Mouse events, by their vf04 category (positions in render pixels):
|
||||||
|
// 1 cEvent_mouseMove: previous x, y at +4 / +8, current at +0xc / +0x10
|
||||||
|
// 2 cEvent_mouseButton_down,
|
||||||
|
// 3 cEvent_mouseButton_up: button at +4 (0: left), x, y at +8 / +0xc
|
||||||
|
enum class MouseEvent { None = 0, Move = 1, ButtonDown = 2, ButtonUp = 3 };
|
||||||
|
|
||||||
|
inline MouseEvent MouseEventKind(void* event) {
|
||||||
|
using CategoryFn = int(__fastcall*)(void* self, void* edx);
|
||||||
|
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
|
||||||
|
const int kind = category(event, nullptr);
|
||||||
|
return kind >= 1 && kind <= 3 ? static_cast<MouseEvent>(kind) : MouseEvent::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t kMouseButton = 0x4;
|
||||||
|
constexpr size_t kMouseButtonX = 0x8;
|
||||||
|
constexpr size_t kMouseButtonY = 0xc;
|
||||||
|
constexpr int kLeftButton = 0;
|
||||||
|
|
||||||
|
inline int32_t EventField(void* event, size_t offset) {
|
||||||
|
return *reinterpret_cast<int32_t*>(static_cast<uint8_t*>(event) + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace game
|
||||||
+19
-14
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "game/addresses.h"
|
#include "game/addresses.h"
|
||||||
#include "game/render/dx_driver7.h"
|
#include "game/render/dx_driver7.h"
|
||||||
|
#include "game/ui/events.h"
|
||||||
#include "mods/widescreen.h"
|
#include "mods/widescreen.h"
|
||||||
#include "re/config.h"
|
#include "re/config.h"
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
@@ -262,22 +263,17 @@ private:
|
|||||||
CursorOffset cursor_;
|
CursorOffset cursor_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mouse events, by vf04 category (positions in render pixels):
|
// Stores the offsets of a mouse event's x fields (game/ui/events.h); returns
|
||||||
// 1 cEvent_mouseMove: previous x, y at +4 / +8, current at +0xc / +0x10
|
// their count (0: no mouse event).
|
||||||
// 2 cEvent_mouseButton_down,
|
|
||||||
// 3 cEvent_mouseButton_up: x, y at +8 / +0xc
|
|
||||||
// Stores the offsets of the x fields; returns their count (0: no mouse event).
|
|
||||||
int MouseEventXOffsets(void* event, int (&offsets)[2]) {
|
int MouseEventXOffsets(void* event, int (&offsets)[2]) {
|
||||||
using CategoryFn = int(__fastcall*)(void* self, void* edx);
|
switch (game::MouseEventKind(event)) {
|
||||||
auto category = reinterpret_cast<CategoryFn>((*static_cast<void***>(event))[1]);
|
case game::MouseEvent::Move:
|
||||||
switch (category(event, nullptr)) {
|
|
||||||
case 1:
|
|
||||||
offsets[0] = 0x4;
|
offsets[0] = 0x4;
|
||||||
offsets[1] = 0xc;
|
offsets[1] = 0xc;
|
||||||
return 2;
|
return 2;
|
||||||
case 2:
|
case game::MouseEvent::ButtonDown:
|
||||||
case 3:
|
case game::MouseEvent::ButtonUp:
|
||||||
offsets[0] = 0x8;
|
offsets[0] = game::kMouseButtonX;
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -574,8 +570,7 @@ HUD_EVENT(0x00726880, UiWindow_HandleEvent, Anchor::Center) // cUI_Window2 (sha
|
|||||||
HUD_RENDER(0x00727040, UiWindow_Render, Anchor::Center) // cUI_Window2 (shared)
|
HUD_RENDER(0x00727040, UiWindow_Render, Anchor::Center) // cUI_Window2 (shared)
|
||||||
HUD_EVENT(0x006a4140, UiBlacksmith_HandleEvent, Anchor::Center)
|
HUD_EVENT(0x006a4140, UiBlacksmith_HandleEvent, Anchor::Center)
|
||||||
HUD_RENDER3(0x006a3160, UiBlacksmith_Render, Anchor::Center)
|
HUD_RENDER3(0x006a3160, UiBlacksmith_Render, Anchor::Center)
|
||||||
HUD_EVENT(0x006a5dd0, UiStatisticsChar_HandleEvent, Anchor::Center)
|
// cUI_StatisticsChar (0x6a5dd0 / 0x6a9be0): hooked by mods/respec.cpp.
|
||||||
HUD_RENDER(0x006a9be0, UiStatisticsChar_Render, Anchor::Center)
|
|
||||||
HUD_EVENT(0x006acc70, UiChest_HandleEvent, Anchor::Center)
|
HUD_EVENT(0x006acc70, UiChest_HandleEvent, Anchor::Center)
|
||||||
HUD_RENDER(0x006ac8b0, UiChest_Render, Anchor::Center)
|
HUD_RENDER(0x006ac8b0, UiChest_Render, Anchor::Center)
|
||||||
HUD_EVENT(0x006adc70, UiConsole_HandleEvent, Anchor::Center)
|
HUD_EVENT(0x006adc70, UiConsole_HandleEvent, Anchor::Center)
|
||||||
@@ -688,6 +683,16 @@ int VisibleDialogTooltips(void** out, int max) {
|
|||||||
return VisibleDialogTooltipsImpl(out, max);
|
return VisibleDialogTooltipsImpl(out, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int RenderCentred(void* window, int (*body)(void* context), void* context) {
|
||||||
|
DrawOffset offset(window, OffsetFor(Anchor::Center));
|
||||||
|
return body(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
int HandleCentred(void* event, int (*body)(void* context), void* context) {
|
||||||
|
EventOffset offset(event, OffsetFor(Anchor::Center));
|
||||||
|
return body(context);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace mods::hud
|
} // namespace mods::hud
|
||||||
|
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|||||||
@@ -17,4 +17,10 @@ void* CursorFor(void* cursor);
|
|||||||
// visible while no moved window was under the cursor) and are visible now.
|
// visible while no moved window was under the cursor) and are visible now.
|
||||||
int VisibleDialogTooltips(void** out, int max);
|
int VisibleDialogTooltips(void** out, int max);
|
||||||
|
|
||||||
|
// For mods that hook a centred window themselves: run body(context) as the
|
||||||
|
// HUD layer runs the window's render method (drawing shifted, the cursor seen
|
||||||
|
// shifted) or event handler (mouse position shifted by the window's offset).
|
||||||
|
int RenderCentred(void* window, int (*body)(void* context), void* context);
|
||||||
|
int HandleCentred(void* event, int (*body)(void* context), void* context);
|
||||||
|
|
||||||
} // namespace mods::hud
|
} // namespace mods::hud
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
// − 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/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 = 6;
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
constexpr size_t kAttributes = 0x1c;
|
||||||
|
constexpr int kFreeAttributesIndex = 7;
|
||||||
|
constexpr size_t kSkillIds = 0x24;
|
||||||
|
constexpr size_t kSkillLevels = 0x2c;
|
||||||
|
constexpr size_t kSkillBonus = 0x34;
|
||||||
|
constexpr size_t kSkillChoices = 0x44;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDirect3DDevice7 as the UI's render methods get it (their vf14 argument).
|
||||||
|
constexpr int kDeviceDrawPrimitive = 25;
|
||||||
|
constexpr int kDeviceSetTexture = 35;
|
||||||
|
constexpr DWORD kTriangleStrip = 5;
|
||||||
|
constexpr DWORD kFvfScreenColourTexture = 0x1c4; // XYZRHW | DIFFUSE | SPECULAR | TEX1
|
||||||
|
struct Vertex {
|
||||||
|
float x, y, z, rhw;
|
||||||
|
uint32_t diffuse, specular;
|
||||||
|
float u, v;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
void** vtable = *static_cast<void***>(device);
|
||||||
|
using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*);
|
||||||
|
using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD);
|
||||||
|
reinterpret_cast<SetTextureFn>(vtable[kDeviceSetTexture])(device, 0, nullptr);
|
||||||
|
for (const Patch& patch : kArmPatches) {
|
||||||
|
const float x0 = rect.x + patch.x0 * sx;
|
||||||
|
const float x1 = rect.x + patch.x1 * sx;
|
||||||
|
const float y0 = rect.y + patch.y0 * sy;
|
||||||
|
const float y1 = rect.y + patch.y1 * sy;
|
||||||
|
Vertex quad[4] = {{x0, y0, 0, 1, kDiscGrey, 0, 0, 0},
|
||||||
|
{x1, y0, 0, 1, kDiscGrey, 0, 1, 0},
|
||||||
|
{x0, y1, 0, 1, kDiscGrey, 0, 0, 1},
|
||||||
|
{x1, y1, 0, 1, kDiscGrey, 0, 1, 1}};
|
||||||
|
reinterpret_cast<DrawPrimitiveFn>(vtable[kDeviceDrawPrimitive])(device, kTriangleStrip,
|
||||||
|
kFvfScreenColourTexture, quad, 4, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -174,3 +174,16 @@
|
|||||||
0x00731ee0 cUI_Control2::ClearFlags # (flags): bit 0 = visible (hide)
|
0x00731ee0 cUI_Control2::ClearFlags # (flags): bit 0 = visible (hide)
|
||||||
0x00aaaf00 g_dialogManager
|
0x00aaaf00 g_dialogManager
|
||||||
0x006360e0 cRecordCache::Get # map at +0xde5c, LRU (see docs/performance.md)
|
0x006360e0 cRecordCache::Get # map at +0xde5c, LRU (see docs/performance.md)
|
||||||
|
# Character panel, points (docs/respec.md)
|
||||||
|
0x006a5dd0 cUI_StatisticsChar::HandleEvent
|
||||||
|
0x006a9be0 cUI_StatisticsChar::Render
|
||||||
|
0x006ac120 cUI_StatisticsChar::Layout
|
||||||
|
0x00580d50 cCreatureArts::RaiseAttribute # (row): + of the attribute panel
|
||||||
|
0x005a03a0 cCreatureArts::AddAttributePoints # (index, delta), index 7 = free points
|
||||||
|
0x00582280 cCreatureArts::SkillIdAt # (row)
|
||||||
|
0x005822c0 cCreatureArts::SkillLevelAt # (row, boughtOnly)
|
||||||
|
0x00582360 cCreatureArts::AddSkillLevels # (row, delta, spendFreePoints)
|
||||||
|
0x00580f30 cCreatureArts::ChoosableSkills # (out[8]) -> count
|
||||||
|
0x00580fe0 cCreatureArts::ChooseSkill # (skill id)
|
||||||
|
0x00579560 cCreatureArts::Recalculate # (0, 1) after point changes
|
||||||
|
0x0072af90 cUI_Button::Render
|
||||||
Reference in new issue
Block a user