Export screen: widescreen frame, black sides, slots can be cleared
- cUI_Character (export) and cUI_Savegame draw inside the centred 1024x768 frame (viewport) with the render target beside it cleared black; the export screen's 3D heroes were stretched across the wide viewport, away from their labels and hit areas. - Export screen shows the game's erase button for an occupied slot and reloads the heroes after a delete. Overwriting was already the game's own path once clicks hit the right slot. - game/ui/control.h: shared show/hide for cUI_Control2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c875d7d6b9
commit
72525cd5ba
9 files changed
+272
-13
No files matched your search
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
// cUI_Control2: flags at +0x10 (bit 0: visible; a hidden control still
|
||||
// renders but handles no events).
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace game {
|
||||
|
||||
constexpr size_t kControlFlagsOffset = 0x10;
|
||||
constexpr uint32_t kControlFlagVisible = 1;
|
||||
|
||||
inline bool ControlShown(const void* control) {
|
||||
return (static_cast<const uint8_t*>(control)[kControlFlagsOffset] & kControlFlagVisible) != 0;
|
||||
}
|
||||
|
||||
// cUI_Control2::SetFlags (0x731e70) / ClearFlags (0x731ee0), thiscall (flags),
|
||||
// ret 4; setting the visible flag calls the control's vf24 (show).
|
||||
inline void SetControlShown(void* control, bool shown) {
|
||||
using FlagFn = void(__fastcall*)(void*, void*, uint32_t);
|
||||
reinterpret_cast<FlagFn>(shown ? 0x00731e70 : 0x00731ee0)(control, nullptr, kControlFlagVisible);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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
|
||||
+2
-3
@@ -12,6 +12,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "game/ui/control.h"
|
||||
#include "mods/hud.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/log.h"
|
||||
@@ -39,15 +40,13 @@ constexpr int kEscMenuSlot = 0x2c;
|
||||
// ends the dialog's input lock, and passes answers below 100000 to the quest
|
||||
// script (0x46bd80). Esc does the same with an answer of 100000 or more: the
|
||||
// dialog closes and the heroes are free, nothing is chosen.
|
||||
constexpr uint32_t kHideVisible = 1;
|
||||
constexpr uint32_t kDialogManager = 0x00aaaf00;
|
||||
constexpr size_t kTooltipDialogOwner = 0x21cc;
|
||||
constexpr uint32_t kNoAnswer = 0xffffffff; // >= 100000: no script callback
|
||||
constexpr uint32_t kDialogPending = 0x00ab5314;
|
||||
|
||||
void CloseDialog(uint8_t* tooltip) {
|
||||
using HideFn = void(__fastcall*)(void*, void*, uint32_t);
|
||||
reinterpret_cast<HideFn>(0x00731ee0)(tooltip, nullptr, kHideVisible);
|
||||
game::SetControlShown(tooltip, false);
|
||||
using AnswerFn = void(__fastcall*)(void*, void*, uint32_t owner, uint32_t answer);
|
||||
reinterpret_cast<AnswerFn>(0x0049a090)(reinterpret_cast<void*>(kDialogManager), nullptr,
|
||||
*reinterpret_cast<uint32_t*>(tooltip + kTooltipDialogOwner), kNoAnswer);
|
||||
|
||||
+66
-2
@@ -430,6 +430,49 @@ HRESULT __stdcall ClearShifted(void* device, DWORD count, Rect7* rects, DWORD fl
|
||||
return g_clear(device, count, shifted.data(), flags, color, z, stencil);
|
||||
}
|
||||
|
||||
using GetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
|
||||
constexpr int kVtableGetViewport = 15; // IDirect3DDevice7 (13 SetViewport, 14 MultiplyTransform)
|
||||
constexpr DWORD kClearTarget = 1; // D3DCLEAR_TARGET
|
||||
|
||||
// RAII for full-screen windows (character export, save games) that fill the
|
||||
// 1024 frame with a picture: the render target left and right of the frame is
|
||||
// cleared black, and the window draws with a viewport on the frame. The
|
||||
// export screen's 3D scene (heroes, background) uses a projection for a
|
||||
// 1024x768 viewport; across the widened one it was stretched 1.8x wide, away
|
||||
// from the slot labels and hit areas (measured on a screenshot, 2026-10-18).
|
||||
// Clear clips to the viewport, so the sides are cleared before it is set.
|
||||
class FrameScope {
|
||||
public:
|
||||
FrameScope(void* window, void* device, int dx) {
|
||||
const auto* w = static_cast<const uint8_t*>(window);
|
||||
if (dx == 0 || g_drawScoped || !device || !g_setViewport || !g_clear || (w[kControlFlags] & 1) == 0) {
|
||||
return;
|
||||
}
|
||||
auto getViewport = reinterpret_cast<GetViewportFn>((*static_cast<void***>(device))[kVtableGetViewport]);
|
||||
if (FAILED(getViewport(device, &previous_))) {
|
||||
return;
|
||||
}
|
||||
const LONG height = static_cast<LONG>(previous_.height);
|
||||
Rect7 sides[] = {{0, 0, dx, height},
|
||||
{dx + kLayoutWidth, 0, mods::widescreen::RenderWidth(), height}};
|
||||
g_clear(device, 2, sides, kClearTarget, 0, 1.0f, 0);
|
||||
Viewport7 frame = {static_cast<DWORD>(dx), 0, kLayoutWidth, 768, previous_.minZ, previous_.maxZ};
|
||||
device_ = device;
|
||||
g_setViewport(device, &frame);
|
||||
}
|
||||
~FrameScope() {
|
||||
if (device_) {
|
||||
g_setViewport(device_, &previous_);
|
||||
}
|
||||
}
|
||||
FrameScope(const FrameScope&) = delete;
|
||||
FrameScope& operator=(const FrameScope&) = delete;
|
||||
|
||||
private:
|
||||
void* device_ = nullptr;
|
||||
Viewport7 previous_ = {};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int CursorOffsetX() {
|
||||
@@ -571,6 +614,27 @@ int TooltipOffset(void* tooltip) {
|
||||
return name##_original(self, edx, device, a, b); \
|
||||
}
|
||||
|
||||
// Centred full-screen windows: black beside the frame, viewport on the frame.
|
||||
#define HUD_RENDER_FRAMED(address, name) \
|
||||
int __fastcall name(void* self, void* edx, void* device); \
|
||||
RE_HOOK(address, name); \
|
||||
int __fastcall name(void* self, void* edx, void* device) { \
|
||||
const int dx = mods::hud::OffsetFor(mods::hud::Anchor::Center); \
|
||||
mods::hud::FrameScope frame(self, device, dx); \
|
||||
mods::hud::DrawOffset offset(self, dx); \
|
||||
return name##_original(self, edx, device); \
|
||||
}
|
||||
|
||||
#define HUD_RENDER3_FRAMED(address, name) \
|
||||
int __fastcall name(void* self, void* edx, void* device, int a, int b); \
|
||||
RE_HOOK(address, name); \
|
||||
int __fastcall name(void* self, void* edx, void* device, int a, int b) { \
|
||||
const int dx = mods::hud::OffsetFor(mods::hud::Anchor::Center); \
|
||||
mods::hud::FrameScope frame(self, device, dx); \
|
||||
mods::hud::DrawOffset offset(self, dx); \
|
||||
return name##_original(self, edx, device, a, b); \
|
||||
}
|
||||
|
||||
namespace game {
|
||||
|
||||
using mods::hud::Anchor;
|
||||
@@ -629,11 +693,11 @@ HUD_RENDER(0x006ea750, UiTrade_Render, Anchor::Center)
|
||||
HUD_EVENT(0x006f8960, UiNetworkInfo_HandleEvent, Anchor::Center)
|
||||
HUD_RENDER(0x006f73f0, UiNetworkInfo_Render, Anchor::Center)
|
||||
HUD_EVENT(0x0070e860, UiCharacter_HandleEvent, Anchor::Center)
|
||||
HUD_RENDER3(0x0070f160, UiCharacter_Render, Anchor::Center)
|
||||
HUD_RENDER3_FRAMED(0x0070f160, UiCharacter_Render) // export screen in-game
|
||||
HUD_EVENT(0x00716b30, UiOptions_HandleEvent, Anchor::Center)
|
||||
HUD_RENDER3(0x00716640, UiOptions_Render, Anchor::Center)
|
||||
HUD_EVENT(0x0071d130, UiSavegame_HandleEvent, Anchor::Center)
|
||||
HUD_RENDER(0x0071dd80, UiSavegame_Render, Anchor::Center)
|
||||
HUD_RENDER_FRAMED(0x0071dd80, UiSavegame_Render)
|
||||
HUD_EVENT(0x00720c10, UiBusyDlg_HandleEvent, Anchor::Center)
|
||||
HUD_RENDER(0x007213a0, UiBusyDlg_Render, Anchor::Center)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "game/engine/items.h"
|
||||
#include "game/engine/objects.h"
|
||||
#include "game/engine/orders.h"
|
||||
#include "game/ui/control.h"
|
||||
#include "game/ui/events.h"
|
||||
#include "game/ui/tabs.h"
|
||||
#include "game/ui/text.h"
|
||||
@@ -184,12 +185,6 @@ void Select(uint8_t* window, uint32_t mode) {
|
||||
reinterpret_cast<SelectFn>(0x006d3fd0)(window, nullptr, HeroType(), mode); // doLottoSelect
|
||||
}
|
||||
|
||||
// Shows a control (0x731e70) / hides it (0x731ee0); flag 1 = visible.
|
||||
void SetShown(uint8_t* control, bool shown) {
|
||||
using FlagFn = void(__fastcall*)(void*, void*, uint32_t);
|
||||
reinterpret_cast<FlagFn>(shown ? 0x00731e70 : 0x00731ee0)(control, nullptr, kControlVisible);
|
||||
}
|
||||
|
||||
// Chooses the hero's last bought rune (steps through the choices).
|
||||
void ChooseRemembered(uint8_t* window) {
|
||||
const int hero = HeroIndex();
|
||||
@@ -373,8 +368,8 @@ void UpdateChoice(uint8_t* window) {
|
||||
return;
|
||||
}
|
||||
updating = true;
|
||||
SetShown(window + kNextArrow, true);
|
||||
SetShown(window + kPreviousArrow, true);
|
||||
game::SetControlShown(window + kNextArrow, true);
|
||||
game::SetControlShown(window + kPreviousArrow, true);
|
||||
if (Field<uint32_t>(window, kChosenRune) == 0) {
|
||||
Select(window, kSelectKeep);
|
||||
ChooseRemembered(window);
|
||||
|
||||
Reference in new issue
Block a user