From 037fc73158b55c2527ca5da597863a4b087f568a Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Fri, 16 Oct 2026 01:44:20 +0200 Subject: [PATCH] Character panel: minus buttons take back attribute and skill points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- CMakeLists.txt | 1 + docs/respec.md | 54 ++++++++ src/game/ui/events.h | 30 +++++ src/mods/hud.cpp | 33 ++--- src/mods/hud.h | 6 + src/mods/respec.cpp | 290 +++++++++++++++++++++++++++++++++++++++++++ tools/names.txt | 13 ++ 7 files changed, 413 insertions(+), 14 deletions(-) create mode 100644 docs/respec.md create mode 100644 src/game/ui/events.h create mode 100644 src/mods/respec.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4919bcd..1fa9dea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ add_library(sacred_re SHARED src/mods/escape.cpp src/mods/recordcache.cpp src/mods/megamap.cpp + src/mods/respec.cpp ) target_include_directories(sacred_re PRIVATE src) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) diff --git a/docs/respec.md b/docs/respec.md new file mode 100644 index 0000000..d88adbf --- /dev/null +++ b/docs/respec.md @@ -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. diff --git a/src/game/ui/events.h b/src/game/ui/events.h new file mode 100644 index 0000000..4b526cf --- /dev/null +++ b/src/game/ui/events.h @@ -0,0 +1,30 @@ +#pragma once +// UI events (cEvent subclasses handed to the windows' vf10). + +#include + +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((*static_cast(event))[1]); + const int kind = category(event, nullptr); + return kind >= 1 && kind <= 3 ? static_cast(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(static_cast(event) + offset); +} + +} // namespace game diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index c235943..7d441c3 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -29,6 +29,7 @@ #include "game/addresses.h" #include "game/render/dx_driver7.h" +#include "game/ui/events.h" #include "mods/widescreen.h" #include "re/config.h" #include "re/hook.h" @@ -262,22 +263,17 @@ private: CursorOffset cursor_; }; -// Mouse events, by 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: x, y at +8 / +0xc -// Stores the offsets of the x fields; returns their count (0: no mouse event). +// Stores the offsets of a mouse event's x fields (game/ui/events.h); returns +// their count (0: no mouse event). int MouseEventXOffsets(void* event, int (&offsets)[2]) { - using CategoryFn = int(__fastcall*)(void* self, void* edx); - auto category = reinterpret_cast((*static_cast(event))[1]); - switch (category(event, nullptr)) { - case 1: + switch (game::MouseEventKind(event)) { + case game::MouseEvent::Move: offsets[0] = 0x4; offsets[1] = 0xc; return 2; - case 2: - case 3: - offsets[0] = 0x8; + case game::MouseEvent::ButtonDown: + case game::MouseEvent::ButtonUp: + offsets[0] = game::kMouseButtonX; return 1; default: 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_EVENT(0x006a4140, UiBlacksmith_HandleEvent, Anchor::Center) HUD_RENDER3(0x006a3160, UiBlacksmith_Render, Anchor::Center) -HUD_EVENT(0x006a5dd0, UiStatisticsChar_HandleEvent, Anchor::Center) -HUD_RENDER(0x006a9be0, UiStatisticsChar_Render, Anchor::Center) +// cUI_StatisticsChar (0x6a5dd0 / 0x6a9be0): hooked by mods/respec.cpp. HUD_EVENT(0x006acc70, UiChest_HandleEvent, Anchor::Center) HUD_RENDER(0x006ac8b0, UiChest_Render, Anchor::Center) HUD_EVENT(0x006adc70, UiConsole_HandleEvent, Anchor::Center) @@ -688,6 +683,16 @@ int VisibleDialogTooltips(void** out, int 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 game { diff --git a/src/mods/hud.h b/src/mods/hud.h index c978e1e..7c2cf6c 100644 --- a/src/mods/hud.h +++ b/src/mods/hud.h @@ -17,4 +17,10 @@ void* CursorFor(void* cursor); // visible while no moved window was under the cursor) and are visible now. 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 diff --git a/src/mods/respec.cpp b/src/mods/respec.cpp new file mode 100644 index 0000000..36f8c4d --- /dev/null +++ b/src/mods/respec.cpp @@ -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 +#include + +#include + +#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(0x005a03a0); +const auto AddSkillLevels = reinterpret_cast(0x00582360); +const auto Recalculate = reinterpret_cast(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(panel + kPanelFlags) & kPanelShowsAttributes) != 0; +} + +uint8_t* HeroArts() { + auto* hero = static_cast(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(image); + *reinterpret_cast(button + texture) = *reinterpret_cast(descriptor + kImageTexture); + for (int i = 0; i < 4; ++i) { + *reinterpret_cast(button + uvs + 4 * i) = + *reinterpret_cast(descriptor + kImageUvOrder[i]); + } +} + +bool Visible(uint8_t* control) { + return (*reinterpret_cast(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(minus + kControlParent) != panel) { + *reinterpret_cast(minus + kControlParent) = panel; + CopyImage(minus, kButtonImage, kButtonTexture, kPlusImage); + CopyImage(minus, kButtonPressedImage, kButtonPressedTexture, kPlusPressedImage); + } + std::memcpy(minus + kControlRect, plus + kControlRect, 12); + *reinterpret_cast(minus + kControlRect) = kMinusX; + auto& flags = *reinterpret_cast(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(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(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(device); + using SetTextureFn = HRESULT(__stdcall*)(void*, DWORD, void*); + using DrawPrimitiveFn = HRESULT(__stdcall*)(void*, DWORD, DWORD, void*, DWORD, DWORD); + reinterpret_cast(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(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(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((*reinterpret_cast(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(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((*reinterpret_cast(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(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(self), device, UiStatisticsChar_Render_original}; + return mods::hud::RenderCentred(self, &mods::respec::RenderBody, &call); +} + +} // namespace game diff --git a/tools/names.txt b/tools/names.txt index cdbe972..0d94be7 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -174,3 +174,16 @@ 0x00731ee0 cUI_Control2::ClearFlags # (flags): bit 0 = visible (hide) 0x00aaaf00 g_dialogManager 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