Teleportationstrank: yellow potions open the portal window

- Every yellow potion (potion kind 5, all stages) is a Teleportationstrank:
  drinking it posts the teleporter's event to the busy dialog with the
  hero's known portals; the potion is used up only when the hero teleports
  from that window (closing it keeps the potion)
- Names replaced in every game text (ReplaceInTexts), so item names and the
  bottom bar's quick-select tooltip read Teleportationstrank; GameText reads
  the game's own text
- HeroIndex / HeroInventory moved to skillslots for shared use
- docs/teleport-potion.md; walking animation after loading noted in todo

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-17 04:04:21 +02:00
1 parent 6ca96d54b4
commit 46a08e97e9
13 files changed
+368 -22

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
// 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) 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/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/skillslots.h"
#include "re/hook.h"
#include "re/log.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;
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 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, kBusyModePortals, 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
}
}
// The yellow potions' names become kName in every text (names, the bottom
// bar's tooltip): the shortest name, so stage words in front ("Großer ...")
// stay, and any name that does not contain it.
void OnGameStart() {
static bool renamed = false;
if (renamed) {
return;
}
std::vector<std::wstring> names;
for (uint32_t type = 1; type < game::kItemTypeCount; ++type) {
if (IsTeleportPotion(type)) {
std::wstring name = game::GameText(game::ItemNameText(type));
if (!name.empty() && std::find(names.begin(), names.end(), name) == names.end()) {
names.push_back(std::move(name));
}
}
}
std::sort(names.begin(), names.end(), [](const auto& a, const auto& b) { return a.size() < b.size(); });
std::vector<std::wstring> replaced;
for (const std::wstring& name : names) {
const bool covered = std::any_of(replaced.begin(), replaced.end(),
[&](const std::wstring& r) { return name.find(r) != std::wstring::npos; });
if (!covered) {
game::ReplaceInTexts(name, kName);
replaced.push_back(name);
}
}
renamed = !names.empty();
re::Log("teleportpotion: %zu potion names, %zu replaced", names.size(), replaced.size());
}
void OnGameEnd() {
g_pending = 0;
g_dialog = nullptr;
}
struct Registration {
Registration() {
game::OnGameStart(&OnGameStart);
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) {
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