mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-22 05:49:01 -04:00
5f0f3d4ebe
* ItemService improvements * Move Poe increment to item
410 lines
12 KiB
C++
410 lines
12 KiB
C++
#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 <fmt/format.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
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<CheckOverride> overrides;
|
|
std::vector<CheckResolver> 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<LoadedMod*, ModItemChecks> s_modChecks;
|
|
std::unordered_set<std::string> 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<uint16_t, 5> heartPieceMessages{0x86, 0x9C, 0x9D, 0x9E, 0x9F};
|
|
return heartPieceMessages[dComIfGs_getMaxLife() % heartPieceMessages.size()];
|
|
}
|
|
return static_cast<uint16_t>(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<PendingResolve> 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
|