Map: zoomed-view symbols centred, 10x reveal and enemy range; progress screen on the menu background

- Megamap / TAB map zoomed view: heroes' markers and the other symbols
  (integer 200h centre immediates) follow the view's centre.
- Map reveal: the hero's reveal stamp scaled 10x (mods/mapreveal.cpp,
  docs/map-reveal.md); creatures on the zoomed map shown within 10x the
  sight range.
- Load / save progress screen drawn on the rock background across the full
  width, the picture blended over it (mods/loadingscreen.cpp); the classic
  1024x768 frame is gone.

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 02:07:59 +02:00
1 parent 3f03228c29
commit b452558122
18 files changed
+435 -40

No files matched your search

+2
View File
@@ -74,8 +74,10 @@ add_library(sacredK SHARED
src/mods/screen.cpp
src/mods/characters.cpp
src/mods/mainmenu.cpp
src/mods/mapreveal.cpp
src/mods/questfail.cpp
src/mods/anticheat.cpp
src/mods/loadingscreen.cpp
)
target_include_directories(sacredK PRIVATE src)
target_compile_options(sacredK PRIVATE ${SACREDK_CXX_FLAGS})
+2
View File
@@ -101,6 +101,8 @@ widened instead, when the render size is applied (`PatchLayout`):
| view centre (zoom frame, centred image) | 0x6ca53f, 0x6ca637, 0x6ca645 | 0x200 | width / 2 |
| scroll centred on the hero; zoomed view; its quest markers | `[0x88e6fc]` (512.0) at 0x6c7bfe, 0x6cb27a, 0x6ccb7f, 0x6cf782 | 512 | own float |
| zoomed view: other markers at 768 − d (centre + 256) | `[0x890040]` (768.0) at 0x6ce156 | 768 | width / 2 + 256 |
| zoomed view: heroes' markers, 6 px quad at d + 509 / 515 (2026-10-25) | `[0x894780]` (509.0) at 0x6cf1bd, `[0x894778]` (515.0) at 0x6cf1dd | 512 ± 3 | centre ± 3 |
| zoomed view: other symbols (quad, icons, labels) as integers (2026-10-25) | mov / add reg, 200h at 0x6ced82, 0x6cedb0, 0x6cf47a, 0x6cf4ac, 0x6cf5e4 | 0x200 | centre, rewritten when it changes |
| dark band, right outer / inner edge | vertex immediates 0x6ca774, 0x6ca808 / 0x6ca7bc, 0x6ca84c | 1024 / 954 | width / width − 70 |
The frame is child statics of the window (vector at +0x78..+0x7c; measured:
+44
View File
@@ -0,0 +1,44 @@
# Map reveal (fog of war)
## Request (2026-10-25)
"Can we increase the base vision range (for map fog of war reveal) by x10 or
so?"
## Measured
- Each sector (64x64 tiles) keeps a 32x32 bit mask at +0x180 (128 bytes, one
bit per 2x2 tiles): `Sector_SetExplored` 0x622bf0, copied out by 0x622c60,
loaded from the save by 0x622c80 (0x606ae0). The zoomed map view turns it
into its fog texture (FOGOFWAR01.TGA, 0x639030).
- `World_SetExplored` 0x638f00 (thiscall on `*0xad14e0`, (tile x, tile y,
explored), ret 0xc) finds the tile's sector; tiles >= 0x2000 and sectors
that aren't loaded are ignored.
- `RevealAroundHero` 0x599730 (creature thiscall (tile x, tile y, sight), ret
0xc), called from about ten places in the creature code: only for the local
hero, only when its tile changed (last tile 0xad2384 / 0xad2388), it calls
`World_SetExplored` for every tile of a stamp centred on the hero. Sight 0:
the 29x29 circle at 0x88f5cc (radius 14 tiles). The callers choose 1 or 2
from the weather object (0x417e50: +0x9e0c set -> 2, 0x41ba90 true -> 1);
those use the 15x15 stamps at 0x88f328 + (sight - 1) * 225 (radius 7, 5;
a third one, radius 3, is unused).
## Mod (`mods/mapreveal.cpp`)
After the original revealed at a new tile, the same stamp scaled up 10x is
revealed too, one cell (2x2 tiles) at a time through `World_SetExplored`: by
day a circle of about 140 tiles radius, at night / in fog 70 / 50. About 15000
cells per tile step by day. Sectors beyond the loaded ones are not marked.
## Creatures on the map (2026-10-25)
"Enemies on the TAB map don't show unless in very short range." The zoomed
map view (0x6cc950, large map and TAB map) draws a creature off explored
ground, or a hostile one, only within a sight range of the hero: |dx| and |dy|
(world units, about 53.7 per tile) against a float at [esp+14h], 650 by
default (0x6ce677), 300 where 0x63d280 holds for the hero's area (0x6ce6b5),
350 when the weather object's 0x41ba40 is false (0x6ce6cf), plus a skill
(0x355, 0x570d90) and equipment bonuses. Out of range only the debug flag
0x182cdb8 & 2 shows them. The mod scales the three base values by 10 as well
(about 120 tiles by day). The view only lists creatures of the sectors around
the hero (25 entries at [esp+1ech]).
+18
View File
@@ -88,3 +88,21 @@ window's own pictures removed.
offset 0 while the menu showed. Now only while the menu frame is drawn
centred (it shifts the windows itself); otherwise dialogs are centred and
their full-layout dimming stretched, as in a game.
## Load / save progress screen (2026-10-25)
Request: the progress screen shown while loading / saving should use the
menu screens' background. The game's screen (`DxDriver7::LoadingScreen`
`0x6469d0`, thiscall (progress, text)) draws with GDI into the back buffer's
DC (surface +0xbc, GetDC vf 0x44 / ReleaseDC vf 0x68): the 1024x768 picture
(HBITMAP +0x200) at 0, 0, the bar (+0x204 / +0x208) and the text at y 700,
then Flip; it redraws only when the bar moves. Until now it was presented as
a 1024x768 "classic" frame scaled to the monitor.
`mods/loadingscreen.cpp`: before the game's drawing, `DrawBackground` across
the whole render width in a scene of its own (`DxDriver7` BeginScene
`0x646f50` / EndScene `0x646fd0`); the game's GDI drawing is moved to the
centred 1024 frame with `SetViewportOrgEx` on the back buffer's DC, caught in
`CreateCompatibleDC(hdc)` which the game calls right after getting it. Every
frame is now presented at the full render width (borderless.cpp has one
layout).
+11 -9
View File
@@ -234,15 +234,17 @@ repeatable ones (ids 2600..3599, cloned by Quest_End) and DQ_ dynamic quests;
which of them count for an area, and how the book groups by area, is still to
be measured.
## Megamap zoomed view: markers shifted (2026-10-25)
## Megamap: zoom not centred on the hero (2026-10-25)
Reported: on the M map, clicking a location to zoom in shows the markers on
the zoomed map wrong / shifted (megamap.cpp).
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.
## Load / save progress screen: rock background (2026-10-25)
## Inventory drawn without its dark backgrounds (2026-10-25, intermittent)
Reported: the progress screen shown while loading / saving does not use the
rock-wall background of the other menu screens yet (hud::RenderMenuFrame,
docs/screens.md). The GDI progress screen is DxDriver7_LoadingScreen
0x6469d0 (1024x768, game/addresses.h); pictures PAK\loading0/1.bmp,
loadgame.bmp, savegame.bmp, LoadingUW00/01.bmp.
Reported twice: on opening the inventory the item list had no dark
background behind the grid, and the tab texts had an odd background colour.
Looks like render state (texture / blend / colour) left behind by an earlier
draw; not reproduced yet. To find out: what was open or shown just before.
+4 -3
View File
@@ -67,9 +67,10 @@ Elsewhere (unknown / UI, leave alone for now): `0x40694a`, `0x4ec036`,
| `0x006469d0` | loading screen: GDI BitBlt of bitmap + progress bar + text into the back buffer (fixed 1024x768) | render thread |
| `0x006a0c60` | video/intro playback (unexamined) | |
The widescreen mod gives the loading screen a 1024x768 D3D viewport (set at
`BeginScene` when the scene depth `+0x1d8` is 0) and the borderless mod
presents only that area, scaled to the monitor. The main menu thread renders
Since 2026-10-25 the loading screen uses the full render width too: the menu
background is drawn across it and the game's GDI drawing is centred
(mods/loadingscreen.cpp, docs/screens.md "Load / save progress screen"); the
1024x768 "classic" viewport and present are gone. The main menu thread renders
the full width since 2026-10-20: its 1024x768 frame is drawn centred on the
menu screens' background (docs/screens.md, "Main menu frame").
+2 -1
View File
@@ -16,7 +16,8 @@ constexpr uint32_t GetMouseClientPos = 0x0066e500;
constexpr uint32_t MainWindowProc = 0x008122a0;
constexpr uint32_t DxDriver7_SetDisplayMode = 0x00645390;
constexpr uint32_t DxDriver7_Flip = 0x00645f70;
constexpr uint32_t DxDriver7_BeginScene = 0x00646f50;
constexpr uint32_t DxDriver7_BeginScene = 0x00646f50; // thiscall, plain ret; nests (+0x1d8)
constexpr uint32_t DxDriver7_EndScene = 0x00646fd0; // thiscall, plain ret
constexpr uint32_t DxDriver7_LoadingScreen = 0x006469d0; // GDI progress screen, 1024x768
constexpr uint32_t UiManager_ThreadLoop = 0x00756940; // main menu render loop (own thread)
constexpr uint32_t Engine_RenderThreadLoop = 0x0060e3f0; // in-game render loop
+47
View File
@@ -0,0 +1,47 @@
#pragma once
// The explored map (fog of war on the large map and the TAB map), measured
// 2026-10-25. docs/map-reveal.md.
//
// Each sector (64x64 tiles) holds a 32x32 bit mask at +0x180 (one bit per 2x2
// tiles, 128 bytes; Sector_SetExplored 0x622bf0, copied out by 0x622c60 and
// in from the save by 0x622c80). The zoomed map view builds its fog texture
// (FOGOFWAR01.TGA) from it (0x639030).
#include <cstdint>
namespace game {
// The world's sector grid (ecx of World_SetExplored).
inline void* World() { return *reinterpret_cast<void**>(0x00ad14e0); }
// World_SetExplored (0x638f00, thiscall (tile x, tile y, explored), ret 0xc):
// sets the bit of the tile's sector; tiles outside the world (>= 0x2000) or in
// a sector that isn't loaded are ignored.
inline void SetExplored(int tileX, int tileY) {
using Fn = void(__fastcall*)(void*, void*, int, int, int);
reinterpret_cast<Fn>(0x00638f00)(World(), nullptr, tileX, tileY, 1);
}
// RevealAroundHero (0x599730, creature thiscall (tile x, tile y, sight), ret
// 0xc): for the local hero, when its tile changed (last tile 0xad2384 /
// 0xad2388), sets every tile of a stamp centred on it explored. Sight 0 (day):
// a 29x29 circle (radius 14); sight 1..3 (night, weather; chosen by the
// callers from the weather object 0x417e50) a 15x15 stamp, the smaller ones
// (radius 7, 5, 3) at 0x88f328 + (sight - 1) * 225.
constexpr uintptr_t kRevealAroundHero = 0x00599730;
inline int32_t LastRevealTileX() { return *reinterpret_cast<const int32_t*>(0x00ad2384); }
inline int32_t LastRevealTileY() { return *reinterpret_cast<const int32_t*>(0x00ad2388); }
struct RevealStamp {
const uint8_t* cells; // size x size bytes, non-zero = revealed
int size;
};
inline RevealStamp RevealStampFor(int sight) {
if (sight <= 0) {
return {reinterpret_cast<const uint8_t*>(0x0088f5cc), 29};
}
return {reinterpret_cast<const uint8_t*>(0x0088f328) + (sight - 1) * 15 * 15, 15};
}
} // namespace game
+1 -11
View File
@@ -56,14 +56,12 @@ struct Viewport {
HWND window = nullptr;
bool runInBackground = false;
Layout full; // the whole back buffer
Layout classic; // its top-left 1024x768 (menus, loading screen; widescreen mod)
};
Viewport g_viewport;
std::atomic<bool> g_classicShown = false; // which layout the last presented frame used
const Layout& CurrentLayout() {
return g_classicShown ? g_viewport.classic : g_viewport.full;
return g_viewport.full;
}
bool Enabled() {
@@ -116,10 +114,8 @@ void ApplyBorderless(game::DxDriver7* driver) {
viewport.window = window;
viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0;
viewport.full = makeLayout(driver->mode.width, driver->mode.height);
viewport.classic = makeLayout(std::min<int>(driver->mode.width, 1024), std::min<int>(driver->mode.height, 768));
viewport.active = true;
g_viewport = viewport;
g_classicShown = false;
// Paint the bars: the window class background brush is black.
InvalidateRect(window, nullptr, TRUE);
@@ -188,12 +184,6 @@ int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx) {
// The original computes the blit rectangles once, for a centred window of
// exactly the game's size; override them on every present.
if (g_viewport.active && self->window == g_viewport.window) {
// Menus and the loading screen only fill the top-left 1024x768 of a
// widened back buffer; show just that part, scaled like before.
const bool classic = mods::widescreen::IsClassicFrame();
if (g_classicShown.exchange(classic) != classic) {
InvalidateRect(self->window, nullptr, TRUE); // repaint the bars for the new layout
}
const Layout& layout = CurrentLayout();
self->blitSource = {0, 0, layout.gameWidth, layout.gameHeight};
self->blitDest = layout.screen;
+1 -1
View File
@@ -66,7 +66,7 @@ int OffsetFor(Anchor anchor) {
// The main menu's frame shifts the windows in it; over a 16:9 screen
// (no frame) dialogs are centred like in a game (user, 2026-10-21).
const bool inMenuFrame = mods::widescreen::MenuIsShowing() && g_menuFrameDx != 0;
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || inMenuFrame ||
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsLoadingScreen() || inMenuFrame ||
anchor == Anchor::Screen) {
return 0;
}
+134
View File
@@ -0,0 +1,134 @@
// The load / save progress screen on the menu screens' background (request of
// 2026-10-25).
//
// The game's progress screen (DxDriver7::LoadingScreen 0x6469d0, thiscall
// (progress, text)) draws with GDI into the back buffer's DC (surface +0xbc,
// GetDC vf 0x44 / ReleaseDC vf 0x68): the 1024x768 picture (HBITMAP +0x200)
// at 0, 0, the progress bar (+0x204 / +0x208) and the text at y 700, then
// Flip. It only redraws when the bar moves.
//
// The mod: first the menu background across the whole render width in a
// Direct3D scene (screen::DrawBackground), then the game's drawing with the
// DC's origin moved to the centred 1024 frame. The game asks for a memory DC
// compatible with the back buffer's DC right after getting it
// (CreateCompatibleDC(hdc)), which is where the origin is set.
#include <windows.h>
#include <algorithm>
#include <cstdint>
#include "game/addresses.h"
#include "game/render/dx_driver7.h"
#include "mods/loadingscreen.h"
#include "mods/screen.h"
#include "mods/widescreen.h"
#include "re/hook.h"
namespace mods::loadingscreen {
namespace {
constexpr int kLayoutWidth = 1024;
using BeginSceneFn = char(__fastcall*)(game::DxDriver7* self, void* edx);
using EndSceneFn = void(__fastcall*)(game::DxDriver7* self, void* edx);
} // namespace
// While the progress screen draws on this thread: its frame's x.
thread_local int g_offsetX = 0;
void Begin(game::DxDriver7* driver) {
g_offsetX = 0;
const int width = widescreen::RenderWidth();
if (width <= kLayoutWidth || !driver || !driver->device || !driver->backBuffer) {
return;
}
if (reinterpret_cast<BeginSceneFn>(static_cast<uintptr_t>(game::addr::DxDriver7_BeginScene))(driver, nullptr)) {
screen::DrawBackground(driver->device);
reinterpret_cast<EndSceneFn>(static_cast<uintptr_t>(game::addr::DxDriver7_EndScene))(driver, nullptr);
}
g_offsetX = (width - kLayoutWidth) / 2;
}
void End() {
g_offsetX = 0;
}
} // namespace mods::loadingscreen
namespace mods::loadingscreen {
namespace {
constexpr int kLayoutHeight = 768;
// The game's pictures are artwork on black (PAK\loading0.bmp, loadgame.bmp,
// ...). Blended instead of copied: each pixel's alpha is its brightest
// channel, colours stay as they are (premultiplied), so black shows the
// background and the glow fades into it.
bool BlendPicture(HDC target, HDC source) {
const int offsetX = g_offsetX;
g_offsetX = 0; // our own GDI calls below pass the hooks unchanged
BITMAPINFO info = {};
info.bmiHeader.biSize = sizeof(info.bmiHeader);
info.bmiHeader.biWidth = kLayoutWidth;
info.bmiHeader.biHeight = -kLayoutHeight;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HBITMAP picture = CreateDIBSection(source, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
if (!picture) {
g_offsetX = offsetX;
return false;
}
HDC copy = CreateCompatibleDC(source);
HGDIOBJ previous = SelectObject(copy, picture);
BitBlt(copy, 0, 0, kLayoutWidth, kLayoutHeight, source, 0, 0, SRCCOPY);
GdiFlush();
auto* pixel = static_cast<uint8_t*>(bits);
for (int i = 0; i < kLayoutWidth * kLayoutHeight; ++i, pixel += 4) {
pixel[3] = std::max({pixel[0], pixel[1], pixel[2]});
}
const BLENDFUNCTION blend = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
const bool done =
GdiAlphaBlend(target, 0, 0, kLayoutWidth, kLayoutHeight, copy, 0, 0, kLayoutWidth, kLayoutHeight, blend) != 0;
SelectObject(copy, previous);
DeleteDC(copy);
DeleteObject(picture);
g_offsetX = offsetX;
return done;
}
} // namespace
} // namespace mods::loadingscreen
// The picture: BitBlt(hdc, 0, 0, 1024, 768, picture DC, 0, 0, SRCCOPY), or
// BLACKNESS without a picture (then nothing is drawn over the background).
BOOL WINAPI BitBltForLoadingScreen(HDC target, int x, int y, int width, int height, HDC source, int sx, int sy,
DWORD rop);
RE_HOOK_API(BitBlt, BitBltForLoadingScreen);
BOOL WINAPI BitBltForLoadingScreen(HDC target, int x, int y, int width, int height, HDC source, int sx, int sy,
DWORD rop) {
using namespace mods::loadingscreen;
if (g_offsetX != 0 && x == 0 && y == 0 && width == 1024 && height == 768) {
if (rop == BLACKNESS) {
return TRUE;
}
if (rop == SRCCOPY && source && BlendPicture(target, source)) {
return TRUE;
}
}
return BitBltForLoadingScreen_original(target, x, y, width, height, source, sx, sy, rop);
}
HDC WINAPI CreateCompatibleDCForLoadingScreen(HDC hdc);
RE_HOOK_API(CreateCompatibleDC, CreateCompatibleDCForLoadingScreen);
HDC WINAPI CreateCompatibleDCForLoadingScreen(HDC hdc) {
if (hdc && mods::loadingscreen::g_offsetX != 0) {
SetViewportOrgEx(hdc, mods::loadingscreen::g_offsetX, 0, nullptr);
}
return CreateCompatibleDCForLoadingScreen_original(hdc);
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
// The load / save progress screen on the menu background (loadingscreen.cpp).
#include "game/render/dx_driver7.h"
namespace mods::loadingscreen {
// Around DxDriver7::LoadingScreen (widescreen.cpp's hook): Begin draws the
// background and centres the game's GDI drawing, End stops centring.
void Begin(game::DxDriver7* driver);
void End();
} // namespace mods::loadingscreen
+108
View File
@@ -0,0 +1,108 @@
// A larger vision range for the explored map (request of 2026-10-25: "increase
// the base vision range for the map's fog of war reveal by x10 or so").
//
// Measured (game/engine/explored.h): RevealAroundHero stamps a fixed pattern
// around the hero into the sectors' explored masks whenever the hero's tile
// changes, a 14-tile circle by day, smaller ones at night.
//
// The mod: after the original revealed at a new tile, the same pattern scaled
// up kScale times is revealed too (by day a 140-tile circle), one cell (2x2
// tiles) at a time through the game's own World_SetExplored. Only loaded
// sectors can be marked; they reach further than the scaled stamp.
//
// Creatures on the zoomed map views (large map, TAB map; 0x6cc950): one off
// explored ground, or a hostile one, is only drawn within a sight range of the
// hero (|dx| and |dy| in world units, about 53.7 per tile): 650 (about 12
// tiles), 300 where 0x63d280 holds for the hero's area, 350 when the weather
// object's 0x41ba40 is false; plus bonuses from a skill (0x355) and the hero's
// equipment values. The three base values are scaled by kScale as well.
#include <cstdint>
#include <cstring>
#include "game/engine/explored.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::mapreveal {
namespace {
constexpr int kScale = 10;
constexpr int kCellTiles = 2; // one mask bit per 2x2 tiles
// The stamp cell a tile offset falls into: cell c covers offsets
// c * kScale - kScale / 2 .. c * kScale + kScale / 2 - 1.
int StampOffset(int tiles) {
const int shifted = tiles + kScale / 2;
return (shifted >= 0 ? shifted : shifted - kScale + 1) / kScale;
}
// mov dword ptr [esp+14h], <float>: the map's creature sight range.
struct SightRange {
uint32_t instruction;
float value;
};
constexpr SightRange kCreatureSightRanges[] = {
{0x006ce677, 650.0f},
{0x006ce6b5, 300.0f},
{0x006ce6cf, 350.0f},
};
uint32_t FloatBits(float value) {
uint32_t bits;
std::memcpy(&bits, &value, sizeof bits);
return bits;
}
void Startup() {
int failed = 0;
for (const SightRange& range : kCreatureSightRanges) {
failed += !re::PatchInstructionDword(range.instruction, FloatBits(range.value), FloatBits(range.value * kScale));
}
if (failed) {
re::Log("mapreveal: %d creature sight range patches FAILED", failed);
}
}
void RevealScaled(int tileX, int tileY, int sight) {
const game::RevealStamp stamp = game::RevealStampFor(sight);
const int centre = stamp.size / 2;
const int radius = centre * kScale + kScale / 2;
// Cells start at even tiles (sector origins are multiples of 64).
const int left = (tileX - radius) & ~(kCellTiles - 1);
const int top = (tileY - radius) & ~(kCellTiles - 1);
for (int y = top; y <= tileY + radius; y += kCellTiles) {
const int row = centre + StampOffset(y + kCellTiles / 2 - tileY);
if (row < 0 || row >= stamp.size) {
continue;
}
for (int x = left; x <= tileX + radius; x += kCellTiles) {
const int column = centre + StampOffset(x + kCellTiles / 2 - tileX);
if (column >= 0 && column < stamp.size && stamp.cells[row * stamp.size + column]) {
game::SetExplored(x, y);
}
}
}
}
} // namespace
RE_STARTUP(MapRevealStartup, Startup);
} // namespace mods::mapreveal
void __fastcall RevealAroundHero(void* creature, void* edx, int tileX, int tileY, int sight);
RE_HOOK(game::kRevealAroundHero, RevealAroundHero);
void __fastcall RevealAroundHero(void* creature, void* edx, int tileX, int tileY, int sight) {
const int lastX = game::LastRevealTileX();
const int lastY = game::LastRevealTileY();
RevealAroundHero_original(creature, edx, tileX, tileY, sight);
// The original revealed (local hero, new tile) iff it moved its last tile
// here; it ignores tile 0, 0.
const bool revealed = (tileX != 0 || tileY != 0) && (lastX != tileX || lastY != tileY) &&
game::LastRevealTileX() == tileX && game::LastRevealTileY() == tileY;
if (revealed && sight >= 0 && sight <= 3) {
mods::mapreveal::RevealScaled(tileX, tileY, sight);
}
}
+26
View File
@@ -51,6 +51,20 @@ constexpr uint32_t kZoomView = 0x006cc950;
constexpr uint32_t kZoomCentreUses[] = {0x006ccb7f, 0x006cf782};
constexpr uint32_t kZoomMarkerX = 0x006ce156;
constexpr float kZoomMarkerFromCentre = 256.0f;
// The heroes' markers in the zoomed view: a 6 px quad at x = d + 509 / 515
// (fadd [509.0] 0x6cf1bd, fadd [515.0] 0x6cf1dd), i.e. the centre 512 +- 3
// (2026-10-25: they stayed at the 1024 layout's centre).
constexpr uint32_t kZoomHeroMarkerLeft = 0x006cf1bd;
constexpr uint32_t kZoomHeroMarkerRight = 0x006cf1dd;
constexpr uintptr_t kZoomHeroMarkerLeftF = 0x00894780; // 509.0
constexpr uintptr_t kZoomHeroMarkerRightF = 0x00894778; // 515.0
constexpr float kZoomHeroMarkerHalf = 3.0f;
// The other symbols in the zoomed view take the centre as an integer
// immediate 200h (mov reg / add reg, 200h): a quad x = 200h - w / 200h + w
// (0x6ced82, 0x6cedb0), icons (0x6cf47a, 0x6cf4ac) and labels centred on
// their x (0x6cf5e4) (2026-10-25: they stayed at the 1024 layout's centre).
// Rewritten whenever the centre changes.
constexpr uint32_t kZoomCentreImmediates[] = {0x006ced82, 0x006cedb0, 0x006cf47a, 0x006cf4ac, 0x006cf5e4};
// 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};
@@ -60,6 +74,9 @@ constexpr int kBandWidth = 70;
float g_halfWidth = 512.0f;
float g_zoomCentre = 512.0f;
float g_zoomMarkerX = 768.0f;
float g_zoomHeroMarkerLeft = 509.0f;
float g_zoomHeroMarkerRight = 515.0f;
uint32_t g_zoomCentreImmediate = kLayoutWidth / 2;
uint32_t FloatBits(float value) {
uint32_t bits;
@@ -183,6 +200,8 @@ void PatchLayout(int width) {
failed += !re::RedirectOperand(instruction, game::addr::kScreenHalfWidthF, &g_zoomCentre);
}
failed += !re::RedirectOperand(kZoomMarkerX, game::addr::kScreenHeightF, &g_zoomMarkerX);
failed += !re::RedirectOperand(kZoomHeroMarkerLeft, kZoomHeroMarkerLeftF, &g_zoomHeroMarkerLeft);
failed += !re::RedirectOperand(kZoomHeroMarkerRight, kZoomHeroMarkerRightF, &g_zoomHeroMarkerRight);
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)));
@@ -196,6 +215,13 @@ 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;
g_zoomHeroMarkerLeft = g_zoomCentre - kZoomHeroMarkerHalf;
g_zoomHeroMarkerRight = g_zoomCentre + kZoomHeroMarkerHalf;
const auto immediate = static_cast<uint32_t>(width / 2 - mods::hud::DrawOffsetX());
if (immediate != g_zoomCentreImmediate &&
re::PatchAll(kZoomCentreImmediates, g_zoomCentreImmediate, immediate) == 0) {
g_zoomCentreImmediate = immediate;
}
}
} // namespace mods::megamap
+1 -1
View File
@@ -259,7 +259,7 @@ uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument);
RE_HOOK(0x007587b0, UiManager_Render);
uint32_t __fastcall UiManager_Render(void* self, void* edx, uint32_t argument) {
using namespace mods;
if (!widescreen::IsMenuThread() || widescreen::IsClassicFrame() || widescreen::RenderWidth() == 0) {
if (!widescreen::IsMenuThread() || widescreen::IsLoadingScreen() || widescreen::RenderWidth() == 0) {
return UiManager_Render_original(self, edx, argument);
}
auto* driver = *reinterpret_cast<DxDriver7**>(addr::g_dxDriver);
+11 -11
View File
@@ -24,6 +24,7 @@
#include "mods/widescreen.h"
#include "mods/hud.h"
#include "mods/loadingscreen.h"
#include "mods/megamap.h"
#include <algorithm>
@@ -63,9 +64,9 @@ bool g_pushesPatched = false;
int g_renderWidth = 0;
int g_renderHeight = 0;
// The loading screen is laid out for 1024x768 ("classic"); the main menu
// thread renders the full width too (its windows drawn centred by
// mods/screen.cpp), the in-game view is widened.
// Every frame uses the full render width: the main menu thread (its windows
// drawn centred by mods/screen.cpp), the loading screen (centred on the menu
// background, mods/loadingscreen.cpp), the widened in-game view.
std::atomic<DWORD> g_menuThread = 0;
// Whether the most recent scene (on any thread) was the main menu's or the
// loading screen's. UI input is processed outside the menu thread.
@@ -343,7 +344,7 @@ bool MenuIsShowing() {
return g_lastSceneMenu;
}
bool IsClassicFrame() {
bool IsLoadingScreen() {
return g_renderWidth != 0 && g_loadingScreenDepth > 0;
}
@@ -369,22 +370,21 @@ void __fastcall UiManager_ThreadLoop(void* self, void* edx) {
void __fastcall DxDriver7_LoadingScreen(DxDriver7* self, void* edx, uint32_t progress, const char* text) {
++g_loadingScreenDepth;
mods::loadingscreen::Begin(self);
DxDriver7_LoadingScreen_original(self, edx, progress, text);
mods::loadingscreen::End();
--g_loadingScreenDepth;
}
// The loading screen draws for a 1024x768 screen: give it a 1024x768
// viewport (the borderless mod then presents just that area, scaled), and
// everything else the full render size.
// Every frame gets the full render size as its viewport.
int __fastcall DxDriver7_BeginScene(DxDriver7* self, void* edx) {
mods::hud::OnDevice(self->device);
if (self->sceneDepth == 0) {
g_lastSceneMenu = IsClassicFrame() || IsMenuThread();
g_lastSceneMenu = IsLoadingScreen() || IsMenuThread();
}
if (g_renderWidth != 0 && self->sceneDepth == 0 && self->device) {
const bool classic = IsClassicFrame();
Viewport7 viewport = {0, 0, classic ? 1024u : static_cast<DWORD>(g_renderWidth),
classic ? 768u : static_cast<DWORD>(g_renderHeight), 0.0f, 1.0f};
Viewport7 viewport = {0, 0, static_cast<DWORD>(g_renderWidth), static_cast<DWORD>(g_renderHeight), 0.0f,
1.0f};
using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
auto setViewport = reinterpret_cast<SetViewportFn>((*static_cast<void***>(self->device))[13]);
setViewport(self->device, &viewport);
+2 -3
View File
@@ -11,9 +11,8 @@ namespace mods::widescreen {
// the world view to match. No-op when disabled.
void ApplyRenderSize(game::DxDisplayMode& mode, HWND window);
// True while the current thread renders a frame laid out for 1024x768 (the
// loading screen); the borderless mod then presents only that area.
bool IsClassicFrame();
// True while the current thread draws the load / save progress screen.
bool IsLoadingScreen();
// Applied render width (0 while the original 1024x768 is in use).
int RenderWidth();
+8
View File
@@ -372,3 +372,11 @@
0x0075e620 MenuScene::Render # (1 argument), ret 4: sky + water (UI_UW_MAIN_CENTER, UI_MAIN_REF)
0x0075e820 MenuScene::Update # (seconds)
0x005905f0 VerifyHeroMirror # anti-cheat: local hero vs protected mirror (0x182cddc); resets mismatches, returns 1 if all matched (mods/anticheat.cpp)
# --- explored map (fog of war, 2026-10-25) -------------------------------------
0x00599730 RevealAroundHero # creature thiscall (tile x, y, sight 0..3), ret 0xc; stamps 0x88f5cc / 0x88f328 when the hero's tile changed
0x00638f00 World_SetExplored # thiscall (tile x, y, explored), ret 0xc; ecx *0xad14e0
0x00622bf0 Sector_SetExplored # thiscall (tile x, y, explored), ret 0xc; bit in the 32x32 mask +0x180
0x00622c60 Sector_GetExploredMask # (out 128 bytes), ret 4
0x00622c80 Sector_SetExploredMask # (128 bytes), ret 4; from the save (0x606ae0)
0x00639030 Sector_BuildFogTexture # FOGOFWAR01.TGA from a mask, for the zoomed map view