Maps in widened views: TAB overlay in place, M map across the full width

- hud: shift SetViewport/Clear with moved windows (TAB overview map)
- megamap: view constants patched to the render width (clip, scroll limit,
  edge scrolling, centre, dark band, zoomed view and its markers); frame
  stretched to the screen edges, zoom controls and their click area centred
- hud: Anchor::Screen windows are drawn and clicked unshifted but still own
  the event scope
- autotest waits for log markers instead of fixed delays (~18 s per run)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 01:12:34 +02:00
1 parent a7f8496b2b
commit c6c5530709
9 files changed
+327 -18

No files matched your search

+1
View File
@@ -52,6 +52,7 @@ add_library(sacred_re SHARED
src/mods/chainlightning.cpp src/mods/chainlightning.cpp
src/mods/escape.cpp src/mods/escape.cpp
src/mods/recordcache.cpp src/mods/recordcache.cpp
src/mods/megamap.cpp
) )
target_include_directories(sacred_re PRIVATE src) target_include_directories(sacred_re PRIVATE src)
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
+29
View File
@@ -86,3 +86,32 @@ tests. Visible dialog tooltips: `mods::hud::VisibleDialogTooltips`.
The standalone control hooks (buttons, texts) above turned out not to be what The standalone control hooks (buttons, texts) above turned out not to be what
the quest dialog is made of; they stay, for top-level controls in general. the quest dialog is made of; they stay, for top-level controls in general.
## Large map (M) across the render width
`mods/megamap.cpp`. cUI_Megamap (vtable 0x894714) is not moved like the other
windows (`Anchor::Screen`: drawn and clicked unshifted); its 1024x768 view is
widened instead, when the render size is applied (`PatchLayout`):
| What | Where | Game | Now |
|---|---|---|---|
| cursor at the right edge (edge scrolling, cursor shape) | 9 × `cmp reg, 3FFh` 0x6c7625..0x6c9e32 | 1023 | width − 1 |
| tiles right of the view skipped; labels flipped left | `cmp reg, 400h` 0x6c9aa3, 0x6ca2c6, 0x6ca2e5 | 1024 | width |
| largest scroll x = map width (+0xc0160, 2048) − view | `add eax, -400h` 0x6c9edd | −1024 | −width |
| 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 |
| 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:
corners 41×44, 32 px tiles, left side x < 48, right side from x 976). On
Show (vf24 0x6c7930) the right side moves to the screen edge, the top and
bottom tiles stretch over the wider span, and the four controls the map holds
itself (+0xc0440, +0xc04bc statics, +0xc0544, +0xc0600 buttons: the zoom
controls, absolute positions) stay centred, and so does the rectangle around
them at +0xc0538: the map passes only clicks inside it to the buttons
(0x6c7f32). Each control is placed once (the
game lays them out at creation only).
A window anchored `Screen` still counts as the outermost scope for events, so
its child buttons (hooked as standalone controls, `Center`) don't shift again.
+3 -3
View File
@@ -56,9 +56,9 @@ follow.
## Map and map zoom in widened views ## Map and map zoom in widened views
Reported 2026-10-15: the map (and zooming it) does not support the widened Reported 2026-10-15: the map (and zooming it) does not support the widened
render sizes yet (docs/widescreen.md, docs/hud.md: windows are laid out for render sizes yet. 2026-10-16: TAB map fixed (viewport shifted), M map spans
1024 and centred). To check: the map window's draw area and the zoom's the render width (docs/hud.md, "Large map"), zoomed view and its markers included.
hardcoded 1024/768 (or 512/384 centre) values. Confirmed in-game 2026-10-16.
## Quest dialog glitches in widened views ## Quest dialog glitches in widened views
+55 -7
View File
@@ -45,13 +45,14 @@ thread_local int g_offsetX = 0;
thread_local bool g_eventShifted = false; thread_local bool g_eventShifted = false;
enum class Anchor { Center, Right }; // Screen: laid out across the whole render width by its mod (drawn unshifted).
enum class Anchor { Center, Right, Screen };
int OffsetFor(Anchor anchor) { int OffsetFor(Anchor anchor) {
static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0; static const bool enabled = re::ConfigInt("hud", "Enabled", 1) != 0;
const int width = mods::widescreen::RenderWidth(); const int width = mods::widescreen::RenderWidth();
if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() || if (!enabled || width <= kLayoutWidth || mods::widescreen::IsClassicFrame() ||
mods::widescreen::MenuIsShowing()) { mods::widescreen::MenuIsShowing() || anchor == Anchor::Screen) {
return 0; return 0;
} }
return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth; return anchor == Anchor::Center ? (width - kLayoutWidth) / 2 : width - kLayoutWidth;
@@ -283,11 +284,12 @@ int MouseEventXOffsets(void* event, int (&offsets)[2]) {
} }
} }
// RAII: shift the mouse position by -dx for the scope (outermost window only). // RAII: shift the mouse position by -dx for the scope (outermost window only;
// one with dx 0, e.g. Anchor::Screen, still keeps its children from shifting).
class EventOffset { class EventOffset {
public: public:
EventOffset(void* event, int dx) : cursor_(dx) { EventOffset(void* event, int dx) : outermost_(!g_eventShifted), cursor_(outermost_ ? dx : 0) {
if (dx == 0 || g_eventShifted) { if (!outermost_) {
return; return;
} }
int offsets[2]; int offsets[2];
@@ -309,6 +311,7 @@ public:
} }
private: private:
bool outermost_;
int count_ = 0; int count_ = 0;
int32_t* x_[2] = {}; int32_t* x_[2] = {};
int32_t original_[2] = {}; int32_t original_[2] = {};
@@ -387,6 +390,45 @@ HRESULT __stdcall DrawIndexedPrimitiveShifted(void* device, DWORD type, DWORD fv
indexCount, flags); indexCount, flags);
} }
// Viewports and cleared rectangles in screen space (the overview map, TAB,
// sets a viewport at its frame, 162,120 700x525, and draws the map inside with
// its own projection): moved like the vertices while a window draws.
struct Viewport7 {
DWORD x, y, width, height;
float minZ, maxZ;
};
struct Rect7 {
LONG x1, y1, x2, y2;
};
using SetViewportFn = HRESULT(__stdcall*)(void* device, Viewport7* viewport);
using ClearFn = HRESULT(__stdcall*)(void* device, DWORD count, Rect7* rects, DWORD flags, DWORD color, float z,
DWORD stencil);
SetViewportFn g_setViewport = nullptr;
ClearFn g_clear = nullptr;
HRESULT __stdcall SetViewportShifted(void* device, Viewport7* viewport) {
if (g_offsetX == 0 || !viewport) {
return g_setViewport(device, viewport);
}
Viewport7 shifted = *viewport;
shifted.x = static_cast<DWORD>(static_cast<int>(shifted.x) + g_offsetX);
return g_setViewport(device, &shifted);
}
HRESULT __stdcall ClearShifted(void* device, DWORD count, Rect7* rects, DWORD flags, DWORD color, float z,
DWORD stencil) {
if (g_offsetX == 0 || count == 0 || !rects) {
return g_clear(device, count, rects, flags, color, z, stencil);
}
thread_local std::vector<Rect7> shifted;
shifted.assign(rects, rects + count);
for (Rect7& r : shifted) {
r.x1 += g_offsetX;
r.x2 += g_offsetX;
}
return g_clear(device, count, shifted.data(), flags, color, z, stencil);
}
} // namespace } // namespace
int CursorOffsetX() { int CursorOffsetX() {
@@ -414,6 +456,12 @@ void OnDevice(void* device) {
g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]); g_drawIndexedPrimitive = reinterpret_cast<DrawIndexedPrimitiveFn>(vtable[26]);
re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive), re::AttachRuntimeHook("D3D7 DrawIndexedPrimitive", reinterpret_cast<void**>(&g_drawIndexedPrimitive),
reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted)); reinterpret_cast<void*>(&DrawIndexedPrimitiveShifted));
// Clear 10, SetViewport 13.
g_clear = reinterpret_cast<ClearFn>(vtable[10]);
re::AttachRuntimeHook("D3D7 Clear", reinterpret_cast<void**>(&g_clear), reinterpret_cast<void*>(&ClearShifted));
g_setViewport = reinterpret_cast<SetViewportFn>(vtable[13]);
re::AttachRuntimeHook("D3D7 SetViewport", reinterpret_cast<void**>(&g_setViewport),
reinterpret_cast<void*>(&SetViewportShifted));
} }
} }
@@ -545,8 +593,8 @@ HUD_EVENT(0x006c5f40, UiInventory_HandleEvent, Anchor::Center)
HUD_RENDER(0x006c6140, UiInventory_Render, Anchor::Center) HUD_RENDER(0x006c6140, UiInventory_Render, Anchor::Center)
HUD_EVENT(0x006c70e0, UiOverviewMap_HandleEvent, Anchor::Center) HUD_EVENT(0x006c70e0, UiOverviewMap_HandleEvent, Anchor::Center)
HUD_RENDER(0x006c69d0, UiOverviewMap_Render, Anchor::Center) HUD_RENDER(0x006c69d0, UiOverviewMap_Render, Anchor::Center)
HUD_EVENT(0x006c7ec0, UiMegamap_HandleEvent, Anchor::Center) HUD_EVENT(0x006c7ec0, UiMegamap_HandleEvent, Anchor::Screen)
HUD_RENDER(0x006c9ce0, UiMegamap_Render, Anchor::Center) HUD_RENDER(0x006c9ce0, UiMegamap_Render, Anchor::Screen) // laid out by mods/megamap.cpp
HUD_EVENT(0x006d1080, UiMaster_HandleEvent, Anchor::Center) HUD_EVENT(0x006d1080, UiMaster_HandleEvent, Anchor::Center)
HUD_RENDER(0x006d0f00, UiMaster_Render, Anchor::Center) HUD_RENDER(0x006d0f00, UiMaster_Render, Anchor::Center)
HUD_EVENT(0x006d2450, UiMasterCombo_HandleEvent, Anchor::Center) HUD_EVENT(0x006d2450, UiMasterCombo_HandleEvent, Anchor::Center)
+197
View File
@@ -0,0 +1,197 @@
// The large map (M, cUI_Megamap) spans the whole render width.
//
// The game lays the map out for a 1024x768 screen: a scrollable view of the
// 2048x2816 world map (256 px tiles, scroll position at +0xc016c / +0xc0170),
// clipped and edge-scrolled at x 1024, centred at x 512, under a dark band
// (inner edge 70 px) and a frame of child statics (corners, 32 px edge tiles).
// Moving it as a centred window left the world visible at the sides; instead
// the view constants are set to the render width, the frame's right side moves
// to the right screen edge and its top and bottom tiles stretch across, and
// the zoom buttons at the bottom stay centred. The window is drawn and clicked
// unshifted (mods/hud.cpp, Anchor::Screen). See docs/hud.md.
#include <cstdint>
#include <cstring>
#include <unordered_map>
#include "game/addresses.h"
#include "mods/megamap.h"
#include "mods/widescreen.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::megamap {
namespace {
constexpr int kLayoutWidth = 1024;
// cmp reg, 3FFh: cursor at the right edge (edge scrolling, cursor shape).
constexpr uint32_t kRightEdgeCompares[] = {0x006c7625, 0x006c764c, 0x006c768c, 0x006c76d9, 0x006c76fe,
0x006c773a, 0x006c9dce, 0x006c9df4, 0x006c9e32};
// cmp reg, 400h: tiles right of the view are skipped (0x6c9aa3); a label that
// would end right of it is placed left of its point (0x6ca2c6, 0x6ca2e5).
constexpr uint32_t kWidthCompares[] = {0x006c9aa3, 0x006ca2c6, 0x006ca2e5};
// add eax, -400h: the largest scroll x is the map width minus the view.
constexpr uint32_t kScrollLimit = 0x006c9edd;
// The view centre 200h: the zoom-in frame (0x6ca53f) and the image centred
// on it (0x6ca637, 0x6ca645).
constexpr uint32_t kCentreImmediates[] = {0x006ca53f, 0x006ca637, 0x006ca645};
// fsub/fld/fadd [512.0]: centring the scroll on the hero (opening the map
// 0x6c7bfe, 0x6cb27a), the zoomed view (0x6ccb7f) and its quest markers
// (0x6cf782).
constexpr uint32_t kCentreFloatUses[] = {0x006c7bfe, 0x006cb27a, 0x006ccb7f, 0x006cf782};
// fld [768.0] as an x: markers in the zoomed view at 768 - d (0x6ce156), the
// centre plus 256.
constexpr uint32_t kZoomMarkerX = 0x006ce156;
constexpr float kZoomMarkerFromCentre = 256.0f;
// The dark band around the view (10 vertices, mov [esp+..], imm32): its right
// outer edge x 1024 and inner edge x 954.
constexpr uint32_t kBandRightOuter[] = {0x006ca774, 0x006ca808};
constexpr uint32_t kBandRightInner[] = {0x006ca7bc, 0x006ca84c};
constexpr int kBandWidth = 70;
float g_halfWidth = 512.0f;
float g_zoomMarkerX = 768.0f;
uint32_t FloatBits(float value) {
uint32_t bits;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
// UI control layout (relative to the parent at +0x50, if any).
// Rectangles: x, y (int32), width, height (int16); a control's at +0x24.
constexpr size_t kControlRect = 0x24;
// cUI_Window2: child controls, a vector of pointers.
constexpr size_t kChildrenBegin = 0x78;
constexpr size_t kChildrenEnd = 0x7c;
// The frame: left side x < 48, right side from x 976, top tiles above y 48,
// bottom tiles from y 720 (measured).
constexpr int kFrameLeft = 48;
constexpr int kFrameRight = 976;
constexpr int kFrameTop = 48;
constexpr int kFrameBottom = 720;
// Controls the map holds itself (absolute positions, drawn by its render):
// two statics and two buttons, the zoom controls at the bottom centre.
constexpr size_t kOwnControls[] = {0xc0440, 0xc04bc, 0xc0544, 0xc0600};
// Rectangle around them: the map passes only clicks inside it to the buttons
// (its event handler, 0x6c7f32).
constexpr size_t kZoomControlsRect = 0xc0538;
int& RectX(uint8_t* rect) {
return *reinterpret_cast<int*>(rect);
}
int RectY(uint8_t* rect) {
return *reinterpret_cast<int*>(rect + 4);
}
int16_t& RectWidth(uint8_t* rect) {
return *reinterpret_cast<int16_t*>(rect + 8);
}
// The game lays the controls out once, when it creates the map; they are
// placed for the render width once, too (the map is opened many times).
struct Placement {
int x;
int16_t width;
};
std::unordered_map<const void*, Placement> g_placed;
void Place(uint8_t* rect, int x, int width) {
RectX(rect) = x;
RectWidth(rect) = static_cast<int16_t>(width);
g_placed[rect] = {x, static_cast<int16_t>(width)};
}
bool AlreadyPlaced(uint8_t* rect) {
const auto it = g_placed.find(rect);
return it != g_placed.end() && it->second.x == RectX(rect) && it->second.width == RectWidth(rect);
}
// Frame pieces: the right side moves by `extra`, the top and bottom tiles
// between the corners stretch over the wider span; anything else stays
// centred.
void PlaceFramePiece(uint8_t* rect, int extra) {
const int x = RectX(rect);
const int width = RectWidth(rect);
const int y = RectY(rect);
if (x + width <= kFrameLeft) {
return;
}
if (x >= kFrameRight) {
Place(rect, x + extra, width);
return;
}
if (y < kFrameTop || y >= kFrameBottom) {
const double scale = static_cast<double>(kFrameRight - kFrameLeft + extra) / (kFrameRight - kFrameLeft);
const int left = kFrameLeft + static_cast<int>((x - kFrameLeft) * scale + 0.5);
const int right = kFrameLeft + static_cast<int>((x + width - kFrameLeft) * scale + 0.5);
Place(rect, left, right - left);
return;
}
Place(rect, x + extra / 2, width);
}
void PlaceCentred(uint8_t* rect, int extra) {
if (!AlreadyPlaced(rect)) {
Place(rect, RectX(rect) + extra / 2, RectWidth(rect));
}
}
void Layout(uint8_t* megamap) {
const int extra = mods::widescreen::RenderWidth() - kLayoutWidth;
if (extra <= 0) {
return;
}
auto** child = *reinterpret_cast<uint8_t***>(megamap + kChildrenBegin);
auto** end = *reinterpret_cast<uint8_t***>(megamap + kChildrenEnd);
for (; child && child != end; ++child) {
if (*child && !AlreadyPlaced(*child + kControlRect)) {
PlaceFramePiece(*child + kControlRect, extra);
}
}
for (size_t offset : kOwnControls) {
PlaceCentred(megamap + offset + kControlRect, extra);
}
PlaceCentred(megamap + kZoomControlsRect, extra);
uint8_t* window = megamap + kControlRect;
if (!AlreadyPlaced(window) && RectX(window) == 0 && RectWidth(window) == kLayoutWidth) {
Place(window, 0, kLayoutWidth + extra);
}
}
} // namespace
void PatchLayout(int width) {
g_halfWidth = width / 2.0f;
int failed = re::PatchAll(kRightEdgeCompares, 0x3ff, static_cast<uint32_t>(width - 1));
failed += re::PatchAll(kWidthCompares, 0x400, static_cast<uint32_t>(width));
failed += !re::PatchInstructionDword(kScrollLimit, static_cast<uint32_t>(-kLayoutWidth),
static_cast<uint32_t>(-width));
failed += re::PatchAll(kCentreImmediates, 0x200, static_cast<uint32_t>(width / 2));
for (uint32_t instruction : kCentreFloatUses) {
failed += !re::RedirectOperand(instruction, game::addr::kScreenHalfWidthF, &g_halfWidth);
}
g_zoomMarkerX = g_halfWidth + kZoomMarkerFromCentre;
failed += !re::RedirectOperand(kZoomMarkerX, game::addr::kScreenHeightF, &g_zoomMarkerX);
failed += re::PatchAll(kBandRightOuter, FloatBits(1024.0f), FloatBits(static_cast<float>(width)));
failed += re::PatchAll(kBandRightInner, FloatBits(1024.0f - kBandWidth),
FloatBits(static_cast<float>(width - kBandWidth)));
re::Log("megamap: view width %d%s", width, failed ? " (some patches FAILED)" : "");
}
} // namespace mods::megamap
namespace game {
// cUI_Megamap::Show (vf24 0x6c7930, the argument: show / hide).
void __fastcall UiMegamap_Show(void* self, void* edx, int show);
RE_HOOK(0x006c7930, UiMegamap_Show);
void __fastcall UiMegamap_Show(void* self, void* edx, int show) {
if (self && (show & 0xff)) {
mods::megamap::Layout(static_cast<uint8_t*>(self));
}
UiMegamap_Show_original(self, edx, show);
}
} // namespace game
+9
View File
@@ -0,0 +1,9 @@
#pragma once
namespace mods::megamap {
// Sets the large map's view constants to the render width (once, when the
// render size is applied).
void PatchLayout(int width);
} // namespace mods::megamap
+2
View File
@@ -24,6 +24,7 @@
#include "mods/widescreen.h" #include "mods/widescreen.h"
#include "mods/hud.h" #include "mods/hud.h"
#include "mods/megamap.h"
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
@@ -318,6 +319,7 @@ void ApplyRenderSize(game::DxDisplayMode& mode, HWND window) {
PatchGrannyOrthoPushes(width, height); PatchGrannyOrthoPushes(width, height);
PatchLabelArea(width, height); PatchLabelArea(width, height);
PatchLetterbox(width); PatchLetterbox(width);
mods::megamap::PatchLayout(width);
} }
const double scaleX = width / kOriginalWidth; const double scaleX = width / kOriginalWidth;
const double scaleY = height / kOriginalHeight; const double scaleY = height / kOriginalHeight;
+4 -3
View File
@@ -11,7 +11,7 @@
param( param(
[string]$Name = "autotest", [string]$Name = "autotest",
[string]$Ini = "", [string]$Ini = "",
[int]$LoadSeconds = 30, [int]$LoadTimeoutSeconds = 60,
# Row of the save game in the load screen's list (1-3 visible rows). # Row of the save game in the load screen's list (1-3 visible rows).
[int]$Save = 1, [int]$Save = 1,
# Runs in-game after loading, before the screenshots (e.g. clicks). # Runs in-game after loading, before the screenshots (e.g. clicks).
@@ -28,12 +28,13 @@ if ($Ini) { Set-Content $iniPath ($iniBackup + "`r`n" + $Ini + "`r`n") -Encoding
$game = $null $game = $null
try { try {
$game = Start-Sacred $game = Start-Sacred
Click-At 1720 779 -AfterMs 4000 # main menu: "Spiel laden" Click-At 1720 779 -AfterMs 1500 # main menu: "Spiel laden"
if ($Save -gt 1) { if ($Save -gt 1) {
Click-At 1342 (609 + ($Save - 1) * 254) -AfterMs 800 # load screen: the save's name (rows 254 px apart) Click-At 1342 (609 + ($Save - 1) * 254) -AfterMs 800 # load screen: the save's name (rows 254 px apart)
} }
Click-At 2382 568 -AfterMs 2000 # load screen: "Laden" Click-At 2382 568 -AfterMs 2000 # load screen: "Laden"
Click-At 1551 826 -AfterMs ($LoadSeconds * 1000) # confirm: "Ok" Click-At 1551 826 -AfterMs 0 # confirm: "Ok"
Wait-Log "cEngine End and exit constructor" -TimeoutSeconds $LoadTimeoutSeconds -AfterMs 1500 -Game $game
if ($InGame) { & $InGame } if ($InGame) { & $InGame }
Save-Screen "screenshots\${Name}_ingame.png" | Out-Null Save-Screen "screenshots\${Name}_ingame.png" | Out-Null
Press-Key 0x1B -AfterMs 2500 # ESC menu Press-Key 0x1B -AfterMs 2500 # ESC menu
+27 -5
View File
@@ -31,16 +31,38 @@ function Focus-Sacred($Game) {
[SacredRe.Input]::SetForegroundWindow($Game.MainWindowHandle) [SacredRe.Input]::SetForegroundWindow($Game.MainWindowHandle)
} }
function Start-Sacred([string]$Arguments = "log", [int]$WaitSeconds = 18) { $script:LogPath = Join-Path $script:Root "build\x86-debug\sacred_re.log"
# Waits until sacred_re.log (written anew at every start) has at least $Count
# lines containing $Pattern, focusing the game meanwhile; then waits $AfterMs.
function Wait-Log([string]$Pattern, [int]$Count = 1, [int]$TimeoutSeconds = 60, [int]$AfterMs = 500, $Game = $null) {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if ($Game) { Focus-Sacred $Game | Out-Null }
$found = 0
if (Test-Path $script:LogPath) {
$found = @(Select-String -Path $script:LogPath -Pattern $Pattern -SimpleMatch).Count
}
if ($found -ge $Count) { Start-Sleep -Milliseconds $AfterMs; return }
Start-Sleep -Milliseconds 200
}
throw "timeout waiting for '$Pattern' in sacred_re.log"
}
# Starts the game and returns once its main menu is up (the UI manager
# starts its menu thread a second time when the menu appears).
function Start-Sacred([string]$Arguments = "log") {
if (Get-Process sacred -ErrorAction SilentlyContinue) { throw "sacred.exe is already running" } if (Get-Process sacred -ErrorAction SilentlyContinue) { throw "sacred.exe is already running" }
if (Test-Path $script:LogPath) { Remove-Item $script:LogPath -ErrorAction SilentlyContinue }
$null = Start-Process (Join-Path $script:Root "build\x86-debug\sacred_re.exe") -ArgumentList $Arguments $null = Start-Process (Join-Path $script:Root "build\x86-debug\sacred_re.exe") -ArgumentList $Arguments
$game = $null $game = $null
for ($i = 0; $i -lt $WaitSeconds * 4; $i++) { for ($i = 0; $i -lt 80 -and -not $game; $i++) {
Start-Sleep -Milliseconds 250 Start-Sleep -Milliseconds 250
if (-not $game) { $game = Get-Process sacred -ErrorAction SilentlyContinue | Select-Object -First 1 } $game = Get-Process sacred -ErrorAction SilentlyContinue | Select-Object -First 1
if ($game) { Focus-Sacred $game | Out-Null }
} }
if (-not $game -or $game.HasExited) { throw "sacred.exe did not start (or crashed)" } if (-not $game) { throw "sacred.exe did not start" }
Wait-Log "starting menu thread!" -Count 2 -AfterMs 1500 -Game $game
if ($game.HasExited) { throw "sacred.exe crashed" }
$game $game
} }