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:
1 parent
6ca96d54b4
commit
46a08e97e9
13 files changed
+368
-22
No files matched your search
@@ -62,6 +62,7 @@ add_library(sacred_re SHARED
|
|||||||
src/mods/megamap.cpp
|
src/mods/megamap.cpp
|
||||||
src/mods/respec.cpp
|
src/mods/respec.cpp
|
||||||
src/mods/runetrade.cpp
|
src/mods/runetrade.cpp
|
||||||
|
src/mods/teleportpotion.cpp
|
||||||
src/mods/guards.cpp
|
src/mods/guards.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacred_re PRIVATE src)
|
target_include_directories(sacred_re PRIVATE src)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Teleportationstrank
|
||||||
|
|
||||||
|
Request (2026-10-17): the yellow potion becomes "Teleportationstrank";
|
||||||
|
drinking it opens the window of known portals and the hero can teleport to
|
||||||
|
one of them.
|
||||||
|
|
||||||
|
## Measured
|
||||||
|
|
||||||
|
- The yellow potions come in stages (small / medium / large; seen types
|
||||||
|
0x1434 and 0x141a), all potion kind 5 (logged in
|
||||||
|
cCreature::UseItem 0x54a8f0).
|
||||||
|
- Touching a teleporter logs `cInterpretSQW::Teleporter(hex=N)` (0x4ae330):
|
||||||
|
it sets bit N in the hero's known portals (hero +0x578) and posts a UI event
|
||||||
|
(constructor 0x553080) of type 0x39 to `DEFAULT_BUSYDLG` with +8 = 2,
|
||||||
|
+0xc = 5 (mode: portals; 6 for underworld portals above 0xd) and +0x10 = the
|
||||||
|
mask. cUI_BusyDlg::HandleEvent (0x720c10) calls SetMode (0x720180).
|
||||||
|
- Choosing a portal: the engine's portal case (0x619671, log `EE_USEPORTAL`)
|
||||||
|
teleports the hero with cCreature::Teleport (0x54d840), returning to 0x6197c8.
|
||||||
|
|
||||||
|
## Mod (src/mods/teleportpotion.cpp)
|
||||||
|
|
||||||
|
- Every item type of potion kind 5 counts. Their names (text 0x428700 of each
|
||||||
|
type) are replaced in every game text (`game::ReplaceInTexts`): the item
|
||||||
|
names and the bottom bar's quick-select tooltip ("Trank der Konzentration
|
||||||
|
[R]", its own text id). The shortest name is replaced, so stage words stay.
|
||||||
|
- cCreature::UseItem (hooked in itemstack.cpp) asks `teleportpotion::UseItem`
|
||||||
|
first: for the hero's Teleportationstrank it posts the teleporter's event
|
||||||
|
with the hero's mask instead of the drink.
|
||||||
|
- The potion is used up (flag, RemoveItem, DestroyObject as UseItem does; a
|
||||||
|
stack loses one) only when cCreature::Teleport is called from the portal
|
||||||
|
case while it is pending; closing the window keeps it.
|
||||||
@@ -134,3 +134,10 @@ stack in one inventory cell, up to 20 per stack (a full stack starts a new
|
|||||||
one). Drag, sell and drop move the whole stack, as with any other item. The
|
one). Drag, sell and drop move the whole stack, as with any other item. The
|
||||||
counts on the potion slots then add up the stacks. In progress, see
|
counts on the potion slots then add up the stacks. In progress, see
|
||||||
docs/item-stacks.md (open: sale price per stack, splitting on the ground).
|
docs/item-stacks.md (open: sale price per stack, splitting on the ground).
|
||||||
|
|
||||||
|
## Walking animation after loading (2026-10-17)
|
||||||
|
|
||||||
|
The hero sometimes plays a walking animation after a save game load while
|
||||||
|
standing still. Not reproduced yet: a load log (hero mode, action, animation
|
||||||
|
fields, orders) showed nothing unusual; right after a load the game itself
|
||||||
|
sends one walk order (0x4) while the hero is idle. Measure again when it shows.
|
||||||
@@ -14,6 +14,7 @@ constexpr uint32_t kObjectUsedUp = 0x8000000; // kObjectFlags: set by cPotion3D
|
|||||||
|
|
||||||
// The item type table at *0xaa9564 (ecx of ItemPotionKind).
|
// The item type table at *0xaa9564 (ecx of ItemPotionKind).
|
||||||
constexpr uint32_t kItemTypeTable = 0x00aa9564;
|
constexpr uint32_t kItemTypeTable = 0x00aa9564;
|
||||||
|
constexpr uint32_t kItemTypeCount = 0x7e60; // types 1..0x7e5f (bounds check in 0x428700)
|
||||||
|
|
||||||
// ItemPotionKind (0x43f630): the potion kind (1..5, the bottom bar's potion
|
// ItemPotionKind (0x43f630): the potion kind (1..5, the bottom bar's potion
|
||||||
// slots; 1 = healing) of item type `type`, 0 if it is no potion.
|
// slots; 1 = healing) of item type `type`, 0 if it is no potion.
|
||||||
@@ -46,6 +47,15 @@ inline bool ItemIsConsumable(uint32_t type) {
|
|||||||
return reinterpret_cast<bool(__stdcall*)(uint32_t)>(0x0043f5f0)(type);
|
return reinterpret_cast<bool(__stdcall*)(uint32_t)>(0x0043f5f0)(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0x428700 (thiscall on the type table, (type), ret 4): the text id of the
|
||||||
|
// item type's name (entry +0x20), which cItemBase::GetName (0x44a420) shows
|
||||||
|
// unless the object has its own (+0x3c).
|
||||||
|
inline uint32_t ItemNameText(uint32_t type) {
|
||||||
|
using Fn = uint32_t(__fastcall*)(void* table, void* edx, uint32_t type);
|
||||||
|
void* table = *reinterpret_cast<void**>(kItemTypeTable);
|
||||||
|
return table ? reinterpret_cast<Fn>(0x00428700)(table, nullptr, type) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
inline uint32_t ItemType(const void* item) {
|
inline uint32_t ItemType(const void* item) {
|
||||||
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
|
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-11
@@ -8,11 +8,17 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
|
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
||||||
|
// GetText (0x672f70, ret 4): (id) -> string; ours first.
|
||||||
|
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id);
|
||||||
|
RE_HOOK(0x00672f70, TextManager_GetText);
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Our ids: above the game's (resource ids are small; bit 31 means "by index").
|
// Our ids: above the game's (resource ids are small; bit 31 means "by index").
|
||||||
@@ -26,10 +32,13 @@ struct GameString {
|
|||||||
const wchar_t* capacity;
|
const wchar_t* capacity;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Capitalized {
|
// A game text as we show it: made from `source` (the game's) when that
|
||||||
|
// changes; `changed` false if ours is the same.
|
||||||
|
struct Shown {
|
||||||
std::wstring source;
|
std::wstring source;
|
||||||
std::wstring text;
|
std::wstring text;
|
||||||
GameString string;
|
GameString string;
|
||||||
|
bool changed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::mutex g_lock;
|
std::mutex g_lock;
|
||||||
@@ -41,8 +50,12 @@ std::unordered_set<uint32_t>& CapitalizedIds() {
|
|||||||
static std::unordered_set<uint32_t> ids;
|
static std::unordered_set<uint32_t> ids;
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
std::vector<std::pair<std::wstring, std::wstring>>& Replacements() {
|
||||||
|
static std::vector<std::pair<std::wstring, std::wstring>> replacements;
|
||||||
|
return replacements;
|
||||||
|
}
|
||||||
// By id; nodes stay put, so the strings handed out stay valid.
|
// By id; nodes stay put, so the strings handed out stay valid.
|
||||||
std::unordered_map<uint32_t, Capitalized> g_capitalized;
|
std::unordered_map<uint32_t, Shown> g_shown;
|
||||||
|
|
||||||
std::wstring Capitalize(std::wstring text) {
|
std::wstring Capitalize(std::wstring text) {
|
||||||
bool wordStart = true;
|
bool wordStart = true;
|
||||||
@@ -63,6 +76,21 @@ std::wstring Capitalize(std::wstring text) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::wstring Transform(uint32_t id, std::wstring text) {
|
||||||
|
for (const auto& [from, to] : Replacements()) {
|
||||||
|
for (size_t at = text.find(from); at != std::wstring::npos; at = text.find(from, at + to.size())) {
|
||||||
|
text.replace(at, from.size(), to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CapitalizedIds().count(id) ? Capitalize(std::move(text)) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameString* GameTextString(uint32_t id) {
|
||||||
|
using ManagerFn = void*(__cdecl*)();
|
||||||
|
void* manager = reinterpret_cast<ManagerFn>(0x006728c0)();
|
||||||
|
return manager ? static_cast<const GameString*>(TextManager_GetText_original(manager, nullptr, id)) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint32_t RegisterText(const wchar_t* text) {
|
uint32_t RegisterText(const wchar_t* text) {
|
||||||
@@ -75,11 +103,29 @@ uint32_t RegisterText(const wchar_t* text) {
|
|||||||
void CapitalizeText(uint32_t id) {
|
void CapitalizeText(uint32_t id) {
|
||||||
std::lock_guard guard(g_lock);
|
std::lock_guard guard(g_lock);
|
||||||
CapitalizedIds().insert(id);
|
CapitalizedIds().insert(id);
|
||||||
|
g_shown.erase(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplaceInTexts(const std::wstring& from, const std::wstring& to) {
|
||||||
|
if (from.empty() || from == to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard guard(g_lock);
|
||||||
|
for (const auto& replacement : Replacements()) {
|
||||||
|
if (replacement.first == from) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Replacements().emplace_back(from, to);
|
||||||
|
g_shown.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring GameText(uint32_t id) {
|
||||||
|
const GameString* string = GameTextString(id);
|
||||||
|
return string && string->begin && string->end > string->begin ? std::wstring(string->begin, string->end)
|
||||||
|
: std::wstring();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetText (0x672f70, ret 4): (id) -> string; ours first.
|
|
||||||
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id);
|
|
||||||
RE_HOOK(0x00672f70, TextManager_GetText);
|
|
||||||
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
||||||
if (id >= kFirstOwnText && id < 0x80000000) {
|
if (id >= kFirstOwnText && id < 0x80000000) {
|
||||||
std::lock_guard guard(g_lock);
|
std::lock_guard guard(g_lock);
|
||||||
@@ -90,22 +136,23 @@ const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
|||||||
}
|
}
|
||||||
const void* original = TextManager_GetText_original(self, edx, id);
|
const void* original = TextManager_GetText_original(self, edx, id);
|
||||||
std::lock_guard guard(g_lock);
|
std::lock_guard guard(g_lock);
|
||||||
if (!original || !CapitalizedIds().count(id)) {
|
if (!original || (Replacements().empty() && !CapitalizedIds().count(id))) {
|
||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
const auto* string = static_cast<const GameString*>(original);
|
const auto* string = static_cast<const GameString*>(original);
|
||||||
if (!string->begin || string->end <= string->begin) {
|
if (!string->begin || string->end <= string->begin) {
|
||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
Capitalized& entry = g_capitalized[id];
|
Shown& entry = g_shown[id];
|
||||||
if (entry.source.compare(0, std::wstring::npos, string->begin, string->end - string->begin) != 0 ||
|
if (entry.text.empty() ||
|
||||||
entry.text.empty()) {
|
entry.source.compare(0, std::wstring::npos, string->begin, string->end - string->begin) != 0) {
|
||||||
entry.source.assign(string->begin, string->end);
|
entry.source.assign(string->begin, string->end);
|
||||||
entry.text = Capitalize(entry.source);
|
entry.text = Transform(id, entry.source);
|
||||||
|
entry.changed = entry.text != entry.source;
|
||||||
const wchar_t* text = entry.text.c_str();
|
const wchar_t* text = entry.text.c_str();
|
||||||
entry.string = {text, text + entry.text.size(), text + entry.text.size()};
|
entry.string = {text, text + entry.text.size(), text + entry.text.size()};
|
||||||
}
|
}
|
||||||
return &entry.string;
|
return entry.changed ? &entry.string : original;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
// tab headers), and changes to the game's own texts.
|
// tab headers), and changes to the game's own texts.
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace game {
|
namespace game {
|
||||||
|
|
||||||
@@ -15,4 +16,11 @@ uint32_t RegisterText(const wchar_t* text);
|
|||||||
// "Rucksack"). Call at startup.
|
// "Rucksack"). Call at startup.
|
||||||
void CapitalizeText(uint32_t id);
|
void CapitalizeText(uint32_t id);
|
||||||
|
|
||||||
|
// Shows `to` in place of `from` wherever a game text contains it (item names,
|
||||||
|
// tooltips, ...).
|
||||||
|
void ReplaceInTexts(const std::wstring& from, const std::wstring& to);
|
||||||
|
|
||||||
|
// The game's text `id` without our changes (empty if there is none).
|
||||||
|
std::wstring GameText(uint32_t id);
|
||||||
|
|
||||||
} // namespace game
|
} // namespace game
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
// (0x5dc3f0; cell rects from CellRect 0x5dd530).
|
// (0x5dc3f0; cell rects from CellRect 0x5dd530).
|
||||||
|
|
||||||
#include "mods/itemstack.h"
|
#include "mods/itemstack.h"
|
||||||
|
#include "mods/teleportpotion.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
@@ -308,11 +309,16 @@ uint32_t __fastcall ObjMgr_DestroyObject(void* self, void* edx, uint32_t id, uin
|
|||||||
return ObjMgr_DestroyObject_original(self, edx, id, a, b, c);
|
return ObjMgr_DestroyObject_original(self, edx, id, a, b, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cCreature::UseItem (0x54a8f0, ret 4): a new drink starts.
|
|
||||||
|
// cCreature::UseItem (0x54a8f0, ret 4): a new drink starts (a
|
||||||
|
// Teleportationstrank opens the portal window instead, teleportpotion.cpp).
|
||||||
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id);
|
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id);
|
||||||
RE_HOOK(0x0054a8f0, Creature_UseItem);
|
RE_HOOK(0x0054a8f0, Creature_UseItem);
|
||||||
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id) {
|
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id) {
|
||||||
g_drunk = 0;
|
g_drunk = 0;
|
||||||
|
if (mods::teleportpotion::UseItem(self, id)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return Creature_UseItem_original(self, edx, id);
|
return Creature_UseItem_original(self, edx, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -160,11 +160,8 @@ int RuneCount(uint8_t* window) {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
int HeroIndex() {
|
using skillslots::HeroIndex;
|
||||||
void* hero = game::GetPlayerHero();
|
using skillslots::HeroInventory;
|
||||||
const int index = hero ? game::CreatureField(hero, game::kCreatureId) : -1;
|
|
||||||
return index >= 0 && index < game::kChestInventory ? index : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hero's object type (hero +0x10), which doLottoSelect takes.
|
// The hero's object type (hero +0x10), which doLottoSelect takes.
|
||||||
uint32_t HeroType() {
|
uint32_t HeroType() {
|
||||||
@@ -172,11 +169,6 @@ uint32_t HeroType() {
|
|||||||
return hero ? game::ItemType(hero) : 0;
|
return hero ? game::ItemType(hero) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* HeroInventory() {
|
|
||||||
const int hero = HeroIndex();
|
|
||||||
return hero >= 0 ? skillslots::Inventory(static_cast<unsigned>(hero)) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free space for a 1 x 1 item in `inventory` (cInventoryEntry::FindFreeSpace 0x5dab60).
|
// Free space for a 1 x 1 item in `inventory` (cInventoryEntry::FindFreeSpace 0x5dab60).
|
||||||
bool FindFreeSpace(uint8_t* inventory, uint16_t* cell) {
|
bool FindFreeSpace(uint8_t* inventory, uint16_t* cell) {
|
||||||
using FindFn = char(__fastcall*)(void*, void*, uint32_t, uint32_t, uint16_t*, uint32_t);
|
using FindFn = char(__fastcall*)(void*, void*, uint32_t, uint32_t, uint16_t*, uint32_t);
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "game/engine/items.h"
|
||||||
|
#include "game/engine/objects.h"
|
||||||
|
#include "game/engine/orders.h"
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
|
|
||||||
namespace mods::skillslots {
|
namespace mods::skillslots {
|
||||||
@@ -51,6 +54,17 @@ uint8_t* Inventory(unsigned index) {
|
|||||||
return base && index < kInventories ? base + index * kInventorySize : nullptr;
|
return base && index < kInventories ? base + index * kInventorySize : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int HeroIndex() {
|
||||||
|
void* hero = game::GetPlayerHero();
|
||||||
|
const int index = hero ? game::CreatureField(hero, game::kCreatureId) : -1;
|
||||||
|
return index >= 0 && index < game::kChestInventory ? index : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* HeroInventory() {
|
||||||
|
const int hero = HeroIndex();
|
||||||
|
return hero >= 0 ? Inventory(static_cast<unsigned>(hero)) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t* ShadowFor(const void* inventory, bool create) {
|
uint8_t* ShadowFor(const void* inventory, bool create) {
|
||||||
const int index = IndexOf(inventory);
|
const int index = IndexOf(inventory);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ constexpr size_t kSelected = 0xd58; // u16 [0] weapon set, [1] art slot
|
|||||||
// Inventory `index` (0-31) of cInventoryManager's block, or null.
|
// Inventory `index` (0-31) of cInventoryManager's block, or null.
|
||||||
uint8_t* Inventory(unsigned index);
|
uint8_t* Inventory(unsigned index);
|
||||||
|
|
||||||
|
// The local player hero's inventory index (its object id; 0..0x10), or -1;
|
||||||
|
// and its inventory, or null.
|
||||||
|
int HeroIndex();
|
||||||
|
uint8_t* HeroInventory();
|
||||||
|
|
||||||
// The shadow inventory holding slots 6-7 (at indices 0-1) of `inventory`, or
|
// The shadow inventory holding slots 6-7 (at indices 0-1) of `inventory`, or
|
||||||
// null if it has none yet and `create` is false.
|
// null if it has none yet and `create` is false.
|
||||||
uint8_t* ShadowFor(const void* inventory, bool create);
|
uint8_t* ShadowFor(const void* inventory, bool create);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace mods::teleportpotion {
|
||||||
|
|
||||||
|
// cCreature::UseItem (mods/itemstack.cpp) asks first: true if `id` is a
|
||||||
|
// Teleportationstrank used by the player hero; it then opened the portal
|
||||||
|
// window instead of the drink.
|
||||||
|
bool UseItem(void* creature, uint32_t id);
|
||||||
|
|
||||||
|
} // namespace mods::teleportpotion
|
||||||
@@ -245,3 +245,10 @@
|
|||||||
0x004ec170 cCreature::CanCollectItem # (item object), ret 4; pickup filter, no distance check
|
0x004ec170 cCreature::CanCollectItem # (item object), ret 4; pickup filter, no distance check
|
||||||
0x00592ef0 WideString_Append # thiscall (first, last, 0), ret 0xc; {begin,end,cap}
|
0x00592ef0 WideString_Append # thiscall (first, last, 0), ret 0xc; {begin,end,cap}
|
||||||
0x00589270 cCreatureArts::ArtTooltip # (art, text*, flag), ret 0xc
|
0x00589270 cCreatureArts::ArtTooltip # (art, text*, flag), ret 0xc
|
||||||
|
0x004ae330 cInterpretSQW::Teleporter # script: adds the portal bit (hero +0x578), posts busy dialog mode 5/6
|
||||||
|
0x00720180 cUI_BusyDlg::SetMode # (mode, a, b), ret 0xc; 5 = known portals
|
||||||
|
0x00720c10 cUI_BusyDlg::HandleEvent # UI event 0x39 to DEFAULT_BUSYDLG, +8 == 2: SetMode(+0xc)
|
||||||
|
0x0054d840 cCreature::Teleport # (x, y, 0, 1), ret 0x10
|
||||||
|
0x00553080 cEvent_UI::cEvent_UI # (type, target, sender, a..f), ret 0x24
|
||||||
|
0x00428700 cItemTypeTable::NameText # (type), ret 4: text id of the type's name (+0x20)
|
||||||
|
0x0054a8f0 cCreature::UseItem # (id), ret 4: event 0x10 to the item, removes it if used up
|
||||||
Reference in new issue
Block a user