- megamap: the zoomed view (0x6cc950) is shared by the large map and the TAB overview map; its centre is now set per call from the drawing window's offset (hud::DrawOffsetX) instead of fixed to the render width - input: drop the Ctrl/Alt pass-through; GetKeyState reports Alt as held after Alt+Tab, which switched all bindings back to the game's handling Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
228 lines
9.0 KiB
C++
228 lines
9.0 KiB
C++
// 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 <algorithm>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <unordered_map>
|
|
|
|
#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<int*>(rect);
|
|
}
|
|
int RectY(uint8_t* rect) {
|
|
return *reinterpret_cast<int*>(rect + 4);
|
|
}
|
|
int16_t& RectWidth(uint8_t* rect) {
|
|
return *reinterpret_cast<int16_t*>(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<const void*, Placement> g_placed;
|
|
|
|
void Place(uint8_t* rect, int x, int width) {
|
|
RectX(rect) = x;
|
|
RectWidth(rect) = static_cast<int16_t>(width);
|
|
g_placed[rect] = {x, static_cast<int16_t>(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<double>(kFrameRight - kFrameLeft + extra) / (kFrameRight - kFrameLeft);
|
|
const int left = kFrameLeft + static_cast<int>((x - kFrameLeft) * scale + 0.5);
|
|
const int right = kFrameLeft + static_cast<int>((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<uint8_t***>(megamap + kChildrenBegin);
|
|
auto** end = *reinterpret_cast<uint8_t***>(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<uint32_t>(width - 1));
|
|
failed += re::PatchAll(kWidthCompares, 0x400, static_cast<uint32_t>(width));
|
|
failed += !re::PatchInstructionDword(kScrollLimit, static_cast<uint32_t>(-kLayoutWidth),
|
|
static_cast<uint32_t>(-width));
|
|
failed += re::PatchAll(kCentreImmediates, 0x200, static_cast<uint32_t>(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<float>(width)));
|
|
failed += re::PatchAll(kBandRightInner, FloatBits(1024.0f - kBandWidth),
|
|
FloatBits(static_cast<float>(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<float>(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<uint8_t*>(self));
|
|
}
|
|
UiMegamap_Show_original(self, edx, show);
|
|
}
|
|
|
|
} // namespace game
|