// Teleportationstrank (request of 2026-10-17): the yellow potion is renamed // and, instead of its own effect, opens the window of known portals; the // potion is used up when the hero teleports from it (closing the window // keeps it). // // Measured (2026-10-17): the yellow potions (small / medium / large, e.g. // types 0x141a, 0x1434) are potion kind 5 (the bottom bar's fifth slot, // whose tooltip names the potion too). Touching a teleporter runs // cInterpretSQW::Teleporter (0x4ae330): it adds the portal to the hero's known ones (bit mask at hero +0x578) and posts // a UI event (type 0x39) to DEFAULT_BUSYDLG with +8 = 2, +0xc = 5 (portal // mode; 6 for the underworld portals above 0xd, the same mask) and +0x10 = // the mask; // cUI_BusyDlg::HandleEvent (0x720c10) then calls SetMode (0x720180, (mode, a, // b), ret 0xc). Choosing a portal sends the engine an event whose case // (0x619671, "EE_USEPORTAL") teleports the hero with cCreature::Teleport // (0x54d840, (x, y, 0, 1), ret 0x10), returning to 0x6197c8. // // cCreature::UseItem (0x54a8f0) is hooked by mods/itemstack.cpp, which asks // UseItem() here first. #include "mods/teleportpotion.h" #include #include #include #include #include #include "game/addresses.h" #include "game/engine/events.h" #include "game/engine/items.h" #include "game/engine/objects.h" #include "game/engine/orders.h" #include "game/ui/texts.h" #include "mods/charfile.h" #include "mods/skillslots.h" #include "re/hook.h" #include "re/log.h" #include "re/patch.h" namespace mods::teleportpotion { namespace { constexpr int kTeleportPotionKind = 5; // the yellow potions (game::ItemPotionKind) constexpr const wchar_t* kName = L"Teleportationstrank"; bool IsTeleportPotion(uint32_t type) { return game::ItemPotionKind(type) == kTeleportPotionKind; } constexpr size_t kHeroKnownPortals = 0x578; // u32 bit mask, bit = portal index // The UI event cInterpretSQW::Teleporter posts (built with the UI event // constructor 0x553080: (type, target name, sender name, a, b, c, d, e, f), // thiscall, ret 0x24; its wide string at +0x68 is freed with 0x6e85d0). constexpr uint32_t kUiEventBusyDialog = 0x39; constexpr uintptr_t kBusyDialogName = 0x0094d700; // "DEFAULT_BUSYDLG" constexpr uintptr_t kNoSender = 0x00aa24bc; // the game's empty name constexpr uint32_t kBusyDialogSetMode = 2; constexpr uint32_t kBusyModePortals = 5; // Ancaria's portals constexpr uint32_t kBusyModeUnderworldPortals = 6; constexpr size_t kUiEventSize = 0x80; constexpr size_t kUiEventString = 0x68; // cCreature::Teleport's return address in the engine's portal case. constexpr uintptr_t kPortalTeleportReturn = 0x006197c8; constexpr size_t kControlFlags = 0x10; constexpr uint32_t kControlVisible = 1; // The portal list of the running campaign (user, 2026-10-21: the potion // showed Ancaria's portals in the Underworld), as the teleporters choose it. uint32_t PortalMode() { const uint16_t campaign = *reinterpret_cast(game::addr::g_campaign); return campaign == characters::kCampaignUnderworld ? kBusyModeUnderworldPortals : kBusyModePortals; } // The potion that opened the portal window, until it is used or the window // closes; and the dialog, once it showed for it. uint32_t g_pending = 0; uint8_t* g_dialog = nullptr; bool g_shown = false; void OpenPortalWindow(uint32_t knownPortals) { alignas(4) uint8_t event[kUiEventSize] = {}; using CtorFn = void*(__fastcall*)(void*, void*, uint32_t, const char*, const char*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); reinterpret_cast(0x00553080)(event, nullptr, kUiEventBusyDialog, reinterpret_cast(kBusyDialogName), reinterpret_cast(kNoSender), kBusyDialogSetMode, PortalMode(), knownPortals, 0, 0, 0); game::PostEvent(event); using FreeFn = void(__fastcall*)(void*, void*); reinterpret_cast(0x006e85d0)(event + kUiEventString, nullptr); } // Uses up the potion as cCreature::UseItem does after a drink (flag, remove // from the inventory, destroy); mods/itemstack.cpp takes one off a stack. void UseUp(uint32_t id) { auto* item = static_cast(game::GetObjectById(id)); if (!item || !IsTeleportPotion(game::ItemType(item))) { return; } *reinterpret_cast(item + game::kObjectFlags) |= game::kObjectUsedUp; if (uint8_t* inventory = skillslots::HeroInventory()) { using RemoveFn = int(__fastcall*)(void*, void*, uint32_t, uint32_t); reinterpret_cast(0x005da5f0)(inventory, nullptr, id, 1); // cInventoryEntry::RemoveItem } game::DestroyObject(id); } void OnTick(uint8_t*) { if (!g_pending || !g_dialog) { return; } const bool visible = (*reinterpret_cast(g_dialog + kControlFlags) & kControlVisible) != 0; if (visible) { g_shown = true; } else if (g_shown) { g_pending = 0; // closed without a teleport } } // Every kind 5 potion is a Teleportationstrank in every text, from the start: // the yellow ones ("Trank der Konzentration", also the bottom bar's tooltip) // and the stamina ones, kind 5 as well ("Schwacher / Starker / Kompletter // Ausdauertrank", measured 2026-10-19); stage words in front stay. constexpr const wchar_t* kOriginalNames[] = {L"Trank der Konzentration", L"Ausdauertrank"}; void Startup() { for (const wchar_t* name : kOriginalNames) { game::ReplaceInTexts(name, kName); } } RE_STARTUP(TeleportPotionStartup, Startup); void OnGameEnd() { g_pending = 0; g_dialog = nullptr; } struct Registration { Registration() { game::OnGameEnd(&OnGameEnd); game::OnEngineTick(&OnTick); } } g_registration; } // namespace bool UseItem(void* creature, uint32_t id) { const void* item = id ? game::GetObjectById(id) : nullptr; if (!item || !IsTeleportPotion(game::ItemType(item)) || creature != game::GetPlayerHero()) { return false; } g_pending = id; g_shown = false; OpenPortalWindow( *reinterpret_cast(static_cast(creature) + kHeroKnownPortals)); re::Log("teleportpotion: portal window for potion %u", id); return true; } } // namespace mods::teleportpotion namespace game { // cUI_BusyDlg::SetMode (0x720180, (mode, a, b), ret 0xc): remembers the dialog // when it shows the portals for a potion. void __fastcall BusyDlg_SetMode(void* self, void* edx, uint32_t mode, uint32_t a, uint32_t b); RE_HOOK(0x00720180, BusyDlg_SetMode); void __fastcall BusyDlg_SetMode(void* self, void* edx, uint32_t mode, uint32_t a, uint32_t b) { using namespace mods::teleportpotion; if (g_pending) { g_dialog = static_cast(self); if (mode != kBusyModePortals && mode != kBusyModeUnderworldPortals) { g_pending = 0; } } BusyDlg_SetMode_original(self, edx, mode, a, b); } // cCreature::Teleport (0x54d840, (x, y, 0, 1), ret 0x10): a teleport from the // portal window uses up the potion that opened it. uint32_t __fastcall Creature_Teleport(void* self, void* edx, uint32_t x, uint32_t y, uint32_t a, uint32_t b); RE_HOOK(0x0054d840, Creature_Teleport); uint32_t __fastcall Creature_Teleport(void* self, void* edx, uint32_t x, uint32_t y, uint32_t a, uint32_t b) { using namespace mods::teleportpotion; const uint32_t result = Creature_Teleport_original(self, edx, x, y, a, b); if (g_pending && reinterpret_cast(_ReturnAddress()) == kPortalTeleportReturn) { const uint32_t id = g_pending; g_pending = 0; UseUp(id); re::Log("teleportpotion: potion %u used for a teleport", id); } return result; } } // namespace game