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>
200 lines
7.7 KiB
C++
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
|