diff options
| author | Briggs Baltzell <b3apollo13@gmail.com> | 2023-01-27 11:30:24 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-27 18:30:24 +0100 |
| commit | 2340dc7bc09a354ff6e487e723c667d873dd7073 (patch) | |
| tree | 98ad419392cf54bebb1ec3385e0c291f2635a9d0 /src/Game/Cooking/cookManager.cpp | |
| parent | 969b1c3f33b4edb3ed4cf74af063c371280a8fe9 (diff) | |
Implement CookingMgr class (#113)
* Added CookingMgr fields and started on ctor
* Implemented CookItem::copy
* Implemented CookingMgr dtor
* Marked CookingMgr createInstance as matching
* Implemented CookingMgr::getCookItem
* Added CookingMgr::cook decl
* Added CookingMgr method decls
* Replaced part of CookingMgr with sead::FixedObjArray
* Added line separators in cookManager.h
* Defined structs Ingredient and BoostArg in CookingMgr
* Defined structs CookArg and CookIngredient
* Renamed some fields of CookingMgr
* Added struct CookingMgr::CookingEffectEntry
* Started implementing CookingMgr::init
* Set more entries in init
* Set field _548
* Set field default values in init
* Loaded string values from config
* Set mCookingEffectEntries in init
* Added conditional mConfig allocation
* Added FixedTreeMap class to sead
* Used FixedTreeMap in CookingMgr
* Set CookingMgr constructor to default and marked it as matching
* Removed field _31C from CookingMgr
* Updated sead
* Cleared _548 before filling
* Used name hash for _548 keys
* Fixed path setup
* Changed type of _548 to sead::FixedTreeMap<u32, u32, 13>
* Set mNMSSR to correct values
* Fixed field init order
* Rearranged variables
* Swapped entry_hash comparisons
* Changed CRC32 names and index values
* Cast i to u64
* Renamed static crc32 variables
* Renamed _548 to mCookingEffectNameIdMap
* Marked static vars as const and used decimal numbers
* Used sead::SafeArray for mNMMR and mNMSSR
* Rearranged mNMMR and mNMSSR conditions
* Removed size check from mNMMR and mNMSSR conditions
* Removed size check from CEI conditions
* Renamed mRes2 to mResHandle
* Renamed cook args
* Moved crc32 statics into Constants struct
* Marked CookingMgr::init as matching
* Renamed Constants to Crc32Constants
* Added cookFail and cookFailForMissingConfig
* Added NumIngredientsMax, NumEffects, and NumEffectSlots constants
* Renamed getCookItem to setCookItem
* Renamed x to getCookItem
* Implemented getCookItem
* Made setCookItem take a const ref
* Decompiled cookCalcItemPrice
* Marked cookCalcItemPrice as const
* Fixed both cookFail and cookFailForMissingConfig
* Added CookItem::reset
* Added CookingMgr::resetCookItem
* Moved ctor and dtor below static variables
* Changed cookFailForMissingConfig arg to sead::SafeString
* Started on cookCalcBoost
* Moved CookItem into cookManager.h
* Moved Cooking into Game
* Moved CookingMgr to uking namespace
* Inlined CookItem::reset
* Renamed CookItem members
* Fixed CookItem mangled names in uking_functions.csv
* Added enum class CookEffectId
* Moved CookingMgr ctor below disposer impl
* Updated sead
* Started on cookHandleBoostSuccessInner
* Made changes to cookCalcBoost and cookHandleBoostSuccessInner
* Extracted cookHandleBoostMonsterExtractInner
* Removed found_monster_extract in cookCalcBoost
* Marked cookCalcBoost as matching
* Got cookHandleBoostMonsterExtractInner matching
* Made changes to cookHandleBoostSuccessInner
* Renamed discriminator to bonus in cookHandleBoostSuccessInner
* Added CookingMgr::cookCalcSpiceBoost
* Made changes to CookingMgr::cookCalcSpiceBoost
* Added CookingMgr::cookCalcPotencyBoost
* Mostly matched CookingMgr::cookCalcPotencyBoost
* Added CookingMgr::getCookEffectId
* Used getCookEffectId in cookCalcPotencyBoost
* Set access modifiers in CookingMgr
* Made cookCalcPotencyBoost const-correct
* Marked global init as mismatching
* Renamed CookingMgr functions in uking_functions.csv
* Added CookingMgr::prepareCookArg
* Added IngredientArray type
* Added cookWithItems
* Added CookingMgr::prepareCookArg
* Fixed getCookEffectId
* Fixed cookCalcPotencyBoost
* Made changes to prepareCookArg
* Got cookWithItems matching
* Marked vars const
* Removed commented-out destructor and UnkItem
* Renamed CookIngredient._58 to count
* Started on CookingMgr::cook
* Extracted cookAdjustItem
* Fixed ingredient setup
* Extracted actor_info_data
* Fixed some control flow for single ingredients
* Fixed more control flow for single ingredients
* Handled no effect and empty actor name
* Renamed Ingredient::_10 to used_in_recipe
* Fixed recipe control flow
* Matched cookAdjustItem
* Fixed InfoData instance uses
* Removed BAD_RECIPE label
* Rearranged to remove goto
* Added cookCalcRecipeBoost
* Extracted getCookEffectId
* Extracted isCookFailure and isMedicine
* Extracted getCookEffectIdFromTreeMap
* Extracted findIngredientByName and findIngredientByTag
* Marked nonmatching functions
* Added resetArgCookData
* Matched resetArgCookData and prepareCookArg
* Added fields and handleMessage to CookPotRoot
* Started on CookPotRoot::init_
* Added hasMonsterExtract
* Added to CookPotRoot::handleMessage_
* Removed Ingredient::_4 field
* Renamed CookItem::_224 to is_crit
* Renamed CookingEffectEntry members
* Renamed CookingMgr fields
* Changed comparison in cook
* Renamed actor_tag_iter to hash_iter and added num checks
* Got cookCalcSpiceBoost closer to matching
* Removed int_val init from cookCalcSpiceBoost
* Inlined actor_data
* Added `using namespace ksys::act`
* Moved int_val outside loop
* Marked cookCalcSpiceBoost as matching
* Rearranged cookHandleBoostSuccessInner
* Renamed cookCalcPotencyBoost to cookCalcIngredientsBoost
* Renamed stamina_recover to vitality_boost
* Renamed CookItem args to cook_item
* Renamed cookFailForMissingConfig arg
* Used IngredientArray in findIngredient functions
* Renamed crit functions
* Renamed getCookEffectId arg
* Renamed getCookEffectId to getCookEffectIdByName
* Added SetCookItemInDemo::oneShot_
* Matched SetCookItemInDemo::oneShot_
* Added comment about CookArg constructor and formatted
* Removed TODO from CookingMgr
* Marked WIP functions as non-matching
* Fixed formatting
* Matched sead::SafeArray<uking::CookIngredient, 5>::SafeArray
* Added BoostArg default member initializers
* Used auto for cooking_mgr in CookPotRoot::handleMessage_
* Used designated initializers for boost_arg and added whitespace
* Used angle brackets for sead import
* Removed [[maybe_unused]] from declaration
* Used safeDelete
* Inverted condition in CookingMgr::cookCalcCritBoost
* Added getCookingEffectEntry getter and moved CookingEffectEntry up
* Cleaned up CookingMgr::cookCalcSpiceBoost
* Used loops to init ingredient num members
* Converted CookingMgr fields to s32
* Cleaned up CookingMgr::cookCalcItemPrice
* Renamed CookItem::item_price to sell_price
* Used min and max in CookingMgr::cookCalcItemPrice
* Inlined count vars in CookingMgr::cookCalcItemPrice
* Removed explicit casts in CookingMgr::cookCalcItemPrice
* Cleaned up and reformatted CookingMgr::cookCalcIngredientsBoost
* Added casts back into CookingMgr::cookCalcItemPrice to silence warnings
Diffstat (limited to 'src/Game/Cooking/cookManager.cpp')
| -rw-r--r-- | src/Game/Cooking/cookManager.cpp | 1195 |
1 files changed, 1195 insertions, 0 deletions
diff --git a/src/Game/Cooking/cookManager.cpp b/src/Game/Cooking/cookManager.cpp new file mode 100644 index 00000000..3b6a31ff --- /dev/null +++ b/src/Game/Cooking/cookManager.cpp @@ -0,0 +1,1195 @@ +#include "Game/Cooking/cookManager.h" +#include <codec/seadHashCRC32.h> +#include <random/seadGlobalRandom.h> +#include <typeindex> +#include "KingSystem/ActorSystem/actInfoData.h" +#include "KingSystem/Resource/resLoadRequest.h" +#include "KingSystem/Utils/InitTimeInfo.h" +#include "KingSystem/Utils/SafeDelete.h" + +namespace uking { + +struct CookingEffect { + sead::SafeString name; + CookEffectId effect_id; +}; + +static const CookingEffect sCookingEffects[CookingMgr::NumEffects]{ + {"None", CookEffectId::None}, + {"LifeRecover", CookEffectId::LifeRecover}, + {"LifeMaxUp", CookEffectId::LifeMaxUp}, + {"ResistHot", CookEffectId::ResistHot}, + {"ResistCold", CookEffectId::ResistCold}, + {"ResistElectric", CookEffectId::ResistElectric}, + {"AttackUp", CookEffectId::AttackUp}, + {"DefenseUp", CookEffectId::DefenseUp}, + {"Quietness", CookEffectId::Quietness}, + {"MovingSpeed", CookEffectId::MovingSpeed}, + {"GutsRecover", CookEffectId::GutsRecover}, + {"ExGutsMaxUp", CookEffectId::ExGutsMaxUp}, + {"Fireproof", CookEffectId::Fireproof}, +}; + +struct Crc32Constants { + ksys::util::InitConstants init_constants; + const u32 crc32_life_recover = sead::HashCRC32::calcStringHash("LifeRecover"); + const u32 crc32_guts_performance = sead::HashCRC32::calcStringHash("GutsPerformance"); + const u32 crc32_stamina_recover = sead::HashCRC32::calcStringHash("StaminaRecover"); + const u32 crc32_life_max_up = sead::HashCRC32::calcStringHash("LifeMaxUp"); + const u32 crc32_resist_hot = sead::HashCRC32::calcStringHash("ResistHot"); + const u32 crc32_resist_cold = sead::HashCRC32::calcStringHash("ResistCold"); + const u32 crc32_resist_electric = sead::HashCRC32::calcStringHash("ResistElectric"); + const u32 crc32_all_speed = sead::HashCRC32::calcStringHash("AllSpeed"); + const u32 crc32_attack_up = sead::HashCRC32::calcStringHash("AttackUp"); + const u32 crc32_defense_up = sead::HashCRC32::calcStringHash("DefenseUp"); + const u32 crc32_quietness = sead::HashCRC32::calcStringHash("Quietness"); + const u32 crc32_fireproof = sead::HashCRC32::calcStringHash("Fireproof"); +}; + +static Crc32Constants sCrc32Constants; + +CookItem::CookItem() = default; + +void CookItem::reset() { + actor_name.clear(); + life_recover = 0.0f; + effect_time = 0; + is_crit = false; + sell_price = 0; + effect_id = CookEffectId::None; + vitality_boost = 0.0f; + for (auto& ingredient : ingredients) { + ingredient.clear(); + } +} + +void CookItem::copy(CookItem& to) const { + to.actor_name = actor_name; + to.life_recover = life_recover; + to.effect_time = effect_time; + to.is_crit = is_crit; + to.sell_price = sell_price; + to.vitality_boost = vitality_boost; + to.effect_id = effect_id; + to.ingredients = ingredients; +} + +SEAD_SINGLETON_DISPOSER_IMPL(CookingMgr) + +CookingMgr::CookingMgr() = default; + +CookingMgr::~CookingMgr() { + if (mConfig) { + ksys::util::safeDelete(mConfig); + } +} + +void CookingMgr::cookFail(CookItem& cook_item) { + if (cook_item.actor_name.isEmpty()) + cook_item.actor_name.copy(mFailActorName); + + f32 life_recover; + if (cook_item.actor_name == mFailActorName) { + // Dubious food + cook_item.effect_time = 0; + const f32 min_recovery = (f32)mFailActorLifeRecover; + life_recover = + min_recovery > cook_item.life_recover ? min_recovery : cook_item.life_recover; + } else { + // Rock-hard food + cook_item.effect_time = 0; + life_recover = (f32)mStoneFoodActorLifeRecover; + } + + cook_item.life_recover = life_recover; + cook_item.vitality_boost = 0.0f; + cook_item.effect_id = CookEffectId::None; + cook_item.sell_price = 2; +} + +void CookingMgr::cookFailForMissingConfig(CookItem& cook_item, const sead::SafeString& actor_name) { + f32 life_recover; + if (actor_name.isEmpty() || actor_name == mFailActorName) { + cook_item.actor_name.copy(mFailActorName); + cook_item.effect_time = 0; + life_recover = (f32)mFailActorLifeRecover; + } else { + cook_item.actor_name = actor_name; + cook_item.effect_time = 0; + life_recover = (f32)mStoneFoodActorLifeRecover; + } + + cook_item.life_recover = life_recover; + cook_item.vitality_boost = 0.0f; + cook_item.effect_id = CookEffectId::None; + cook_item.sell_price = 1; +} + +void CookingMgr::cookCalcCritBoost(const IngredientArray& ingredients, CookItem& cook_item, + const BoostArg* boost_arg) const { + // Find if any of the ingredients are Monster Extract. + if (hasMonsterExtract(ingredients)) { + cookHandleMonsterExtract(ingredients, cook_item); + return; + } + + if (!boost_arg || !boost_arg->always_boost) { + if (boost_arg && !boost_arg->enable_random_boost) + return; + + s32 threshold = 0; + s32 num_ingredients = 0; + s32 success_rate = 0; + + for (int i = 0; i < NumIngredientsMax; i++) { + const auto& ingredient = ingredients[i]; + + if (!ingredient.arg) + continue; + + num_ingredients++; + + if (ingredient.actor_data.tryGetIntByKey(&success_rate, "cookSpiceBoostSuccessRate") && + success_rate > threshold) { + threshold = success_rate; + } + } + + if (num_ingredients >= 1) + threshold += mIngredientNumSuccessRates[num_ingredients - 1]; + + if ((s32)sead::GlobalRandom::instance()->getU32(100) >= threshold) + return; + } + cookHandleCrit(ingredients, cook_item); +} + +void CookingMgr::cookHandleMonsterExtract([[maybe_unused]] const IngredientArray& ingredients, + CookItem& cook_item) const { + // Monster Extract found; calculate boosts. + + s32 effect_min = 0; + s32 effect_max = 4; + + if (cook_item.life_recover <= 0.0f || cook_item.effect_id == CookEffectId::LifeMaxUp) + effect_min = 2; + + if (cook_item.effect_id == CookEffectId::None) + effect_max = 2; + + switch (sead::GlobalRandom::instance()->getS32Range(effect_min, effect_max)) { + case 0: + cook_item.life_recover += (f32)getCookingEffectEntry(CookEffectId::LifeRecover).ssa; + break; + case 1: + cook_item.life_recover = (f32)getCookingEffectEntry(CookEffectId::LifeRecover).min; + break; + case 2: + if (cook_item.effect_id != CookEffectId::None) { + if (cook_item.vitality_boost > 0.0f && cook_item.vitality_boost < 1.0f) { + cook_item.vitality_boost = 1.0; + } + cook_item.vitality_boost = (f32)((s32)cook_item.vitality_boost + + getCookingEffectEntry(cook_item.effect_id).ssa); + } + break; + case 3: + if (cook_item.effect_id != CookEffectId::None) { + cook_item.vitality_boost = (f32)getCookingEffectEntry(cook_item.effect_id).min; + } + break; + default: + break; + } + + // Effect time + if (cook_item.effect_time >= 1) { + const u32 roll = sead::GlobalRandom::instance()->getU32(3); + + if (roll == 0) + // 1 minute + cook_item.effect_time = 60; + if (roll == 1) + // 10 minutes + cook_item.effect_time = 600; + if (roll == 2) + // 30 minutes + cook_item.effect_time = 1800; + } +} + +// NON_MATCHING +void CookingMgr::cookHandleCrit([[maybe_unused]] const IngredientArray& ingredients, + CookItem& cook_item) const { + enum Bonus { + LifeBonus = 0, + VitalityBonus = 1, + TimeBonus = 2, + }; + + Bonus bonus = LifeBonus; + + cook_item.is_crit = true; + + const auto& life_entry = getCookingEffectEntry(CookEffectId::LifeRecover); + + if (cook_item.effect_id != CookEffectId::None) { + const f32 life_recover = cook_item.life_recover; + const s32 vitality_bonus = sead::Mathi::clampMin((s32)cook_item.vitality_boost, 1); + + const f32 life_recover_max = (f32)life_entry.max; + const s32 vitality_bonus_max = getCookingEffectEntry(cook_item.effect_id).max; + + const bool life_recover_maxed = life_recover >= life_recover_max; + const bool vitality_bonus_maxed = vitality_bonus >= vitality_bonus_max; + + switch (cook_item.effect_id) { + case CookEffectId::LifeMaxUp: + bonus = VitalityBonus; + break; + + case CookEffectId::GutsRecover: + case CookEffectId::ExGutsMaxUp: + if (vitality_bonus_maxed) + bonus = LifeBonus; + else if (life_recover_maxed) + bonus = VitalityBonus; + else + bonus = sead::GlobalRandom::instance()->getBool() ? VitalityBonus : LifeBonus; + break; + + default: + if (vitality_bonus_maxed) { + if (life_recover_maxed) { + bonus = TimeBonus; + } else { + bonus = sead::GlobalRandom::instance()->getBool() ? TimeBonus : LifeBonus; + } + } else { + if (life_recover_maxed) { + bonus = sead::GlobalRandom::instance()->getBool() ? TimeBonus : VitalityBonus; + } else { + bonus = (Bonus)sead::GlobalRandom::instance()->getU32(3); + } + } + break; + } + } + + switch (bonus) { + case VitalityBonus: + if (cook_item.effect_id != CookEffectId::None) { + if (cook_item.vitality_boost > 0.0f && cook_item.vitality_boost < 1.0f) + cook_item.vitality_boost = 1.0f; + cook_item.vitality_boost = (f32)((int)cook_item.vitality_boost + + getCookingEffectEntry(cook_item.effect_id).ssa); + } + break; + case TimeBonus: + cook_item.effect_time += mCritEffectTime; + break; + case LifeBonus: + cook_item.life_recover += (f32)life_entry.ssa; + break; + } +} + +void CookingMgr::cookCalcSpiceBoost(const IngredientArray& ingredients, CookItem& cook_item) const { + using namespace ksys::act; + + int int_val; + + for (int i = 0; i < NumIngredientsMax; i++) { + if (!ingredients[i].arg) + continue; + + if (InfoData::instance()->hasTag(ingredients[i].actor_data, tags::CookEnemy) || + !InfoData::instance()->hasTag(ingredients[i].actor_data, tags::CookSpice)) { + continue; + } + + if (ingredients[i].actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostHitPointRecover") && + int_val > 0) { + cook_item.life_recover += (f32)int_val; + } + + if (ingredients[i].actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostEffectiveTime") && + int_val > 0) { + cook_item.effect_time += int_val; + } + + // The following loops are buggy, but will never be run, as their config values are left 0. + + if (ingredients[i].actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostMaxHeartLevel") && + int_val > 0) { + // i < 1 check needs to come after others. + for ([[maybe_unused]] int _ = 0; i < 1; i++) { + if (cook_item.effect_id == CookEffectId::LifeMaxUp) { + cook_item.vitality_boost += (f32)int_val; + } + } + } + + if (ingredients[i].actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostStaminaLevel") && + int_val > 0) { + // i < 1 check needs to come after others. + for ([[maybe_unused]] int _ = 0; i < 1; i++) { + if (cook_item.effect_id == CookEffectId::GutsRecover || + cook_item.effect_id == CookEffectId::ExGutsMaxUp) { + cook_item.vitality_boost += (f32)int_val; + } + } + } + } +} + +void CookingMgr::cookCalcItemPrice(const IngredientArray& ingredients, CookItem& cook_item) const { + cook_item.sell_price = 0; + + if (mFairyTonicName == cook_item.actor_name) { + // Fairy Tonic is sold for 2 rupees. + cook_item.sell_price = 2; + return; + } + + s32 int_val = 0; + s32 max_price = 0; + s32 mult_idx = 0; + + for (int i = 0; i < NumIngredientsMax; ++i) { + const auto& ingredient = ingredients[i]; + const auto& actor_data = ingredient.actor_data; + + if (!ingredient.arg) + break; + + if (ksys::act::InfoData::instance()->hasTag(actor_data, ksys::act::tags::CookLowPrice)) { + // This ingredient is only worth 1 rupee. + mult_idx += ingredient.arg->count; + cook_item.sell_price += ingredient.arg->count; + max_price += ingredient.arg->count; + } else { + if (actor_data.tryGetIntByKey(&int_val, "itemSellingPrice")) { + mult_idx += ingredient.arg->count; + cook_item.sell_price += int_val * ingredient.arg->count; + } + if (actor_data.tryGetIntByKey(&int_val, "itemBuyingPrice")) { + max_price += int_val * ingredient.arg->count; + } + } + } + + if (mult_idx >= 1) { + cook_item.sell_price = + (s32)(mIngredientNumMultipliers[mult_idx - 1] * (f32)cook_item.sell_price); + } + + if (cook_item.sell_price >= 1) { + // Round up to the nearest power of 10 + if (cook_item.sell_price % 10 != 0) { + cook_item.sell_price = cook_item.sell_price + 10 - cook_item.sell_price % 10; + } + } + + // clamp and clampMin don't work here. + cook_item.sell_price = sead::Mathi::min(max_price, cook_item.sell_price); + cook_item.sell_price = sead::Mathi::max(cook_item.sell_price, 2); +} + +void CookingMgr::cookCalcIngredientsBoost(const IngredientArray& ingredients, + CookItem& cook_item) const { + const bool is_medicine = isMedicine(cook_item); + const bool is_not_fairy_tonic = mFairyTonicName != cook_item.actor_name; + + sead::SafeArray<s32, NumEffectSlots> effect_counts{}; + sead::SafeArray<s32, NumEffectSlots> cure_levels{}; + + s32 stamina_boost = 0; + s32 life_boost = 0; + s32 time_boost = 0; + s32 total_count = 0; + s32 life_recover = 0; + s32 int_val; + + for (int i = 0; i < NumIngredientsMax; i++) { + if (!ingredients[i].arg) + break; + + const al::ByamlIter& actor_data = ingredients[i].actor_data; + const s32 count = ingredients[i].arg->count; + + total_count += count; + + if (ksys::act::InfoData::instance()->hasTag(actor_data, ksys::act::tags::CookEnemy)) { + if (actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostEffectiveTime") && int_val > 0) { + time_boost += int_val * count; + } + + if (actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostMaxHeartLevel") && int_val > 0) { + life_boost += int_val * count; + } + + if (actor_data.tryGetIntByKey(&int_val, "cookSpiceBoostStaminaLevel") && int_val > 0) { + stamina_boost += int_val * count; + } + } else { + if (actor_data.tryGetIntByKey(&int_val, "cureItemHitPointRecover") && int_val > 0) { + life_recover += int_val * count; + } + + if (actor_data.tryGetIntByKey(&int_val, "cureItemEffectLevel") && int_val > 0) { + const char* string_val = nullptr; + if (actor_data.tryGetStringByKey(&string_val, "cureItemEffectType")) { + const auto effect_id = getCookEffectIdByName(string_val); + if (effect_id != CookEffectId::None) { + effect_counts[(int)effect_id] += count; + cure_levels[(int)effect_id] += int_val * count; + } + } + } + } + } + + bool effect_found = false; + for (int i = 0; i < NumEffectSlots; i++) { + const s32 effect_count = effect_counts[i]; + if (effect_count > 0) { + if (effect_found) { + // Finding a second effect makes them cancel out. + effect_found = false; + cook_item.vitality_boost = 0.0f; + cook_item.effect_id = CookEffectId::None; + cook_item.effect_time = 0; + break; + } + + const auto& entry = mCookingEffectEntries[i]; + + cook_item.vitality_boost = (f32)cure_levels[i] * entry.multiplier; + + const auto effect_id = (CookEffectId)i; + cook_item.effect_id = effect_id; + + const s32 boost_time = entry.boost_time; + if (boost_time > 0) + cook_item.effect_time = time_boost + 30 * total_count + boost_time * effect_count; + + if (effect_id == CookEffectId::LifeMaxUp) { + cook_item.vitality_boost += (f32)life_boost; + } else if (effect_id == CookEffectId::GutsRecover || + effect_id == CookEffectId::ExGutsMaxUp) { + cook_item.vitality_boost += (f32)stamina_boost; + } + + effect_found = true; + } + } + + if (!is_not_fairy_tonic && effect_found) { + effect_found = false; + cook_item.vitality_boost = 0.0f; + cook_item.effect_id = CookEffectId::None; + cook_item.effect_time = 0; + } + + if (is_medicine && !effect_found) { + cook_item.actor_name = mFailActorName; + } + + if (isCookFailure(cook_item)) { + cook_item.life_recover = (f32)life_recover * mFailActorLifeRecoverMultiplier; + } else if (effect_found) { + cook_item.life_recover = (f32)life_recover * mLifeRecoverMultiplier; + } else { + cook_item.life_recover = + (f32)life_recover * getCookingEffectEntry(CookEffectId::LifeRecover).multiplier; + } + + if (cook_item.effect_id != CookEffectId::None) { + const s32 max = getCookingEffectEntry(cook_item.effect_id).max; + if (cook_item.vitality_boost > (f32)max) + cook_item.vitality_boost = (f32)max; + } +} + +bool CookingMgr::findIngredientByName(CookingMgr::IngredientArray& ingredients, u32 name_hash, + int num_ingredients) const { + for (int ingredient_idx = 0; ingredient_idx < num_ingredients; ingredient_idx++) { + Ingredient& ingredient = ingredients[ingredient_idx]; + if (!ingredient.used_in_recipe && ingredient.name_hash == name_hash) { + ingredient.used_in_recipe = true; + return true; + } + } + return false; +} + +bool CookingMgr::findIngredientByTag(CookingMgr::IngredientArray& ingredients, u32 tag_hash, + int num_ingredients) const { + for (int ingredient_idx = 0; ingredient_idx < num_ingredients; ingredient_idx++) { + Ingredient& ingredient = ingredients[ingredient_idx]; + if (!ingredient.used_in_recipe && + ksys::act::InfoData::instance()->hasTag(ingredient.actor_data, tag_hash)) { + ingredient.used_in_recipe = true; + return true; + } + } + return false; +} + +bool CookingMgr::isMedicine(const CookItem& cook_item) const { + return !cook_item.actor_name.isEmpty() && + ksys::act::InfoData::instance()->hasTag(cook_item.actor_name.cstr(), + ksys::act::tags::CookEMedicine); +} + +bool CookingMgr::isCookFailure(const CookItem& cook_item) const { + return !cook_item.actor_name.isEmpty() && + ksys::act::InfoData::instance()->hasTag(cook_item.actor_name.cstr(), + ksys::act::tags::CookFailure); +} + +bool CookingMgr::hasMonsterExtract(const CookingMgr::IngredientArray& ingredients) const { + for (int i = 0; i < NumIngredientsMax; i++) { + const auto& ingredient = ingredients[i]; + if (ingredient.arg && ingredient.arg->name == mMonsterExtractName) { + return true; + } + } + return false; +} + +CookEffectId CookingMgr::getCookEffectId(u32 name_hash) const { + CookEffectId entry_idx; + + if (sCrc32Constants.crc32_life_recover == name_hash) + entry_idx = CookEffectId::LifeRecover; + else if (sCrc32Constants.crc32_guts_performance == name_hash) + entry_idx = CookEffectId::ExGutsMaxUp; + else if (sCrc32Constants.crc32_stamina_recover == name_hash) + entry_idx = CookEffectId::GutsRecover; + else if (sCrc32Constants.crc32_life_max_up == name_hash) + entry_idx = CookEffectId::LifeMaxUp; + else if (sCrc32Constants.crc32_resist_hot == name_hash) + entry_idx = CookEffectId::ResistHot; + else if (sCrc32Constants.crc32_resist_cold == name_hash) + entry_idx = CookEffectId::ResistCold; + else if (sCrc32Constants.crc32_resist_electric == name_hash) + entry_idx = CookEffectId::ResistElectric; + else if (sCrc32Constants.crc32_all_speed == name_hash) + entry_idx = CookEffectId::MovingSpeed; + else if (sCrc32Constants.crc32_attack_up == name_hash) + entry_idx = CookEffectId::AttackUp; + else if (sCrc32Constants.crc32_defense_up == name_hash) + entry_idx = CookEffectId::DefenseUp; + else if (sCrc32Constants.crc32_quietness == name_hash) + entry_idx = CookEffectId::Quietness; + else if (sCrc32Constants.crc32_fireproof == name_hash) + entry_idx = CookEffectId::Fireproof; + else + entry_idx = CookEffectId::None; + return entry_idx; +} + +CookEffectId CookingMgr::getCookEffectIdByName(const sead::SafeString& effect_name) const { + const auto name_hash = sead::HashCRC32::calcStringHash(effect_name); + return getCookEffectIdFromTreeMap(name_hash); +} + +CookEffectId CookingMgr::getCookEffectIdFromTreeMap(const u32 name_hash) const { + if (const auto* node = mCookingEffectNameIdMap.find(name_hash)) { + return node->value(); + } + return CookEffectId::None; +} + +void CookingMgr::init(sead::Heap* heap) { + ksys::res::LoadRequest req; + + req.mRequester = "CookingMgr"; + req._22 = false; + + sead::FixedSafeString<0x80> path; + path.format("Cooking/CookData.byml"); + + auto* res = sead::DynamicCast<sead::DirectResource>(mResHandle.load(path, &req)); + if (!res) + return; + + mConfig = mConfig ? new (mConfig) al::ByamlIter(res->getRawData()) : + new (heap) al::ByamlIter(res->getRawData()); + + mCookingEffectNameIdMap.clear(); + + for (int effect_idx = 0; effect_idx < NumEffects; effect_idx++) { + auto& effect = sCookingEffects[effect_idx]; + const u32 name_hash = sead::HashCRC32::calcStringHash(effect.name); + mCookingEffectNameIdMap.insert(name_hash, effect.effect_id); + } + + for (int i = 0; i < NumEffectSlots; i++) { + mCookingEffectEntries[i] = CookingEffectEntry{}; + mCookingEffectEntries[i].ssa = 0; + } + + for (int i = 0; i < NumIngredientsMax; i++) { + mIngredientNumMultipliers[i] = 1.0f; + } + + // Must be separate from previous loop. + for (int i = 0; i < NumIngredientsMax; i++) { + mIngredientNumSuccessRates[i] = 5 * i; + } + + mFairyTonicName = "Item_Cook_C_16"; + mFairyTonicNameHash = sead::HashCRC32::calcStringHash(mFairyTonicName); + + mFailActorName = "Item_Cook_O_01"; + mFailActorNameHash = sead::HashCRC32::calcStringHash(mFailActorName); + + mMonsterExtractName = "Item_Material_08"; + mMonsterExtractNameHash = sead::HashCRC32::calcStringHash(mMonsterExtractName); + + mLifeRecoverMultiplier = 1.0; + mStoneFoodActorLifeRecover = 1; + mCritEffectTime = 300; + mFailActorLifeRecoverMultiplier = 1.0; + mFailActorLifeRecover = 4; + + al::ByamlIter iter; + al::ByamlIter cei_iter; + al::ByamlIter entry_iter; + + const char* string_val = nullptr; + int int_val; + u32 uint_val; + float float_val; + + if (mConfig->tryGetIterByKey(&iter, "System")) { + if (iter.tryGetStringByKey(&string_val, "FA")) { + mFailActorName = string_val; + mFailActorNameHash = sead::HashCRC32::calcStringHash(mFailActorName); + } + if (iter.tryGetStringByKey(&string_val, "FCA")) { + mFairyTonicName = string_val; + mFairyTonicNameHash = sead::HashCRC32::calcStringHash(mFairyTonicName); + } + if (iter.tryGetStringByKey(&string_val, "MEA")) { + mMonsterExtractName = string_val; + mMonsterExtractNameHash = sead::HashCRC32::calcStringHash(mMonsterExtractName); + } + + if (iter.tryGetFloatByKey(&float_val, "LRMR") && float_val >= 0) + mLifeRecoverMultiplier = float_val; + if (iter.tryGetFloatByKey(&float_val, "FALRMR") && float_val >= 0) + mFailActorLifeRecoverMultiplier = float_val; + if (iter.tryGetIntByKey(&int_val, "FALR") && int_val >= 0) + mFailActorLifeRecover = int_val; + if (iter.tryGetIntByKey(&int_val, "SFALR") && int_val >= 0) + mStoneFoodActorLifeRecover = int_val; + if (iter.tryGetIntByKey(&int_val, "SSAET") && int_val >= 0) + mCritEffectTime = int_val; + + if (iter.tryGetIterByKey(&cei_iter, "CEI")) { + const int size = cei_iter.getSize(); + + for (int i = 0; i < size; i++) { + if (cei_iter.tryGetIterByIndex(&entry_iter, i) && + entry_iter.tryGetUIntByKey(&uint_val, "T")) { + const u32 name_hash = uint_val; + + const CookEffectId entry_idx = getCookEffectId(name_hash); + + if (entry_idx == CookEffectId::None) + continue; + + if (entry_iter.tryGetIntByKey(&int_val, "BT")) + getCookingEffectEntry(entry_idx).boost_time = int_val; + + if (entry_iter.tryGetIntByKey(&int_val, "Ma")) + getCookingEffectEntry(entry_idx).max = int_val; + + if (entry_iter.tryGetIntByKey(&int_val, "Mi")) + getCookingEffectEntry(entry_idx).min = int_val; + + if (entry_iter.tryGetFloatByKey(&float_val, "MR")) + getCookingEffectEntry(entry_idx).multiplier = float_val; + + if (entry_iter.tryGetIntByKey(&int_val, "SSA")) + getCookingEffectEntry(entry_idx).ssa = int_val; + } + } + } + + if (iter.tryGetIterByKey(&cei_iter, "NMMR")) { + const int size = cei_iter.getSize(); + + for (int i = 0; i < size; i++) { + if (cei_iter.tryGetFloatByIndex(&float_val, i) && i < NumIngredientsMax) { + mIngredientNumMultipliers[i] = sead::Mathf::clamp(float_val, 0.0f, 5.0f); + } + } + } + + if (iter.tryGetIterByKey(&cei_iter, "NMSSR")) { + const int size = cei_iter.getSize(); + + for (int i = 0; i < size; i++) { + if (cei_iter.tryGetIntByIndex(&int_val, i) && i < NumIngredientsMax) { + mIngredientNumSuccessRates[i] = sead::Mathi::clamp(int_val, -100, 100); + } + } + } + } +} + +// NON_MATCHING +bool CookingMgr::cook(const CookArg& arg, CookItem& cook_item, + const CookingMgr::BoostArg& boost_arg) { + ksys::act::InfoData* actor_info_data = ksys::act::InfoData::instance(); + + al::ByamlIter recipes_iter; + sead::SafeArray<Ingredient, NumIngredientsMax> ingredients; + + int num_ingredients = 0; + if (mConfig && actor_info_data) { + for (int i = 0; i < NumIngredientsMax; i++) { + const auto& cook_ingredient = arg.ingredients[i]; + if (!cook_ingredient.name.isEmpty()) { + const u32 name_hash = sead::HashCRC32::calcStringHash(cook_ingredient.name); + auto& ingredient = ingredients[num_ingredients]; + if (actor_info_data->getActorIter(&ingredient.actor_data, name_hash)) { + ingredient.name_hash = name_hash; + ingredient.arg = &cook_ingredient; + num_ingredients++; + } + } + } + } + + if (!mConfig || !actor_info_data || num_ingredients == 0) { + // Unused label: + // COOK_FAILURE_FOR_MISSING_CONFIG: + cookFailForMissingConfig(cook_item, mFailActorName); + return false; + } + + const Ingredient* single_ingredient = nullptr; + bool multiple_non_spice_ingredients = false; + + if (num_ingredients > 1) { + if (mConfig->tryGetIterByKey(&recipes_iter, "Recipes")) { + const s32 num_recipes = recipes_iter.getSize(); + const char* string_val = nullptr; + u32 uint_val = 0; + al::ByamlIter recipe_iter; + al::ByamlIter hash_iter; + al::ByamlIter actors_iter; + al::ByamlIter tags_iter; + + if (num_recipes > 0) { + for (int recipe_idx = 0; recipe_idx < num_recipes; recipe_idx++) { + if (!recipes_iter.tryGetIterByIndex(&recipe_iter, recipe_idx)) + continue; + + recipe_iter.tryGetStringByKey(&string_val, "Result"); + recipe_iter.tryGetUIntByKey(&uint_val, "Recipe"); + const s32 num_actors = recipe_iter.tryGetIterByKey(&actors_iter, "Actors") ? + actors_iter.getSize() : + 0; + const s32 num_tags = + recipe_iter.tryGetIterByKey(&tags_iter, "Tags") ? tags_iter.getSize() : 0; + + if (num_actors + num_tags > num_ingredients || + (num_actors == 0 && num_tags == 0)) + continue; + + ingredients[0].used_in_recipe = false; + ingredients[1].used_in_recipe = false; + ingredients[2].used_in_recipe = false; + ingredients[3].used_in_recipe = false; + ingredients[4].used_in_recipe = false; + + // Each recipe entry can have a list of sets of Actors, and a list of sets of + // Tags. An ingredient must be found for each set of Actors and Tags. + + if (num_actors > 0) { + bool any_actors_missed = false; + + for (int actor_idx = 0; actor_idx < num_actors; actor_idx++) { + if (actors_iter.tryGetIterByIndex(&hash_iter, actor_idx)) { + const s32 num_hashes = hash_iter.getSize(); + if (num_hashes < 1) + continue; + bool found = false; + for (int hash_idx = 0; hash_idx < num_hashes; hash_idx++) { + u32 hash_val; + if (hash_iter.tryGetUIntByIndex(&hash_val, hash_idx)) { + // Any actor in this list will work. + found = findIngredientByName(ingredients, hash_val, + num_ingredients); + if (found) + break; + } + } + if (!found) { + any_actors_missed = true; + break; + } + } + if (any_actors_missed) + break; + } + + if (any_actors_missed) + continue; + } + + if (num_tags > 0) { + bool any_tags_missed = false; + + for (int tag_idx = 0; tag_idx < num_tags; tag_idx++) { + if (tags_iter.tryGetIterByIndex(&hash_iter, tag_idx)) { + const s32 num_hashes = hash_iter.getSize(); + bool found = false; + for (int hash_idx = 0; hash_idx < num_hashes; hash_idx++) { + u32 hash_val; + if (hash_iter.tryGetUIntByIndex(&hash_val, hash_idx)) { + // Any tag in this list will work. + found = findIngredientByTag(ingredients, hash_val, + num_ingredients); + if (found) + break; + } + } + if (!found) { + any_tags_missed = true; + break; + } + } + if (any_tags_missed) + break; + } + + if (any_tags_missed) + continue; + } + + al::ByamlIter actor_iter; + if (!actor_info_data->getActorIter(&actor_iter, uint_val)) + continue; + + actor_iter.tryGetStringByKey(&string_val, "name"); + cook_item.actor_name = string_val; + + cookCalcIngredientsBoost(ingredients, cook_item); + + if (isCookFailure(cook_item)) { + goto COOK_FAILURE; + } + + cookCalcCritBoost(ingredients, cook_item, &boost_arg); + cookCalcSpiceBoost(ingredients, cook_item); + + cookCalcRecipeBoost(recipe_iter, cook_item); + + cookAdjustItem(cook_item); + + cookCalcItemPrice(ingredients, cook_item); + return true; + } + } + } + + single_ingredient = nullptr; + for (int ingredient_idx = 0; ingredient_idx < num_ingredients; ingredient_idx++) { + const auto& ingredient = ingredients[ingredient_idx]; + if (actor_info_data->hasTag(ingredient.actor_data, ksys::act::tags::CookSpice)) + continue; + + if (single_ingredient) { + multiple_non_spice_ingredients = true; + break; + } + + single_ingredient = &ingredient; + } + } else { + single_ingredient = &ingredients[0]; + } + + if (single_ingredient && !multiple_non_spice_ingredients) { + if (mConfig->tryGetIterByKey(&recipes_iter, "SingleRecipes")) { + const s32 num_recipes = recipes_iter.getSize(); + const char* string_val = nullptr; + s32 int_val; + u32 uint_val = 0; + al::ByamlIter recipe_iter; + al::ByamlIter actors_iter; + al::ByamlIter tags_iter; + + if (num_recipes > 0) { + for (int recipe_idx = 0; recipe_idx < num_recipes; recipe_idx++) { + if (!recipes_iter.tryGetIterByIndex(&recipe_iter, recipe_idx)) + continue; + + recipe_iter.tryGetStringByKey(&string_val, "Result"); + recipe_iter.tryGetUIntByKey(&uint_val, "Recipe"); + + if (!recipe_iter.tryGetIntByKey(&int_val, "Num") || num_ingredients < int_val) + continue; + + const s32 num_actors = recipe_iter.tryGetIterByKey(&actors_iter, "Actors") ? + actors_iter.getSize() : + 0; + const s32 num_tags = + recipe_iter.tryGetIterByKey(&tags_iter, "Tags") ? tags_iter.getSize() : 0; + + if (num_actors + num_tags > num_ingredients || + (num_actors == 0 && num_tags == 0)) + continue; + + if (num_actors > 0) { + bool found = false; + for (int hash_idx = 0; hash_idx < num_actors; hash_idx++) { + u32 hash_val; + if (actors_iter.tryGetUIntByIndex(&hash_val, hash_idx) && + single_ingredient->name_hash == hash_val) { + found = true; + break; + } + } + if (!found) + continue; + } + + if (num_tags > 0) { + bool found = false; + for (int hash_idx = 0; hash_idx < num_tags; hash_idx++) { + u32 hash_val; + if (tags_iter.tryGetUIntByIndex(&hash_val, hash_idx) && + actor_info_data->hasTag(single_ingredient->actor_data, hash_val)) { + found = true; + break; + } + } + if (!found) + continue; + } + + al::ByamlIter actorIter; + if (!actor_info_data->getActorIter(&actorIter, uint_val)) + continue; + + actorIter.tryGetStringByKey(&string_val, "name"); + cook_item.actor_name = string_val; + + cookCalcIngredientsBoost(ingredients, cook_item); + + if (isCookFailure(cook_item)) { + goto COOK_FAILURE; + } + + cookCalcCritBoost(ingredients, cook_item, &boost_arg); + cookCalcSpiceBoost(ingredients, cook_item); + + cookCalcRecipeBoost(recipe_iter, cook_item); + + cookAdjustItem(cook_item); + + cookCalcItemPrice(ingredients, cook_item); + return true; + } + } + } + } + + cook_item.actor_name = mFailActorName; + cookCalcIngredientsBoost(ingredients, cook_item); + +COOK_FAILURE: + cookFail(cook_item); + return true; +} + +void CookingMgr::cookCalcRecipeBoost(const al::ByamlIter& recipe_iter, CookItem& cook_item) const { + int int_val; + + if (recipe_iter.tryGetIntByKey(&int_val, "HB")) + cook_item.life_recover += (f32)int_val; + + if (recipe_iter.tryGetIntByKey(&int_val, "TB")) { + if (cook_item.effect_time > 0) { + cook_item.effect_time += int_val; + } + } +} + +void CookingMgr::cookAdjustItem(CookItem& cook_item) const { + cook_item.life_recover = (f32)(s32)cook_item.life_recover; + + const f32 life_recover_max = (f32)getCookingEffectEntry(CookEffectId::LifeRecover).max; + if (cook_item.life_recover > life_recover_max) + cook_item.life_recover = life_recover_max; + if (cook_item.life_recover < 0.0f) + cook_item.life_recover = 0.0f; + + if (cook_item.effect_id == CookEffectId::None) { + if (cook_item.life_recover == 0.0f) + cook_item.life_recover = 1.0f; + } else { + if (cook_item.vitality_boost > 0.0f && cook_item.vitality_boost < 1.0f) + cook_item.vitality_boost = 1.0f; + + s32 vitality_boost = (s32)cook_item.vitality_boost; + + const s32 vitality_boost_max = getCookingEffectEntry(cook_item.effect_id).max; + if (vitality_boost > vitality_boost_max) + vitality_boost = vitality_boost_max; + + if (cook_item.effect_id == CookEffectId::GutsRecover) + vitality_boost = vitality_boost * 200; + + f32 vitality_boost_f = cook_item.vitality_boost = (f32)vitality_boost; + + if (cook_item.effect_id == CookEffectId::LifeMaxUp) { + if ((s32)vitality_boost_f % 4 != 0) { + // Round up to whole heart. + vitality_boost_f = (f32)(((s32)vitality_boost_f + 4) & ~3u); + cook_item.vitality_boost = vitality_boost_f; + } + + if (vitality_boost_f < 4.0f) { + vitality_boost_f = 4.0f; + cook_item.vitality_boost = 4.0f; + } + + cook_item.life_recover = vitality_boost_f; + } + } + + cook_item.effect_time = sead::Mathi::clamp(cook_item.effect_time, 0, 1800); +} + +void CookingMgr::resetArgCookData(CookArg& arg, + const sead::Buffer<sead::FixedSafeString<64>>& ingredient_names, + int num_ingredients, CookItem& cook_item) const { + for (int i = 0; i < NumIngredientsMax; i++) { + arg.ingredients[i].name = ""; + arg.ingredients[i].count = 0; + } + + arg.ingredients[0].count = 1; + arg.ingredients[0].name = ingredient_names[0]; + + for (int i = 1; i < num_ingredients; i++) { + for (int j = 0; j < NumIngredientsMax; j++) { + auto& ingredient = arg.ingredients[j]; + if (ingredient.name == ingredient_names[i]) { + ingredient.count++; + break; + } + if (ingredient.name.isEmpty()) { + ingredient.count = 1; + ingredient.name = ingredient_names[i]; + break; + } + } + } + + cook_item.reset(); + + for (int i = 0; i < NumIngredientsMax && i < num_ingredients; i++) { + cook_item.ingredients[i] = ingredient_names[i]; + } +} + +void CookingMgr::prepareCookArg( + CookArg& arg, const sead::SafeArray<sead::FixedSafeString<64>, NumIngredientsMax>& item_names, + int num_items, CookItem& cook_item) const { + for (int i = 0; i < NumIngredientsMax; i++) { + arg.ingredients[i].name = ""; + arg.ingredients[i].count = 0; + } + + arg.ingredients[0].count = 1; + arg.ingredients[0].name = item_names[0]; + + for (int i = 1; i < num_items; i++) { + const auto& item_name = item_names[i]; + for (int j = 0; j < NumIngredientsMax; j++) { + auto& ingredient = arg.ingredients[j]; + if (ingredient.name == item_name) { + ingredient.count++; + break; + } + if (ingredient.name.isEmpty()) { + ingredient.count = 1; + ingredient.name = item_name; + break; + } + } + } + + cook_item.reset(); + + for (int i = 0; i < NumIngredientsMax && i < num_items; i++) { + cook_item.ingredients[i] = item_names[i]; + } +} + +bool CookingMgr::cookWithItems(const sead::SafeString& item1, const sead::SafeString& item2, + const sead::SafeString& item3, const sead::SafeString& item4, + const sead::SafeString& item5, CookItem& cook_item, + const CookingMgr::BoostArg& boost_arg) { + CookArg arg; + sead::SafeArray<sead::FixedSafeString<64>, NumIngredientsMax> item_names; + + int num_items = 0; + + if (!item1.isEmpty()) { + item_names[num_items].copy(item1); + num_items++; + } + + if (!item2.isEmpty()) { + item_names[num_items].copy(item2); + num_items++; + } + + if (!item3.isEmpty()) { + item_names[num_items].copy(item3); + num_items++; + } + + if (!item4.isEmpty()) { + item_names[num_items].copy(item4); + num_items++; + } + + if (!item5.isEmpty()) { + item_names[num_items].copy(item5); + num_items++; + } + + if (num_items > 0) { + prepareCookArg(arg, item_names, num_items, cook_item); + if (cook(arg, cook_item, boost_arg)) + return true; + } + return false; +} + +void CookingMgr::setCookItem(const CookItem& from) { + from.copy(mCookItem); +} + +void CookingMgr::resetCookItem() { + mCookItem.reset(); +} + +void CookingMgr::getCookItem(CookItem& to) const { + mCookItem.copy(to); +} + +} // namespace uking |
