Files
sacred_k/src/mods/teleportpotion.cpp
T
khallaandClaude Opus 5.5 3978060954 Teleportationstrank: the running campaign's portal list
In the Underworld the potion opens the Underworld portals (busy dialog mode
6, as the teleporters there do); g_campaign moved to game/addresses.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 00:42:55 +02:00

200 lines
7.7 KiB
C++

// 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 <intrin.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
#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<const uint16_t*>(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<CtorFn>(0x00553080)(event, nullptr, kUiEventBusyDialog, reinterpret_cast<const char*>(kBusyDialogName),
reinterpret_cast<const char*>(kNoSender),
kBusyDialogSetMode, PortalMode(), knownPortals, 0, 0, 0);
game::PostEvent(event);
using FreeFn = void(__fastcall*)(void*, void*);
reinterpret_cast<FreeFn>(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<uint8_t*>(game::GetObjectById(id));
if (!item || !IsTeleportPotion(game::ItemType(item))) {
return;
}
*reinterpret_cast<uint32_t*>(item + game::kObjectFlags) |= game::kObjectUsedUp;
if (uint8_t* inventory = skillslots::HeroInventory()) {
using RemoveFn = int(__fastcall*)(void*, void*, uint32_t, uint32_t);
reinterpret_cast<RemoveFn>(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<uint32_t*>(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<const uint32_t*>(static_cast<const uint8_t*>(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<uint8_t*>(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<uintptr_t>(_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