Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
198c21dbfa | ||
|
|
ce3d46a568 |
No files matched your search
@@ -6,3 +6,4 @@
|
|||||||
sacredK.ini
|
sacredK.ini
|
||||||
/ghidra/
|
/ghidra/
|
||||||
/screenshots/
|
/screenshots/
|
||||||
|
*.stackdump
|
||||||
@@ -141,6 +141,6 @@ into a breakpoint exception (0x80000003, "ESP was not properly saved").
|
|||||||
## Memory
|
## Memory
|
||||||
|
|
||||||
Project memory lives in
|
Project memory lives in
|
||||||
`C:\Users\torha\.claude\projects\D--Projekte-sacred-decompile\memory\`
|
`C:\Users\torha\.claude\projects\D--Projekte-sacred-k\memory\`
|
||||||
(`project-status.md`: what is done and confirmed, what is open). Update it
|
(`project-status.md`: what is done and confirmed, what is open). Update it
|
||||||
after commits and before long pauses.
|
after commits and before long pauses.
|
||||||
@@ -69,6 +69,7 @@ add_library(sacredK SHARED
|
|||||||
src/mods/guards.cpp
|
src/mods/guards.cpp
|
||||||
src/mods/charexport.cpp
|
src/mods/charexport.cpp
|
||||||
src/mods/options.cpp
|
src/mods/options.cpp
|
||||||
|
src/mods/lootfilter.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacredK PRIVATE src)
|
target_include_directories(sacredK PRIVATE src)
|
||||||
target_compile_options(sacredK PRIVATE ${SACREDK_CXX_FLAGS})
|
target_compile_options(sacredK PRIVATE ${SACREDK_CXX_FLAGS})
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Loot filter (Beute page)
|
||||||
|
|
||||||
|
Request (2026-10-19): on the options window's *Beute* tab, per category —
|
||||||
|
Gold, Tränke, and the item colours white / blue / yellow / brown / green —
|
||||||
|
one of:
|
||||||
|
|
||||||
|
- **Löschen** (Void): the drop is deleted at once;
|
||||||
|
- **Ignorieren** (Ignore): the pickup key leaves it;
|
||||||
|
- **Standard** (Default): picked up, label with the labels key (Alt);
|
||||||
|
- **Immer anzeigen** (Always Show): picked up, label without the key too.
|
||||||
|
|
||||||
|
Decided with the user: the filter replaces the game's PICKUPAUTO choice
|
||||||
|
(all / valuable / gold only; its radio buttons are hidden, the animation
|
||||||
|
checkbox stays); Void deletes loot only (monster drops, chests), not items
|
||||||
|
the player puts down. Settings: `[loot]` in `sacredK.ini`
|
||||||
|
(`Gold=Default`, keys Gold, Potions, White, Blue, Yellow, Brown, Green;
|
||||||
|
values Void, Ignore, Default, AlwaysShow). Code: `src/mods/lootfilter.cpp`,
|
||||||
|
page in `src/mods/options.cpp`.
|
||||||
|
|
||||||
|
## What decides the category (read from the game)
|
||||||
|
|
||||||
|
- Gold: `0x43ae20` (thiscall on the item type table, (type)): one of four
|
||||||
|
types listed at `0x947920`. The labels show its amount, the collect mode
|
||||||
|
takes it without inventory space, the loot drop sets the amount after
|
||||||
|
creating it.
|
||||||
|
- Potions: item class 9 (type table +0x2e).
|
||||||
|
- Item colour (`0x5c1330`, the items' vf24 case 2): set items green
|
||||||
|
(type group record `0x433f00` +0x23); otherwise, with the game's
|
||||||
|
`UNIQUE_COLOR` option (on by default), by the quality tier, bits 16..19 of
|
||||||
|
the item's quality flags (+0x11c): up to 4 white `ffefefef`, 5..11 blue
|
||||||
|
`ffa0a0ff`, 12..14 yellow `fff0e040`, 15 brown `ffc19a33`; set green
|
||||||
|
`ff15ff25`. (Without UNIQUE_COLOR, items with special bits get other
|
||||||
|
colours from `0x5c1260`; the filter uses the tier regardless.)
|
||||||
|
- Not filtered: runes (class 0x1c), objects with flag 0x800 and the types
|
||||||
|
`0x428550` reports (CanCollectItem always takes both; quest items,
|
||||||
|
presumably).
|
||||||
|
|
||||||
|
## Where it acts
|
||||||
|
|
||||||
|
- **Pickup**: `cCreature::CanCollectItem` (`0x4ec170`, (item), ret 4) is the
|
||||||
|
filter of the collect mode (`0x4ec3a0`, the pickup key's order 0x113): its
|
||||||
|
ring scan and the follow-up scan next to a picked item. A clicked item does
|
||||||
|
not ask it. Original: refuses carried items (+0x14 bit 2), takes the
|
||||||
|
always-collected ones, then filters by PICKUPAUTO (`0x819580`; 1 by value
|
||||||
|
against the hero level, 2 gold only). Now: carried → no; else by mode.
|
||||||
|
- **Void**: the loot drop `cCreature 0x559a30` ((forced type, ?, ?, killer),
|
||||||
|
ret 0x10; logs `Drop from [%d] %s`) makes its items with
|
||||||
|
`cObjectManager::CreateAt` (`0x5fbaa0`, ret 0x14) and tosses them
|
||||||
|
(`0x44e0a0`). Chests keep their contents as ids at +0x1e4/+0x1e8;
|
||||||
|
`cChest3D::dropItems` (`0x5c9b10`, ret 4) throws them out. Ids from both
|
||||||
|
are queued and handled on the next engine tick (the callers use the item
|
||||||
|
after creating it, e.g. `0x4d0f29` uses the returned object): still on the
|
||||||
|
map (no carried / dead flag) and Void → `RemoveFromMap` (`0x5fc910`) and
|
||||||
|
`DestroyObject(id, 0, 0, 0)`, as the loot drop itself replaces an item.
|
||||||
|
Player drops go through `cEngine::dropItem` (`0x6116c0`), not touched.
|
||||||
|
- **Always Show**: `cWorldView0::DrawObjectLabels` (`0x627260`, (renderer),
|
||||||
|
ret 4) draws the labels list (+0x96b30, 0x1c per entry, +0 object id; an
|
||||||
|
entry with the high bit set is skipped) only with the key held (it sets
|
||||||
|
+0x10 bit 0x4000 from the key). Without the key, if an always-shown item
|
||||||
|
is listed: all other entries are marked skipped, the key reads as held
|
||||||
|
(`input::ForceLabels`), the original runs, the marks and bit 0x4000 go back.
|
||||||
|
Clicking such a label without the key does not pick it up (PickObjectAt
|
||||||
|
`0x626d30` uses its own list); clicking the item does.
|
||||||
|
|
||||||
|
## The page
|
||||||
|
|
||||||
|
The auto-pickup frame (now only *Animation*) stays at the top left; below it,
|
||||||
|
across both columns, a framed table: a row per category (name in the game's
|
||||||
|
colour), a box per mode, the chosen one highlighted; the title "Beutefilter"
|
||||||
|
in the header row (above the frame it met the backdrop); hints at the bottom.
|
||||||
|
Changes are kept until OK (`0x717460`), Cancel drops them, like the key
|
||||||
|
bindings.
|
||||||
@@ -109,3 +109,11 @@ Request: M (map), Tab (minimap) and Alt (text overlay) bindable too.
|
|||||||
instead: `LabelsKeyState` returns the bound key's state.
|
instead: `LabelsKeyState` returns the bound key's state.
|
||||||
- Alt and F10 come as WM_SYSKEYDOWN: the key capture takes those too.
|
- Alt and F10 come as WM_SYSKEYDOWN: the key capture takes those too.
|
||||||
- Key page: 20 lines per column (line 25 px) to fit the three rows.
|
- Key page: 20 lines per column (line 25 px) to fit the three rows.
|
||||||
|
|
||||||
|
## Beute: loot filter (2026-10-19)
|
||||||
|
|
||||||
|
The page holds the loot filter (`docs/loot-filter.md`): the game's pickup
|
||||||
|
choice (PICKUPAUTO radio buttons, their labels, the separator; matched by
|
||||||
|
pointer and control name at +0x30) is never shown, a framed table below the
|
||||||
|
auto-pickup frame sets a mode per category. Its frame is made like the key
|
||||||
|
page's (`AddFrame`).
|
||||||
@@ -60,6 +60,61 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0x43ae20 (thiscall on the type table, (type), ret 4): the type is a gold
|
||||||
|
// pile (one of the four types at 0x947920; labels show its amount, the loot
|
||||||
|
// drop sets it, the collect mode takes it without inventory space).
|
||||||
|
inline bool ItemIsGold(uint32_t type) {
|
||||||
|
using Fn = bool(__fastcall*)(void* table, void* edx, uint32_t type);
|
||||||
|
void* table = *reinterpret_cast<void**>(kItemTypeTable);
|
||||||
|
return table && reinterpret_cast<Fn>(0x0043ae20)(table, nullptr, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x433f00 (thiscall on the type table, (type), ret 4): the type's group
|
||||||
|
// record (0x102 bytes), null if none. +0x23 set: a set item (the item colour
|
||||||
|
// 0x5c1330 shows it green).
|
||||||
|
inline bool ItemIsSetItem(uint32_t type) {
|
||||||
|
using Fn = const uint8_t*(__fastcall*)(void* table, void* edx, uint32_t type);
|
||||||
|
void* table = *reinterpret_cast<void**>(kItemTypeTable);
|
||||||
|
const uint8_t* group = table ? reinterpret_cast<Fn>(0x00433f00)(table, nullptr, type) : nullptr;
|
||||||
|
return group && group[0x23] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x428550 (thiscall on the type table, (type), ret 4): cCreature::
|
||||||
|
// CanCollectItem takes items of such types whatever the pickup setting (type
|
||||||
|
// flag 0x1000000 with +0x61 / +0x62 / +0x1c set; quest items, presumably).
|
||||||
|
inline bool ItemAlwaysCollected(uint32_t type) {
|
||||||
|
using Fn = bool(__fastcall*)(void* table, void* edx, uint32_t type);
|
||||||
|
void* table = *reinterpret_cast<void**>(kItemTypeTable);
|
||||||
|
return table && reinterpret_cast<Fn>(0x00428550)(table, nullptr, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item's quality block (+0x118, cArmor3D::vf24 case 2 hands it to the
|
||||||
|
// item colour 0x5c1330): flags at +4, bits 16..19 the quality tier. With the
|
||||||
|
// game's UNIQUE_COLOR option (on by default) the colour follows the tier:
|
||||||
|
// up to 4 white, 5..11 blue, 12..14 yellow, 15 brown; set items green.
|
||||||
|
constexpr size_t kItemQualityFlags = 0x118 + 4;
|
||||||
|
inline unsigned ItemQualityTier(const void* item) {
|
||||||
|
const uint32_t flags =
|
||||||
|
*reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemQualityFlags);
|
||||||
|
return (flags >> 16) & 0xf;
|
||||||
|
}
|
||||||
|
constexpr unsigned kTierBlue = 5;
|
||||||
|
constexpr unsigned kTierYellow = 12;
|
||||||
|
constexpr unsigned kTierBrown = 15;
|
||||||
|
|
||||||
|
// The item colours of 0x5c1330 (ARGB).
|
||||||
|
constexpr uint32_t kItemColourWhite = 0xffefefef;
|
||||||
|
constexpr uint32_t kItemColourBlue = 0xffa0a0ff;
|
||||||
|
constexpr uint32_t kItemColourYellow = 0xfff0e040;
|
||||||
|
constexpr uint32_t kItemColourBrown = 0xffc19a33;
|
||||||
|
constexpr uint32_t kItemColourGreen = 0xff15ff25;
|
||||||
|
|
||||||
|
// The item colour the game shows (vf24(2, 0): 0x5c1330).
|
||||||
|
inline uint32_t ItemColour(void* item) {
|
||||||
|
using Fn = uint32_t(__fastcall*)(void*, void*, int, int);
|
||||||
|
return reinterpret_cast<Fn>((*static_cast<void***>(item))[0x24 / 4])(item, nullptr, 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// The potion kind of object `id`, 0 if it is no potion (or gone).
|
// The potion kind of object `id`, 0 if it is no potion (or gone).
|
||||||
inline int PotionKindOf(uint32_t id) {
|
inline int PotionKindOf(uint32_t id) {
|
||||||
const void* item = id ? GetObjectById(id) : nullptr;
|
const void* item = id ? GetObjectById(id) : nullptr;
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ inline bool IsPlayerHeroId(uint32_t id) {
|
|||||||
// manager (saving marks effect objects dead, cObjectManager::Place refuses them).
|
// manager (saving marks effect objects dead, cObjectManager::Place refuses them).
|
||||||
constexpr size_t kObjectFlags = 0x14;
|
constexpr size_t kObjectFlags = 0x14;
|
||||||
constexpr uint32_t kObjectDead = 0x10;
|
constexpr uint32_t kObjectDead = 0x10;
|
||||||
|
// Set while an item is carried (in an inventory), not on the map:
|
||||||
|
// cCreature::CanCollectItem refuses it, cEngine::dropItem (0x6116c0) only
|
||||||
|
// takes it off the map without it.
|
||||||
|
constexpr uint32_t kObjectCarried = 0x2;
|
||||||
|
|
||||||
// The creatures near the player (g_nearbyCreatures 0xad1c60: {object id, ?}
|
// The creatures near the player (g_nearbyCreatures 0xad1c60: {object id, ?}
|
||||||
// x g_nearbyCreatureCount 0xad1b48), as the game uses them for aura targets.
|
// x g_nearbyCreatureCount 0xad1b48), as the game uses them for aura targets.
|
||||||
@@ -31,6 +35,7 @@ constexpr uint32_t kTypeObject = 0x008e9648;
|
|||||||
constexpr uint32_t kTypeCreature = 0x008e9660;
|
constexpr uint32_t kTypeCreature = 0x008e9660;
|
||||||
constexpr uint32_t kTypeControl = 0x009de168;
|
constexpr uint32_t kTypeControl = 0x009de168;
|
||||||
constexpr uint32_t kTypeEditControl = 0x009e3608;
|
constexpr uint32_t kTypeEditControl = 0x009e3608;
|
||||||
|
constexpr uint32_t kTypeItemBase = 0x0094cbc0;
|
||||||
|
|
||||||
// __RTDynamicCast (0x849701).
|
// __RTDynamicCast (0x849701).
|
||||||
inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType) {
|
inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType) {
|
||||||
@@ -51,11 +56,22 @@ inline void* GetObjectById(uint32_t id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cObjectManager::DestroyObject (0x5fbe10), with the arguments cCreature::UseItem
|
// cObjectManager::DestroyObject (0x5fbe10), with the arguments cCreature::UseItem
|
||||||
// destroys a drunk potion with (id, 1, 0, 0).
|
// destroys a drunk potion with (id, 1, 0, 0); the loot drop (0x559a30) an
|
||||||
inline void DestroyObject(uint32_t id) {
|
// item it replaces with (id, 0, 0, 0), after RemoveFromMap.
|
||||||
|
inline void DestroyObject(uint32_t id, uint32_t a = 1) {
|
||||||
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, uint32_t, uint32_t, uint32_t);
|
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||||
if (void* manager = ObjectManager()) {
|
if (void* manager = ObjectManager()) {
|
||||||
reinterpret_cast<Fn>(0x005fbe10)(manager, nullptr, id, 1, 0, 0);
|
reinterpret_cast<Fn>(0x005fbe10)(manager, nullptr, id, a, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cObjectManager 0x5fc910 (thiscall (id), ret 4): takes object `id` off the
|
||||||
|
// map. The loot drop (0x559a30) replaces an item it made with this and
|
||||||
|
// DestroyObject.
|
||||||
|
inline void RemoveFromMap(uint32_t id) {
|
||||||
|
using Fn = void(__fastcall*)(void* manager, void* edx, uint32_t);
|
||||||
|
if (void* manager = ObjectManager()) {
|
||||||
|
reinterpret_cast<Fn>(0x005fc910)(manager, nullptr, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -343,7 +343,14 @@ constexpr uint32_t kGetAsyncKeyStateImport = 0x0088e2b0;
|
|||||||
constexpr uint32_t kLabelsKeyCalls[] = {0x00599ebf, 0x0060f227, 0x00612015, 0x006176ec,
|
constexpr uint32_t kLabelsKeyCalls[] = {0x00599ebf, 0x0060f227, 0x00612015, 0x006176ec,
|
||||||
0x00626ffe, 0x00627299, 0x006c80b0};
|
0x00626ffe, 0x00627299, 0x006c80b0};
|
||||||
|
|
||||||
|
// Set by mods/lootfilter.cpp around DrawObjectLabels to draw the labels of
|
||||||
|
// items shown without the key (game thread, like the polls).
|
||||||
|
bool g_labelsForced = false;
|
||||||
|
|
||||||
SHORT WINAPI LabelsKeyState(int) {
|
SHORT WINAPI LabelsKeyState(int) {
|
||||||
|
if (g_labelsForced) {
|
||||||
|
return static_cast<SHORT>(0x8000);
|
||||||
|
}
|
||||||
for (const Binding& binding : g_bindings) {
|
for (const Binding& binding : g_bindings) {
|
||||||
if (binding.kind == Kind::Labels) {
|
if (binding.kind == Kind::Labels) {
|
||||||
return binding.vk ? GetAsyncKeyState(binding.vk) : 0;
|
return binding.vk ? GetAsyncKeyState(binding.vk) : 0;
|
||||||
@@ -487,6 +494,14 @@ bool TypingIntoTextField() {
|
|||||||
return FocusedTextField() != nullptr;
|
return FocusedTextField() != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool LabelsKeyHeld() {
|
||||||
|
return (LabelsKeyState(VK_MENU) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ForceLabels(bool forced) {
|
||||||
|
g_labelsForced = forced;
|
||||||
|
}
|
||||||
|
|
||||||
int MovementKey(Move direction) {
|
int MovementKey(Move direction) {
|
||||||
for (const Binding& binding : g_bindings) {
|
for (const Binding& binding : g_bindings) {
|
||||||
if (binding.kind == Kind::Move && binding.index == static_cast<int>(direction)) {
|
if (binding.kind == Kind::Move && binding.index == static_cast<int>(direction)) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam
|
|||||||
enum class Move { Up, Left, Down, Right };
|
enum class Move { Up, Left, Down, Right };
|
||||||
int MovementKey(Move direction);
|
int MovementKey(Move direction);
|
||||||
|
|
||||||
|
// The labels key (Beschriftungen) is held. While forced, the game's polls of
|
||||||
|
// it read it held (mods/lootfilter.cpp, around DrawObjectLabels).
|
||||||
|
bool LabelsKeyHeld();
|
||||||
|
void ForceLabels(bool forced);
|
||||||
|
|
||||||
// The key bound to drinking potion kind `kind` (1..5); 0 when unbound.
|
// The key bound to drinking potion kind `kind` (1..5); 0 when unbound.
|
||||||
int PotionKey(int kind);
|
int PotionKey(int kind);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
// Loot filter (request of 2026-10-19), set on the options window's Beute page
|
||||||
|
// (mods/options.cpp): per category - Gold, Tränke, and the item colours white,
|
||||||
|
// blue, yellow, brown, green - one of
|
||||||
|
// Void a drop is deleted at once (monster loot, chests; items the
|
||||||
|
// player puts down stay),
|
||||||
|
// Ignore the pickup key (Q, the collect mode) leaves it,
|
||||||
|
// Default picked up, its label shows with the labels key,
|
||||||
|
// AlwaysShow picked up, its label shows without the key too.
|
||||||
|
// It replaces the game's PICKUPAUTO setting (all / valuable / gold only),
|
||||||
|
// whose radio buttons are gone from the page.
|
||||||
|
//
|
||||||
|
// - Categories from the game's data: gold by type (0x43ae20), potions by item
|
||||||
|
// class 9, set items green (0x433f00 +0x23), otherwise the quality tier
|
||||||
|
// that picks the item colour (0x5c1330, game/engine/items.h). Runes and the
|
||||||
|
// types CanCollectItem always takes (quest items) are not filtered.
|
||||||
|
// - Pickup: cCreature::CanCollectItem (0x4ec170) is the collect mode's filter
|
||||||
|
// (both of its scans; a clicked item does not ask it).
|
||||||
|
// - Void: items made by the loot drop (cCreature 0x559a30, via
|
||||||
|
// cObjectManager::CreateAt) and the contents a chest throws out
|
||||||
|
// (cChest3D::dropItems 0x5c9b10) are taken off the map and destroyed on the
|
||||||
|
// next engine tick, when the callers are done with them.
|
||||||
|
// - AlwaysShow: cWorldView0::DrawObjectLabels (0x627260) draws the labels
|
||||||
|
// list (+0x96b30, 0x1c per entry, +0 object id, high bit: skip) when the
|
||||||
|
// labels key is held. Without the key it runs with the key read as held and
|
||||||
|
// every other entry marked skipped, then the marks and the world view's
|
||||||
|
// labels flag (+0x10 bit 0x4000, which it sets from the key) go back.
|
||||||
|
|
||||||
|
#include "mods/lootfilter.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/engine/events.h"
|
||||||
|
#include "game/engine/items.h"
|
||||||
|
#include "game/engine/objects.h"
|
||||||
|
#include "mods/input.h"
|
||||||
|
#include "re/config.h"
|
||||||
|
#include "re/hook.h"
|
||||||
|
#include "re/log.h"
|
||||||
|
#include "re/patch.h"
|
||||||
|
|
||||||
|
namespace mods::lootfilter {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum Category : int { kGold, kPotions, kWhite, kBlue, kYellow, kBrown, kGreen, kNone = -1 };
|
||||||
|
|
||||||
|
struct CategoryInfo {
|
||||||
|
const char* key; // in [loot]
|
||||||
|
const wchar_t* label;
|
||||||
|
uint32_t colour;
|
||||||
|
};
|
||||||
|
// Gold and potions as the game's labels show them: white.
|
||||||
|
constexpr CategoryInfo kCategories[kCategoryCount] = {
|
||||||
|
{"Gold", L"Gold", game::kItemColourWhite}, {"Potions", L"Tränke", game::kItemColourWhite},
|
||||||
|
{"White", L"Weiß", game::kItemColourWhite}, {"Blue", L"Blau", game::kItemColourBlue},
|
||||||
|
{"Yellow", L"Gelb", game::kItemColourYellow}, {"Brown", L"Braun", game::kItemColourBrown},
|
||||||
|
{"Green", L"Grün (Set)", game::kItemColourGreen},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModeInfo {
|
||||||
|
const char* key;
|
||||||
|
const wchar_t* label;
|
||||||
|
};
|
||||||
|
constexpr ModeInfo kModes[kModeCount] = {
|
||||||
|
{"Void", L"Löschen"}, {"Ignore", L"Ignorieren"}, {"Default", L"Standard"}, {"AlwaysShow", L"Immer anzeigen"}};
|
||||||
|
|
||||||
|
Mode g_modes[kCategoryCount] = {Mode::Default, Mode::Default, Mode::Default, Mode::Default,
|
||||||
|
Mode::Default, Mode::Default, Mode::Default};
|
||||||
|
|
||||||
|
// cObject +0x14 bit 0x800: CanCollectItem takes such objects whatever the
|
||||||
|
// pickup setting (as the types of ItemAlwaysCollected).
|
||||||
|
constexpr uint32_t kObjectAlwaysCollected = 0x800;
|
||||||
|
|
||||||
|
int CategoryOf(void* object) {
|
||||||
|
// Types are one table for all objects; the quality block needs a cItemBase.
|
||||||
|
if (!object) {
|
||||||
|
return kNone;
|
||||||
|
}
|
||||||
|
const uint32_t type = game::ItemType(object);
|
||||||
|
if (type == 0 || type >= game::kItemTypeCount) {
|
||||||
|
return kNone;
|
||||||
|
}
|
||||||
|
const uint32_t flags = *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(object) + game::kObjectFlags);
|
||||||
|
if ((flags & kObjectAlwaysCollected) || game::ItemAlwaysCollected(type)) {
|
||||||
|
return kNone;
|
||||||
|
}
|
||||||
|
if (game::ItemIsGold(type)) {
|
||||||
|
return kGold;
|
||||||
|
}
|
||||||
|
const uint8_t itemClass = game::ItemClass(type);
|
||||||
|
if (itemClass == game::kItemClassPotion) {
|
||||||
|
return kPotions;
|
||||||
|
}
|
||||||
|
if (itemClass == game::kItemClassRune) {
|
||||||
|
return kNone;
|
||||||
|
}
|
||||||
|
void* item = game::DynamicCast(object, game::kTypeObject, game::kTypeItemBase);
|
||||||
|
if (!item) {
|
||||||
|
return kNone;
|
||||||
|
}
|
||||||
|
if (game::ItemIsSetItem(type)) {
|
||||||
|
return kGreen;
|
||||||
|
}
|
||||||
|
const unsigned tier = game::ItemQualityTier(item);
|
||||||
|
return tier >= game::kTierBrown ? kBrown : tier >= game::kTierYellow ? kYellow : tier >= game::kTierBlue ? kBlue : kWhite;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode ModeOf(void* object) {
|
||||||
|
const int category = CategoryOf(object);
|
||||||
|
return category == kNone ? Mode::Default : g_modes[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnyMode(Mode mode) {
|
||||||
|
for (const Mode m : g_modes) {
|
||||||
|
if (m == mode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Load() {
|
||||||
|
for (int i = 0; i < kCategoryCount; ++i) {
|
||||||
|
const std::string text = re::ConfigString("loot", kCategories[i].key, kModes[int(Mode::Default)].key);
|
||||||
|
for (int m = 0; m < kModeCount; ++m) {
|
||||||
|
if (_stricmp(text.c_str(), kModes[m].key) == 0) {
|
||||||
|
g_modes[i] = static_cast<Mode>(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RE_STARTUP(LootFilterStartup, Load);
|
||||||
|
|
||||||
|
// --- Void ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
int g_dropDepth = 0; // inside the loot drop
|
||||||
|
std::vector<uint32_t> g_dropped; // its items and thrown out chest contents
|
||||||
|
|
||||||
|
void OnTick(uint8_t*) {
|
||||||
|
if (g_dropped.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::vector<uint32_t> dropped = std::move(g_dropped);
|
||||||
|
g_dropped.clear();
|
||||||
|
for (const uint32_t id : dropped) {
|
||||||
|
auto* object = static_cast<uint8_t*>(game::GetObjectById(id));
|
||||||
|
if (!object) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t flags = *reinterpret_cast<const uint32_t*>(object + game::kObjectFlags);
|
||||||
|
if ((flags & (game::kObjectCarried | game::kObjectDead)) || ModeOf(object) != Mode::Void) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
re::Log("lootfilter: drop %u (type %u) deleted", id, game::ItemType(object));
|
||||||
|
game::RemoveFromMap(id);
|
||||||
|
game::DestroyObject(id, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Registration {
|
||||||
|
Registration() {
|
||||||
|
game::OnEngineTick(&OnTick);
|
||||||
|
}
|
||||||
|
} g_registration;
|
||||||
|
|
||||||
|
// --- labels --------------------------------------------------------------------
|
||||||
|
|
||||||
|
constexpr size_t kLabelsBegin = 0x96b30;
|
||||||
|
constexpr size_t kLabelEntrySize = 0x1c;
|
||||||
|
constexpr uint32_t kLabelSkipped = 0x80000000;
|
||||||
|
constexpr size_t kViewFlags = 0x10; // cWorldView0
|
||||||
|
constexpr uint32_t kLabelsShown = 0x4000;
|
||||||
|
|
||||||
|
uint32_t* LabelEntry(uint8_t* view, size_t index) {
|
||||||
|
auto* begin = *reinterpret_cast<uint8_t**>(view + kLabelsBegin);
|
||||||
|
return reinterpret_cast<uint32_t*>(begin + index * kLabelEntrySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t LabelCount(const uint8_t* view) {
|
||||||
|
const auto* begin = *reinterpret_cast<const uint8_t* const*>(view + kLabelsBegin);
|
||||||
|
const auto* end = *reinterpret_cast<const uint8_t* const*>(view + kLabelsBegin + 4);
|
||||||
|
return begin && end > begin ? static_cast<size_t>(end - begin) / kLabelEntrySize : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlwaysShown(uint32_t entryId) {
|
||||||
|
if (entryId & kLabelSkipped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void* object = game::GetObjectById(entryId);
|
||||||
|
const int category = object ? CategoryOf(object) : kNone;
|
||||||
|
return category != kNone && g_modes[category] == Mode::AlwaysShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const wchar_t* CategoryLabel(int category) {
|
||||||
|
return kCategories[category].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t CategoryColour(int category) {
|
||||||
|
return kCategories[category].colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* ModeLabel(Mode mode) {
|
||||||
|
return kModes[static_cast<int>(mode)].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode GetMode(int category) {
|
||||||
|
return g_modes[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetModes(const Mode* modes) {
|
||||||
|
for (int i = 0; i < kCategoryCount; ++i) {
|
||||||
|
g_modes[i] = modes[i];
|
||||||
|
re::SetConfigString("loot", kCategories[i].key, kModes[static_cast<int>(modes[i])].key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mods::lootfilter
|
||||||
|
|
||||||
|
namespace game {
|
||||||
|
|
||||||
|
using namespace mods::lootfilter;
|
||||||
|
|
||||||
|
// cCreature::CanCollectItem (0x4ec170, (item), ret 4): carried items never;
|
||||||
|
// quest items, runes and anything not filtered always; else by the mode.
|
||||||
|
bool __fastcall Creature_CanCollectItem(void* self, void* edx, uint8_t* item);
|
||||||
|
RE_HOOK(0x004ec170, Creature_CanCollectItem);
|
||||||
|
bool __fastcall Creature_CanCollectItem(void*, void*, uint8_t* item) {
|
||||||
|
if (!item || (*reinterpret_cast<const uint32_t*>(item + kObjectFlags) & kObjectCarried)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Mode mode = ModeOf(item);
|
||||||
|
return mode == Mode::Default || mode == Mode::AlwaysShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loot drop (cCreature 0x559a30, (forced type, ?, ?, killer id), ret 0x10;
|
||||||
|
// returns the last item made or null).
|
||||||
|
void* __fastcall Creature_DropLoot(void* self, void* edx, uint32_t type, uint32_t a, uint32_t b, uint32_t killer);
|
||||||
|
RE_HOOK(0x00559a30, Creature_DropLoot);
|
||||||
|
void* __fastcall Creature_DropLoot(void* self, void* edx, uint32_t type, uint32_t a, uint32_t b, uint32_t killer) {
|
||||||
|
++g_dropDepth;
|
||||||
|
void* result = Creature_DropLoot_original(self, edx, type, a, b, killer);
|
||||||
|
--g_dropDepth;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cObjectManager::CreateAt (0x5fbaa0, (type, position, link, 0, 0), ret 0x14).
|
||||||
|
uint32_t __fastcall ObjMgr_CreateAt(void* self, void* edx, uint32_t type, const void* position, uint32_t link,
|
||||||
|
uint32_t a, uint32_t b);
|
||||||
|
RE_HOOK(0x005fbaa0, ObjMgr_CreateAt);
|
||||||
|
uint32_t __fastcall ObjMgr_CreateAt(void* self, void* edx, uint32_t type, const void* position, uint32_t link,
|
||||||
|
uint32_t a, uint32_t b) {
|
||||||
|
const uint32_t id = ObjMgr_CreateAt_original(self, edx, type, position, link, a, b);
|
||||||
|
if (g_dropDepth > 0 && id && AnyMode(Mode::Void)) {
|
||||||
|
g_dropped.push_back(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cChest3D::dropItems (0x5c9b10, (?), ret 4): throws out its contents (ids at
|
||||||
|
// +0x1e4 .. +0x1e8), unless already opened (+0x14 bit 0x4000).
|
||||||
|
constexpr size_t kChestContents = 0x1e4;
|
||||||
|
void __fastcall Chest_DropItems(void* self, void* edx, uint32_t a);
|
||||||
|
RE_HOOK(0x005c9b10, Chest_DropItems);
|
||||||
|
void __fastcall Chest_DropItems(void* self, void* edx, uint32_t a) {
|
||||||
|
if (AnyMode(Mode::Void)) {
|
||||||
|
auto* chest = static_cast<uint8_t*>(self);
|
||||||
|
const auto* begin = *reinterpret_cast<const uint32_t* const*>(chest + kChestContents);
|
||||||
|
const auto* end = *reinterpret_cast<const uint32_t* const*>(chest + kChestContents + 4);
|
||||||
|
for (const uint32_t* it = begin; it && it < end; ++it) {
|
||||||
|
g_dropped.push_back(*it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Chest_DropItems_original(self, edx, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cWorldView0::DrawObjectLabels (0x627260, (renderer), ret 4).
|
||||||
|
void __fastcall WorldView_DrawObjectLabels(void* self, void* edx, void* renderer);
|
||||||
|
RE_HOOK(0x00627260, WorldView_DrawObjectLabels);
|
||||||
|
void __fastcall WorldView_DrawObjectLabels(void* self, void* edx, void* renderer) {
|
||||||
|
auto* view = static_cast<uint8_t*>(self);
|
||||||
|
if (mods::input::LabelsKeyHeld() || !AnyMode(Mode::AlwaysShow)) {
|
||||||
|
WorldView_DrawObjectLabels_original(self, edx, renderer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const size_t count = LabelCount(view);
|
||||||
|
bool any = false;
|
||||||
|
for (size_t i = 0; i < count && !any; ++i) {
|
||||||
|
any = AlwaysShown(LabelEntry(view, i)[0]);
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
WorldView_DrawObjectLabels_original(self, edx, renderer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<uint32_t*> marked;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
uint32_t* entry = LabelEntry(view, i);
|
||||||
|
if (!(entry[0] & kLabelSkipped) && !AlwaysShown(entry[0])) {
|
||||||
|
entry[0] |= kLabelSkipped;
|
||||||
|
marked.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mods::input::ForceLabels(true);
|
||||||
|
WorldView_DrawObjectLabels_original(self, edx, renderer);
|
||||||
|
mods::input::ForceLabels(false);
|
||||||
|
for (uint32_t* entry : marked) {
|
||||||
|
entry[0] &= ~kLabelSkipped;
|
||||||
|
}
|
||||||
|
*reinterpret_cast<uint32_t*>(view + kViewFlags) &= ~kLabelsShown;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace game
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
// Loot filter (mods/lootfilter.cpp): per item category what happens to loot.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace mods::lootfilter {
|
||||||
|
|
||||||
|
enum class Mode : int {
|
||||||
|
Void, // a drop of the category is deleted at once
|
||||||
|
Ignore, // not collected by the pickup key
|
||||||
|
Default, // collected; label with the labels key
|
||||||
|
AlwaysShow, // collected; label without the key too
|
||||||
|
};
|
||||||
|
constexpr int kModeCount = 4;
|
||||||
|
|
||||||
|
// Categories: Gold, Tränke, then the item colours white, blue, yellow, brown,
|
||||||
|
// green.
|
||||||
|
constexpr int kCategoryCount = 7;
|
||||||
|
|
||||||
|
const wchar_t* CategoryLabel(int category);
|
||||||
|
uint32_t CategoryColour(int category); // as the game colours their names
|
||||||
|
const wchar_t* ModeLabel(Mode mode);
|
||||||
|
|
||||||
|
Mode GetMode(int category);
|
||||||
|
// Sets all modes (`modes[kCategoryCount]`) and writes them to sacredK.ini.
|
||||||
|
void SetModes(const Mode* modes);
|
||||||
|
|
||||||
|
} // namespace mods::lootfilter
|
||||||
+179
-20
@@ -22,6 +22,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
#include "game/ui/texts.h"
|
#include "game/ui/texts.h"
|
||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
#include "mods/input.h"
|
#include "mods/input.h"
|
||||||
|
#include "mods/lootfilter.h"
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.h"
|
#include "re/log.h"
|
||||||
|
|
||||||
@@ -45,6 +47,13 @@ namespace {
|
|||||||
constexpr size_t kWindowControls = 0x90; // vector of its controls: begin, +4 end
|
constexpr size_t kWindowControls = 0x90; // vector of its controls: begin, +4 end
|
||||||
constexpr size_t kCancelButton = 0x158;
|
constexpr size_t kCancelButton = 0x158;
|
||||||
constexpr size_t kOkButton = 0x15c;
|
constexpr size_t kOkButton = 0x15c;
|
||||||
|
// The game's pickup choice (PICKUPAUTO all / valuable / gold only): radio
|
||||||
|
// buttons, their labels (UI_LABEL_COLLECT, UI_LABEL_COLLECT1..3) and the
|
||||||
|
// separator above them (UI_FRAME_SEP_AUTOCOLLECT). The loot filter replaces
|
||||||
|
// it; they are never shown.
|
||||||
|
constexpr size_t kPickupRadios[] = {0x16c, 0x170, 0x174};
|
||||||
|
constexpr const char* kPickupControlNames[] = {"UI_LABEL_COLLECT", "UI_FRAME_SEP_AUTOCOLLECT"};
|
||||||
|
constexpr size_t kControlName = 0x30; // char[32], set by 0x732090
|
||||||
// cUI_Frame (vtable 0x8955ac): its render (0x73a540) doesn't test the visible
|
// cUI_Frame (vtable 0x8955ac): its render (0x73a540) doesn't test the visible
|
||||||
// flag but +0xdc (set to 1 by its constructors, read nowhere else).
|
// flag but +0xdc (set to 1 by its constructors, read nowhere else).
|
||||||
constexpr uint32_t kFrameVtable = 0x008955ac;
|
constexpr uint32_t kFrameVtable = 0x008955ac;
|
||||||
@@ -75,7 +84,7 @@ constexpr game::UiRect kSheetRect = {0x2c, 0x2c, 0, 0};
|
|||||||
constexpr int kContentShift = 24;
|
constexpr int kContentShift = 24;
|
||||||
|
|
||||||
enum Page : uint16_t { kPageCommon, kPageLoot, kPageKeys };
|
enum Page : uint16_t { kPageCommon, kPageLoot, kPageKeys };
|
||||||
enum class Group { Always, Common, Loot, Keys };
|
enum class Group { Always, Common, Loot, Keys, Hidden };
|
||||||
|
|
||||||
struct Control {
|
struct Control {
|
||||||
uint8_t* control;
|
uint8_t* control;
|
||||||
@@ -135,6 +144,21 @@ uint8_t* Field(uint8_t* window, size_t offset) {
|
|||||||
return *reinterpret_cast<uint8_t**>(window + offset);
|
return *reinterpret_cast<uint8_t**>(window + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsPickupChoice(uint8_t* window, const uint8_t* control) {
|
||||||
|
for (const size_t radio : kPickupRadios) {
|
||||||
|
if (control == Field(window, radio)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char* name = reinterpret_cast<const char*>(control + kControlName);
|
||||||
|
for (const char* prefix : kPickupControlNames) {
|
||||||
|
if (std::strncmp(name, prefix, std::strlen(prefix)) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Sorts the controls into pages, moves the auto-pickup and general frames.
|
// Sorts the controls into pages, moves the auto-pickup and general frames.
|
||||||
void Classify(Window& w) {
|
void Classify(Window& w) {
|
||||||
auto** begin = *reinterpret_cast<uint8_t***>(w.window + kWindowControls);
|
auto** begin = *reinterpret_cast<uint8_t***>(w.window + kWindowControls);
|
||||||
@@ -152,7 +176,7 @@ void Classify(Window& w) {
|
|||||||
if (control == ok || control == cancel) {
|
if (control == ok || control == cancel) {
|
||||||
group = Group::Always;
|
group = Group::Always;
|
||||||
} else if (x < kColumnSplit && y >= kMiddleRowTop && y < kBottomRowTop) {
|
} else if (x < kColumnSplit && y >= kMiddleRowTop && y < kBottomRowTop) {
|
||||||
group = Group::Loot;
|
group = IsPickupChoice(w.window, control) ? Group::Hidden : Group::Loot;
|
||||||
MoveDown(control, -kRowStep);
|
MoveDown(control, -kRowStep);
|
||||||
} else if (x < kColumnSplit && y >= kBottomRowTop) {
|
} else if (x < kColumnSplit && y >= kBottomRowTop) {
|
||||||
MoveDown(control, -kRowStep);
|
MoveDown(control, -kRowStep);
|
||||||
@@ -182,6 +206,7 @@ void CreateTabs(Window& w) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool OnPage(Group group, uint16_t page) {
|
bool OnPage(Group group, uint16_t page) {
|
||||||
|
// Group::Hidden: on no page.
|
||||||
return group == Group::Always || (group == Group::Common && page == kPageCommon) ||
|
return group == Group::Always || (group == Group::Common && page == kPageCommon) ||
|
||||||
(group == Group::Loot && page == kPageLoot) || (group == Group::Keys && page == kPageKeys);
|
(group == Group::Loot && page == kPageLoot) || (group == Group::Keys && page == kPageKeys);
|
||||||
}
|
}
|
||||||
@@ -438,34 +463,132 @@ uint8_t* CreateResetButton(uint8_t* window) {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The key bindings page's own controls, shown only there: a backdrop and
|
// A backdrop and a frame at `frame`, as the window's own.
|
||||||
// frame around each column, the reset button.
|
void AddFrame(uint8_t* window, game::UiRect frame, const char* name) {
|
||||||
void CreateKeyControls(Window& w) {
|
|
||||||
static const char* const kFrameNames[2] = {"UI_FRAME_KEYS_LEFT", "UI_FRAME_KEYS_RIGHT"};
|
|
||||||
auto* controls = w.window + kWindowControls;
|
|
||||||
const auto existing = static_cast<size_t>(*reinterpret_cast<uint8_t***>(controls + 4) -
|
|
||||||
*reinterpret_cast<uint8_t***>(controls));
|
|
||||||
for (int column = 0; column < 2; ++column) {
|
|
||||||
game::UiRect frame = {kFrameX[column], kFrameTop, kFrameWidth, kFrameHeight};
|
|
||||||
game::UiRect backdrop = {frame.x - kFrameBorder, frame.y - kFrameBorder,
|
game::UiRect backdrop = {frame.x - kFrameBorder, frame.y - kFrameBorder,
|
||||||
static_cast<int16_t>(frame.w + 2 * kFrameBorder),
|
static_cast<int16_t>(frame.w + 2 * kFrameBorder),
|
||||||
static_cast<int16_t>(frame.h + 2 * kFrameBorder)};
|
static_cast<int16_t>(frame.h + 2 * kFrameBorder)};
|
||||||
using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
|
using AddBackdropFn = void(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
|
||||||
reinterpret_cast<AddBackdropFn>(0x00724ed0)(w.window, nullptr, &backdrop, kBackdropColour);
|
reinterpret_cast<AddBackdropFn>(0x00724ed0)(window, nullptr, &backdrop, kBackdropColour);
|
||||||
if (void* memory = game::GameNew(kFrameSize)) {
|
if (void* memory = game::GameNew(kFrameSize)) {
|
||||||
using FrameFn = void*(__fastcall*)(void*, void*, const char*, uint32_t, game::UiRect*, uint32_t);
|
using FrameFn = void*(__fastcall*)(void*, void*, const char*, uint32_t, game::UiRect*, uint32_t);
|
||||||
PushControl(w.window, reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, kFrameNames[column],
|
PushControl(window, reinterpret_cast<FrameFn>(0x00739f90)(memory, nullptr, name, kMainFrame, &frame, 1));
|
||||||
kMainFrame, &frame, 1));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ControlCount(uint8_t* window) {
|
||||||
|
auto* controls = window + kWindowControls;
|
||||||
|
return static_cast<size_t>(*reinterpret_cast<uint8_t***>(controls + 4) - *reinterpret_cast<uint8_t***>(controls));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window's controls from index `first` on go to page group `group`.
|
||||||
|
void AssignFrom(Window& w, size_t first, Group group) {
|
||||||
|
auto** begin = *reinterpret_cast<uint8_t***>(w.window + kWindowControls);
|
||||||
|
auto** end = *reinterpret_cast<uint8_t***>(w.window + kWindowControls + 4);
|
||||||
|
for (auto** it = begin + first; it < end; ++it) {
|
||||||
|
if (*it) {
|
||||||
|
w.controls.push_back({*it, group});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key bindings page's own controls, shown only there: a backdrop and
|
||||||
|
// frame around each column, the reset button.
|
||||||
|
void CreateKeyControls(Window& w) {
|
||||||
|
static const char* const kFrameNames[2] = {"UI_FRAME_KEYS_LEFT", "UI_FRAME_KEYS_RIGHT"};
|
||||||
|
const size_t existing = ControlCount(w.window);
|
||||||
|
for (int column = 0; column < 2; ++column) {
|
||||||
|
AddFrame(w.window, {kFrameX[column], kFrameTop, kFrameWidth, kFrameHeight}, kFrameNames[column]);
|
||||||
}
|
}
|
||||||
w.resetButton = CreateResetButton(w.window);
|
w.resetButton = CreateResetButton(w.window);
|
||||||
// Everything after the window's own controls is ours.
|
AssignFrom(w, existing, Group::Keys);
|
||||||
auto** begin = *reinterpret_cast<uint8_t***>(controls);
|
}
|
||||||
auto** end = *reinterpret_cast<uint8_t***>(controls + 4);
|
|
||||||
for (auto** it = begin + existing; it < end; ++it) {
|
// --- loot filter page ----------------------------------------------------------
|
||||||
if (*it) {
|
|
||||||
w.controls.push_back({*it, Group::Keys});
|
// Below the auto-pickup frame, across both columns: from the middle row's
|
||||||
|
// frame top to the bottom of the lowest frame. A row per category, a box per
|
||||||
|
// mode; two lines of hints at the bottom.
|
||||||
|
constexpr game::UiRect kLootFrame = {0x40, 0x120 + kContentShift, 0x220 + kFrameWidth - 0x40,
|
||||||
|
0x200 + 0x40 - 0x120};
|
||||||
|
constexpr int kLootTop = kLootFrame.y + 10;
|
||||||
|
constexpr int kLootLabelWidth = 0xd0;
|
||||||
|
constexpr int kLootBoxGap = 8;
|
||||||
|
constexpr int kLootBoxWidth =
|
||||||
|
(kLootFrame.w - 2 * kTextInset - kLootLabelWidth - (mods::lootfilter::kModeCount - 1) * kLootBoxGap) /
|
||||||
|
mods::lootfilter::kModeCount;
|
||||||
|
constexpr uint32_t kBoxSelectedColour = kBoxCaptureColour;
|
||||||
|
const wchar_t* const kLootHints[2] = {
|
||||||
|
L"Löschen: Beute verschwindet sofort. Ignorieren: die Aufheben-Taste lässt sie liegen.",
|
||||||
|
L"Standard: wird aufgehoben, Name mit der Beschriftungstaste. Immer anzeigen: Name auch ohne."};
|
||||||
|
|
||||||
|
mods::lootfilter::Mode g_lootPending[mods::lootfilter::kCategoryCount] = {};
|
||||||
|
int g_lootHover = -1; // category * kModeCount + mode under the mouse
|
||||||
|
|
||||||
|
void LoadLootPending() {
|
||||||
|
for (int category = 0; category < mods::lootfilter::kCategoryCount; ++category) {
|
||||||
|
g_lootPending[category] = mods::lootfilter::GetMode(category);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateLootControls(Window& w) {
|
||||||
|
const size_t existing = ControlCount(w.window);
|
||||||
|
AddFrame(w.window, kLootFrame, "UI_FRAME_LOOTFILTER");
|
||||||
|
AssignFrom(w, existing, Group::Loot);
|
||||||
|
}
|
||||||
|
|
||||||
|
game::UiRect LootRowRect(void* window, int row, int dx, int width) {
|
||||||
|
const game::UiRect origin = game::ControlScreenRect(window);
|
||||||
|
return {origin.x + kLootFrame.x + kTextInset + dx, origin.y + kLootTop + row * kLine,
|
||||||
|
static_cast<int16_t>(width), static_cast<int16_t>(kBoxHeight)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 0: the mode names; rows 1.. the categories.
|
||||||
|
game::UiRect LootBoxRect(void* window, int row, int mode) {
|
||||||
|
return LootRowRect(window, row, kLootLabelWidth + mode * (kLootBoxWidth + kLootBoxGap), kLootBoxWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LootBoxAt(void* window, int x, int y) {
|
||||||
|
for (int category = 0; category < mods::lootfilter::kCategoryCount; ++category) {
|
||||||
|
for (int mode = 0; mode < mods::lootfilter::kModeCount; ++mode) {
|
||||||
|
if (Inside(LootBoxRect(window, category + 1, mode), x, y)) {
|
||||||
|
return category * mods::lootfilter::kModeCount + mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawLootPage(const Window& w, void* device) {
|
||||||
|
using namespace mods::lootfilter;
|
||||||
|
void* window = w.window;
|
||||||
|
const game::UiRect origin = game::ControlScreenRect(window);
|
||||||
|
// The title in the header row's label cell (above the frame it met the
|
||||||
|
// frame's backdrop).
|
||||||
|
game::DrawText(device, kFont, game::CenterLineVertically(kFont, LootRowRect(window, 0, 0, kLootLabelWidth)),
|
||||||
|
L"Beutefilter", kHeadingColour, 0);
|
||||||
|
for (int mode = 0; mode < kModeCount; ++mode) {
|
||||||
|
game::DrawText(device, kFont, game::CenterLineVertically(kFont, LootBoxRect(window, 0, mode)),
|
||||||
|
ModeLabel(static_cast<Mode>(mode)), kHeadingColour, game::kTextCenter);
|
||||||
|
}
|
||||||
|
for (int category = 0; category < kCategoryCount; ++category) {
|
||||||
|
const int row = category + 1;
|
||||||
|
game::DrawText(device, kFont, game::CenterLineVertically(kFont, LootRowRect(window, row, 0, kLootLabelWidth)),
|
||||||
|
CategoryLabel(category), CategoryColour(category), 0);
|
||||||
|
for (int mode = 0; mode < kModeCount; ++mode) {
|
||||||
|
const bool selected = g_lootPending[category] == static_cast<Mode>(mode);
|
||||||
|
const bool hover = g_lootHover == category * kModeCount + mode;
|
||||||
|
FillBox(device, LootBoxRect(window, row, mode),
|
||||||
|
selected ? kBoxSelectedColour : hover ? kBoxHoverColour : kBoxColour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int lineHeight = game::FontLineHeight(kFont);
|
||||||
|
game::UiRect hint = {origin.x + kLootFrame.x + kTextInset,
|
||||||
|
origin.y + kLootFrame.y + kLootFrame.h - 2 * lineHeight - 10,
|
||||||
|
static_cast<int16_t>(kLootFrame.w - 2 * kTextInset), static_cast<int16_t>(lineHeight)};
|
||||||
|
for (const wchar_t* text : kLootHints) {
|
||||||
|
game::DrawText(device, kFont, hint, text, kTextColour, game::kTextCenter);
|
||||||
|
hint.y += lineHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +612,7 @@ int RenderBody(void* context) {
|
|||||||
const bool visible = game::ControlShown(call->window);
|
const bool visible = game::ControlShown(call->window);
|
||||||
if (visible && !w->wasVisible) {
|
if (visible && !w->wasVisible) {
|
||||||
LoadPending();
|
LoadPending();
|
||||||
|
LoadLootPending();
|
||||||
}
|
}
|
||||||
w->wasVisible = visible;
|
w->wasVisible = visible;
|
||||||
const uint16_t page = game::ActiveTab(w->sheet);
|
const uint16_t page = game::ActiveTab(w->sheet);
|
||||||
@@ -498,12 +622,17 @@ int RenderBody(void* context) {
|
|||||||
}
|
}
|
||||||
g_hover = -1;
|
g_hover = -1;
|
||||||
}
|
}
|
||||||
|
if (!visible || page != kPageLoot) {
|
||||||
|
g_lootHover = -1;
|
||||||
|
}
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
ShowPage(*w, page);
|
ShowPage(*w, page);
|
||||||
if (page == kPageKeys && g_pendingLoaded) {
|
if (page == kPageKeys && g_pendingLoaded) {
|
||||||
DrawKeysPage(*w, call->device);
|
DrawKeysPage(*w, call->device);
|
||||||
|
} else if (page == kPageLoot && g_pendingLoaded) {
|
||||||
|
DrawLootPage(*w, call->device);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -514,9 +643,37 @@ struct EventCall {
|
|||||||
int(__fastcall* original)(void*, void*, void*);
|
int(__fastcall* original)(void*, void*, void*);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The loot filter page: a click on a box picks that mode.
|
||||||
|
bool HandleLootEvent(Window& w, void* event) {
|
||||||
|
switch (game::MouseEventKind(event)) {
|
||||||
|
case game::MouseEvent::Move:
|
||||||
|
g_lootHover = LootBoxAt(w.window, game::EventField(event, 0xc), game::EventField(event, 0x10));
|
||||||
|
return false;
|
||||||
|
case game::MouseEvent::ButtonDown: {
|
||||||
|
if (game::EventField(event, game::kMouseButton) != game::kLeftButton) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int box = LootBoxAt(w.window, game::EventField(event, game::kMouseButtonX),
|
||||||
|
game::EventField(event, game::kMouseButtonY));
|
||||||
|
if (box < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_lootPending[box / mods::lootfilter::kModeCount] =
|
||||||
|
static_cast<mods::lootfilter::Mode>(box % mods::lootfilter::kModeCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int EventBody(void* context) {
|
int EventBody(void* context) {
|
||||||
auto* call = static_cast<EventCall*>(context);
|
auto* call = static_cast<EventCall*>(context);
|
||||||
Window* w = Find(call->window);
|
Window* w = Find(call->window);
|
||||||
|
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageLoot &&
|
||||||
|
g_pendingLoaded && HandleLootEvent(*w, call->event)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
|
if (w && w->sheet && game::ControlShown(call->window) && game::ActiveTab(w->sheet) == kPageKeys &&
|
||||||
g_pendingLoaded) {
|
g_pendingLoaded) {
|
||||||
switch (game::MouseEventKind(call->event)) {
|
switch (game::MouseEventKind(call->event)) {
|
||||||
@@ -571,6 +728,7 @@ void* __fastcall UiOptions_Construct(void* self, void* edx, const char* name, Ui
|
|||||||
Window& w = g_windows.back();
|
Window& w = g_windows.back();
|
||||||
Classify(w);
|
Classify(w);
|
||||||
CreateKeyControls(w);
|
CreateKeyControls(w);
|
||||||
|
CreateLootControls(w);
|
||||||
CreateTabs(w);
|
CreateTabs(w);
|
||||||
re::Log("options: tabs created (%u controls)", static_cast<unsigned>(w.controls.size()));
|
re::Log("options: tabs created (%u controls)", static_cast<unsigned>(w.controls.size()));
|
||||||
}
|
}
|
||||||
@@ -610,6 +768,7 @@ void __fastcall UiOptions_Apply(void* self, void* edx) {
|
|||||||
UiOptions_Apply_original(self, edx);
|
UiOptions_Apply_original(self, edx);
|
||||||
if (g_pendingLoaded) {
|
if (g_pendingLoaded) {
|
||||||
mods::input::SetBindingKeys(g_pending);
|
mods::input::SetBindingKeys(g_pending);
|
||||||
|
mods::lootfilter::SetModes(g_lootPending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -320,3 +320,12 @@
|
|||||||
0x0075a690 cUI_Manager::CanOpenWindow # (1), ret 4: false while a blocking window is open
|
0x0075a690 cUI_Manager::CanOpenWindow # (1), ret 4: false while a blocking window is open
|
||||||
0x007320c0 cUI_Control2::SetTooltipText # (text id) -> +0x20, ret 4
|
0x007320c0 cUI_Control2::SetTooltipText # (text id) -> +0x20, ret 4
|
||||||
0x007320d0 cUI_Control2::TooltipText # +0x20 once hovered long enough, else 0
|
0x007320d0 cUI_Control2::TooltipText # +0x20 once hovered long enough, else 0
|
||||||
|
0x0043ae20 cItemTypeTable::IsGold # (type), ret 4: one of the four gold pile types (0x947920)
|
||||||
|
0x00433f00 cItemTypeTable::GetGroup # (type), ret 4: group record (0x102 bytes), +0x23 set item
|
||||||
|
0x00428550 cItemTypeTable::IsAlwaysCollected # (type), ret 4: CanCollectItem takes it whatever PICKUPAUTO
|
||||||
|
0x005c1330 cItemBase::GetColour # (quality block or 0), ret 4: name colour by quality tier / set
|
||||||
|
0x00559a30 cCreature::DropLoot # (forced type, ?, ?, killer), ret 0x10; "Drop from [%d] %s"
|
||||||
|
0x005c9b10 cChest3D::dropItems # (?), ret 4: throws out the contents (ids at +0x1e4)
|
||||||
|
0x006116c0 cEngine::dropItem # the player's drop into the world
|
||||||
|
0x005fc910 cObjectManager::RemoveFromMap # (id), ret 4
|
||||||
|
0x00819580 Options_PickupAuto # PICKUPAUTO from the config (0 all, 1 valuable, 2 gold)
|
||||||
Reference in new issue
Block a user