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:
1 parent
3f03228c29
commit
b452558122
18 files changed
+435
-40
No files matched your search
@@ -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})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]).
|
||||
@@ -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
@@ -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
@@ -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").
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user