// worldTimeMgr.cpp
mIsTimeFlowingNormally = false;This is at 0x14f, or byte 35 of
firstItem.mIngredients.mWork[1].elem.mBuffer. Since the longest item actor
name is Get_TwnObj_DLC_MemorialPicture_A_01, which is 35 characters long, this
is guaranteed to be 00 already. This is a no-op.
float &time = mTime;
gdm->getF32(mTimeFlag, &time);
// gdtManager.h
GDT_GET_(getF32, f32)
#define GDT_GET(NAME, T) \
bool NAME( \
FlagHandle handle, T *value, bool debug = false, \
bool ignore_trigger_param_result = false \
) { return unwrapHandle<false>(handle, debug, [&]( \
u32 idx, TriggerParamRef &ref \
) { \
const bool result = ref.get().NAME(value, idx); \
return ignore_trigger_param_result || result; \
}); } \
\
// ...
template <bool Write, typename Fn>
KSYS_ALWAYS_INLINE bool unwrapHandle(
FlagHandle handle, bool debug, const Fn &fn
) { return debug ? unwrapHandle<Write, true>(
handle, fn
) : unwrapHandle<Write, false>(handle, fn); }
template <bool Write, bool BypassPerm, typename Fn>
KSYS_ALWAYS_INLINE bool unwrapHandle(FlagHandle handle, const Fn &fn) {
const u32 idx = std::to_underlying(handle);
auto &ref = BypassPerm ? getParamBypassPerm() : getParam();
const auto check = [&] { return !Write || !ref.shouldChangeOnlyOnce(); };
if (mBitFlags.isOff(BitFlag::_8000) && handle != InvalidHandle)
return check() && fn(idx, ref);
return idx >> 24 == mCurrentFlagHandlePrefix && check() && fn(
idx & 0xffffff, ref
);
}
TriggerParamRef &getParam() { return mParam; }
TriggerParamRef mParam{&mFlagBuffer1, &mFlagBuffer, true, false, false};
TriggerParamRef(
TriggerParam **param_1, TriggerParam **param, bool check_permissions,
bool propagate_param_1_changes, bool change_only_once
) : mParam1(param_1), mParam(param), mCheckPermissions(check_permissions),
mPropagateParam1Changes(propagate_param_1_changes),
mChangeOnlyOnce(change_only_once) {}
TriggerParam **mParam1;
TriggerParam **mParam;
bool mCheckPermissions;
bool mPropagateParam1Changes;
bool mChangeOnlyOnce;
Proxy get() const { return Proxy(*this, false); }
Proxy(
const TriggerParamRef &ref, bool param1
) : mUseParam1(param1), mRef(ref) {}
bool mUseParam1;
const TriggerParamRef &mRef;
PROXY_GET_SET_IMPL_(getF32, setF32, f32)
#define PROXY_GET_SET_IMPL_(GET_NAME, SET_NAME, TYPE) \
bool GET_NAME(TYPE *value, s32 index) const { \
return getBuffer()->GET_NAME(value, index, mRef.mCheckPermissions); \
} \
\
// ...
TriggerParam *getBuffer() const {
return mUseParam1 ? *mRef.mParam1 : *mRef.mParam;
}
// gdtTriggerParam.cpp
bool TriggerParam::getF32(f32 *value, s32 index, bool check_permissions) const {
return getFlagValue(mF32Flags, value, index, check_permissions);
}
template <typename T, typename FlagValueType = T>
inline bool getFlagValue(
const sead::PtrArray<FlagBase> &array, T *out_value, s32 index,
bool check_permissions
) {
static_assert(isValidFlagValueType<FlagValueType>());
const auto *flag = getFlagByIndex<FlagValueType>(array, index);
if (!flag) return false;
if (check_permissions && !flag->isProgramReadable()) return false;
if constexpr (std::is_same<T, const char *>())
*out_value = flag->getValueRef().cstr();
else *out_value = flag->getValue();
return true;
}
template <typename T>
inline Flag<T> *getFlagByIndex(
const sead::PtrArray<FlagBase> &flags, s32 index
) { return static_cast<Flag<T> *>(getFlagByIndexBase(flags, index)); }
inline FlagBase *getFlagByIndexBase(
const sead::PtrArray<FlagBase> &flags, s32 index
) { return index < 0 || index >= flags.size() ? nullptr : flags[index]; }
// seadPtrArray.h
T *operator[](s32 pos) const { return at(pos); }
T *at(s32 pos) const { return static_cast<T *>(PtrArrayImpl::at(pos)); }
void *at(s32 idx) const { return static_cast<u32>(mPtrNum) <= static_cast<u32>(
idx
) ? nullptr : mPtrs[idx]; }
s32 mPtrNum = 0;
void **mPtrs = nullptr;
// gdtTriggerParam.cpp
void TriggerParam::copyFromGameDataRes(res::GameData *gdata, sead::Heap *heap) {
if (!gdata) return;
copyGameDataResBuffer(gdata->getBoolFlags(), mBoolFlags, heap);
copyGameDataResBuffer(gdata->getS32Flags(), mS32Flags, heap);
copyGameDataResBuffer(gdata->getF32Flags(), mF32Flags, heap);
copyGameDataResBuffer(gdata->getStringFlags(), mStringFlags, heap);
copyGameDataResBuffer(gdata->getString64Flags(), mString64Flags, heap);
copyGameDataResBuffer(gdata->getString256Flags(), mString256Flags, heap);
copyGameDataResBuffer(gdata->getVector2fFlags(), mVector2fFlags, heap);
copyGameDataResBuffer(gdata->getVector3fFlags(), mVector3fFlags, heap);
copyGameDataResBuffer(gdata->getVector4fFlags(), mVector4fFlags, heap);
copyGameDataResBuffer(gdata->getBoolArrayFlags(), mBoolArrayFlags, heap);
copyGameDataResBuffer(gdata->getS32ArrayFlags(), mS32ArrayFlags, heap);
copyGameDataResBuffer(gdata->getF32ArrayFlags(), mF32ArrayFlags, heap);
copyGameDataResBuffer(gdata->getStringArrayFlags(), mStringArrayFlags, heap);
copyGameDataResBuffer(
gdata->getString64ArrayFlags(), mString64ArrayFlags, heap
);
copyGameDataResBuffer(
gdata->getString256ArrayFlags(), mString256ArrayFlags, heap
);
copyGameDataResBuffer(
gdata->getVector2fArrayFlags(), mVector2fArrayFlags, heap
);
copyGameDataResBuffer(
gdata->getVector3fArrayFlags(), mVector3fArrayFlags, heap
);
copyGameDataResBuffer(
gdata->getVector4fArrayFlags(), mVector4fArrayFlags, heap
);
mResourceFlags = gdata->getField14();
}
// resResourceGameData.cpp
sead::Buffer<gdt::FlagF32> &getF32Flags() { return mF32Flags; }
sead::Buffer<gdt::FlagF32> mF32Flags;
void GameData::doCreate_(
u8 *data, [[maybe_unused]] u32 unused1, [[maybe_unused]] sead::Heap *unused2
) {
auto *heap = gdt::Manager::instance()->getSaveAreaHeap();
const al::ByamlIter root_iter{data};
const char *key = nullptr;
root_iter.getKeyName(&key, 0);
const sead::SafeString data_type = key;
// ...
loadFlags(
data_type, root_iter, heap, mF32Flags, "f32_data", gdt::FlagType::F32, [](
gdt::FlagF32::ConfigType *config, const al::ByamlIter &iter
) {
f32 init_value{};
f32 min_value{};
f32 max_value{};
tryGetFloatOrInt(iter, "InitValue", &init_value);
tryGetFloatOrInt(iter, "MinValue", &min_value);
tryGetFloatOrInt(iter, "MaxValue", &max_value);
config->initial_value = init_value;
config->min_value = min_value;
config->max_value = max_value;
}
);
// ...
}idx == handle, which is mTimeFlag, which is at 0xd4.
mCurrentFlagHandlePrefix == 0x00, so this code is checking if the most
significant byte of idx is 00. Only if it is will it call fn; otherwise,
this is a no-op. 0xd4 in newItem.mName is
firstItem.mIngredients.mWork[0].elem.mBuffer[0x8:0xc], which has a 00 as its
most significant byte only if firstItem is not a meal or its first ingredient
has an actor name shorter than 12 (0xc) bytes long. As mTimeFlag is a handle
for an f32 flag, and there are 46 (0x2e) f32 flags, the 3 most significant
bytes of the handle must all be 00 in order for this to refer to a valid flag;
otherwise, this is a no-op. Hence, the actor name must also be shorter than 10
(0xa) bytes long. All such items are listed below, along with which flag they
correspond to.
| Actor name | Handle | Flag | Success? |
|---|---|---|---|
BeeHome (Courser Bee Honey) |
0x00000000 |
dummy_float |
❌ |
IceArrow (Ice Arrow) |
0x00000000 |
dummy_float |
❌ |
FireArrow (Fire Arrow) |
0x00000077 |
❌ |
As no items with actor names shorter than 10 bytes long refer to valid and readable flags, this line is a no-op.
// worldTimeMgr.cpp
gdm->getS32(mNumberOfDaysFlag, &mNumberOfDays);
gdm->getS32(mTimeDivisionFlag, &mTimeDivision);
gdm->getBool(mIsDaytimeFlag, &is_daytime_flag_set);
gdm->getF32(mBloodyMoonTimerFlag, &mBloodMoonTimer);
if (mBloodyEndReserveTimerFlag != gdt::InvalidHandle)
gdm->getS32(mBloodyEndReserveTimerFlag, &mBloodyEndReserveTimer);
if (!mPlayedDemo103Or997) {
mTime = DefaultTime;
gdm->getBool(mIsPlayedDemo103Flag, &mPlayedDemo103Or997, true);
if (!mPlayedDemo103Or997)
gdm->getBool(mDemo997Flag, &mPlayedDemo103Or997, true);
}
if (!mFindDungeonActivated)
gdm->getBool(mFindDungeonActivatedFlag, &mFindDungeonActivated, true);Similarly, these lines write a flag of a particular type at an index to a variable. The following table describes all flag reads performed here, in order.
| Prefix | Type | Handle/Offset | PouchItem Handle |
Member/Offset | PouchItem Flag |
|---|---|---|---|---|---|
?? |
f32 |
mTimeFlag (0xd4) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x8:0xc] |
||
?? |
s32 |
mNumberOfDaysFlag (0xd8) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0xc:0x10]Table 1 |
mNumberOfDays (0x13c) |
firstItem.mIngredients.mWork[1].elem.mBuffer[0x10:0x14] |
?? |
s32 |
mTimeDivisionFlag (0xdc) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x10:0x14]Table 2 |
mTimeDivision (0x28) |
Least significant dword of firstItem.mName.mStringTop |
?? |
bool |
mIsDaytimeFlag (0xe0) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x14:0x18]Table 3 |
||
?? |
f32 |
mBloodyMoonTimerFlag (0x120) |
Least significant dword of firstItem.mIngredients.mWork[1].elem.mStringTop |
mBloodMoonTimer (0xc8) |
firstItem.mIngredients.mWork[0].elem.mBufferSize |
00 |
s32 |
mBloodyEndReserveTimerFlag (0x124) |
Most significant dword of firstItem.mIngredients.mWork[1].elem.mStringTop (0x00-0x7f) |
mBloodyEndReserveTimer (0x144) |
firstItem.mIngredients.mWork[1].elem.mBuffer[0x18:0x1c] |
?? |
bool |
mIsPlayedDemo103Flag (0xe8) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x1c:0x20] |
||
00 |
bool |
mDemo997Flag (0xec) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x20:0x24] |
||
00 |
bool |
mFindDungeonActivatedFlag (0xf0) |
firstItem.mIngredients.mWork[0].elem.mBuffer[0x24:0x28] |
Click to expand
| Actor name | Handle | Flag |
|---|---|---|
ElectricArrow (Shock Arrow) |
0x00000077 |
EquipTime_Armor_004_Head1 |
Item_Enemy_00 (Bokoblin Horn) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Enemy_01 (Bokoblin Fang) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Enemy_02 (Bokoblin Guts) |
0x00000032 |
RinelSeries_Counter2 |
Item_Enemy_03 (Lizalfos Horn) |
0x00000033 |
Location_RemainsWind3 |
Item_Enemy_04 (Lizalfos Talon) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_05 (Lizalfos Tail) |
0x00000035 |
Location_RemainsFire5 |
Item_Enemy_06 (Moblin Horn) |
0x00000036 |
Location_RemainsElectric6 |
Item_Enemy_07 (Moblin Fang) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Enemy_08 (Moblin Guts) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Enemy_12 (Lynel Horn) |
0x00000032 |
RinelSeries_Counter2 |
Item_Enemy_13 (Lynel Hoof) |
0x00000033 |
Location_RemainsWind3 |
Item_Enemy_14 (Lynel Guts) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_15 (Red Chuchu Jelly) |
0x00000035 |
Location_RemainsFire5 |
Item_Enemy_16 (Yellow Chuchu Jelly) |
0x00000036 |
Location_RemainsElectric6 |
Item_Enemy_17 (White Chuchu Jelly) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Enemy_18 (Keese Wing) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Enemy_19 (Keese Eyeball) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Enemy_20 (Octorok Tentacle) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Enemy_21 (Octorok Eyeball) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Enemy_24 (Molduga Fin) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_25 (Modluga Guts) |
0x00000035 |
Location_RemainsFire5 |
Item_Enemy_26 (Ancient Gear) |
0x00000036 |
Location_RemainsElectric6 |
Item_Enemy_27 (Ancient Screw) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Enemy_28 (Ancient Spring) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Enemy_29 (Ancient Shaft) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Enemy_30 (Ancient Core) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Enemy_31 (Giant Ancient Core) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Enemy_32 (Hinox Toenail) |
0x00000032 |
RinelSeries_Counter2 |
Item_Enemy_33 (Hinox Tooth) |
0x00000033 |
Location_RemainsWind3 |
Item_Enemy_34 (Hinox Guts) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_38 (Dinraal's Scale) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Enemy_39 (Dinraal's Claw) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Enemy_40 (Chuchu Jelly) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Enemy_41 (Red Lizalfos Tail) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Enemy_42 (Icy Lizalfos Tail) |
0x00000032 |
RinelSeries_Counter2 |
Item_Enemy_43 (Yellow Lizalfos Tail) |
0x00000033 |
Location_RemainsWind3 |
Item_Enemy_44 (Fire Keese Wing) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_45 (Electric Keese Wing) |
0x00000035 |
Location_RemainsFire5 |
Item_Enemy_46 (Ice Keese Wing) |
0x00000036 |
Location_RemainsElectric6 |
Item_Enemy_47 (Shard of Dinraal's Fang) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Enemy_48 (Shard of Dinraal's Horn) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Enemy_49 (Naydra's Scale) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Enemy_50 (Naydra's Claw) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Enemy_51 (Shard of Naydra's Fang) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Enemy_52 (Shard of Naydra's Horn) |
0x00000032 |
RinelSeries_Counter2 |
Item_Enemy_53 (Farosh's Scale) |
0x00000033 |
Location_RemainsWind3 |
Item_Enemy_54 (Farosh's Claw) |
0x00000034 |
Location_RemainsWater4 |
Item_Enemy_55 (Shard of Farosh's Fang) |
0x00000035 |
Location_RemainsFire5 |
Item_Enemy_56 (Shard of Farosh's Horn) |
0x00000036 |
Location_RemainsElectric6 |
Item_Enemy_57 (Octo Balloon) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Roast_01 (Seared Steak) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Roast_02 (Roasted Bird Drumstick) |
0x00000032 |
RinelSeries_Counter2 |
Item_Roast_03 (Baked Apple) |
0x00000033 |
Location_RemainsWind3 |
Item_Roast_04 (Toasty Stamella Shroom) |
0x00000034 |
Location_RemainsWater4 |
Item_Roast_05 (Toasted Hearty Truffle) |
0x00000035 |
Location_RemainsFire5 |
Item_Roast_06 (Toasty Hylian Shroom) |
0x00000036 |
Location_RemainsElectric6 |
Item_Roast_07 (Roasted Wildberry) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Roast_08 (Roasted Voltfruit) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Roast_09 (Roasted Hearty Durian) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Roast_10 (Baked Palm Fruit) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Roast_11 (Roasted Mighty Bananas) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Roast_12 (Roasted Hydromelon) |
0x00000032 |
RinelSeries_Counter2 |
Item_Roast_13 (Charred Pepper) |
0x00000033 |
Location_RemainsWind3 |
Item_Roast_15 (Baked Foritifed Pumpkin) |
0x00000035 |
Location_RemainsFire5 |
Item_Roast_16 (Roasted Lotus Seeds) |
0x00000036 |
Location_RemainsElectric6 |
Item_Roast_18 (Roasted Radish) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Roast_19 (Roasted Big Radish) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Roast_24 (Roasted Swift Carrot) |
0x00000034 |
Location_RemainsWater4 |
Item_Roast_27 (Roasted Mighty Thistle) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Roast_28 (Roasted Armoranth) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Roast_31 (Toasty Chillshroom) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Roast_32 (Toasty Sunshroom) |
0x00000032 |
RinelSeries_Counter2 |
Item_Roast_33 (Toasty Zapshroom) |
0x00000033 |
Location_RemainsWind3 |
Item_Roast_36 (Toasty Rushroom) |
0x00000036 |
Location_RemainsElectric6 |
Item_Roast_37 (Toasty Razorshroom) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_Roast_38 (Toasty Ironshroom) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Roast_39 (Toasty Silent Shroom) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Roast_40 (Seared Prime Steak) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Roast_41 (Roasted Bird Thigh) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Roast_45 (Seared Gourmet Steak) |
0x00000035 |
Location_RemainsFire5 |
Item_Roast_46 (Roasted Whole Bird) |
0x00000036 |
Location_RemainsElectric6 |
Item_Roast_48 (Roasted Acorn) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Item_Roast_49 (Toasted Big Hearty Truffle) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_Roast_50 (Roasted Endura Carrot) |
0x00000030 |
LizarfosSeries_Counter2 |
Item_Roast_51 (Campfire Egg) |
0x00000031 |
TartnackSeries_Counter2 |
Item_Roast_52 (Roasted Tree Nut) |
0x00000032 |
RinelSeries_Counter2 |
Item_Roast_53 (Toasty Endura Shroom) |
0x00000033 |
Location_RemainsWind3 |
Obj_KorokNuts (Korok Seed) |
0x00000073 |
EquipTime_Armor_001_Head7 |
Obj_ProofBook (Classified Envelope) |
0x0000006b |
Item_DefenceRate2 |
Click to expand
| Actor name | Handle | Flag |
|---|---|---|
Item_RoastFish_01 (Roasted Bass) |
0x00000031 |
TartnackSeries_Counter2 |
Item_RoastFish_02 (Roasted Hearty Bass) |
0x00000032 |
RinelSeries_Counter2 |
Item_RoastFish_03 (Roasted Trout) |
0x00000033 |
Location_RemainsWind3 |
Item_RoastFish_04 (Roasted Hearty Salmon) |
0x00000034 |
Location_RemainsWater4 |
Item_RoastFish_07 (Roasted Carp) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Item_RoastFish_09 (Roasted Porgy) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Item_RoastFish_11 (Blueshell Escargot) |
0x00000031 |
TartnackSeries_Counter2 |
Item_RoastFish_13 (Sneaky River Escargot) |
0x00000033 |
Location_RemainsWind3 |
Item_RoastFish_15 (Blackened Crab) |
0x00000035 |
Location_RemainsFire5 |
Obj_HeroSoul_Rito (Revali's Gale) |
0x0000006f |
Item_ElectricLevelAdd2 |
Obj_HeroSoul_Zora (Mipha's Grace) |
0x00000061 |
Counter_TerminalElectric8 |
Weapon_Lsword_001 (Traveler's Claymore) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Lsword_002 (Soldier's Claymore) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Lsword_003 (Knight's Claymore) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Lsword_004 (Boko Bat) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_005 (Spiked Boko Bat) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Lsword_006 (Dragonbone Boko Bat) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Lsword_010 (Moblin Club) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Lsword_011 (Spiked Moblin Club) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Lsword_012 (Dragonbone Moblin Club) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Lsword_013 (Ancient Battle Axe) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Lsword_014 (Ancient Battle Axe+) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_015 (Ancient Battle Axe++) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Lsword_016 (Lynel Crusher) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Lsword_017 (Mighty Lynel Crusher) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Lsword_018 (Savage Lynel Crusher) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Weapon_Lsword_019 (Moblin Arm) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Weapon_Lsword_020 (Rusty Claymore) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Lsword_023 (Ancient Bladesaw) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Lsword_024 (Royal Claymore) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_027 (Silver Longsword) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Lsword_029 (Golden Claymore) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Weapon_Lsword_030 (Double Axe) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Lsword_031 (Iron Sledgehammer) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Lsword_032 (Woodcutter's Axe) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Lsword_033 (Great Flameblade) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Lsword_034 (Great Frostblade) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_035 (Great Thunderblade) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Lsword_036 (Cobble Crusher) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Lsword_037 (Stone Smasher) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Lsword_038 (Boat Oar) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Weapon_Lsword_041 (Eightfold Longblade) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Lsword_045 (Farming Hoe) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Lsword_047 (Royal Guard's Claymore) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Lsword_051 (Giant Boomerang) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Lsword_054 (Boulder Breaker) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_055 (Edge of Duality) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Lsword_056 (Korok Leaf) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Lsword_057 (Sword of the Six Sages) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Lsword_059 (Biggoron's Sword) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Weapon_Lsword_060 (Fierce Deity Sword) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Lsword_074 (Windcleaver) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Lsword_097 (Biggoron's Sword) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Shield_001 (Wooden Shield) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Shield_002 (Soldier's Shield) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Shield_003 (Knight's Shield) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Shield_004 (Boko Shield) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Shield_005 (Spiked Boko Shield) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Shield_006 (Dragonbone Boko Shield) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Shield_007 (Lizal Shield) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Shield_008 (Reinforced Lizal Shield) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Weapon_Shield_009 (Steel Lizal Shield) |
0x00000039 |
RemainsWind_SmallKeyNum2 |
Weapon_Shield_013 (Guardian Shield) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Shield_014 (Guardian Shield+) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Shield_015 (Guardian Shield++) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Shield_016 (Lynel Shield) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Shield_017 (Mighty Lynel Shield) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Shield_018 (Savage Lynel Shield) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Weapon_Shield_021 (Rusty Shield) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Shield_022 (Royal Shield) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Shield_023 (Forest Dweller's Shield) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Shield_025 (Silver Shield) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Shield_026 (Gerudo Shield) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Shield_030 (Hylian Shield) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Shield_031 (Hunter's Shield) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Shield_032 (Fisherman's Shield) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Shield_033 (Royal Guard's Shield) |
0x00000033 |
Location_RemainsWind3 |
Weapon_Shield_034 (Emblazoned Shield) |
0x00000034 |
Location_RemainsWater4 |
Weapon_Shield_035 (Traveler's Shield) |
0x00000035 |
Location_RemainsFire5 |
Weapon_Shield_036 (Radiant Shield) |
0x00000036 |
Location_RemainsElectric6 |
Weapon_Shield_037 (Daybreaker) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Weapon_Shield_038 (Ancient Shield) |
0x00000038 |
RemainsFire_SmallKeyNum2 |
Weapon_Shield_040 (Pot Lid) |
0x00000030 |
LizarfosSeries_Counter2 |
Weapon_Shield_041 (Shield of the Mind's Eye) |
0x00000031 |
TartnackSeries_Counter2 |
Weapon_Shield_042 (Kite Shield) |
0x00000032 |
RinelSeries_Counter2 |
Weapon_Shield_057 (Hero's Shield) |
0x00000037 |
RemainsWater_SmallKeyNum2 |
Click to expand
| Actor name | Handle | Flag |
|---|---|---|
Obj_DLC_HeroSeal_Rito (Medoh's Emblem) |
0x0000006f |
Clear_Dungeon0599 |
Obj_DLC_HeroSeal_Zora (Ruta's Emblem) |
0x00000061 |
Clear_Dungeon04510 |
Obj_DLC_HeroSoul_Rito (Revali's Gale +) |
0x0000006f |
Clear_Dungeon0599 |
Obj_DLC_HeroSoul_Zora (Mipha's Grace +) |
0x00000061 |
Clear_Dungeon04510 |
Some of these flag reads are conditional, and their conditions include member
variables. These members corresponding to the following members of PouchItem:
| Member/Offset | PouchItem Member |
Result |
|---|---|---|
mBloodyEndReserveTimerFlag (0x124) |
Most significant dword of firstItem.mIngredients.mWork[1].elem.mStringTop |
true |
mPlayedDemo103Or997 (0x14d) |
firstItem.mIngredients.mWork[1].elem.mBuffer[0x21] |
true if a true flag hasn't been written here yet and firstItem's 2nd ingredient, if any, is not the Picture of the Champions, otherwise false |
mFindDungeonActivated (0x14e) |
firstItem.mIngredients.mWork[1].elem.mBuffer[0x22] |
Whether firstItem's 2nd ingredient, if any, is not the Picture of the Champions |
if (mNewTime >= 0.0 && (
mTimeUpdateMode == TimeUpdateMode::Forced
|| mTimeUpdateMode == TimeUpdateMode::Normal
)) {
mTime = mNewTime;
mNewTime = -1.0;
wm->getSkyMgr()->onTimeUpdate();
}mNewTimeis at offset0xc0, which corresponds to the least significant dword offirstItem.mIngredients.mWork[0].elem.mStringTop. Since>= 0.0is a sign check, both0.0and-0.0are equal to0.0, and the sign is the most significant bit of a float, this is essentially a<= 0x80000000check. Due to ASLR, this is unpredictable, but has a slightly higher chance of beingfalsedue to addresses0x0000000000000000-0x0000000007ffffffbeing outside the ASLR range.mTimeUpdateModeis at offset0x149, which corresponds tofirstItem.mIngredients.mWork[1].elem.mBuffer[0x1d]. Actor names will never contain the byte0xff(TimeUpdateMode::Forced), so the only option is for this to be0x00(TimeUpdateMode::Normal), meaning the name must be at most0x1d(29) bytes long for the condition to betrue. This is true for every item except the Picture of the Champions (Get_TwnObj_DLC_MemorialPicture_A_01), which has the longest actor name of any item at 35 bytes. The condition will also betrueiffirstItemis not a meal or only has 1 ingredient.
If the condition is true, the following will happen:
mNewTimeis read from offset0xc0, which corresponds to the least significant dword offirstItem.mIngredients.mWork[0].elem.mStringTop, and copied tomTime, which is at offset0xb8and corresponds to the least significant dword offirstItem.mIngredients.mWork[0].elem's vptr.mNewTime(least significant dword offirstItem.mIngredients.mWork[0].elem.mStringTop) is set to-1.0f, or0xbf800000.
if (mNeedToHandleNewDay) {
handleNewDay(HandleNewDayMode::Forced);
mNeedToHandleNewDay = false;
}mNeedToHandleNewDay is at offset 0x148, which corresponds to
firstItem.mIngredients.mWork[1].elem.mBuffer[0x1c]. Since a
bool being anything other than true or false is UB, checking the
executable shows that this specific check uses a cbz (Compare and Branch on
Zero) instruction. Similarly to the mTimeUpdateMode check, the name must be
longer than 0x1c (28) bytes for this byte to be nonzero, and, again, the only
item with an actor name this long is the Picture of the Champions, so this will
only be true if firstItem is a meal with at least 2 ingredients and its
second ingredient is the Picture of the Champions.
// void TimeMgr::handleNewDay(HandleNewDayMode mode)
++mNumberOfDays;mNumberOfDays is at offset 0x13c, which corresponds to
firstItem.mIngredients.mWork[1].elem.mBuffer[0x10:0x14], so mBuffer[0x10]
will be incremented, either changing the ingredient, invalidating it, or adding
a 01 byte to the end (also invalidating it).
if (mode == HandleNewDayMode::Normal && wm->getEnvMgr()->isBloodMoonNight()) {
// ...
}mode is HandleNewDayMode::Forced, so the condition is false.
if (gdm) {
gdm->getBool(mBloodyDayFlag, &mWasBloodyDay);
if (playerHasLeftPlateau() && mBloodMoonTimer > 7_days) {
mBloodMoonTimer = 0.0;
gdm->setBool(true, mBloodyDayFlag);
}
else mBloodyEndReserveTimer = 150;
}
// worldTimeMgr.cpp
static bool playerHasLeftPlateau() {
bool left_plateau = false;
gdt::Manager::instance()->getParamBypassPerm().get().getBool(
&left_plateau, "FirstTouchdown"
);
return left_plateau;
}
// worldTimeMgr.h
constexpr float operator""_days(unsigned long long days) {
return timeToFloat(static_cast<int>(24 * days), 0);
}
constexpr float timeToFloat(int h, int m) {
return float(h) * 15.0f + float(m) * (15.0f / 60.0f);
}mBloodyDayFlag is at offset 0x11c, which corresponds to the most significant
dword of firstItem.mIngredients.mWork[1].elem's vptr. This allows reading any
boolean flag in the range 0x00-0x7f into mWasBloodyDay, which is at offset
0x152 and corresponds to firstItem.mIngredients.mWork[1].elem.mBuffer[0x26].
Since no item actor names are longer than or equal to 0x26 (38) bytes, this is
effectively a no-op.
mBloodMoonTimer is at offset 0xc8, which corresponds to
firstItem.mIngredients.mWork[0].elem.mBufferSize. Since this is always 0x40,
interpreting it as an f32 gives 8.96831017167882925391e-44. This is not
greater than 2520.0f, so mBloodyEndReserveTimer (offset 0x144,
firstItem.mIngredients.mWork[1].elem.mBuffer[0x18:0x1c]) is set to 150
(0x96), invalidating the actor name if it was the Picture of the Champions.
// worldTimeMgr.cpp
NewDayEvent event;
if (mNewDaySignal.getNumSlots() > 0) mNewDaySignal.emit(event);
sead::DelegateEvent<const NewDayEvent &> mNewDaySignal;
// seadDelegateEventSlot.h
int getNumSlots() const { return mList.size(); }
SlotList mList;
using SlotList = TList<Slot *>;
// seadTList.h
template <typename T>
class TList : public ListImpl {
// ...
}
// seadListImpl.h
s32 size() const { return mCount; }
s32 mCount;
// seadDelegateEventSlot.h
void emit(T arg) {
for (auto &slot_node : mList.robustRange()) slot_node.mData->invoke_(arg);
}
// seadTList.h
RobustRange robustRange() const { return {*this}; }
struct RobustRange {
auto begin() const { return mList.robustBegin(); }
auto end() const { return mList.robustEnd(); }
const TList &mList;
}
robustIterator robustBegin() const {
return robustIterator(static_cast<TListNode<T> *>(mStartEnd.next()));
}
robustIterator robustEnd() const {
return robustIterator(static_cast<TListNode<T> *>(
const_cast<ListNode *>(&mStartEnd)
));
}
class robustIterator {
public:
explicit robustIterator(TListNode<T> *ptr) : mPtr(ptr) {
mPtrNext = static_cast<TListNode<T> *>(mPtr->next());
}
robustIterator &operator++() {
mPtr = mPtrNext;
mPtrNext = static_cast<TListNode<T> *>(mPtrNext->next());
return *this;
}
robustIterator operator++() {
robustIterator copy = *this;
mPtr = mPtrNext;
mPtrNext = static_cast<TListNode<T> *>(mPtrNext->next());
return copy;
}
TListNode<T> &operator*() const { return *mPtr; }
TListNode<T> *operator->() const { return mPtr; }
friend bool operator==(robustIterator it1, robustIterator it2) {
return it1.mPtr == it2.mPtr;
}
friend bool operator!=(robustIterator it1, robustIterator it2) {
return !(it1 == it2);
}
private:
TListNode<T> *mPtr;
TListNode<T> *mPtrNext;
};
// seadListImpl.h
ListNode mStartEnd;
ListNode *next() const { return mNext; }
ListNode *mNext = nullptr;
// seadTList.h
T mData;
// seadDelegateEventSlot.h
void invoke_(T arg) { if (mDelegatePtr) (*mDelegatePtr)(arg); }
IDelegate1<T> *mDelegatePtr = nullptr;
// seadDelegate.h
template <typename A1>
class IDelegate1 {
virtual void invoke(A1 a1) = 0;
// ...
void operator()(A1 a1) { return invoke(a1); }
};mNewDaySignal is at offset 0x80, which corresponds to
firstItem.mData.mSellPrice/firstItem.mData.mModifier,
firstItem.mData.mEffect, firstItem.mData.mEffectLevel, padding,
firstItem.mIngredients.mPtrNum, firstItem.mIngredients.mPtrNumMax, and
firstItem.mIngredients.mPtrs. mList is at offset 0x8 in
sead::DelegateEvent<T>, and mCount is at offset 0x10 in sead::ListImpl,
so getNumSlots() returns the s32 at offset 0x18 from mNewDaySignal,
which is the least significant dword of firstItem.mIngredients.mPtrs. This
will never be 0, so the only requirement is for it to be positive, or for the
sign bit to be 0. This is unpredictable due to ASLR. If the condition is
true, the following will happen:
- The
forloop begins iteration atmList.robustBegin(), which isrobustIterator(static_cast<TListNode<T> *>(mStartEnd.next())).mStartEndis at offset0x0insead::ListImpl, andmNextis at offset0x8insead::ListNode, so a pointer is read from offset0x10frommNewDaySignal, which ismPtrNumandmPtrNumMax.mPtrNumMaxwill always be5, and there is very little control overmPtrNum, being0-5, so this will not point to a valid pointer. It is immediately dereferenced with->inexplicit robustIterator(TListNode<T> *ptr), crashing the game.
Footnotes
-
Time you have worn the Hylian Hood ★★★ for, in seconds ↩
-
Unused, always 0 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27 ↩28 ↩29 ↩30 ↩31 ↩32 ↩33 ↩34 ↩35 ↩36 ↩37 ↩38 ↩39 ↩40 ↩41 ↩42 ↩43 ↩44 ↩45 ↩46 ↩47 ↩48 ↩49 ↩50 ↩51 ↩52 ↩53 ↩54 ↩55 ↩56 ↩57 ↩58 ↩59 ↩60 ↩61 ↩62 ↩63 ↩64 ↩65 ↩66 ↩67 ↩68 ↩69 ↩70 ↩71 ↩72 ↩73 ↩74 ↩75 ↩76 ↩77 ↩78 ↩79 ↩80 ↩81 ↩82 ↩83 ↩84 ↩85 ↩86 ↩87 ↩88 ↩89 ↩90 ↩91 ↩92 ↩93 ↩94 ↩95 ↩96 ↩97 ↩98 ↩99 ↩100 ↩101
-
Times you have visited Divine Beast Vah Medoh ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19
-
Times you have visited Divine Beast Vah Ruta ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18
-
Times you have visited Divine Beast Vah Rudania ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18
-
Times you have visited Divine Beast Vah Naboris ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17
-
Time you have worn the Hylian Hood for, in seconds ↩
-
Number of activated terminals in Divine Beast Vah Naboris ↩