Loot filter: quest items are their own category, first and never voided

Quest items are recognized by the type's quest number or the scripts'
QuestItem / MainQuestItem marks, checked before the other categories.
Their row in the Beute tab has no Löschen box; Void in the ini reads as
Default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-25 18:24:28 +01:00
1 parent c0eedbce7f
commit 401e20b1f2
5 files changed
+61 -28

No files matched your search

+12 -4
View File
@@ -13,12 +13,21 @@ Decided with the user: the filter replaces the game's PICKUPAUTO choice
(all / valuable / gold only; its radio buttons are hidden, the animation (all / valuable / gold only; its radio buttons are hidden, the animation
checkbox stays); Void deletes loot only (monster drops, chests), not items checkbox stays); Void deletes loot only (monster drops, chests), not items
the player puts down. Settings: `[loot]` in `sacredK.ini` the player puts down. Settings: `[loot]` in `sacredK.ini`
(`Gold=Default`, keys Gold, Potions, White, Blue, Yellow, Brown, Green; (`Gold=Default`, keys Quest, Gold, Potions, White, Blue, Yellow, Brown, Green;
values Void, Ignore, Default, AlwaysShow). Code: `src/mods/lootfilter.cpp`, values Void, Ignore, Default, AlwaysShow). Code: `src/mods/lootfilter.cpp`,
page in `src/mods/options.cpp`. page in `src/mods/options.cpp`.
## What decides the category (read from the game) ## What decides the category (read from the game)
- Quest items (request 2026-10-25: "Quest Items are currently potentially
being destroyed, it needs to be top priority, also Löschen is not an option
for those"), checked first: the type's quest number (type table +0x1c) or
the scripts' QuestItem / MainQuestItem marks (+0x61 / +0x62; the script
parser 0x4584a9 turns those words into tokens 0x61 / 0x62), as the quest
book reads them (0x43ad80). The game's always-collect test `0x428550`
requires type flag 0x1000000 besides, so quest items without it fell into
their colour's category and could be voided. Their row has no *Löschen*
box; a `Void` in the ini is read as Default.
- Gold: `0x43ae20` (thiscall on the item type table, (type)): one of four - Gold: `0x43ae20` (thiscall on the item type table, (type)): one of four
types listed at `0x947920`. The labels show its amount, the collect mode types listed at `0x947920`. The labels show its amount, the collect mode
takes it without inventory space, the loot drop sets the amount after takes it without inventory space, the loot drop sets the amount after
@@ -31,9 +40,8 @@ page in `src/mods/options.cpp`.
`ffa0a0ff`, 12..14 yellow `fff0e040`, 15 brown `ffc19a33`; set green `ffa0a0ff`, 12..14 yellow `fff0e040`, 15 brown `ffc19a33`; set green
`ff15ff25`. (Without UNIQUE_COLOR, items with special bits get other `ff15ff25`. (Without UNIQUE_COLOR, items with special bits get other
colours from `0x5c1260`; the filter uses the tier regardless.) colours from `0x5c1260`; the filter uses the tier regardless.)
- Not filtered: runes (class 0x1c), objects with flag 0x800 and the types - Not filtered: runes (class 0x1c) and objects with flag 0x800 (the game's
`0x428550` reports (CanCollectItem always takes both; quest items, CanCollectItem always takes those).
presumably).
## Where it acts ## Where it acts
+15 -7
View File
@@ -79,13 +79,21 @@ inline bool ItemIsSetItem(uint32_t type) {
return group && group[0x23] != 0; return group && group[0x23] != 0;
} }
// 0x428550 (thiscall on the type table, (type), ret 4): cCreature:: // Quest items: the type's quest number (+0x1c) or the scripts' QuestItem /
// CanCollectItem takes items of such types whatever the pickup setting (type // MainQuestItem marks (+0x61 / +0x62; tokens of those script words, 0x4584a9),
// flag 0x1000000 with +0x61 / +0x62 / +0x1c set; quest items, presumably). // as the quest book reads them (0x43ad80). The game's CanCollectItem
inline bool ItemAlwaysCollected(uint32_t type) { // (0x428550) takes them always, but only with type flag 0x1000000 set too.
using Fn = bool(__fastcall*)(void* table, void* edx, uint32_t type); constexpr size_t kItemTypeQuest = 0x1c;
void* table = *reinterpret_cast<void**>(kItemTypeTable); constexpr size_t kItemTypeQuestItem = 0x61;
return table && reinterpret_cast<Fn>(0x00428550)(table, nullptr, type); constexpr size_t kItemTypeMainQuestItem = 0x62;
inline bool ItemIsQuestItem(uint32_t type) {
const auto* table = *reinterpret_cast<const uint8_t* const*>(kItemTypeTable);
if (!table || type == 0 || type >= kItemTypeCount) {
return false;
}
const uint8_t* entry = table + type * kItemTypeEntrySize;
return *reinterpret_cast<const uint32_t*>(entry + kItemTypeQuest) != 0 || entry[kItemTypeQuestItem] ||
entry[kItemTypeMainQuestItem];
} }
// The item's quality block (+0x118, cArmor3D::vf24 case 2 hands it to the // The item's quality block (+0x118, cArmor3D::vf24 case 2 hands it to the
+25 -13
View File
@@ -1,6 +1,6 @@
// Loot filter (request of 2026-10-19), set on the options window's Beute page // 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, // (mods/options.cpp): per category - Questgegenstände, Gold, Tränke, and the
// blue, yellow, brown, green - one of // item colours white, blue, yellow, brown, green - one of
// Void a drop is deleted at once (monster loot, chests; items the // Void a drop is deleted at once (monster loot, chests; items the
// player puts down stay), // player puts down stay),
// Ignore the pickup key (Q, the collect mode) leaves it, // Ignore the pickup key (Q, the collect mode) leaves it,
@@ -9,10 +9,14 @@
// It replaces the game's PICKUPAUTO setting (all / valuable / gold only), // It replaces the game's PICKUPAUTO setting (all / valuable / gold only),
// whose radio buttons are gone from the page. // whose radio buttons are gone from the page.
// //
// - Categories from the game's data: gold by type (0x43ae20), potions by item // - Categories from the game's data: quest items first (request of
// class 9, set items green (0x433f00 +0x23), otherwise the quality tier // 2026-10-25: they were voided as their colour's category; "it needs to be
// that picks the item colour (0x5c1330, game/engine/items.h). Runes and the // top priority, also Löschen is not an option for those"; type's quest
// types CanCollectItem always takes (quest items) are not filtered. // number or quest-item mark, game::ItemIsQuestItem), 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 objects flagged always-collected are not
// filtered.
// - Pickup: cCreature::CanCollectItem (0x4ec170) is the collect mode's filter // - Pickup: cCreature::CanCollectItem (0x4ec170) is the collect mode's filter
// (both of its scans; a clicked item does not ask it). // (both of its scans; a clicked item does not ask it).
// - Void: items made by the loot drop (cCreature 0x559a30, via // - Void: items made by the loot drop (cCreature 0x559a30, via
@@ -44,7 +48,7 @@
namespace mods::lootfilter { namespace mods::lootfilter {
namespace { namespace {
enum Category : int { kGold, kPotions, kWhite, kBlue, kYellow, kBrown, kGreen, kNone = -1 }; enum Category : int { kQuest, kGold, kPotions, kWhite, kBlue, kYellow, kBrown, kGreen, kNone = -1 };
struct CategoryInfo { struct CategoryInfo {
const char* key; // in [loot] const char* key; // in [loot]
@@ -53,6 +57,7 @@ struct CategoryInfo {
}; };
// Gold and potions as the game's labels show them: white. // Gold and potions as the game's labels show them: white.
constexpr CategoryInfo kCategories[kCategoryCount] = { constexpr CategoryInfo kCategories[kCategoryCount] = {
{"Quest", L"Questgegenstände", game::kItemColourWhite},
{"Gold", L"Gold", game::kItemColourWhite}, {"Potions", L"Tränke", game::kItemColourWhite}, {"Gold", L"Gold", game::kItemColourWhite}, {"Potions", L"Tränke", game::kItemColourWhite},
{"White", L"Weiß", game::kItemColourWhite}, {"Blue", L"Blau", game::kItemColourBlue}, {"White", L"Weiß", game::kItemColourWhite}, {"Blue", L"Blau", game::kItemColourBlue},
{"Yellow", L"Gelb", game::kItemColourYellow}, {"Brown", L"Braun", game::kItemColourBrown}, {"Yellow", L"Gelb", game::kItemColourYellow}, {"Brown", L"Braun", game::kItemColourBrown},
@@ -67,10 +72,10 @@ constexpr ModeInfo kModes[kModeCount] = {
{"Void", L"Löschen"}, {"Ignore", L"Ignorieren"}, {"Default", L"Standard"}, {"AlwaysShow", L"Immer anzeigen"}}; {"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 g_modes[kCategoryCount] = {Mode::Default, Mode::Default, Mode::Default, Mode::Default,
Mode::Default, Mode::Default, Mode::Default}; Mode::Default, Mode::Default, Mode::Default, Mode::Default};
// cObject +0x14 bit 0x800: CanCollectItem takes such objects whatever the // cObject +0x14 bit 0x800: the game's CanCollectItem takes such objects
// pickup setting (as the types of ItemAlwaysCollected). // whatever the pickup setting.
constexpr uint32_t kObjectAlwaysCollected = 0x800; constexpr uint32_t kObjectAlwaysCollected = 0x800;
int CategoryOf(void* object) { int CategoryOf(void* object) {
@@ -82,8 +87,11 @@ int CategoryOf(void* object) {
if (type == 0 || type >= game::kItemTypeCount) { if (type == 0 || type >= game::kItemTypeCount) {
return kNone; return kNone;
} }
if (game::ItemIsQuestItem(type)) {
return kQuest;
}
const uint32_t flags = *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(object) + game::kObjectFlags); const uint32_t flags = *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(object) + game::kObjectFlags);
if ((flags & kObjectAlwaysCollected) || game::ItemAlwaysCollected(type)) { if (flags & kObjectAlwaysCollected) {
return kNone; return kNone;
} }
if (game::ItemIsGold(type)) { if (game::ItemIsGold(type)) {
@@ -125,7 +133,7 @@ void Load() {
for (int i = 0; i < kCategoryCount; ++i) { for (int i = 0; i < kCategoryCount; ++i) {
const std::string text = re::ConfigString("loot", kCategories[i].key, kModes[int(Mode::Default)].key); const std::string text = re::ConfigString("loot", kCategories[i].key, kModes[int(Mode::Default)].key);
for (int m = 0; m < kModeCount; ++m) { for (int m = 0; m < kModeCount; ++m) {
if (_stricmp(text.c_str(), kModes[m].key) == 0) { if (_stricmp(text.c_str(), kModes[m].key) == 0 && ModeAllowed(i, static_cast<Mode>(m))) {
g_modes[i] = static_cast<Mode>(m); g_modes[i] = static_cast<Mode>(m);
} }
} }
@@ -207,6 +215,10 @@ const wchar_t* ModeLabel(Mode mode) {
return kModes[static_cast<int>(mode)].label; return kModes[static_cast<int>(mode)].label;
} }
bool ModeAllowed(int category, Mode mode) {
return category != kQuest || mode != Mode::Void;
}
Mode GetMode(int category) { Mode GetMode(int category) {
return g_modes[category]; return g_modes[category];
} }
@@ -225,7 +237,7 @@ namespace game {
using namespace mods::lootfilter; using namespace mods::lootfilter;
// cCreature::CanCollectItem (0x4ec170, (item), ret 4): carried items never; // cCreature::CanCollectItem (0x4ec170, (item), ret 4): carried items never;
// quest items, runes and anything not filtered always; else by the mode. // runes and anything not filtered always; else by the mode.
bool __fastcall Creature_CanCollectItem(void* self, void* edx, uint8_t* item); bool __fastcall Creature_CanCollectItem(void* self, void* edx, uint8_t* item);
RE_HOOK(0x004ec170, Creature_CanCollectItem); RE_HOOK(0x004ec170, Creature_CanCollectItem);
bool __fastcall Creature_CanCollectItem(void*, void*, uint8_t* item) { bool __fastcall Creature_CanCollectItem(void*, void*, uint8_t* item) {
+4 -3
View File
@@ -13,13 +13,14 @@ enum class Mode : int {
}; };
constexpr int kModeCount = 4; constexpr int kModeCount = 4;
// Categories: Gold, Tränke, then the item colours white, blue, yellow, brown, // Categories: Questgegenstände (first: checked before the others, never
// green. // Void), Gold, Tränke, then the item colours white, blue, yellow, brown, green.
constexpr int kCategoryCount = 7; constexpr int kCategoryCount = 8;
const wchar_t* CategoryLabel(int category); const wchar_t* CategoryLabel(int category);
uint32_t CategoryColour(int category); // as the game colours their names uint32_t CategoryColour(int category); // as the game colours their names
const wchar_t* ModeLabel(Mode mode); const wchar_t* ModeLabel(Mode mode);
bool ModeAllowed(int category, Mode mode);
Mode GetMode(int category); Mode GetMode(int category);
// Sets all modes (`modes[kCategoryCount]`) and writes them to sacredK.ini. // Sets all modes (`modes[kCategoryCount]`) and writes them to sacredK.ini.
+5 -1
View File
@@ -523,7 +523,8 @@ game::UiRect LootBoxRect(void* window, int row, int mode) {
int LootBoxAt(void* window, int x, int y) { int LootBoxAt(void* window, int x, int y) {
for (int category = 0; category < mods::lootfilter::kCategoryCount; ++category) { for (int category = 0; category < mods::lootfilter::kCategoryCount; ++category) {
for (int mode = 0; mode < mods::lootfilter::kModeCount; ++mode) { for (int mode = 0; mode < mods::lootfilter::kModeCount; ++mode) {
if (game::Inside(LootBoxRect(window, category + 1, mode), x, y)) { if (mods::lootfilter::ModeAllowed(category, static_cast<mods::lootfilter::Mode>(mode)) &&
game::Inside(LootBoxRect(window, category + 1, mode), x, y)) {
return category * mods::lootfilter::kModeCount + mode; return category * mods::lootfilter::kModeCount + mode;
} }
} }
@@ -548,6 +549,9 @@ void DrawLootPage(const Window& w, void* device) {
game::DrawText(device, kFont, game::CenterLineVertically(kFont, LootRowRect(window, row, 0, kLootLabelWidth)), game::DrawText(device, kFont, game::CenterLineVertically(kFont, LootRowRect(window, row, 0, kLootLabelWidth)),
CategoryLabel(category), CategoryColour(category), 0); CategoryLabel(category), CategoryColour(category), 0);
for (int mode = 0; mode < kModeCount; ++mode) { for (int mode = 0; mode < kModeCount; ++mode) {
if (!ModeAllowed(category, static_cast<Mode>(mode))) {
continue;
}
const bool selected = g_lootPending[category] == static_cast<Mode>(mode); const bool selected = g_lootPending[category] == static_cast<Mode>(mode);
const bool hover = g_lootHover == category * kModeCount + mode; const bool hover = g_lootHover == category * kModeCount + mode;
FillBox(device, LootBoxRect(window, row, mode), FillBox(device, LootBoxRect(window, row, mode),