Esc menu without "Exportieren"; export screen mod removed
- cUI_EscMenu::BuildLines hook: the UI_ESC_EXPORT line is removed, the lines below move up, the window shrinks by one line, centred. - game/ui/control.h: control vector helpers (ControlsIn, RemoveControl, SetControlAt, DeleteControl, ControlNamed), used by the main menu too. - The export screen is unreachable now: charexport.cpp and docs/char-export.md deleted. Spielen screen redesign noted in todo.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
618288e310
commit
e0d8d0ce7e
9 files changed
+105
-195
No files matched your search
@@ -67,7 +67,6 @@ add_library(sacredK SHARED
|
|||||||
src/mods/runetrade.cpp
|
src/mods/runetrade.cpp
|
||||||
src/mods/teleportpotion.cpp
|
src/mods/teleportpotion.cpp
|
||||||
src/mods/guards.cpp
|
src/mods/guards.cpp
|
||||||
src/mods/charexport.cpp
|
|
||||||
src/mods/options.cpp
|
src/mods/options.cpp
|
||||||
src/mods/lootfilter.cpp
|
src/mods/lootfilter.cpp
|
||||||
src/mods/userfolders.cpp
|
src/mods/userfolders.cpp
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# Character export screen (`src/mods/charexport.cpp`)
|
|
||||||
|
|
||||||
Request (2026-10-18): the export screen (Esc menu "Exportieren") was not
|
|
||||||
adapted to widescreen (layout and clicks shifted), slots could not be
|
|
||||||
cleared or overwritten; the screen and the save game window need black
|
|
||||||
beside the background picture.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
See `hud.md`, "Full-screen windows": the 3D scene was stretched across the
|
|
||||||
wide viewport; the window now draws with a viewport on the centred frame.
|
|
||||||
|
|
||||||
## cUI_Character
|
|
||||||
|
|
||||||
One window for the main menu's character selection / import and the export
|
|
||||||
screen (vtable 0x894fa8, render vf44 0x70f160, events vf10 0x70e860).
|
|
||||||
|
|
||||||
| Field | Meaning |
|
|
||||||
|---|---|
|
|
||||||
| +0x390 | mode: 4 selection, 5 export, 6 overwrite confirmation (7 import) |
|
|
||||||
| +0x394 | previous mode |
|
|
||||||
| +0x1b4 | 8 slot controls; hero id at +0x2f8 (0: empty) |
|
|
||||||
| +0x1d6 | selected slot (char, -1 none) |
|
|
||||||
| +0x3b4 | erase button ("UI_CHAR_ERASE_TOOLTIP") |
|
|
||||||
|
|
||||||
| Address | Function |
|
|
||||||
|---|---|
|
|
||||||
| 0x70c760 | SetMode (mode), ret 4; entering mode 5 reloads the heroes |
|
|
||||||
| 0x70c440 | SelectSlot (char slot), ret 4; shows the erase button in modes 4 / 7 only |
|
|
||||||
| 0x70d0f0 | ExecuteAction (action, slot, extra), ret 0xc: 6 export into slot (`Save\Hero%.2d.pax`), 9 export with confirmation if occupied |
|
|
||||||
| 0x70e650 | right-hand button: in mode 5 action 6 (empty slot) or 9 (occupied) |
|
|
||||||
| 0x7f01a0 | HeroSlots_DeleteHero (slot + 1), thiscall on 0x7d7c40(), ret 4 |
|
|
||||||
|
|
||||||
## Overwrite
|
|
||||||
|
|
||||||
Measured with a log: exporting to an occupied slot was already the game's own
|
|
||||||
path (action 9 -> mode 6 confirmation -> action 6). It only failed because
|
|
||||||
clicks hit the wrong place before the layout fix.
|
|
||||||
|
|
||||||
## Clear
|
|
||||||
|
|
||||||
The erase button is created for every mode, and its click handler (vf10)
|
|
||||||
does not check the mode: it opens the game's confirmation
|
|
||||||
(UI_CHAR_ERASE_CONFIRM, cUI_BusyDlg), whose answer (0x72087b) calls
|
|
||||||
DeleteHero. The mod shows the button in export mode whenever an occupied slot
|
|
||||||
is selected (after SetMode / SelectSlot). After DeleteHero, if the screen is
|
|
||||||
in export mode, it enters export mode again (reloads the heroes) and clears
|
|
||||||
the selection; otherwise the erased hero stayed until the next export.
|
|
||||||
+8
-1
@@ -258,6 +258,14 @@ flag +0x54 |= 0x40. The Esc menu line keeps its action (Show
|
|||||||
direct save while a game runs. Its text is patched at 0x6bc1f7 (id 0x41b is
|
direct save while a game runs. Its text is patched at 0x6bc1f7 (id 0x41b is
|
||||||
used by other windows too). Loading is from the main menu only.
|
used by other windows too). Loading is from the main menu only.
|
||||||
|
|
||||||
|
The Esc menu has no "Exportieren" (request 2026-10-20): `cUI_EscMenu::BuildLines`
|
||||||
|
(0x6bb870, called by the constructor and on an event) makes five
|
||||||
|
cUI_StaticText64 lines in the vector +0x158, 0x24 apart; each handles its own
|
||||||
|
events. After it the UI_ESC_EXPORT line is removed and deleted, the lines
|
||||||
|
below move up, and the window (0x108 x 0xc8) loses 0x24 of height, staying
|
||||||
|
centred. With it the export screen (cUI_Character mode 5) is unreachable;
|
||||||
|
its mod (charexport.cpp, an erase button for export slots) was deleted.
|
||||||
|
|
||||||
### Migration
|
### Migration
|
||||||
|
|
||||||
On first start (no `characters\` folder): after a backup in
|
On first start (no `characters\` folder): after a backup in
|
||||||
@@ -268,7 +276,6 @@ kept, the newest file overall is the shared hero.
|
|||||||
## Open
|
## Open
|
||||||
|
|
||||||
- Reset a difficulty.
|
- Reset a difficulty.
|
||||||
- Remove the export / import UI.
|
|
||||||
- Silent autosave every 10 minutes.
|
- Silent autosave every 10 minutes.
|
||||||
- Quickload (row 0) does not know characters.
|
- Quickload (row 0) does not know characters.
|
||||||
- Multiplayer (see `todo.md`).
|
- Multiplayer (see `todo.md`).
|
||||||
+10
-5
@@ -182,13 +182,10 @@ From the characters rework (`docs/characters.md`, request of 2026-10-19):
|
|||||||
- **Unlock difficulties by story completion.** Bronze and Silber stay open;
|
- **Unlock difficulties by story completion.** Bronze and Silber stay open;
|
||||||
difficulty d >= 2 opens when the story on d - 1 is completed. The counters
|
difficulty d >= 2 opens when the story on d - 1 is completed. The counters
|
||||||
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
|
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
|
||||||
travel in the save header (+0x88..+0x98) and the hero file. Today every
|
travel in the save header (+0x88..+0x98) and the hero file. Done
|
||||||
difficulty up to the highest played one is offered (and Silber).
|
2026-10-20 (docs/characters.md).
|
||||||
- **Reset a difficulty.** Remove one world from a character (the shared hero
|
- **Reset a difficulty.** Remove one world from a character (the shared hero
|
||||||
stays); a button / dialog entry in the load window.
|
stays); a button / dialog entry in the load window.
|
||||||
- **Remove the export / import UI.** The Esc menu's "Exportieren" and the
|
|
||||||
character screen's import (cUI_Character mode 5, `docs/char-export.md`):
|
|
||||||
characters replace exported heroes.
|
|
||||||
- **Silent autosave every 10 minutes** (a setting later) into the
|
- **Silent autosave every 10 minutes** (a setting later) into the
|
||||||
autosave file, without the busy dialog, like the direct save
|
autosave file, without the busy dialog, like the direct save
|
||||||
(`SaveOnTick`) but marked as an autosave.
|
(`SaveOnTick`) but marked as an autosave.
|
||||||
@@ -196,3 +193,11 @@ From the characters rework (`docs/characters.md`, request of 2026-10-19):
|
|||||||
current character, but the quickload event (0x116, engine +0x68 = row,
|
current character, but the quickload event (0x116, engine +0x68 = row,
|
||||||
flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the
|
flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the
|
||||||
current character's world (or its newer autosave).
|
current character's world (or its newer autosave).
|
||||||
|
|
||||||
|
## Savegames screen redesign ("Spielen")
|
||||||
|
|
||||||
|
Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen"
|
||||||
|
screen, characters.cpp): show a render of the selected character, use the
|
||||||
|
space far better. The user looks for 16:9 artwork so the screens can be
|
||||||
|
widened beyond the 1024 frame (now drawn in a centred 1024x768 viewport with
|
||||||
|
black sides, hud.cpp FrameScope).
|
||||||
@@ -2,13 +2,54 @@
|
|||||||
// cUI_Control2: flags at +0x10 (bit 0: visible; a hidden control still
|
// cUI_Control2: flags at +0x10 (bit 0: visible; a hidden control still
|
||||||
// renders but handles no events).
|
// renders but handles no events).
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
||||||
constexpr size_t kControlFlagsOffset = 0x10;
|
constexpr size_t kControlFlagsOffset = 0x10;
|
||||||
constexpr uint32_t kControlFlagVisible = 1;
|
constexpr uint32_t kControlFlagVisible = 1;
|
||||||
|
constexpr size_t kControlName = 0x30; // char[], set by 0x732090 (e.g. "UI_LABEL_NEWGAME")
|
||||||
|
|
||||||
|
inline bool ControlNamed(const void* control, const char* name) {
|
||||||
|
return std::strcmp(static_cast<const char*>(control) + kControlName, name) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows keep their lines / entries in game vectors of control pointers
|
||||||
|
// {begin, end, capacity end} at `offset` of the window.
|
||||||
|
inline std::vector<uint8_t*> ControlsIn(void* window, size_t offset) {
|
||||||
|
auto* field = static_cast<uint8_t*>(window) + offset;
|
||||||
|
auto** begin = *reinterpret_cast<uint8_t***>(field);
|
||||||
|
auto** end = *reinterpret_cast<uint8_t***>(field + 4);
|
||||||
|
return std::vector<uint8_t*>(begin, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes `control` out of the vector (the order of the others stays).
|
||||||
|
inline void RemoveControl(void* window, size_t offset, const void* control) {
|
||||||
|
auto* field = static_cast<uint8_t*>(window) + offset;
|
||||||
|
auto** begin = *reinterpret_cast<uint8_t***>(field);
|
||||||
|
auto**& end = *reinterpret_cast<uint8_t***>(field + 4);
|
||||||
|
auto** found = std::find(begin, end, control);
|
||||||
|
if (found != end) {
|
||||||
|
std::memmove(found, found + 1, (end - found - 1) * sizeof(*found));
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces the vector's entry `index`.
|
||||||
|
inline void SetControlAt(void* window, size_t offset, size_t index, uint8_t* control) {
|
||||||
|
(*reinterpret_cast<uint8_t***>(static_cast<uint8_t*>(window) + offset))[index] = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys a control made with the game's allocator (vf00, the scalar deleting
|
||||||
|
// destructor, e.g. cUI_StaticText64 0x754680); out of every list first.
|
||||||
|
inline void DeleteControl(void* control) {
|
||||||
|
using DeleteFn = void*(__fastcall*)(void*, void*, int);
|
||||||
|
reinterpret_cast<DeleteFn>((*static_cast<void***>(control))[0])(control, nullptr, 1);
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ControlShown(const void* control) {
|
inline bool ControlShown(const void* control) {
|
||||||
return (static_cast<const uint8_t*>(control)[kControlFlagsOffset] & kControlFlagVisible) != 0;
|
return (static_cast<const uint8_t*>(control)[kControlFlagsOffset] & kControlFlagVisible) != 0;
|
||||||
|
|||||||
@@ -1345,6 +1345,43 @@ int __fastcall UiManager_ReceiveEvent(void* self, void* edx, uint8_t* event) {
|
|||||||
constexpr uint32_t kEscMenuSaveLineText = 0x006bc1f7;
|
constexpr uint32_t kEscMenuSaveLineText = 0x006bc1f7;
|
||||||
constexpr uint32_t kSavegamesText = 0x41b;
|
constexpr uint32_t kSavegamesText = 0x41b;
|
||||||
|
|
||||||
|
// The Esc menu without "Exportieren" (request of 2026-10-20; exported heroes
|
||||||
|
// are gone). cUI_EscMenu::BuildLines (0x6bb870, thiscall, plain ret; called
|
||||||
|
// by the constructor 0x6bae60 and on an event, 0x6bc497) makes five
|
||||||
|
// cUI_StaticText64 lines (constructor 0x753470) in the vector at +0x158, 0x24
|
||||||
|
// apart (y 8 .. 0x98); each line handles its own events. The window is
|
||||||
|
// 0x108 x 0xc8, centred (0x6bae9b). The export line goes, the lines below it
|
||||||
|
// move up and the window loses one line's height, staying centred.
|
||||||
|
constexpr size_t kEscMenuLines = 0x158;
|
||||||
|
constexpr char kEscExportLine[] = "UI_ESC_EXPORT";
|
||||||
|
constexpr int kEscLinePitch = 0x24;
|
||||||
|
constexpr int16_t kEscMenuHeight = 0xc8;
|
||||||
|
|
||||||
|
void __fastcall EscMenu_BuildLines(uint8_t* self, void* edx);
|
||||||
|
RE_HOOK(0x006bb870, EscMenu_BuildLines);
|
||||||
|
void __fastcall EscMenu_BuildLines(uint8_t* self, void* edx) {
|
||||||
|
EscMenu_BuildLines_original(self, edx);
|
||||||
|
bool removed = false;
|
||||||
|
for (uint8_t* line : game::ControlsIn(self, kEscMenuLines)) {
|
||||||
|
auto& rect = *reinterpret_cast<game::UiRect*>(line + game::kControlRect);
|
||||||
|
if (removed) {
|
||||||
|
rect.y -= kEscLinePitch;
|
||||||
|
} else if (game::ControlNamed(line, kEscExportLine)) {
|
||||||
|
game::RemoveControl(self, kEscMenuLines, line);
|
||||||
|
game::DeleteControl(line);
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto& window = *reinterpret_cast<game::UiRect*>(self + game::kControlRect);
|
||||||
|
if (removed && window.h == kEscMenuHeight) {
|
||||||
|
window.h -= kEscLinePitch;
|
||||||
|
window.y += kEscLinePitch / 2;
|
||||||
|
}
|
||||||
|
if (!removed) {
|
||||||
|
re::Log("characters: Esc menu has no %s line", kEscExportLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The load window's second line of a row showed the (preview) file name; it
|
// The load window's second line of a row showed the (preview) file name; it
|
||||||
// shows the level and the chosen difficulty instead. Row texts are set every
|
// shows the level and the chosen difficulty instead. Row texts are set every
|
||||||
// frame (0x71d830): line 1 the save's name (+0x204), line 2 the file name (+4,
|
// frame (0x71d830): line 1 the save's name (+0x204), line 2 the file name (+4,
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
// Character export (Esc menu "Exportieren"): slots can be cleared.
|
|
||||||
//
|
|
||||||
// Request (2026-10-18): the export screen was not adapted to widescreen, and
|
|
||||||
// slots could not be cleared or overwritten. The layout is fixed in hud.cpp
|
|
||||||
// (FrameScope); overwriting turned out to be the game's own path once clicks
|
|
||||||
// hit the right slot (measured: action 9 on an occupied slot -> mode 6
|
|
||||||
// confirmation -> action 6 exports into it).
|
|
||||||
//
|
|
||||||
// cUI_Character serves the main menu's character selection and the export
|
|
||||||
// screen: mode at +0x390 (4 selection, 5 export, 6 overwrite confirmation),
|
|
||||||
// selected slot (char, -1 none) at +0x1d6, slot controls at +0x1b4 (8, hero
|
|
||||||
// id at +0x2f8, 0: empty). Its erase button (+0x3b4, "UI_CHAR_ERASE_TOOLTIP")
|
|
||||||
// is created for both, but SelectSlot (0x70c440) shows it only in modes 4 / 7
|
|
||||||
// and SetMode (0x70c760) hides it. The click handler (vf10 0x70e860) does not
|
|
||||||
// check the mode: it opens the game's confirmation (UI_CHAR_ERASE_CONFIRM, a
|
|
||||||
// cUI_BusyDlg), whose answer deletes the hero (0x7f01a0, slot + 1). So in
|
|
||||||
// export mode the button is shown whenever an occupied slot is selected.
|
|
||||||
//
|
|
||||||
// The export screen reloads its heroes when it enters export mode (SetMode 5,
|
|
||||||
// also after an export); after a delete nothing did, so the erased hero stayed
|
|
||||||
// until the next export (user, 2026-10-18). After a delete from the export
|
|
||||||
// screen it enters export mode again and the selection is cleared.
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
#include "game/ui/control.h"
|
|
||||||
#include "re/hook.h"
|
|
||||||
|
|
||||||
namespace mods::charexport {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr size_t kMode = 0x390;
|
|
||||||
constexpr size_t kSelectedSlot = 0x1d6;
|
|
||||||
constexpr size_t kSlots = 0x1b4;
|
|
||||||
constexpr size_t kSlotHeroId = 0x2f8;
|
|
||||||
constexpr size_t kEraseButton = 0x3b4;
|
|
||||||
constexpr int kSlotCount = 8;
|
|
||||||
constexpr int kModeExport = 5;
|
|
||||||
constexpr int kNoSlot = -1;
|
|
||||||
|
|
||||||
// The cUI_Character instance (one, owned by the UI manager), noted when its
|
|
||||||
// mode or selection changes.
|
|
||||||
void* g_window = nullptr;
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
T& Field(void* window, size_t offset) {
|
|
||||||
return *reinterpret_cast<T*>(static_cast<uint8_t*>(window) + offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SelectedSlotOccupied(void* window) {
|
|
||||||
const int slot = Field<int8_t>(window, kSelectedSlot);
|
|
||||||
if (slot < 0 || slot >= kSlotCount) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
void* control = Field<void*>(window, kSlots + slot * sizeof(void*));
|
|
||||||
return control && Field<uint32_t>(control, kSlotHeroId) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UpdateEraseButton(void* window) {
|
|
||||||
void* button = Field<void*>(window, kEraseButton);
|
|
||||||
if (button && Field<int>(window, kMode) == kModeExport) {
|
|
||||||
game::SetControlShown(button, SelectedSlotOccupied(window));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace mods::charexport
|
|
||||||
|
|
||||||
namespace game {
|
|
||||||
|
|
||||||
// cUI_Character::SetMode (0x70c760, thiscall (mode), ret 4).
|
|
||||||
void __fastcall UiCharacter_SetMode(void* self, void* edx, int mode);
|
|
||||||
RE_HOOK(0x0070c760, UiCharacter_SetMode);
|
|
||||||
void __fastcall UiCharacter_SetMode(void* self, void* edx, int mode) {
|
|
||||||
mods::charexport::g_window = self;
|
|
||||||
UiCharacter_SetMode_original(self, edx, mode);
|
|
||||||
mods::charexport::UpdateEraseButton(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cUI_Character::SelectSlot (0x70c440, thiscall (char slot, -1 none), ret 4).
|
|
||||||
void __fastcall UiCharacter_SelectSlot(void* self, void* edx, int slot);
|
|
||||||
RE_HOOK(0x0070c440, UiCharacter_SelectSlot);
|
|
||||||
void __fastcall UiCharacter_SelectSlot(void* self, void* edx, int slot) {
|
|
||||||
mods::charexport::g_window = self;
|
|
||||||
UiCharacter_SelectSlot_original(self, edx, slot);
|
|
||||||
mods::charexport::UpdateEraseButton(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeroSlots_DeleteHero (0x7f01a0, thiscall on 0x7d7c40() (slot + 1), ret 4;
|
|
||||||
// returns a bool in al), called by the erase confirmation's answer.
|
|
||||||
int __fastcall HeroSlots_DeleteHero(void* self, void* edx, int slotPlusOne);
|
|
||||||
RE_HOOK(0x007f01a0, HeroSlots_DeleteHero);
|
|
||||||
int __fastcall HeroSlots_DeleteHero(void* self, void* edx, int slotPlusOne) {
|
|
||||||
const int result = HeroSlots_DeleteHero_original(self, edx, slotPlusOne);
|
|
||||||
void* window = mods::charexport::g_window;
|
|
||||||
using mods::charexport::Field;
|
|
||||||
if (window && Field<int>(window, mods::charexport::kMode) == mods::charexport::kModeExport) {
|
|
||||||
UiCharacter_SetMode(window, nullptr, mods::charexport::kModeExport);
|
|
||||||
UiCharacter_SelectSlot(window, nullptr, mods::charexport::kNoSlot);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace game
|
|
||||||
+8
-35
@@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/ui/control.h"
|
||||||
#include "game/ui/tabs.h"
|
#include "game/ui/tabs.h"
|
||||||
#include "game/ui/text.h"
|
#include "game/ui/text.h"
|
||||||
#include "game/ui/texts.h"
|
#include "game/ui/texts.h"
|
||||||
@@ -30,54 +30,28 @@ namespace {
|
|||||||
|
|
||||||
constexpr size_t kMenuChildren = 0x90; // vector of all child controls
|
constexpr size_t kMenuChildren = 0x90; // vector of all child controls
|
||||||
constexpr size_t kMenuMainPage = 0x158; // vector of the main page's entries
|
constexpr size_t kMenuMainPage = 0x158; // vector of the main page's entries
|
||||||
constexpr size_t kEntryName = 0x30; // char[]
|
|
||||||
constexpr size_t kEntryTextId = 0x78;
|
constexpr size_t kEntryTextId = 0x78;
|
||||||
constexpr int kEntryPitch = 0x20; // as 0x712f30 places them
|
constexpr int kEntryPitch = 0x20; // as 0x712f30 places them
|
||||||
|
|
||||||
constexpr const char* kRemoved[] = {"UI_LABEL_NEWGAME", "UI_LABEL_NEWADDON"};
|
constexpr const char* kRemoved[] = {"UI_LABEL_NEWGAME", "UI_LABEL_NEWADDON"};
|
||||||
constexpr char kPlayEntry[] = "UI_LABEL_LOADGAME";
|
constexpr char kPlayEntry[] = "UI_LABEL_LOADGAME";
|
||||||
|
|
||||||
// A game vector of pointers {begin, end, capacity end}.
|
|
||||||
std::vector<uint8_t*> Entries(uint8_t* owner, size_t offset) {
|
|
||||||
auto** begin = *reinterpret_cast<uint8_t***>(owner + offset);
|
|
||||||
auto** end = *reinterpret_cast<uint8_t***>(owner + offset + 4);
|
|
||||||
return std::vector<uint8_t*>(begin, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Erase(uint8_t* owner, size_t offset, const uint8_t* item) {
|
|
||||||
auto** begin = *reinterpret_cast<uint8_t***>(owner + offset);
|
|
||||||
auto**& end = *reinterpret_cast<uint8_t***>(owner + offset + 4);
|
|
||||||
auto** found = std::find(begin, end, item);
|
|
||||||
if (found != end) {
|
|
||||||
std::memmove(found, found + 1, (end - found - 1) * sizeof(*found));
|
|
||||||
--end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetAt(uint8_t* owner, size_t offset, size_t index, uint8_t* item) {
|
|
||||||
(*reinterpret_cast<uint8_t***>(owner + offset))[index] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* Name(const uint8_t* entry) {
|
|
||||||
return reinterpret_cast<const char*>(entry + kEntryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t& EntryY(uint8_t* entry) {
|
int32_t& EntryY(uint8_t* entry) {
|
||||||
return reinterpret_cast<game::UiRect*>(entry + game::kControlRect)->y;
|
return reinterpret_cast<game::UiRect*>(entry + game::kControlRect)->y;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsRemoved(const uint8_t* entry) {
|
bool IsRemoved(const uint8_t* entry) {
|
||||||
return std::any_of(std::begin(kRemoved), std::end(kRemoved),
|
return std::any_of(std::begin(kRemoved), std::end(kRemoved),
|
||||||
[&](const char* name) { return std::strcmp(Name(entry), name) == 0; });
|
[&](const char* name) { return game::ControlNamed(entry, name); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void __fastcall MainMenu_Build(uint8_t* self, void* edx);
|
void __fastcall MainMenu_Build(uint8_t* self, void* edx);
|
||||||
RE_HOOK(0x00712f30, MainMenu_Build);
|
RE_HOOK(0x00712f30, MainMenu_Build);
|
||||||
void __fastcall MainMenu_Build(uint8_t* self, void* edx) {
|
void __fastcall MainMenu_Build(uint8_t* self, void* edx) {
|
||||||
MainMenu_Build_original(self, edx);
|
MainMenu_Build_original(self, edx);
|
||||||
const std::vector<uint8_t*> original = Entries(self, kMenuMainPage);
|
const std::vector<uint8_t*> original = game::ControlsIn(self, kMenuMainPage);
|
||||||
auto play = std::find_if(original.begin(), original.end(),
|
auto play = std::find_if(original.begin(), original.end(),
|
||||||
[](const uint8_t* entry) { return std::strcmp(Name(entry), kPlayEntry) == 0; });
|
[](const uint8_t* entry) { return game::ControlNamed(entry, kPlayEntry); });
|
||||||
if (original.empty() || play == original.end()) {
|
if (original.empty() || play == original.end()) {
|
||||||
re::Log("mainmenu: no %s entry, menu left as it is", kPlayEntry);
|
re::Log("mainmenu: no %s entry, menu left as it is", kPlayEntry);
|
||||||
return;
|
return;
|
||||||
@@ -99,14 +73,13 @@ void __fastcall MainMenu_Build(uint8_t* self, void* edx) {
|
|||||||
}
|
}
|
||||||
for (uint8_t* entry : original) {
|
for (uint8_t* entry : original) {
|
||||||
if (IsRemoved(entry)) {
|
if (IsRemoved(entry)) {
|
||||||
Erase(self, kMenuMainPage, entry);
|
game::RemoveControl(self, kMenuMainPage, entry);
|
||||||
Erase(self, kMenuChildren, entry);
|
game::RemoveControl(self, kMenuChildren, entry);
|
||||||
using DeleteFn = void*(__fastcall*)(void*, void*, int); // scalar deleting destructor
|
game::DeleteControl(entry);
|
||||||
reinterpret_cast<DeleteFn>((*reinterpret_cast<void***>(entry))[0])(entry, nullptr, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < kept.size(); ++i) {
|
for (size_t i = 0; i < kept.size(); ++i) {
|
||||||
SetAt(self, kMenuMainPage, i, kept[i]);
|
game::SetControlAt(self, kMenuMainPage, i, kept[i]);
|
||||||
y += keptGap[i];
|
y += keptGap[i];
|
||||||
EntryY(kept[i]) = y;
|
EntryY(kept[i]) = y;
|
||||||
y += kEntryPitch;
|
y += kEntryPitch;
|
||||||
|
|||||||
+1
-1
@@ -302,7 +302,7 @@
|
|||||||
0x006e7730 cUI_Tooltip::Layout # measures the text, width = longest line + 8 unless flag 0x100
|
0x006e7730 cUI_Tooltip::Layout # measures the text, width = longest line + 8 unless flag 0x100
|
||||||
0x0075ac70 cUI_Manager::ShowTooltip # (x, y, text, ?, flags, show)
|
0x0075ac70 cUI_Manager::ShowTooltip # (x, y, text, ?, flags, show)
|
||||||
|
|
||||||
# Character selection / export screen (src/mods/charexport.cpp)
|
# Character selection / export screen (cUI_Character)
|
||||||
0x0070c760 cUI_Character::SetMode # (mode), ret 4: +0x390 (4 selection, 5 export, 6 overwrite confirm)
|
0x0070c760 cUI_Character::SetMode # (mode), ret 4: +0x390 (4 selection, 5 export, 6 overwrite confirm)
|
||||||
0x0070c440 cUI_Character::SelectSlot # (char slot, -1 none), ret 4
|
0x0070c440 cUI_Character::SelectSlot # (char slot, -1 none), ret 4
|
||||||
0x0070d0f0 cUI_Character::ExecuteAction # (action, slot, extra), ret 0xc: 6 export into slot, 9 export (confirm if occupied)
|
0x0070d0f0 cUI_Character::ExecuteAction # (action, slot, extra), ret 0xc: 6 export into slot, 9 export (confirm if occupied)
|
||||||
|
|||||||
Reference in new issue
Block a user