From c7d54a6cfdae494dbe66b85b5e9535c247f34b23 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Sun, 25 Oct 2026 11:54:59 +0100 Subject: [PATCH] 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) --- docs/hud.md | 13 +++++++++ docs/todo.md | 8 ------ src/game/ui/windows.h | 14 ++++++++++ src/mods/input.cpp | 16 +++-------- src/mods/megamap.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++ tools/names.txt | 4 +++ 6 files changed, 98 insertions(+), 20 deletions(-) diff --git a/docs/hud.md b/docs/hud.md index 1afa1e7..c54f277 100644 --- a/docs/hud.md +++ b/docs/hud.md @@ -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 diff --git a/docs/todo.md b/docs/todo.md index b5d62d3..45fae14 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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 diff --git a/src/game/ui/windows.h b/src/game/ui/windows.h index 46855fb..e1e9514 100644 --- a/src/game/ui/windows.h +++ b/src/game/ui/windows.h @@ -13,6 +13,20 @@ inline uint32_t* UiManager() { return *reinterpret_cast(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(static_cast(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(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 diff --git a/src/mods/input.cpp b/src/mods/input.cpp index 8371bf1..575085a 100644 --- a/src/mods/input.cpp +++ b/src/mods/input.cpp @@ -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(static_cast(ui[slot])); - return window && (*reinterpret_cast(window + kControlFlagsOffset) & 1) != 0; -} - bool Flag(uint32_t address) { return *reinterpret_cast(static_cast(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); } } diff --git a/src/mods/megamap.cpp b/src/mods/megamap.cpp index 50642bd..2842419 100644 --- a/src/mods/megamap.cpp +++ b/src/mods/megamap.cpp @@ -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 #include @@ -16,6 +21,10 @@ #include #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 +T& Field(uint8_t* p, size_t offset) { + return *reinterpret_cast(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(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(megamap, kMapFlags) & kZoomedByButton) || Field(megamap, kZoomHeld)) { + return; + } + auto* hero = static_cast(game::GetPlayerHero()); + if (!hero) { + return; + } + const Vec3 world = {static_cast(game::CreatureField(hero, game::kCreatureX)), + static_cast(game::CreatureField(hero, game::kCreatureY)), + static_cast(hero[game::kObjectLevel])}; + const Vec3 pixel = MapPixel(megamap, world); + Field(megamap, kZoomPointX) = static_cast(pixel.x) - Field(megamap, kScrollX); + Field(megamap, kZoomPointY) = static_cast(pixel.y) - Field(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; diff --git a/tools/names.txt b/tools/names.txt index a563332..204e58c 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -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