diff --git a/docs/loot-filter.md b/docs/loot-filter.md index 5cc0122..dbe638d 100644 --- a/docs/loot-filter.md +++ b/docs/loot-filter.md @@ -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 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; +(`Gold=Default`, keys Quest, 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) +- 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 types listed at `0x947920`. The labels show its amount, the collect mode 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 `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). +- Not filtered: runes (class 0x1c) and objects with flag 0x800 (the game's + CanCollectItem always takes those). ## Where it acts diff --git a/src/game/engine/items.h b/src/game/engine/items.h index b8dc9d4..e2907fa 100644 --- a/src/game/engine/items.h +++ b/src/game/engine/items.h @@ -79,13 +79,21 @@ inline bool ItemIsSetItem(uint32_t type) { 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(kItemTypeTable); - return table && reinterpret_cast(0x00428550)(table, nullptr, type); +// Quest items: the type's quest number (+0x1c) or the scripts' QuestItem / +// MainQuestItem marks (+0x61 / +0x62; tokens of those script words, 0x4584a9), +// as the quest book reads them (0x43ad80). The game's CanCollectItem +// (0x428550) takes them always, but only with type flag 0x1000000 set too. +constexpr size_t kItemTypeQuest = 0x1c; +constexpr size_t kItemTypeQuestItem = 0x61; +constexpr size_t kItemTypeMainQuestItem = 0x62; +inline bool ItemIsQuestItem(uint32_t type) { + const auto* table = *reinterpret_cast(kItemTypeTable); + if (!table || type == 0 || type >= kItemTypeCount) { + return false; + } + const uint8_t* entry = table + type * kItemTypeEntrySize; + return *reinterpret_cast(entry + kItemTypeQuest) != 0 || entry[kItemTypeQuestItem] || + entry[kItemTypeMainQuestItem]; } // The item's quality block (+0x118, cArmor3D::vf24 case 2 hands it to the diff --git a/src/mods/lootfilter.cpp b/src/mods/lootfilter.cpp index d4cdbd0..e23a936 100644 --- a/src/mods/lootfilter.cpp +++ b/src/mods/lootfilter.cpp @@ -1,6 +1,6 @@ // 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 +// (mods/options.cpp): per category - Questgegenstände, 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, @@ -9,10 +9,14 @@ // 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. +// - Categories from the game's data: quest items first (request of +// 2026-10-25: they were voided as their colour's category; "it needs to be +// top priority, also Löschen is not an option for those"; type's quest +// 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 // (both of its scans; a clicked item does not ask it). // - Void: items made by the loot drop (cCreature 0x559a30, via @@ -44,7 +48,7 @@ namespace mods::lootfilter { 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 { const char* key; // in [loot] @@ -53,6 +57,7 @@ struct CategoryInfo { }; // Gold and potions as the game's labels show them: white. constexpr CategoryInfo kCategories[kCategoryCount] = { + {"Quest", L"Questgegenstände", game::kItemColourWhite}, {"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}, @@ -67,10 +72,10 @@ 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}; + 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). +// cObject +0x14 bit 0x800: the game's CanCollectItem takes such objects +// whatever the pickup setting. constexpr uint32_t kObjectAlwaysCollected = 0x800; int CategoryOf(void* object) { @@ -82,8 +87,11 @@ int CategoryOf(void* object) { if (type == 0 || type >= game::kItemTypeCount) { return kNone; } + if (game::ItemIsQuestItem(type)) { + return kQuest; + } const uint32_t flags = *reinterpret_cast(static_cast(object) + game::kObjectFlags); - if ((flags & kObjectAlwaysCollected) || game::ItemAlwaysCollected(type)) { + if (flags & kObjectAlwaysCollected) { return kNone; } if (game::ItemIsGold(type)) { @@ -125,7 +133,7 @@ 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) { + if (_stricmp(text.c_str(), kModes[m].key) == 0 && ModeAllowed(i, static_cast(m))) { g_modes[i] = static_cast(m); } } @@ -207,6 +215,10 @@ const wchar_t* ModeLabel(Mode mode) { return kModes[static_cast(mode)].label; } +bool ModeAllowed(int category, Mode mode) { + return category != kQuest || mode != Mode::Void; +} + Mode GetMode(int category) { return g_modes[category]; } @@ -225,7 +237,7 @@ 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. +// 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) { diff --git a/src/mods/lootfilter.h b/src/mods/lootfilter.h index 3c17eb7..7688da9 100644 --- a/src/mods/lootfilter.h +++ b/src/mods/lootfilter.h @@ -13,13 +13,14 @@ enum class Mode : int { }; constexpr int kModeCount = 4; -// Categories: Gold, Tränke, then the item colours white, blue, yellow, brown, -// green. -constexpr int kCategoryCount = 7; +// Categories: Questgegenstände (first: checked before the others, never +// Void), Gold, Tränke, then the item colours white, blue, yellow, brown, green. +constexpr int kCategoryCount = 8; const wchar_t* CategoryLabel(int category); uint32_t CategoryColour(int category); // as the game colours their names const wchar_t* ModeLabel(Mode mode); +bool ModeAllowed(int category, Mode mode); Mode GetMode(int category); // Sets all modes (`modes[kCategoryCount]`) and writes them to sacredK.ini. diff --git a/src/mods/options.cpp b/src/mods/options.cpp index b09864a..dd3e043 100644 --- a/src/mods/options.cpp +++ b/src/mods/options.cpp @@ -523,7 +523,8 @@ game::UiRect LootBoxRect(void* window, int row, int mode) { 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 (game::Inside(LootBoxRect(window, category + 1, mode), x, y)) { + if (mods::lootfilter::ModeAllowed(category, static_cast(mode)) && + game::Inside(LootBoxRect(window, category + 1, mode), x, y)) { 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)), CategoryLabel(category), CategoryColour(category), 0); for (int mode = 0; mode < kModeCount; ++mode) { + if (!ModeAllowed(category, static_cast(mode))) { + continue; + } const bool selected = g_lootPending[category] == static_cast(mode); const bool hover = g_lootHover == category * kModeCount + mode; FillBox(device, LootBoxRect(window, row, mode),