Shared chest tabs, rune stacks and rune master, Q pickup, Kettenblitz tooltip and range
- Shared chest: tabs "Truhe"/"Gemeinsam" as a column left of the grid (game tab sheet), grids placed every tick, persistent across heroes - Inventory tab texts capitalized (RegisterText / CapitalizeText) - Combat-art runes stack like potions (itemstack, renamed from potionstack) - Rune master: any rune for any number of runes, price by rune count (4 runes = 1,000, x10 per missing rune), bought rune goes into the backpack, Buy disabled without runes, gold or room, last rune remembered per hero (saved), the chosen rune's name below its icon, dropping a stack gives up one rune - Keys: the old key handling no longer takes over after the save screen (text field focus only counts while its dialog is shown) - Esc crash: tooltips forgotten when the game destroys them - Pick up (Q) sends the pickup order directly; search reach doubled - Kettenblitz: tooltip shows hits per chain, chains, cast and jump range; cast range 900 (longer ray segments), jumps half of that Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3d9d864c25
commit
6ca96d54b4
29 files changed
+2006
-171
No files matched your search
@@ -17,6 +17,16 @@ std::vector<SaveGameLoadedFn>& LoadedCallbacks() {
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
std::vector<GameEndFn>& GameEndCallbacks() {
|
||||
static std::vector<GameEndFn> callbacks;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
std::vector<GameStartFn>& GameStartCallbacks() {
|
||||
static std::vector<GameStartFn> callbacks;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnEngineTick(EngineTickFn callback) {
|
||||
@@ -27,6 +37,14 @@ void OnSaveGameLoaded(SaveGameLoadedFn callback) {
|
||||
LoadedCallbacks().push_back(callback);
|
||||
}
|
||||
|
||||
void OnGameEnd(GameEndFn callback) {
|
||||
GameEndCallbacks().push_back(callback);
|
||||
}
|
||||
|
||||
void OnGameStart(GameStartFn callback) {
|
||||
GameStartCallbacks().push_back(callback);
|
||||
}
|
||||
|
||||
void NotifySaveGameLoaded() {
|
||||
for (SaveGameLoadedFn callback : LoadedCallbacks()) {
|
||||
callback();
|
||||
@@ -46,4 +64,26 @@ void __fastcall Engine_Tick(void* self, void* edx, void* event) {
|
||||
}
|
||||
}
|
||||
|
||||
// cEngine::cEngine (0x60ac70, ret 4, called once at 0x60d7a3): the save game
|
||||
// and the hero are loaded inside it.
|
||||
void* __fastcall Engine_Construct(void* self, void* edx, uint32_t arg);
|
||||
RE_HOOK(0x0060ac70, Engine_Construct);
|
||||
void* __fastcall Engine_Construct(void* self, void* edx, uint32_t arg) {
|
||||
void* result = Engine_Construct_original(self, edx, arg);
|
||||
for (GameStartFn callback : GameStartCallbacks()) {
|
||||
callback();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// cEngine::~cEngine (0x60d5e0, plain ret).
|
||||
void __fastcall Engine_Destruct(void* self, void* edx);
|
||||
RE_HOOK(0x0060d5e0, Engine_Destruct);
|
||||
void __fastcall Engine_Destruct(void* self, void* edx) {
|
||||
for (GameEndFn callback : GameEndCallbacks()) {
|
||||
callback();
|
||||
}
|
||||
Engine_Destruct_original(self, edx);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -16,4 +16,14 @@ using SaveGameLoadedFn = void (*)();
|
||||
void OnSaveGameLoaded(SaveGameLoadedFn callback);
|
||||
void NotifySaveGameLoaded();
|
||||
|
||||
// The game session ends (cEngine::~cEngine 0x60d5e0, before the engine and the
|
||||
// object manager are torn down): object ids are about to become invalid.
|
||||
using GameEndFn = void (*)();
|
||||
void OnGameEnd(GameEndFn callback);
|
||||
|
||||
// A game session started (cEngine::cEngine 0x60ac70 returned; the save game
|
||||
// and the hero are loaded by then).
|
||||
using GameStartFn = void (*)();
|
||||
void OnGameStart(GameStartFn callback);
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "game/engine/items.h"
|
||||
|
||||
#include "game/engine/stream.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
using SerializeFn = uint32_t(__fastcall*)(void* item, void* edx, Stream*, uint32_t write, uint32_t version);
|
||||
|
||||
SerializeFn ItemSerializer(void* item) {
|
||||
return reinterpret_cast<SerializeFn>((*static_cast<void***>(item))[0x1c / 4]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<uint8_t> SerializeItem(void* item) {
|
||||
std::vector<uint8_t> data;
|
||||
MemoryStream memory = MakeMemoryStream(data);
|
||||
ItemSerializer(item)(item, nullptr, AsStream(memory), 1, 0);
|
||||
return data;
|
||||
}
|
||||
|
||||
uint32_t CreateItem(uint32_t type, const std::vector<uint8_t>& data, uint32_t ownerHeroId) {
|
||||
using CreateFn = uint32_t(__fastcall*)(void*, void*, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
void* manager = ObjectManager();
|
||||
const uint32_t id = manager ? reinterpret_cast<CreateFn>(0x005fb590)(manager, nullptr, type, 0, 0, 0) : 0;
|
||||
auto* item = static_cast<uint8_t*>(id ? GetObjectById(id) : nullptr); // cObjectManager::Create
|
||||
if (!item) {
|
||||
return 0;
|
||||
}
|
||||
std::vector<uint8_t> copy = data; // the stream reads from a vector it may touch
|
||||
MemoryStream memory = MakeMemoryStream(copy);
|
||||
ItemSerializer(item)(item, nullptr, AsStream(memory), 0, kItemFileVersion);
|
||||
// As loadHero after reading an item: the new id, flags, the owner.
|
||||
auto& flags = *reinterpret_cast<uint32_t*>(item + kObjectFlags);
|
||||
*reinterpret_cast<uint32_t*>(item + 0xc) = id;
|
||||
flags &= ~0x200000u;
|
||||
if (flags & 2) {
|
||||
flags &= ~0x30u;
|
||||
}
|
||||
flags &= ~0x10u;
|
||||
item[kItemOwner] = static_cast<uint8_t>(ownerHeroId);
|
||||
return id;
|
||||
}
|
||||
|
||||
uint32_t CloneItem(uint32_t id) {
|
||||
auto* item = static_cast<uint8_t*>(id ? GetObjectById(id) : nullptr);
|
||||
return item ? CreateItem(ItemType(item), SerializeItem(item), item[kItemOwner]) : 0;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -24,6 +24,28 @@ inline int ItemPotionKind(uint32_t type) {
|
||||
return table ? reinterpret_cast<Fn>(0x0043f630)(table, nullptr, type) : 0;
|
||||
}
|
||||
|
||||
// The item class of item type `type` (type table entry: 0x80 bytes, class u8
|
||||
// at +0x2e; read by ItemPotionKind). Usable from the inventory
|
||||
// (cCreature::UseItem 0x54a8f0): 9 (potions), 0x10, 0x1c (runes).
|
||||
constexpr size_t kItemTypeEntrySize = 0x80;
|
||||
constexpr size_t kItemTypeClass = 0x2e;
|
||||
inline uint8_t ItemClass(uint32_t type) {
|
||||
const auto* table = *reinterpret_cast<const uint8_t* const*>(kItemTypeTable);
|
||||
return table ? table[type * kItemTypeEntrySize + kItemTypeClass] : 0;
|
||||
}
|
||||
|
||||
// Item classes (type table +0x2e), measured: 5 weapons / armour, 7, 9 potions,
|
||||
// 0x1c combat-art runes (1x1).
|
||||
constexpr uint8_t kItemClassPotion = 9;
|
||||
constexpr uint8_t kItemClassRune = 0x1c;
|
||||
|
||||
// 0x43f5f0 (stdcall, ret 4): the item type is a potion or a rune (class 9 or
|
||||
// 0x1c). Items of one such type are alike apart from their object state
|
||||
// (measured: two runes of one type serialize the same past the object part).
|
||||
inline bool ItemIsConsumable(uint32_t type) {
|
||||
return reinterpret_cast<bool(__stdcall*)(uint32_t)>(0x0043f5f0)(type);
|
||||
}
|
||||
|
||||
inline uint32_t ItemType(const void* item) {
|
||||
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
|
||||
}
|
||||
@@ -74,3 +96,25 @@ void ForEachGridItem(const uint8_t* inventory, Fn fn) {
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace game {
|
||||
|
||||
// Item objects as hero files store them (ItemDataMgr::saveHero / loadHero,
|
||||
// docs/shared-chest.md): the type plus the object's own serializer
|
||||
// vf1c(stream, write, version), ret 0xc.
|
||||
constexpr uint32_t kItemFileVersion = 0x1b; // the hero file version handed to the item readers
|
||||
constexpr size_t kItemOwner = 0x39; // u8: the owner hero's id
|
||||
|
||||
std::vector<uint8_t> SerializeItem(void* item);
|
||||
|
||||
// A new item object of `type` read from `data`, as loadHero recreates one
|
||||
// (cObjectManager::Create, the serializer, its new id, flags, the owner).
|
||||
// Returns its id, 0 on failure.
|
||||
uint32_t CreateItem(uint32_t type, const std::vector<uint8_t>& data, uint32_t ownerHeroId);
|
||||
|
||||
// A copy of item `id` as a new object (same type and data, same owner).
|
||||
uint32_t CloneItem(uint32_t id);
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "game/engine/stream.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
void* __fastcall MemDestroy(MemoryStream* self, void*, int) {
|
||||
return self;
|
||||
}
|
||||
uint32_t __fastcall MemRead(MemoryStream* self, void*, void* buffer, uint32_t size) {
|
||||
const uint32_t available = static_cast<uint32_t>(self->data->size()) - self->position;
|
||||
const uint32_t n = size < available ? size : available;
|
||||
std::memcpy(buffer, self->data->data() + self->position, n);
|
||||
self->position += n;
|
||||
return n;
|
||||
}
|
||||
uint32_t __fastcall MemWrite(MemoryStream* self, void*, const void* buffer, uint32_t size) {
|
||||
if (self->data->size() < self->position + size) {
|
||||
self->data->resize(self->position + size);
|
||||
}
|
||||
std::memcpy(self->data->data() + self->position, buffer, size);
|
||||
self->position += size;
|
||||
return size;
|
||||
}
|
||||
void __fastcall MemSeek(MemoryStream* self, void*, uint32_t position) {
|
||||
self->position = position;
|
||||
}
|
||||
uint32_t __fastcall MemSize(MemoryStream* self, void*) {
|
||||
return static_cast<uint32_t>(self->data->size());
|
||||
}
|
||||
uint32_t __fastcall MemTell(MemoryStream* self, void*) {
|
||||
return self->position;
|
||||
}
|
||||
|
||||
void* g_memoryVtable[8] = {
|
||||
reinterpret_cast<void*>(&MemDestroy), reinterpret_cast<void*>(&MemRead),
|
||||
reinterpret_cast<void*>(&MemWrite), reinterpret_cast<void*>(&MemSeek),
|
||||
reinterpret_cast<void*>(&MemSize), reinterpret_cast<void*>(&MemTell),
|
||||
reinterpret_cast<void*>(0x0065afb0), reinterpret_cast<void*>(0x0065af80), // cFile helpers
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
MemoryStream MakeMemoryStream(std::vector<uint8_t>& data) {
|
||||
return MemoryStream{g_memoryVtable, &data, 0};
|
||||
}
|
||||
|
||||
std::string Hex(const uint8_t* p, size_t size) {
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
std::string s(size * 2, '0');
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
s[2 * i] = digits[p[i] >> 4];
|
||||
s[2 * i + 1] = digits[p[i] & 15];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void Unhex(const std::string& s, uint8_t* out, size_t size) {
|
||||
if (s.size() != size * 2) {
|
||||
throw std::runtime_error("hex field has the wrong length");
|
||||
}
|
||||
auto nibble = [](char c) -> uint8_t {
|
||||
return static_cast<uint8_t>(c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10);
|
||||
};
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
out[i] = static_cast<uint8_t>(nibble(s[2 * i]) << 4 | nibble(s[2 * i + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
// The game's file interface (cFile) and an in-memory cFile, for code that
|
||||
// lets the game (de)serialize into a buffer: inventories, item objects.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace game {
|
||||
|
||||
// cFile: vf04 read, vf08 write, vf0c seek, vf10 size, vf14 tell; all __thiscall.
|
||||
struct Stream {
|
||||
void** vtable;
|
||||
};
|
||||
|
||||
inline uint32_t Read(Stream* stream, void* buffer, uint32_t size) {
|
||||
using Fn = uint32_t(__fastcall*)(Stream*, void*, void*, uint32_t);
|
||||
return reinterpret_cast<Fn>(stream->vtable[1])(stream, nullptr, buffer, size);
|
||||
}
|
||||
inline uint32_t Write(Stream* stream, const void* buffer, uint32_t size) {
|
||||
using Fn = uint32_t(__fastcall*)(Stream*, void*, const void*, uint32_t);
|
||||
return reinterpret_cast<Fn>(stream->vtable[2])(stream, nullptr, buffer, size);
|
||||
}
|
||||
inline void Seek(Stream* stream, uint32_t position) {
|
||||
using Fn = void(__fastcall*)(Stream*, void*, uint32_t);
|
||||
reinterpret_cast<Fn>(stream->vtable[3])(stream, nullptr, position);
|
||||
}
|
||||
inline uint32_t Tell(Stream* stream) {
|
||||
using Fn = uint32_t(__fastcall*)(Stream*, void*);
|
||||
return reinterpret_cast<Fn>(stream->vtable[5])(stream, nullptr);
|
||||
}
|
||||
|
||||
// A cFile over a byte vector (reads from / writes at `position`).
|
||||
struct MemoryStream {
|
||||
void** vtable;
|
||||
std::vector<uint8_t>* data;
|
||||
uint32_t position;
|
||||
};
|
||||
|
||||
MemoryStream MakeMemoryStream(std::vector<uint8_t>& data);
|
||||
|
||||
inline Stream* AsStream(MemoryStream& memory) {
|
||||
return reinterpret_cast<Stream*>(&memory);
|
||||
}
|
||||
|
||||
// Lower-case hex text of `size` bytes, and back (throws on a wrong length).
|
||||
std::string Hex(const uint8_t* p, size_t size);
|
||||
void Unhex(const std::string& s, uint8_t* out, size_t size);
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
// cUI_TabSheet (0x80 bytes, vtable 0x895258): a control with tab headers
|
||||
// (the inventory's "Rucksack" / "Kampfkünste" / "Combos"). Its cUI_TabControl
|
||||
// at +0x78 (0x98 bytes, constructor 0x7278f0) holds the tabs: a vector of
|
||||
// 0x18-byte entries at +0x78/+0x7c {text id, rect (+4 x, +8 y, +0xc w|h),
|
||||
// header control (+0x10), page (+0x14)}, the active index at +0x84 (u16).
|
||||
// Header controls: +0x24 x, +0x28 y, +0x2c w|h (relative to the sheet).
|
||||
// Layout (0x729210) puts the headers in a row above the sheet; a click on a
|
||||
// header reaches SetActive, which hides the old page and shows the new one
|
||||
// (page vf24(show)). Selected / hover / normal look is the game's own
|
||||
// (cUI_TabControl::Render 0x728090).
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "game/ui/text.h"
|
||||
|
||||
namespace game {
|
||||
|
||||
constexpr size_t kTabSheetSize = 0x80;
|
||||
constexpr size_t kTabSheetControl = 0x78; // cUI_TabControl*
|
||||
constexpr size_t kTabControlTabs = 0x78; // entries begin; +0x7c end
|
||||
constexpr size_t kTabEntrySize = 0x18;
|
||||
constexpr size_t kTabEntryRect = 0x4;
|
||||
constexpr size_t kTabEntryHeader = 0x10;
|
||||
constexpr size_t kControlRect = 0x24; // a control's rect (x, y, w|h), relative to its parent
|
||||
// The header art's height (left piece; static data filled when the UI loads).
|
||||
constexpr uintptr_t kTabHeaderArtHeight = 0x009ed17c;
|
||||
|
||||
// The game's operator new (0x8485e2, cdecl): controls the UI deletes itself
|
||||
// must come from it.
|
||||
inline void* GameNew(size_t size) {
|
||||
return reinterpret_cast<void*(__cdecl*)(size_t)>(0x008485e2)(size);
|
||||
}
|
||||
|
||||
// cUI_TabSheet::cUI_TabSheet (0x728ed0, ret 0x18): (name, rect by value,
|
||||
// parent, 5); the inventory passes 5 as the last argument.
|
||||
inline void* NewTabSheet(const char* name, UiRect rect, void* parent) {
|
||||
void* sheet = GameNew(kTabSheetSize);
|
||||
using ConstructFn = void*(__fastcall*)(void*, void*, const char*, UiRect, void*, uint32_t);
|
||||
return sheet ? reinterpret_cast<ConstructFn>(0x00728ed0)(sheet, nullptr, name, rect, parent, 5) : nullptr;
|
||||
}
|
||||
|
||||
// cUI_TabSheet::AddTab (0x7290d0, ret 8): (text id, page control).
|
||||
inline void AddTab(void* sheet, uint32_t textId, void* page) {
|
||||
using AddFn = char(__fastcall*)(void*, void*, uint32_t, void*);
|
||||
reinterpret_cast<AddFn>(0x007290d0)(sheet, nullptr, textId, page);
|
||||
}
|
||||
|
||||
// cUI_TabSheet::SetActive (0x7291d0, ret 4).
|
||||
inline void SetActiveTab(void* sheet, uint16_t index) {
|
||||
using SetFn = void(__fastcall*)(void*, void*, uint32_t);
|
||||
reinterpret_cast<SetFn>(0x007291d0)(sheet, nullptr, index);
|
||||
}
|
||||
|
||||
inline uint8_t* TabControl(void* sheet) {
|
||||
return *reinterpret_cast<uint8_t**>(static_cast<uint8_t*>(sheet) + kTabSheetControl);
|
||||
}
|
||||
|
||||
// The tab entries of a cUI_TabControl.
|
||||
inline uint8_t* TabEntries(uint8_t* control) {
|
||||
return *reinterpret_cast<uint8_t**>(control + kTabControlTabs);
|
||||
}
|
||||
inline size_t TabCount(uint8_t* control) {
|
||||
uint8_t* begin = TabEntries(control);
|
||||
uint8_t* end = *reinterpret_cast<uint8_t**>(control + kTabControlTabs + 4);
|
||||
return begin ? static_cast<size_t>(end - begin) / kTabEntrySize : 0;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
|
||||
namespace game {
|
||||
|
||||
// cUI_Window2::AddControl (0x727120, ret 4): the window renders the controls
|
||||
// in its list (+0x78) and hands them its events (cUI_Window2::Render 0x727040,
|
||||
// HandleEvent 0x726880). A control's parent alone doesn't do that.
|
||||
inline void AddWindowControl(void* window, void* control) {
|
||||
using AddFn = void(__fastcall*)(void*, void*, void*);
|
||||
reinterpret_cast<AddFn>(0x00727120)(window, nullptr, control);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "game/ui/texts.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cwchar>
|
||||
#include <cwctype>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "re/hook.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
// Our ids: above the game's (resource ids are small; bit 31 means "by index").
|
||||
constexpr uint32_t kFirstOwnText = 0x7fff0000;
|
||||
|
||||
// The string GetText returns: a wide string as {begin, end (at the
|
||||
// terminator), capacity end} (the game's own at cScriptResourceDlg + 8).
|
||||
struct GameString {
|
||||
const wchar_t* begin;
|
||||
const wchar_t* end;
|
||||
const wchar_t* capacity;
|
||||
};
|
||||
|
||||
struct Capitalized {
|
||||
std::wstring source;
|
||||
std::wstring text;
|
||||
GameString string;
|
||||
};
|
||||
|
||||
std::mutex g_lock;
|
||||
std::vector<GameString>& Texts() {
|
||||
static std::vector<GameString> texts;
|
||||
return texts;
|
||||
}
|
||||
std::unordered_set<uint32_t>& CapitalizedIds() {
|
||||
static std::unordered_set<uint32_t> ids;
|
||||
return ids;
|
||||
}
|
||||
// By id; nodes stay put, so the strings handed out stay valid.
|
||||
std::unordered_map<uint32_t, Capitalized> g_capitalized;
|
||||
|
||||
std::wstring Capitalize(std::wstring text) {
|
||||
bool wordStart = true;
|
||||
for (wchar_t& c : text) {
|
||||
if (std::iswalpha(c)) {
|
||||
wchar_t buffer[2] = {c, 0};
|
||||
if (wordStart) {
|
||||
CharUpperBuffW(buffer, 1);
|
||||
} else {
|
||||
CharLowerBuffW(buffer, 1);
|
||||
}
|
||||
c = buffer[0];
|
||||
wordStart = false;
|
||||
} else {
|
||||
wordStart = !(c == L'\'' || c == L'-');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t RegisterText(const wchar_t* text) {
|
||||
std::lock_guard guard(g_lock);
|
||||
const size_t length = std::wcslen(text);
|
||||
Texts().push_back({text, text + length, text + length});
|
||||
return kFirstOwnText + static_cast<uint32_t>(Texts().size() - 1);
|
||||
}
|
||||
|
||||
void CapitalizeText(uint32_t id) {
|
||||
std::lock_guard guard(g_lock);
|
||||
CapitalizedIds().insert(id);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (id >= kFirstOwnText && id < 0x80000000) {
|
||||
std::lock_guard guard(g_lock);
|
||||
const size_t index = id - kFirstOwnText;
|
||||
if (index < Texts().size()) {
|
||||
return &Texts()[index];
|
||||
}
|
||||
}
|
||||
const void* original = TextManager_GetText_original(self, edx, id);
|
||||
std::lock_guard guard(g_lock);
|
||||
if (!original || !CapitalizedIds().count(id)) {
|
||||
return original;
|
||||
}
|
||||
const auto* string = static_cast<const GameString*>(original);
|
||||
if (!string->begin || string->end <= string->begin) {
|
||||
return original;
|
||||
}
|
||||
Capitalized& entry = g_capitalized[id];
|
||||
if (entry.source.compare(0, std::wstring::npos, string->begin, string->end - string->begin) != 0 ||
|
||||
entry.text.empty()) {
|
||||
entry.source.assign(string->begin, string->end);
|
||||
entry.text = Capitalize(entry.source);
|
||||
const wchar_t* text = entry.text.c_str();
|
||||
entry.string = {text, text + entry.text.size(), text + entry.text.size()};
|
||||
}
|
||||
return &entry.string;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
// Game texts by id (TextManager 0x6728c0, GetText 0x672f70): texts of our own
|
||||
// under ids the game does not use, for controls that take a text id (e.g.
|
||||
// tab headers), and changes to the game's own texts.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace game {
|
||||
|
||||
// A new text id for `text` (kept for the rest of the run; `text` must stay
|
||||
// valid, e.g. a literal).
|
||||
uint32_t RegisterText(const wchar_t* text);
|
||||
|
||||
// Shows the game's text `id` with each word capitalized ("RUCKSACK" ->
|
||||
// "Rucksack"). Call at startup.
|
||||
void CapitalizeText(uint32_t id);
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user