From b4525581223a0031d30c0cbd3f469f2a9cad696e Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Sun, 25 Oct 2026 02:07:59 +0200 Subject: [PATCH] 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) --- CMakeLists.txt | 2 + docs/hud.md | 2 + docs/map-reveal.md | 44 ++++++++++++ docs/screens.md | 18 +++++ docs/todo.md | 20 +++--- docs/widescreen.md | 7 +- src/game/addresses.h | 3 +- src/game/engine/explored.h | 47 +++++++++++++ src/mods/borderless.cpp | 12 +--- src/mods/hud.cpp | 2 +- src/mods/loadingscreen.cpp | 134 +++++++++++++++++++++++++++++++++++++ src/mods/loadingscreen.h | 13 ++++ src/mods/mapreveal.cpp | 108 ++++++++++++++++++++++++++++++ src/mods/megamap.cpp | 26 +++++++ src/mods/screen.cpp | 2 +- src/mods/widescreen.cpp | 22 +++--- src/mods/widescreen.h | 5 +- tools/names.txt | 8 +++ 18 files changed, 435 insertions(+), 40 deletions(-) create mode 100644 docs/map-reveal.md create mode 100644 src/game/engine/explored.h create mode 100644 src/mods/loadingscreen.cpp create mode 100644 src/mods/loadingscreen.h create mode 100644 src/mods/mapreveal.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b957f46..0eb028f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/docs/hud.md b/docs/hud.md index ebbb08b..1afa1e7 100644 --- a/docs/hud.md +++ b/docs/hud.md @@ -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: diff --git a/docs/map-reveal.md b/docs/map-reveal.md new file mode 100644 index 0000000..9f617ee --- /dev/null +++ b/docs/map-reveal.md @@ -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]). diff --git a/docs/screens.md b/docs/screens.md index 4c67e94..5590c04 100644 --- a/docs/screens.md +++ b/docs/screens.md @@ -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). diff --git a/docs/todo.md b/docs/todo.md index 62930a5..ac001b4 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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. diff --git a/docs/widescreen.md b/docs/widescreen.md index bcd4112..48e3637 100644 --- a/docs/widescreen.md +++ b/docs/widescreen.md @@ -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"). diff --git a/src/game/addresses.h b/src/game/addresses.h index c3a8475..eb0b3dd 100644 --- a/src/game/addresses.h +++ b/src/game/addresses.h @@ -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 diff --git a/src/game/engine/explored.h b/src/game/engine/explored.h new file mode 100644 index 0000000..6d04319 --- /dev/null +++ b/src/game/engine/explored.h @@ -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 + +namespace game { + +// The world's sector grid (ecx of World_SetExplored). +inline void* World() { return *reinterpret_cast(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(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(0x00ad2384); } +inline int32_t LastRevealTileY() { return *reinterpret_cast(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(0x0088f5cc), 29}; + } + return {reinterpret_cast(0x0088f328) + (sight - 1) * 15 * 15, 15}; +} + +} // namespace game diff --git a/src/mods/borderless.cpp b/src/mods/borderless.cpp index f86965c..8d2d873 100644 --- a/src/mods/borderless.cpp +++ b/src/mods/borderless.cpp @@ -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 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(driver->mode.width, 1024), std::min(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; diff --git a/src/mods/hud.cpp b/src/mods/hud.cpp index c0a04ac..ed1bb1e 100644 --- a/src/mods/hud.cpp +++ b/src/mods/hud.cpp @@ -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; } diff --git a/src/mods/loadingscreen.cpp b/src/mods/loadingscreen.cpp new file mode 100644 index 0000000..031516a --- /dev/null +++ b/src/mods/loadingscreen.cpp @@ -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 + +#include +#include + +#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(static_cast(game::addr::DxDriver7_BeginScene))(driver, nullptr)) { + screen::DrawBackground(driver->device); + reinterpret_cast(static_cast(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(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); +} diff --git a/src/mods/loadingscreen.h b/src/mods/loadingscreen.h new file mode 100644 index 0000000..513da85 --- /dev/null +++ b/src/mods/loadingscreen.h @@ -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 diff --git a/src/mods/mapreveal.cpp b/src/mods/mapreveal.cpp new file mode 100644 index 0000000..0d6a848 --- /dev/null +++ b/src/mods/mapreveal.cpp @@ -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 +#include + +#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], : 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); + } +} diff --git a/src/mods/megamap.cpp b/src/mods/megamap.cpp index c9d7abc..50642bd 100644 --- a/src/mods/megamap.cpp +++ b/src/mods/megamap.cpp @@ -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(width))); failed += re::PatchAll(kBandRightInner, FloatBits(1024.0f - kBandWidth), FloatBits(static_cast(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(mods::hud::DrawOffsetX()); g_zoomMarkerX = g_zoomCentre + kZoomMarkerFromCentre; + g_zoomHeroMarkerLeft = g_zoomCentre - kZoomHeroMarkerHalf; + g_zoomHeroMarkerRight = g_zoomCentre + kZoomHeroMarkerHalf; + const auto immediate = static_cast(width / 2 - mods::hud::DrawOffsetX()); + if (immediate != g_zoomCentreImmediate && + re::PatchAll(kZoomCentreImmediates, g_zoomCentreImmediate, immediate) == 0) { + g_zoomCentreImmediate = immediate; + } } } // namespace mods::megamap diff --git a/src/mods/screen.cpp b/src/mods/screen.cpp index 1289616..cac5f58 100644 --- a/src/mods/screen.cpp +++ b/src/mods/screen.cpp @@ -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(addr::g_dxDriver); diff --git a/src/mods/widescreen.cpp b/src/mods/widescreen.cpp index 11ea663..67f7e74 100644 --- a/src/mods/widescreen.cpp +++ b/src/mods/widescreen.cpp @@ -24,6 +24,7 @@ #include "mods/widescreen.h" #include "mods/hud.h" +#include "mods/loadingscreen.h" #include "mods/megamap.h" #include @@ -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 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(g_renderWidth), - classic ? 768u : static_cast(g_renderHeight), 0.0f, 1.0f}; + Viewport7 viewport = {0, 0, static_cast(g_renderWidth), static_cast(g_renderHeight), 0.0f, + 1.0f}; using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport); auto setViewport = reinterpret_cast((*static_cast(self->device))[13]); setViewport(self->device, &viewport); diff --git a/src/mods/widescreen.h b/src/mods/widescreen.h index 6a10c6a..bdc5ce7 100644 --- a/src/mods/widescreen.h +++ b/src/mods/widescreen.h @@ -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(); diff --git a/tools/names.txt b/tools/names.txt index 07f6059..73874cd 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -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