Characters: Laden in the Esc menu, timed autosave

- Esc menu: "Exportieren" became "Laden"; it opens the load window, always
  in load mode. In a game every world loads (the current one reloads); a
  "(neu)" world stops the game (cCommand_armaStop) and starts the new one
  without the main menu. The character being played cannot be deleted.
- Silent autosave every sacredK.ini [characters] AutosaveMinutes (default
  10, 0 = off) into the character's autosave file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-20 22:41:20 +02:00
1 parent feebe72ab7
commit 732d9c2abb
4 files changed
+161 -71

No files matched your search

+21 -9
View File
@@ -261,21 +261,34 @@ game the character's.
number). Story autosaves (engine event 0xf subtype 2 with the flag at +0x18, number). Story autosaves (engine event 0xf subtype 2 with the flag at +0x18,
noted in `cEngine::ReceiveEvent`) go into the autosave file. noted in `cEngine::ReceiveEvent`) go into the autosave file.
Silent autosave: 10 minutes after the last save of any kind (or the game's
start), `SaveOnTick` does the direct save marked as an autosave, so it goes
into the autosave file. While a window is open or a save runs it waits for
the next tick. A character never saved yet gets its character file from it.
Interval: `sacredK.ini` `[characters] AutosaveMinutes` (default 10, 0 off).
Direct save (K key, Esc menu "Speichern"): on the next engine tick, as the Direct save (K key, Esc menu "Speichern"): on the next engine tick, as the
quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690), quicksave does (0x618ebb): `cUI_Manager::CanOpenWindow` (0x75a690),
`cEngine::CaptureScreen` (0x613710, 1), description +0x6a, row +0x68 = 0, `cEngine::CaptureScreen` (0x613710, 1), description +0x6a, row +0x68 = 0,
flag +0x54 |= 0x40. The Esc menu line keeps its action (Show flag +0x54 |= 0x40. The Esc menu line keeps its action (Show
`UI_WND_SAVEGAME`); `cUI_Manager::ReceiveEvent` (0x757120) turns that into the `UI_WND_SAVEGAME`); `cUI_Manager::ReceiveEvent` (0x757120) turns that into the
direct save while a game runs. Its text is patched at 0x6bc1f7 (id 0x41b is direct save while a game runs. Its text is patched at 0x6bc1f7 (id 0x41b is
used by other windows too). Loading is from the main menu only. used by other windows too).
The Esc menu has no "Exportieren" (request 2026-10-20): `cUI_EscMenu::BuildLines` The Esc menu: "Exportieren" became "Laden" (requests 2026-10-20). The line
(0x6bb870, called by the constructor and on an event) makes five (text id 0x419 pushed at 0x6bc272, now "Laden") keeps its command, which shows
cUI_StaticText64 lines in the vector +0x158, 0x24 apart; each handles its own UI_CHARACTER (the export screen) and hides the menu; in a game
events. After it the UI_ESC_EXPORT line is removed and deleted, the lines `UiManager_ReceiveEvent` turns that into showing the load window. The load
below move up, and the window (0x108 x 0xc8) loses 0x24 of height, staying window always opens in load mode (`cUI_Savegame::Show` vf24 0x71cfb0 picks
centred. With it the export screen (cUI_Character mode 5) is unreachable; save mode in a game; `SetMode` 0x71c4f0 (2) after it). In a game every
its mod (charexport.cpp, an erase button for export slots) was deleted. world can be loaded, the current one too (a reload from its last save; the
game's in-game load, engine flag 0x200). A "(neu)" world needs a new engine:
the running game stops as "Beenden" stops it (`cCommand_armaStop`, execute
0x755b30, vtable 0x88ee2c: posts 0x8001 to the main window, whose handler
deletes the engine), and when the engine is gone (`OnGameEnd`) the new game's
start command follows (0x8002), without the main menu. The character being
played cannot be deleted. The export screen (cUI_Character
mode 5) is unreachable; its mod (charexport.cpp, an erase button for export slots) was deleted.
### Migration ### Migration
@@ -286,6 +299,5 @@ kept, the newest file overall is the shared hero.
## Open ## Open
- Silent autosave every 10 minutes.
- Quickload (row 0) does not know characters. - Quickload (row 0) does not know characters.
- Multiplayer (see `todo.md`). - Multiplayer (see `todo.md`).
+11 -3
View File
@@ -184,14 +184,22 @@ From the characters rework (`docs/characters.md`, request of 2026-10-19):
are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and are one byte per difficulty in cStatsManager +0x5704 (read 0x424be0) and
travel in the save header (+0x88..+0x98) and the hero file. Done travel in the save header (+0x88..+0x98) and the hero file. Done
2026-10-20 (docs/characters.md). 2026-10-20 (docs/characters.md).
- **Silent autosave every 10 minutes** (a setting later) into the - **Autosave interval in the options window** (now only
autosave file, without the busy dialog, like the direct save `sacredK.ini` `[characters] AutosaveMinutes`).
(`SaveOnTick`) but marked as an autosave.
- **Quickload (row 0) does not know characters.** Ctrl+Z saves into the - **Quickload (row 0) does not know characters.** Ctrl+Z saves into the
current character, but the quickload event (0x116, engine +0x68 = row, current character, but the quickload event (0x116, engine +0x68 = row,
flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the flag 0x200) loads `GAME00.PAK`, which is not listed; make it reload the
current character's world (or its newer autosave). current character's world (or its newer autosave).
## Two saves in a row (2026-10-20, not reproduced)
Once two real saves ("saved", not "autosaved") went into the same character
1.8 s apart, about 77 s after loading, with the timed autosave off. Save
flag 0x40 is set in cEngine::ReceiveEvent at 0x618876 (quicksave),
0x618f08 and 0x6190c5 (save event 0xf: +0xc row, +0x18 autosave flag). A
log of the event that sets the flag (dwords 0..9) did not catch it again;
add it back if it shows.
## Savegames screen redesign ("Spielen") ## Savegames screen redesign ("Spielen")
Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen" Asked 2026-10-20: redesign the load window (cUI_Savegame, the "Spielen"
+126 -59
View File
@@ -56,6 +56,7 @@
#include "mods/hud.h" #include "mods/hud.h"
#include "mods/input.h" #include "mods/input.h"
#include "mods/userfolders.h" #include "mods/userfolders.h"
#include "re/config.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/patch.h" #include "re/patch.h"
#include "re/log.h" #include "re/log.h"
@@ -368,7 +369,11 @@ struct Current {
std::string file; std::string file;
} g_current; } g_current;
bool g_autosavePending = false; // The next cEngine::Save (render thread) is an autosave: a story autosave
// (Engine_ReceiveEvent) or the timed one (SaveOnTick).
std::atomic<bool> g_autosavePending{false};
bool g_inGame = false; // a game runs (the load window opens from the Esc menu too)
std::atomic<uint64_t> g_lastSaveAt{0}; // GetTickCount64 of the last save of any kind
std::string Description(const json& meta) { std::string Description(const json& meta) {
return Convert(FromUtf8(meta.value("name", std::string())) + L" - " + ClassName(meta.value("class", 0u)), return Convert(FromUtf8(meta.value("name", std::string())) + L" - " + ClassName(meta.value("class", 0u)),
@@ -436,7 +441,8 @@ void RebuildList() {
highest = std::max(highest, std::min(d + 1, kDifficultyCount - 1)); highest = std::max(highest, std::min(d + 1, kDifficultyCount - 1));
} }
} }
// New games need the shared hero; multiplayer games are not ours. // New games need the shared hero (in a game the running one stops
// first, StartNewWorld); multiplayer games are not ours.
const bool canStart = hasHero && !*reinterpret_cast<const uint8_t*>(kMultiplayer); const bool canStart = hasHero && !*reinterpret_cast<const uint8_t*>(kMultiplayer);
for (int c = 0; c < kCampaignCount; ++c) { for (int c = 0; c < kCampaignCount; ++c) {
for (int d = 0; d < kDifficultyCount; ++d) { for (int d = 0; d < kDifficultyCount; ++d) {
@@ -840,7 +846,6 @@ const Row* EditableRow(uint8_t* window) {
// "Neuer Charakter": in the main menu's load window (not in a game, not in // "Neuer Charakter": in the main menu's load window (not in a game, not in
// multiplayer). // multiplayer).
bool g_inGame = false;
bool NewCharacterAllowed(uint8_t* window) { bool NewCharacterAllowed(uint8_t* window) {
return *reinterpret_cast<uint32_t*>(window + kSavegameMode) == kSavegameModeLoad && !g_inGame && return *reinterpret_cast<uint32_t*>(window + kSavegameMode) == kSavegameModeLoad && !g_inGame &&
@@ -968,7 +973,12 @@ void __fastcall Savegame_Action(uint8_t* self, void* edx, int action) {
GameCampaign() = static_cast<uint16_t>(row->choices[row->chosen].campaign); GameCampaign() = static_cast<uint16_t>(row->choices[row->chosen].campaign);
} }
} }
if (const Row* row = action == kSavegameActionDelete ? SelectedRow(self) : nullptr) { const Row* toDelete = action == kSavegameActionDelete ? SelectedRow(self) : nullptr;
if (toDelete && g_inGame && toDelete->file == g_current.file) {
re::Log("characters: %s is being played, not deleted", toDelete->file.c_str());
return;
}
if (const Row* row = toDelete) {
const std::string deleted = CharactersDir() + "deleted\\"; const std::string deleted = CharactersDir() + "deleted\\";
CreateDirectoryA(deleted.c_str(), nullptr); CreateDirectoryA(deleted.c_str(), nullptr);
for (const std::string& file : {row->file, AutosavePath(row->file)}) { for (const std::string& file : {row->file, AutosavePath(row->file)}) {
@@ -1026,6 +1036,33 @@ void StartNewWorld(uint8_t* window, const Row& row, int campaign, int difficulty
reinterpret_cast<ExecuteFn>(0x00755ac0)(&command, nullptr); reinterpret_cast<ExecuteFn>(0x00755ac0)(&command, nullptr);
} }
// A new world from a running game: the game stops as the Esc menu's "Beenden"
// stops it (cCommand_armaStop, execute 0x755b30, thiscall, plain ret: posts
// 0x8001 to the main window, whose handler 0x811fa6 deletes the engine), and
// the new game starts once the engine is gone (OnGameEnd): its start command
// (0x8002) is handled right after the stop, without the main menu.
struct PendingStart {
uint8_t* window = nullptr;
Row row;
Choice world;
} g_pendingStart;
void StopGameThenStart(uint8_t* window, const Row& row, const Choice& world) {
g_pendingStart = {window, row, world};
re::Log("characters: stopping the game for a new world of %s", row.file.c_str());
struct StopCommand {
uint32_t vtable;
} command{0x0088ee2c};
using ExecuteFn = void(__fastcall*)(void*, void*);
reinterpret_cast<ExecuteFn>(0x00755b30)(&command, nullptr);
}
void StartPendingWorld() {
if (const PendingStart pending = std::exchange(g_pendingStart, {}); pending.window) {
StartNewWorld(pending.window, pending.row, pending.world.campaign, pending.world.difficulty);
}
}
// A new character in `campaign`: the game's character creation, as the main // A new character in `campaign`: the game's character creation, as the main
// menu's campaign entries opened it (window command show UI_CHARACTER, the // menu's campaign entries opened it (window command show UI_CHARACTER, the
// campaign set first); its first save makes the character file. // campaign set first); its first save makes the character file.
@@ -1091,7 +1128,11 @@ void DialogChoose(uint8_t* window, size_t index) {
Savegame_Action_original(window, nullptr, kSavegameActionRescan); Savegame_Action_original(window, nullptr, kSavegameActionRescan);
} else if (const Row row = g_rows[g_dialog.row]; row.choices[index].newWorld) { } else if (const Row row = g_rows[g_dialog.row]; row.choices[index].newWorld) {
CloseDialog(); CloseDialog();
StartNewWorld(window, row, row.choices[index].campaign, row.choices[index].difficulty); if (g_inGame) {
StopGameThenStart(window, row, row.choices[index]);
} else {
StartNewWorld(window, row, row.choices[index].campaign, row.choices[index].difficulty);
}
} else { } else {
g_rows[g_dialog.row].chosen = index; g_rows[g_dialog.row].chosen = index;
GameCampaign() = static_cast<uint16_t>(row.choices[index].campaign); GameCampaign() = static_cast<uint16_t>(row.choices[index].campaign);
@@ -1337,8 +1378,8 @@ uint32_t __fastcall Engine_Save(uint8_t* self, void* edx, const char* path);
RE_HOOK(0x00619c90, Engine_Save); RE_HOOK(0x00619c90, Engine_Save);
uint32_t __fastcall Engine_Save(uint8_t* self, void* edx, const char* path) { uint32_t __fastcall Engine_Save(uint8_t* self, void* edx, const char* path) {
EnsureFolders(); EnsureFolders();
const bool autosave = g_autosavePending; const bool autosave = g_autosavePending.exchange(false);
g_autosavePending = false; g_lastSaveAt = GetTickCount64();
const std::string work = WorkDir(); const std::string work = WorkDir();
DeleteFileA((work + kWorkWorld).c_str()); DeleteFileA((work + kWorkWorld).c_str());
const uint32_t result = Engine_Save_original(self, edx, kGameWorld); const uint32_t result = Engine_Save_original(self, edx, kGameWorld);
@@ -1390,16 +1431,35 @@ constexpr size_t kEngineSaveDescription = 0x6a; // char[], "QUICKSAVE" for quic
constexpr char kSaveDescription[] = "SAVE"; // not shown: the list shows the characters constexpr char kSaveDescription[] = "SAVE"; // not shown: the list shows the characters
std::atomic<bool> g_saveRequested{false}; std::atomic<bool> g_saveRequested{false};
// Silent autosave (request of 2026-10-19: every 10 minutes, a setting later):
// the same save, into the autosave file (Engine_Save with g_autosavePending),
// sacredK.ini [characters] AutosaveMinutes (default 10, 0: off) after the
// last save of any kind. While a window is open or a
// save runs it waits for the next tick. A character never saved yet gets its
// file from it (the list shows character files only).
constexpr int kAutosaveMinutesDefault = 10;
uint64_t AutosaveIntervalMs() {
static const int minutes = re::ConfigInt("characters", "AutosaveMinutes", kAutosaveMinutesDefault);
return minutes > 0 ? uint64_t(minutes) * 60 * 1000 : UINT64_MAX;
}
void SaveOnTick(uint8_t* engine) { void SaveOnTick(uint8_t* engine) {
if (!g_saveRequested.exchange(false)) { const bool requested = g_saveRequested.exchange(false);
const bool autosaveDue = GetTickCount64() - g_lastSaveAt >= AutosaveIntervalMs();
if (!requested && !autosaveDue) {
return; return;
} }
auto& flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags); auto& flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
uint32_t* ui = game::UiManager(); uint32_t* ui = game::UiManager();
if (!ui || !game::UiCanOpenWindow(ui) || (flags & kEngineSaving)) { if (!ui || !game::UiCanOpenWindow(ui) || (flags & kEngineSaving)) {
re::Log("characters: no save now (a window is open or a save is running)"); if (requested) {
re::Log("characters: no save now (a window is open or a save is running)");
}
return; return;
} }
g_autosavePending = !requested;
g_lastSaveAt = GetTickCount64();
using CaptureFn = void(__fastcall*)(void*, void*, int); using CaptureFn = void(__fastcall*)(void*, void*, int);
reinterpret_cast<CaptureFn>(0x00613710)(engine, nullptr, 1); reinterpret_cast<CaptureFn>(0x00613710)(engine, nullptr, 1);
std::memcpy(engine + kEngineSaveDescription, kSaveDescription, sizeof(kSaveDescription)); std::memcpy(engine + kEngineSaveDescription, kSaveDescription, sizeof(kSaveDescription));
@@ -1408,69 +1468,68 @@ void SaveOnTick(uint8_t* engine) {
} }
// cUI_Manager::ReceiveEvent (vf00 0x757120, thiscall (event), ret 4) routes // cUI_Manager::ReceiveEvent (vf00 0x757120, thiscall (event), ret 4) routes
// window commands by name (game/ui/windows.h); in a game, showing the save // window commands by name (game/ui/windows.h). In a game:
// window (the Esc menu line) saves instead. The main menu's load window // - showing the save window (the Esc menu's "Speichern") saves instead;
// still opens. // - showing UI_CHARACTER (the Esc menu's "Laden", the former "Exportieren"
// line, whose command shows the export screen) opens the load window.
// The main menu's load window opens as before.
constexpr size_t kUiEventCode = 4; constexpr size_t kUiEventCode = 4;
constexpr size_t kUiEventSubtype = 8; constexpr size_t kUiEventSubtype = 8;
constexpr uint32_t kUiEventWindowSubtype = 5; constexpr uint32_t kUiEventWindowSubtype = 5;
constexpr size_t kUiEventWindowName = 0x48; // char[0x20] constexpr size_t kUiEventWindowName = 0x48; // char[0x20]
constexpr char kCharacterWindowName[] = "UI_CHARACTER";
thread_local bool g_openingLoadWindow = false; // our own Show of the save window passes
bool IsShowCommand(const uint8_t* event, const char* window, size_t size) {
return *reinterpret_cast<const uint32_t*>(event + kUiEventCode) ==
static_cast<uint32_t>(game::WindowCommand::Show) &&
*reinterpret_cast<const uint32_t*>(event + kUiEventSubtype) == kUiEventWindowSubtype &&
std::strncmp(reinterpret_cast<const char*>(event + kUiEventWindowName), window, size) == 0;
}
int __fastcall UiManager_ReceiveEvent(void* self, void* edx, uint8_t* event); int __fastcall UiManager_ReceiveEvent(void* self, void* edx, uint8_t* event);
RE_HOOK(0x00757120, UiManager_ReceiveEvent); RE_HOOK(0x00757120, UiManager_ReceiveEvent);
int __fastcall UiManager_ReceiveEvent(void* self, void* edx, uint8_t* event) { int __fastcall UiManager_ReceiveEvent(void* self, void* edx, uint8_t* event) {
if (g_inGame && *reinterpret_cast<uint32_t*>(event + kUiEventCode) == static_cast<uint32_t>(game::WindowCommand::Show) && if (g_inGame && !g_openingLoadWindow) {
*reinterpret_cast<uint32_t*>(event + kUiEventSubtype) == kUiEventWindowSubtype && if (IsShowCommand(event, kSaveWindowName, sizeof(kSaveWindowName))) {
std::strncmp(reinterpret_cast<const char*>(event + kUiEventWindowName), kSaveWindowName, RequestSave();
sizeof(kSaveWindowName)) == 0) { return 1;
RequestSave(); }
return 1; if (IsShowCommand(event, kCharacterWindowName, sizeof(kCharacterWindowName))) {
g_openingLoadWindow = true;
game::SendWindowCommand(static_cast<uint32_t*>(self), kSaveWindowName, game::WindowCommand::Show);
g_openingLoadWindow = false;
return 1;
}
} }
return UiManager_ReceiveEvent_original(self, edx, event); return UiManager_ReceiveEvent_original(self, edx, event);
} }
// The Esc menu's line "Spielstände" (built by 0x6bb870, text id 0x41b pushed // cUI_Savegame::Show (vf24 0x71cfb0, thiscall (shown), ret 4) picks load mode
// at 0x6bc1f7; the id is used by other windows too) reads "Speichern". // (2) in the main menu and save mode (1) in a game (0x71cfd2). Saving is
constexpr uint32_t kEscMenuSaveLineText = 0x006bc1f7; // direct now: the window always opens for loading (SetMode 0x71c4f0,
constexpr uint32_t kSavegamesText = 0x41b; // thiscall (mode), ret 4; it also disables the hidden "Speichern" button).
void __fastcall Savegame_Show(uint8_t* self, void* edx, char shown);
// The Esc menu without "Exportieren" (request of 2026-10-20; exported heroes RE_HOOK(0x0071cfb0, Savegame_Show);
// are gone). cUI_EscMenu::BuildLines (0x6bb870, thiscall, plain ret; called void __fastcall Savegame_Show(uint8_t* self, void* edx, char shown) {
// by the constructor 0x6bae60 and on an event, 0x6bc497) makes five Savegame_Show_original(self, edx, shown);
// cUI_StaticText64 lines (constructor 0x753470) in the vector at +0x158, 0x24 if (shown && *reinterpret_cast<uint32_t*>(self + kSavegameMode) == kSavegameModeSave) {
// apart (y 8 .. 0x98); each line handles its own events. The window is using SetModeFn = void(__fastcall*)(void*, void*, uint32_t);
// 0x108 x 0xc8, centred (0x6bae9b). The export line goes, the lines below it reinterpret_cast<SetModeFn>(0x0071c4f0)(self, nullptr, kSavegameModeLoad);
// move up and the window loses one line's height, staying centred.
constexpr size_t kEscMenuLines = 0x158;
constexpr char kEscExportLine[] = "UI_ESC_EXPORT";
constexpr int kEscLinePitch = 0x24;
constexpr int16_t kEscMenuHeight = 0xc8;
void __fastcall EscMenu_BuildLines(uint8_t* self, void* edx);
RE_HOOK(0x006bb870, EscMenu_BuildLines);
void __fastcall EscMenu_BuildLines(uint8_t* self, void* edx) {
EscMenu_BuildLines_original(self, edx);
bool removed = false;
for (uint8_t* line : game::ControlsIn(self, kEscMenuLines)) {
auto& rect = *reinterpret_cast<game::UiRect*>(line + game::kControlRect);
if (removed) {
rect.y -= kEscLinePitch;
} else if (game::ControlNamed(line, kEscExportLine)) {
game::RemoveControl(self, kEscMenuLines, line);
game::DeleteControl(line);
removed = true;
}
}
auto& window = *reinterpret_cast<game::UiRect*>(self + game::kControlRect);
if (removed && window.h == kEscMenuHeight) {
window.h -= kEscLinePitch;
window.y += kEscLinePitch / 2;
}
if (!removed) {
re::Log("characters: Esc menu has no %s line", kEscExportLine);
} }
} }
// The Esc menu (cUI_EscMenu::BuildLines 0x6bb870): "Spielstände" (text id
// 0x41b pushed at 0x6bc1f7; the id is used by other windows too) reads
// "Speichern"; "Exportieren" (text id 0x419 pushed at 0x6bc272, its command
// shows UI_CHARACTER and hides the menu) reads "Laden" (request of
// 2026-10-20).
constexpr uint32_t kEscMenuSaveLineText = 0x006bc1f7;
constexpr uint32_t kSavegamesText = 0x41b;
constexpr uint32_t kEscMenuExportLineText = 0x006bc272;
constexpr uint32_t kExportText = 0x419;
// The load window's second line of a row showed the (preview) file name; it // The load window's second line of a row showed the (preview) file name; it
// shows the level and the chosen difficulty instead. Row texts are set every // shows the level and the chosen difficulty instead. Row texts are set every
// frame (0x71d830): line 1 the save's name (+0x204), line 2 the file name (+4, // frame (0x71d830): line 1 the save's name (+0x204), line 2 the file name (+4,
@@ -1529,14 +1588,22 @@ void Startup() {
re::RedirectCall(kRowFileNameCall, kStaticSetText, reinterpret_cast<const void*>(&SetRowSecondLine)); re::RedirectCall(kRowFileNameCall, kStaticSetText, reinterpret_cast<const void*>(&SetRowSecondLine));
static const uint32_t kSaveText = game::RegisterText(L"Speichern"); static const uint32_t kSaveText = game::RegisterText(L"Speichern");
re::PatchPushImm32(kEscMenuSaveLineText, kSavegamesText, kSaveText); re::PatchPushImm32(kEscMenuSaveLineText, kSavegamesText, kSaveText);
static const uint32_t kLoadText = game::RegisterText(L"Laden");
re::PatchPushImm32(kEscMenuExportLineText, kExportText, kLoadText);
} }
struct Init { struct Init {
Init() { Init() {
userfolders::SetGameSaveSubfolder(kGameSaveSubfolder); userfolders::SetGameSaveSubfolder(kGameSaveSubfolder);
game::OnEngineTick(&SaveOnTick); game::OnEngineTick(&SaveOnTick);
game::OnGameStart([] { g_inGame = true; }); game::OnGameStart([] {
game::OnGameEnd([] { g_inGame = false; }); g_inGame = true;
g_lastSaveAt = GetTickCount64(); // the autosave counts from the start
});
game::OnGameEnd([] {
g_inGame = false;
StartPendingWorld();
});
} }
} init; } init;
+3
View File
@@ -334,10 +334,13 @@
0x0061b790 cEngine::Load # (path, quiet), ret 8: reads a GAME.PAK 0x0061b790 cEngine::Load # (path, quiet), ret 8: reads a GAME.PAK
0x00619c90 cEngine::Save # (path), ret 4; render thread, engine +0x54 & 0x40 0x00619c90 cEngine::Save # (path), ret 4; render thread, engine +0x54 & 0x40
0x0060dc00 cEngine::InitGame # (int), ret 4: new game, start hero 0x0060dc00 cEngine::InitGame # (int), ret 4: new game, start hero
0x0071c4f0 cUI_Savegame::SetMode # (mode), ret 4: 1 save, 2 load (+0x75c)
0x0071cfb0 cUI_Savegame::Show # vf24 (bool), ret 4: picks save mode in a game
0x0071c520 cUI_Savegame::Action # (action), ret 4: 2 save, 4 load, 8 delete, 10 rescan, 0xb default 0x0071c520 cUI_Savegame::Action # (action), ret 4: 2 save, 4 load, 8 delete, 10 rescan, 0xb default
0x0071e330 cUI_Savegame::Scan # (dir, multiplayer), ret 8: "./SAVE/*.PAK" 0x0071e330 cUI_Savegame::Scan # (dir, multiplayer), ret 8: "./SAVE/*.PAK"
0x0071be90 cUI_Savegame::SendSaveLoad # (kind, row): engine event 0xf 0x0071be90 cUI_Savegame::SendSaveLoad # (kind, row): engine event 0xf
0x00755ac0 cCommand_armaPlay::execute # posts 0x8002 (mode, argument) to the main window 0x00755ac0 cCommand_armaPlay::execute # posts 0x8002 (mode, argument) to the main window
0x00755b30 cCommand_armaStop::execute # vf04, plain ret: ends the game, posts 0x8001 to the main window (engine deleted, main menu)
0x007d7c40 Session_Instance 0x007d7c40 Session_Instance
0x007d7cf0 Session::SetHeroClass # (class) -> +0x44, ret 4 0x007d7cf0 Session::SetHeroClass # (class) -> +0x44, ret 4
0x00417e50 GameSetup_Instance # 0x12e6c bytes; +0 set by both character-screen starts 0x00417e50 GameSetup_Instance # 0x12e6c bytes; +0 set by both character-screen starts