// Esc closes open windows before it opens the game menu. // // cUI_Manager::HandleEsc (0x756ba0) closes the ESC menu, the large map, the // quest book or the inventory, hides two more windows, and otherwise opens // the ESC menu. With a trader (and others) open it only closed the inventory, // which the trader window opened again. Now every open window of the table // below is sent the command the game itself uses to close the quest book: a // cEventUI2 "hide" (code 0x12, subtype 5, the window's name) to the UI // manager's event handler. The game's handling stays for the ESC menu itself and when // none of these windows is open. See docs/escape.md. #include #include #include "re/hook.h" #include "re/log.h" namespace mods::escape { namespace { // cUI_Manager: window pointers by slot (dwords). struct ClosableWindow { int slot; const char* name; }; constexpr ClosableWindow kClosable[] = { {0x21, "UI_WND_INVENTORY"}, {0x22, "UI_WND_EQUIPMENT"}, {0x23, "UI_WND_BLACKSMITH"}, {0x24, "UI_WND_MERCHANT"}, {0x25, "UI_WND_MEGAMAP"}, {0x28, "UI_WND_STATS_CHAR"}, {0x2a, "UI_WND_QUESTBOOK"}, {0x2b, "UI_WND_SAVEGAME"}, {0x2d, "UI_WND_PURCHASE"}, {0x2e, "UI_WND_MASTER"}, {0x2f, "UI_WND_OPTIONS"}, {0x30, "UI_WND_CHEST"}, {0x31, "UI_WND_HORSE"}, {0x35, "UI_WND_CUBE"}, }; constexpr int kEscMenuSlot = 0x2c; constexpr size_t kControlFlags = 0x10; // bit 0: visible // cEventUI2 as HandleEsc builds it (event at [esp+10h] there). constexpr uint32_t kEventUiVtable = 0x0088edd0; constexpr uint32_t kEventUiWindowCommand = 0x12; constexpr uint32_t kWindowCommandHide = 5; constexpr size_t kEventUiSize = 0x80; // Names, written by 0x5904e0 (sender at +0, window at +0x20 of this): measured // in the UI commands the game sends when a window is closed with its button or // hotkey (window name at +0x48). constexpr size_t kEventUiNames = 0x28; constexpr size_t kEventUiText = 0x68; // a string, 0x423920 / 0x416660 bool Visible(const uint8_t* window) { return window && (window[kControlFlags] & 1) != 0; } // The window command goes to the UI manager's event handler (vf00, // 0x757120), which routes it by name, as HandleEsc does for the inventory. // (Sent to the window's own handler it was not understood the same way: the // trader changed its stock instead of closing.) void Hide(uint32_t* manager, const char* name) { alignas(4) uint8_t event[kEventUiSize] = {}; *reinterpret_cast(event) = kEventUiVtable; *reinterpret_cast(event + 4) = kEventUiWindowCommand; *reinterpret_cast(event + 8) = kWindowCommandHide; using SetNamesFn = void(__fastcall*)(void*, void*, const char* window, const char* sender); reinterpret_cast(0x005904e0)(event + kEventUiNames, nullptr, name, ""); using StringFn = void(__fastcall*)(void*, void*); reinterpret_cast(0x00423920)(event + kEventUiText, nullptr); // construct using HandleFn = int(__fastcall*)(void*, void*, void*); reinterpret_cast((*reinterpret_cast(manager))[0])(manager, nullptr, event); reinterpret_cast(0x00416660)(event + kEventUiText, nullptr); // destroy } } // namespace // True if Esc closed windows (the game's handling is skipped). bool CloseWindows(uint32_t* manager) { if (Visible(reinterpret_cast(manager[kEscMenuSlot]))) { return false; } bool closed = false; for (const ClosableWindow& w : kClosable) { auto* window = reinterpret_cast(manager[w.slot]); if (Visible(window)) { Hide(manager, w.name); closed = true; } } return closed; } } // namespace mods::escape namespace game { // cUI_Manager::HandleEsc (0x756ba0): 1 if handled. uint32_t __fastcall UiManager_HandleEsc(uint32_t* self, void* edx); RE_HOOK(0x00756ba0, UiManager_HandleEsc); uint32_t __fastcall UiManager_HandleEsc(uint32_t* self, void* edx) { if (self && mods::escape::CloseWindows(self)) { return 1; } return UiManager_HandleEsc_original(self, edx); } } // namespace game