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