Megamap: the zoom button zooms on the hero and follows him

The zoom button never set the zoom point, so it zoomed on the last
magnifier point (or 0,0). While the button's zoom is on, the point is put on
the hero's map pixel every engine tick; the held mouse button still zooms on
the mouse. game::UiWindow / UiWindowShown shared from windows.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-25 11:54:59 +01:00
1 parent 1d16fc65c4
commit c7d54a6cfd
6 files changed
+98 -20

No files matched your search

+13
View File
@@ -118,6 +118,19 @@ game lays them out at creation only).
A window anchored `Screen` still counts as the outermost scope for events, so
its child buttons (hooked as standalone controls, `Center`) don't shift again.
### Zoom on the hero (2026-10-25)
Reported: the zoomed view did not start centred on the hero. Read: the map
zooms on its zoom point +0xc0438 / +0xc043c (screen) plus the scroll
+0xc016c / +0xc0170 (render 0x6ca4d6). Holding the mouse button on the map
(+0xc06bc, 0x6c8274) moves the point with the mouse (outside the zoom
controls, 0x6ca47b); the zoom button (flag 0x100 in +0xc0154, 0x6c7fac)
never sets it, so it zoomed on the last held point, or 0,0 after the map was
created. Now, while the button zoom is on and the mouse is not held, every
engine tick puts the point on the hero: map pixel by 0x6cb130 (thiscall
(out, world x/y/level floats), ret 8; as opening the map centres on the hero,
0x6c7bad..) minus the scroll. The held zoom still follows the mouse.
## Viewports (cursor in the quest book, 2026-10-17)
`SetViewport` inside a moved window is shifted like its vertices. The quest
-8
View File
@@ -230,14 +230,6 @@ Request: NPCs related to a quest are drawn white on the minimaps and always
turn towards the player; once their quest is finished they should stop
turning and be drawn blue (like other NPCs).
## Megamap: zoom not centred on the hero (2026-10-25)
Reported: the zoomed view does not start centred on the hero. The game zooms
on the large map's zoom point +0xc0438 (the mouse position, updated while the
mouse is outside the zoom buttons +0xc0538; 0x6ca47b) plus the scroll. Open:
how the user zoomed (buttons / key / click); then zoom on the hero for
buttons and keys.
## Inventory drawn without its dark backgrounds (2026-10-25, intermittent)
Reported twice: on opening the inventory the item list had no dark
+14
View File
@@ -13,6 +13,20 @@ inline uint32_t* UiManager() {
return *reinterpret_cast<uint32_t**>(kUiManager);
}
// The UI manager's window pointers by slot (dwords).
constexpr int kUiSlotMegamap = 0x25; // cUI_Megamap, UI_WND_MEGAMAP
inline uint8_t* UiWindow(const uint32_t* manager, int slot) {
return manager ? reinterpret_cast<uint8_t*>(static_cast<uintptr_t>(manager[slot])) : nullptr;
}
// The window in `slot` exists and is visible (control flags +0x10 bit 0).
inline bool UiWindowShown(const uint32_t* manager, int slot) {
constexpr size_t kControlFlags = 0x10;
const uint8_t* window = UiWindow(manager, slot);
return window && (*reinterpret_cast<const uint32_t*>(window + kControlFlags) & 1) != 0;
}
// cEventUI2 window commands (code at +4, subtype 5 at +8, names at +0x28): the
// UI manager's event handler (vf00, 0x757120) routes them by window name.
// Measured: HandleEsc hides with 0x12 (docs/escape.md); the S hotkey
+4 -12
View File
@@ -261,7 +261,6 @@ void OnTick(uint8_t* engine) {
// letter switch (upper-case character - 'C': index bytes at 0x75868c, targets
// at 0x758660) sends M to index 10, "no hotkey", and the closing branch
// compares with -1 instead of 'M' (cmp eax, 4Dh at 0x757f86).
constexpr int kMegamapSlot = 0x25;
constexpr int kMapBlockingSlots[] = {0x2b, 0x2c, 0x2f};
constexpr uint32_t kMapBlockedFlag = 0x017e82c4;
constexpr uint32_t kMapDisabledFlag = 0x017e8264;
@@ -270,32 +269,25 @@ constexpr uint32_t kUiLetterIndices = 0x0075868c;
constexpr uint8_t kUiLetterMap = 5;
constexpr uint8_t kUiNoHotkey = 10;
constexpr uint32_t kUiMapCloseCompare = 0x00757f86;
constexpr size_t kControlFlagsOffset = 0x10; // bit 0: visible
bool WindowShown(const uint32_t* ui, int slot) {
const auto* window = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(ui[slot]));
return window && (*reinterpret_cast<const uint32_t*>(window + kControlFlagsOffset) & 1) != 0;
}
bool Flag(uint32_t address) {
return *reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(address)) != 0;
}
void ToggleMap() {
uint32_t* ui = game::UiManager();
if (!ui || !ui[kMegamapSlot]) {
if (!ui || !ui[game::kUiSlotMegamap]) {
return;
}
if (WindowShown(ui, kMegamapSlot)) {
if (game::UiWindowShown(ui, game::kUiSlotMegamap)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Hide);
return;
}
for (const int slot : kMapBlockingSlots) {
if (WindowShown(ui, slot)) {
if (game::UiWindowShown(ui, slot)) {
return;
}
}
if (!Flag(kMapBlockedFlag) && !Flag(kMapDisabledFlag) && !WindowShown(ui, kMapDisabledSlot)) {
if (!Flag(kMapBlockedFlag) && !Flag(kMapDisabledFlag) && !game::UiWindowShown(ui, kMapDisabledSlot)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Show);
}
}
+63
View File
@@ -9,6 +9,11 @@
// to the right screen edge and its top and bottom tiles stretch across, and
// the zoom buttons at the bottom stay centred. The window is drawn and clicked
// unshifted (mods/hud.cpp, Anchor::Screen). See docs/hud.md.
//
// Zoom (request of 2026-10-25: the zoomed view did not start centred on the
// hero): held mouse button zooms on the mouse (the game's magnifier); the zoom
// button now zooms on the hero and follows him (the game kept the last
// magnifier point, or 0,0).
#include <algorithm>
#include <cstdint>
@@ -16,6 +21,10 @@
#include <unordered_map>
#include "game/addresses.h"
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "game/ui/windows.h"
#include "mods/hud.h"
#include "mods/megamap.h"
#include "mods/widescreen.h"
@@ -71,6 +80,60 @@ constexpr uint32_t kBandRightOuter[] = {0x006ca774, 0x006ca808};
constexpr uint32_t kBandRightInner[] = {0x006ca7bc, 0x006ca84c};
constexpr int kBandWidth = 70;
// The zoom (cUI_Megamap's render 0x6c9ce0, 0x6ca442..0x6ca5cf): flags +0xc0154
// bit 0x100 set by the zoom button (0x6c7fac); +0xc06bc set while the mouse
// button is held on the map (0x6c8274). The zoom point +0xc0438 / +0xc043c
// (screen) follows the mouse only while held; the view zooms on point + scroll
// (+0xc016c / +0xc0170), the map pixel.
constexpr size_t kMapFlags = 0xc0154;
constexpr uint32_t kZoomedByButton = 0x100;
constexpr size_t kZoomHeld = 0xc06bc;
constexpr size_t kZoomPointX = 0xc0438;
constexpr size_t kZoomPointY = 0xc043c;
constexpr size_t kScrollX = 0xc016c;
constexpr size_t kScrollY = 0xc0170;
template <typename T>
T& Field(uint8_t* p, size_t offset) {
return *reinterpret_cast<T*>(p + offset);
}
// cUI_Megamap 0x6cb130 (thiscall (out x, y, 0, world x, y, level), ret 8): the
// map pixel of a world position, as opening the map centres on the hero
// (0x6c7bad..).
struct Vec3 {
float x, y, z;
};
Vec3 MapPixel(uint8_t* megamap, const Vec3& world) {
Vec3 pixel = {};
using Fn = void(__fastcall*)(uint8_t*, void*, Vec3*, const Vec3*);
reinterpret_cast<Fn>(0x006cb130)(megamap, nullptr, &pixel, &world);
return pixel;
}
// Every tick while the zoom button's zoom is on: the zoom point on the hero.
void ZoomOnHero(uint8_t*) {
uint8_t* megamap = game::UiWindow(game::UiManager(), game::kUiSlotMegamap);
if (!game::UiWindowShown(game::UiManager(), game::kUiSlotMegamap) ||
!(Field<uint32_t>(megamap, kMapFlags) & kZoomedByButton) || Field<uint8_t>(megamap, kZoomHeld)) {
return;
}
auto* hero = static_cast<uint8_t*>(game::GetPlayerHero());
if (!hero) {
return;
}
const Vec3 world = {static_cast<float>(game::CreatureField(hero, game::kCreatureX)),
static_cast<float>(game::CreatureField(hero, game::kCreatureY)),
static_cast<float>(hero[game::kObjectLevel])};
const Vec3 pixel = MapPixel(megamap, world);
Field<int32_t>(megamap, kZoomPointX) = static_cast<int32_t>(pixel.x) - Field<int32_t>(megamap, kScrollX);
Field<int32_t>(megamap, kZoomPointY) = static_cast<int32_t>(pixel.y) - Field<int32_t>(megamap, kScrollY);
}
struct Registration {
Registration() { game::OnEngineTick(&ZoomOnHero); }
} g_registration;
float g_halfWidth = 512.0f;
float g_zoomCentre = 512.0f;
float g_zoomMarkerX = 768.0f;
+4
View File
@@ -404,3 +404,7 @@
0x00622e30 Sector_Region # thiscall: region number (sector +0x17c -> +0xd7)
0x00425370 Region_NameKey # cdecl (char region): name text key from the region name table 0xaa8ca8, or null
0x00425300 Region_GetInfo # cdecl (char region, out, out): table +0x38 / +0x30 of the region's record
0x006cb130 cUI_Megamap::WorldToMapPixel # thiscall (out vec3, world vec3 x/y/level), ret 8: x * 0.007446 + 1018.75, y * 0.014914 - 186.95 (after 0x622700)
0x0054fa30 cCreature::IsQuestNpc # thiscall, plain ret: +0x200 bit 0x100 / 0x200, or dialog +0x245 of a type < 50; white on the zoomed map view
0x00549790 cCreature::GetDialog # thiscall: dialog +0x245, or a random NPC chatter text (returns 0xd431)
0x0049a180 cDialogManager::DialogType # thiscall (dialog, creature), ret 8: record +0x48 of 0x50-byte records at +0x755c; creature flags 0x1000.. -> 0x20..0x23