#include "item.hpp" #include "dusk/mod_loader.hpp" #include "dusk/mods/loader/loader.hpp" #include "dusk/mods/svc/item.hpp" #include "aurora/lib/logging.hpp" #include "d/d_com_inf_game.h" #include "d/d_item_data.h" #include "mods/items.h" #include #include #include #include #include #include #include #include #include namespace dusk::mods { namespace { aurora::Module Log{"dusk::mods::item_checks"}; struct CheckOverride { std::string name; uint8_t itemNo = 0; }; struct CheckResolver { ItemCheckHandle handle = 0; std::string name; ItemCheckResolveFn fn = nullptr; void* userData = nullptr; }; struct ModItemChecks { std::vector overrides; std::vector resolvers; }; struct PendingResolve { LoadedMod* mod = nullptr; bool fixedValue = false; uint8_t itemNo = 0; ItemCheckResolveFn fn = nullptr; void* userData = nullptr; }; struct MessageCheck { uint16_t group = 0; uint16_t messageId = 0; const char* name = nullptr; uint8_t vanillaItem = 0; bool enqueueAtDisplay = true; }; constexpr std::array kMessageChecks{ MessageCheck{ .group = 0, .messageId = 1822, .name = ITEM_CHECK_FISHING_BOTTLE, .vanillaItem = dItemNo_EMPTY_BOTTLE_e, }, MessageCheck{ .group = 7, .messageId = 7564, .name = ITEM_CHECK_FISHING_HEART_PIECE, .vanillaItem = dItemNo_KAKERA_HEART_e, }, MessageCheck{ .group = 7, .messageId = 7578, .name = ITEM_CHECK_FISHING_HEART_PIECE, .vanillaItem = dItemNo_KAKERA_HEART_e, }, MessageCheck{ .group = 5, .messageId = 5001, .name = ITEM_CHECK_DUNGEON_REWARD_FOREST, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 5, .messageId = 6011, .name = ITEM_CHECK_DUNGEON_REWARD_GORON, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 5, .messageId = 7001, .name = ITEM_CHECK_DUNGEON_REWARD_LAKEBED, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 5, .messageId = 9301, .name = ITEM_CHECK_DUNGEON_REWARD_SNOWPEAK, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 5, .messageId = 9401, .name = ITEM_CHECK_DUNGEON_REWARD_TIME, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 5, .messageId = 11001, .name = ITEM_CHECK_DUNGEON_REWARD_CITY, .vanillaItem = dItemNo_NONE_e, }, MessageCheck{ .group = 0, .messageId = 233, .name = ITEM_CHECK_ILIA_MEMORY, .vanillaItem = dItemNo_HORSE_FLUTE_e, .enqueueAtDisplay = false, }, }; std::unordered_map s_modChecks; std::unordered_set s_warnedCollisions; ItemCheckHandle s_nextCheckHandle = 1; const char* current_stage_name() { const char* stageName = dComIfGp_getStartStageName(); return stageName != nullptr ? stageName : ""; } std::string chest_check_name(uint8_t boxNo) { return fmt::format("chest:{}:{}", current_stage_name(), boxNo); } std::string boss_check_name() { return fmt::format("boss:{}", current_stage_name()); } std::string freestanding_check_name(uint8_t bitNo) { return fmt::format("freestanding:{}:{}", current_stage_name(), bitNo); } std::string poe_check_name(uint8_t bitNo) { return fmt::format("poe:{}:{}", current_stage_name(), bitNo); } std::string shop_check_name(uint8_t itemNo) { return fmt::format("shop:{}:{}", current_stage_name(), itemNo); } std::string bug_check_name(uint8_t insectId) { return fmt::format("bug:{}", insectId); } std::string sky_character_check_name() { return fmt::format("sky:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo()); } bool ilia_memory_context() { return std::strcmp(current_stage_name(), "R_SP109") == 0 && dComIfGp_roomControl_getStayNo() == 0 && dComIfG_play_c::getLayerNo(0) == 9; } uint16_t item_get_message(uint8_t itemNo) { if (itemNo == dItemNo_KAKERA_HEART_e) { constexpr std::array heartPieceMessages{0x86, 0x9C, 0x9D, 0x9E, 0x9F}; return heartPieceMessages[dComIfGs_getMaxLife() % heartPieceMessages.size()]; } return static_cast(itemNo + 0x65); } void invalidate_committed_check(const std::string& name) { if (name.empty()) { item_check_clear_committed(); } else { item_check_cancel(item_give_tag(name.c_str())); } } } // namespace uint8_t item_check(const char* name, uint8_t itemNo, fopAc_ac_c* giver) { if (name == nullptr || *name == '\0' || s_modChecks.empty()) { return itemNo; } // Callbacks may change registrations, so copy the applicable chain before invoking one. std::vector resolves; LoadedMod* previousOverrideOwner = nullptr; for (auto& mod : ModLoader::instance().mods()) { if (!mod.active) { continue; } const auto modIt = s_modChecks.find(&mod); if (modIt == s_modChecks.end()) { continue; } for (const auto& checkOverride : modIt->second.overrides) { if (checkOverride.name != name) { continue; } if (previousOverrideOwner != nullptr && s_warnedCollisions.emplace(name).second) { Log.warn("check '{}' is overridden by [{}] and [{}]; [{}] wins by load order", name, previousOverrideOwner->metadata.id, mod.metadata.id, mod.metadata.id); } previousOverrideOwner = &mod; resolves.push_back({.mod = &mod, .fixedValue = true, .itemNo = checkOverride.itemNo}); } for (const auto& resolver : modIt->second.resolvers) { if (resolver.name.empty() || resolver.name == name) { resolves.push_back({.mod = &mod, .fn = resolver.fn, .userData = resolver.userData}); } } } ItemCheckInfo info{ .name = name, .giver_actor = giver, .vanilla_item = itemNo, .current_item = itemNo, }; for (const auto& resolve : resolves) { if (!resolve.mod->active) { continue; } if (resolve.fixedValue) { info.current_item = resolve.itemNo; continue; } uint8_t resolvedItem = info.current_item; try { if (resolve.fn(resolve.mod->context.get(), &info, &resolvedItem, resolve.userData)) { info.current_item = resolvedItem; } } catch (const std::exception& e) { fail_mod(*resolve.mod, MOD_ERROR, fmt::format("Exception in item check resolver for '{}': {}", name, e.what())); } catch (...) { fail_mod(*resolve.mod, MOD_ERROR, fmt::format("Unknown exception in item check resolver for '{}'", name)); } } return info.current_item; } uint8_t item_check_chest(uint8_t boxNo, uint8_t itemNo, fopAc_ac_c* chest) { if (s_modChecks.empty()) { return itemNo; } const auto name = chest_check_name(boxNo); return item_check(name.c_str(), itemNo, chest); } uint8_t item_check_boss(uint8_t itemNo, fopAc_ac_c* boss) { if (s_modChecks.empty()) { return itemNo; } const auto name = boss_check_name(); return item_check(name.c_str(), itemNo, boss); } uint8_t item_check_freestanding(uint8_t bitNo, uint8_t itemNo, fopAc_ac_c* item) { if (s_modChecks.empty()) { return itemNo; } const auto name = freestanding_check_name(bitNo); return item_check(name.c_str(), itemNo, item); } uint8_t item_check_shop(uint8_t itemNo, fopAc_ac_c* giver) { if (s_modChecks.empty()) { return itemNo; } const auto name = shop_check_name(itemNo); return item_check(name.c_str(), itemNo, giver); } uint32_t item_check_message(uint16_t group, uint32_t messageId) { for (const auto& check : kMessageChecks) { if (check.group != group || check.messageId != messageId || (check.group == 0 && check.messageId == 233 && !ilia_memory_context())) { continue; } const ItemCheckResult result = item_check_commit(check.name, check.vanillaItem, nullptr); if (result.itemNo == check.vanillaItem || result.itemNo == dItemNo_NONE_e) { return messageId; } if (check.enqueueAtDisplay) { item_check_enqueue(result, ItemGiveMode::Silent); } return item_get_message(result.itemNo); } return messageId; } uint32_t item_give_tag_chest(uint8_t boxNo) { return item_give_tag(chest_check_name(boxNo).c_str()); } uint32_t item_give_tag_boss() { return item_give_tag(boss_check_name().c_str()); } uint32_t item_give_tag_freestanding(uint8_t bitNo) { return item_give_tag(freestanding_check_name(bitNo).c_str()); } uint32_t item_give_tag_poe(uint8_t bitNo) { return item_give_tag(poe_check_name(bitNo).c_str()); } uint32_t item_give_tag_shop(uint8_t itemNo) { return item_give_tag(shop_check_name(itemNo).c_str()); } uint32_t item_give_tag_bug(uint8_t insectId) { return item_give_tag(bug_check_name(insectId).c_str()); } uint32_t item_give_tag_sky_character() { return item_give_tag(sky_character_check_name().c_str()); } namespace svc { ModResult item_check_set_override(LoadedMod& mod, const char* name, uint8_t itemNo) { auto& checks = s_modChecks[&mod]; for (auto& checkOverride : checks.overrides) { if (checkOverride.name == name) { checkOverride.itemNo = itemNo; invalidate_committed_check(checkOverride.name); return MOD_OK; } } checks.overrides.push_back({.name = name, .itemNo = itemNo}); invalidate_committed_check(checks.overrides.back().name); return MOD_OK; } ModResult item_check_clear_override(LoadedMod& mod, const char* name) { const auto modIt = s_modChecks.find(&mod); if (modIt == s_modChecks.end()) { return MOD_INVALID_ARGUMENT; } const auto removed = std::erase_if(modIt->second.overrides, [&](const auto& checkOverride) { return checkOverride.name == name; }); if (removed != 0) { invalidate_committed_check(name); } return removed != 0 ? MOD_OK : MOD_INVALID_ARGUMENT; } ModResult item_check_add_resolver(LoadedMod& mod, const char* name, ItemCheckResolveFn fn, void* userData, ItemCheckHandle& outHandle) { auto& resolver = s_modChecks[&mod].resolvers.emplace_back(); resolver.handle = s_nextCheckHandle++; resolver.name = name != nullptr ? name : ""; resolver.fn = fn; resolver.userData = userData; outHandle = resolver.handle; invalidate_committed_check(resolver.name); return MOD_OK; } ModResult item_check_remove_resolver(LoadedMod& mod, ItemCheckHandle handle) { const auto modIt = s_modChecks.find(&mod); if (modIt == s_modChecks.end()) { return MOD_INVALID_ARGUMENT; } const auto resolverIt = std::find_if(modIt->second.resolvers.begin(), modIt->second.resolvers.end(), [&](const auto& resolver) { return resolver.handle == handle; }); if (resolverIt == modIt->second.resolvers.end()) { return MOD_INVALID_ARGUMENT; } const std::string name = resolverIt->name; modIt->second.resolvers.erase(resolverIt); invalidate_committed_check(name); return MOD_OK; } void item_checks_remove_mod(LoadedMod& mod) { if (const auto modIt = s_modChecks.find(&mod); modIt != s_modChecks.end()) { for (const auto& resolver : modIt->second.resolvers) { if (resolver.name.empty()) { item_check_clear_committed(); break; } invalidate_committed_check(resolver.name); } for (const auto& checkOverride : modIt->second.overrides) { invalidate_committed_check(checkOverride.name); } } s_modChecks.erase(&mod); s_warnedCollisions.clear(); } } // namespace svc } // namespace dusk::mods