// The large map (M, cUI_Megamap) spans the whole render width. // // The game lays the map out for a 1024x768 screen: a scrollable view of the // 2048x2816 world map (256 px tiles, scroll position at +0xc016c / +0xc0170), // clipped and edge-scrolled at x 1024, centred at x 512, under a dark band // (inner edge 70 px) and a frame of child statics (corners, 32 px edge tiles). // Moving it as a centred window left the world visible at the sides; instead // the view constants are set to the render width, the frame's right side moves // 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. #include #include #include #include #include "game/addresses.h" #include "mods/hud.h" #include "mods/megamap.h" #include "mods/widescreen.h" #include "re/hook.h" #include "re/log.h" #include "re/patch.h" namespace mods::megamap { namespace { constexpr int kLayoutWidth = 1024; // cmp reg, 3FFh: cursor at the right edge (edge scrolling, cursor shape). constexpr uint32_t kRightEdgeCompares[] = {0x006c7625, 0x006c764c, 0x006c768c, 0x006c76d9, 0x006c76fe, 0x006c773a, 0x006c9dce, 0x006c9df4, 0x006c9e32}; // cmp reg, 400h: tiles right of the view are skipped (0x6c9aa3); a label that // would end right of it is placed left of its point (0x6ca2c6, 0x6ca2e5). constexpr uint32_t kWidthCompares[] = {0x006c9aa3, 0x006ca2c6, 0x006ca2e5}; // add eax, -400h: the largest scroll x is the map width minus the view. constexpr uint32_t kScrollLimit = 0x006c9edd; // The view centre 200h: the zoom-in frame (0x6ca53f) and the image centred // on it (0x6ca637, 0x6ca645). constexpr uint32_t kCentreImmediates[] = {0x006ca53f, 0x006ca637, 0x006ca645}; // fsub [512.0]: centring the scroll on the hero (opening the map 0x6c7bfe, // 0x6cb27a). constexpr uint32_t kCentreFloatUses[] = {0x006c7bfe, 0x006cb27a}; // The zoomed view (0x6cc950, cdecl) is drawn by the large map and by the TAB // overview map (cUI_OverviewMap 0x6c69d0, a centred window: shifted by the // HUD layer). Its centre: fld [512.0] (0x6ccb7f), quest markers fadd [512.0] // (0x6cf782), other markers fld [768.0] as an x, 768 - d = centre + 256 // (0x6ce156). It is set on every call from the window's draw offset. constexpr uint32_t kZoomView = 0x006cc950; constexpr uint32_t kZoomCentreUses[] = {0x006ccb7f, 0x006cf782}; constexpr uint32_t kZoomMarkerX = 0x006ce156; constexpr float kZoomMarkerFromCentre = 256.0f; // The dark band around the view (10 vertices, mov [esp+..], imm32): its right // outer edge x 1024 and inner edge x 954. constexpr uint32_t kBandRightOuter[] = {0x006ca774, 0x006ca808}; constexpr uint32_t kBandRightInner[] = {0x006ca7bc, 0x006ca84c}; constexpr int kBandWidth = 70; float g_halfWidth = 512.0f; float g_zoomCentre = 512.0f; float g_zoomMarkerX = 768.0f; uint32_t FloatBits(float value) { uint32_t bits; std::memcpy(&bits, &value, sizeof(bits)); return bits; } // UI control layout (relative to the parent at +0x50, if any). // Rectangles: x, y (int32), width, height (int16); a control's at +0x24. constexpr size_t kControlRect = 0x24; // cUI_Window2: child controls, a vector of pointers. constexpr size_t kChildrenBegin = 0x78; constexpr size_t kChildrenEnd = 0x7c; // The frame: left side x < 48, right side from x 976, top tiles above y 48, // bottom tiles from y 720 (measured). constexpr int kFrameLeft = 48; constexpr int kFrameRight = 976; constexpr int kFrameTop = 48; constexpr int kFrameBottom = 720; // Controls the map holds itself (absolute positions, drawn by its render): // two statics and two buttons, the zoom controls at the bottom centre. constexpr size_t kOwnControls[] = {0xc0440, 0xc04bc, 0xc0544, 0xc0600}; // Rectangle around them: the map passes only clicks inside it to the buttons // (its event handler, 0x6c7f32). constexpr size_t kZoomControlsRect = 0xc0538; int& RectX(uint8_t* rect) { return *reinterpret_cast(rect); } int RectY(uint8_t* rect) { return *reinterpret_cast(rect + 4); } int16_t& RectWidth(uint8_t* rect) { return *reinterpret_cast(rect + 8); } // The game lays the controls out once, when it creates the map; they are // placed for the render width once, too (the map is opened many times). struct Placement { int x; int16_t width; }; std::unordered_map g_placed; void Place(uint8_t* rect, int x, int width) { RectX(rect) = x; RectWidth(rect) = static_cast(width); g_placed[rect] = {x, static_cast(width)}; } bool AlreadyPlaced(uint8_t* rect) { const auto it = g_placed.find(rect); return it != g_placed.end() && it->second.x == RectX(rect) && it->second.width == RectWidth(rect); } // Frame pieces: the right side moves by `extra`, the top and bottom tiles // between the corners stretch over the wider span; anything else stays // centred. void PlaceFramePiece(uint8_t* rect, int extra) { const int x = RectX(rect); const int width = RectWidth(rect); const int y = RectY(rect); if (x + width <= kFrameLeft) { return; } if (x >= kFrameRight) { Place(rect, x + extra, width); return; } if (y < kFrameTop || y >= kFrameBottom) { const double scale = static_cast(kFrameRight - kFrameLeft + extra) / (kFrameRight - kFrameLeft); const int left = kFrameLeft + static_cast((x - kFrameLeft) * scale + 0.5); const int right = kFrameLeft + static_cast((x + width - kFrameLeft) * scale + 0.5); Place(rect, left, right - left); return; } Place(rect, x + extra / 2, width); } void PlaceCentred(uint8_t* rect, int extra) { if (!AlreadyPlaced(rect)) { Place(rect, RectX(rect) + extra / 2, RectWidth(rect)); } } void Layout(uint8_t* megamap) { const int extra = mods::widescreen::RenderWidth() - kLayoutWidth; if (extra <= 0) { return; } auto** child = *reinterpret_cast(megamap + kChildrenBegin); auto** end = *reinterpret_cast(megamap + kChildrenEnd); for (; child && child != end; ++child) { if (*child && !AlreadyPlaced(*child + kControlRect)) { PlaceFramePiece(*child + kControlRect, extra); } } for (size_t offset : kOwnControls) { PlaceCentred(megamap + offset + kControlRect, extra); } PlaceCentred(megamap + kZoomControlsRect, extra); uint8_t* window = megamap + kControlRect; if (!AlreadyPlaced(window) && RectX(window) == 0 && RectWidth(window) == kLayoutWidth) { Place(window, 0, kLayoutWidth + extra); } } } // namespace void PatchLayout(int width) { g_halfWidth = width / 2.0f; int failed = re::PatchAll(kRightEdgeCompares, 0x3ff, static_cast(width - 1)); failed += re::PatchAll(kWidthCompares, 0x400, static_cast(width)); failed += !re::PatchInstructionDword(kScrollLimit, static_cast(-kLayoutWidth), static_cast(-width)); failed += re::PatchAll(kCentreImmediates, 0x200, static_cast(width / 2)); for (uint32_t instruction : kCentreFloatUses) { failed += !re::RedirectOperand(instruction, game::addr::kScreenHalfWidthF, &g_halfWidth); } for (uint32_t instruction : kZoomCentreUses) { failed += !re::RedirectOperand(instruction, game::addr::kScreenHalfWidthF, &g_zoomCentre); } failed += !re::RedirectOperand(kZoomMarkerX, game::addr::kScreenHeightF, &g_zoomMarkerX); failed += re::PatchAll(kBandRightOuter, FloatBits(1024.0f), FloatBits(static_cast(width))); failed += re::PatchAll(kBandRightInner, FloatBits(1024.0f - kBandWidth), FloatBits(static_cast(width - kBandWidth))); re::Log("megamap: view width %d%s", width, failed ? " (some patches FAILED)" : ""); } // Before each zoomed view: its centre in the coordinates of the window that // draws it (the large map is unshifted: width / 2; a centred window, which // the HUD layer shifts: 512). void __cdecl SetZoomCentre() { const int width = std::max(mods::widescreen::RenderWidth(), kLayoutWidth); g_zoomCentre = width / 2.0f - static_cast(mods::hud::DrawOffsetX()); g_zoomMarkerX = g_zoomCentre + kZoomMarkerFromCentre; } } // namespace mods::megamap namespace game { // The zoomed view (0x6cc950, cdecl): sets its centre, then runs it unchanged. void ZoomView(); RE_HOOK(mods::megamap::kZoomView, ZoomView); __declspec(naked) void ZoomView() { __asm { pushad call mods::megamap::SetZoomCentre popad jmp dword ptr [ZoomView_original] } } // cUI_Megamap::Show (vf24 0x6c7930, the argument: show / hide). void __fastcall UiMegamap_Show(void* self, void* edx, int show); RE_HOOK(0x006c7930, UiMegamap_Show); void __fastcall UiMegamap_Show(void* self, void* edx, int show) { if (self && (show & 0xff)) { mods::megamap::Layout(static_cast(self)); } UiMegamap_Show_original(self, edx, show); } } // namespace game