Skip to content

Instantly share code, notes, and snippets.

@Makonede
Last active July 16, 2024 16:59
Show Gist options
  • Save Makonede/c367acc544fa13628432c5fb357d292a to your computer and use it in GitHub Desktop.
Save Makonede/c367acc544fa13628432c5fb357d292a to your computer and use it in GitHub Desktop.
Effects of calling `TimeMgr::calc_` as `BufferedSafeString::assureTerminationImpl_`
// 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]

Table 1

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

Table 2

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

Table 3

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();
}
  • mNewTime is at offset 0xc0, which corresponds to the least significant dword of firstItem.mIngredients.mWork[0].elem.mStringTop. Since >= 0.0 is a sign check, both 0.0 and -0.0 are equal to 0.0, and the sign is the most significant bit of a float, this is essentially a <= 0x80000000 check. Due to ASLR, this is unpredictable, but has a slightly higher chance of being false due to addresses 0x0000000000000000-0x0000000007ffffff being outside the ASLR range.
  • mTimeUpdateMode is at offset 0x149, which corresponds to firstItem.mIngredients.mWork[1].elem.mBuffer[0x1d]. Actor names will never contain the byte 0xff (TimeUpdateMode::Forced), so the only option is for this to be 0x00 (TimeUpdateMode::Normal), meaning the name must be at most 0x1d (29) bytes long for the condition to be true. 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 be true if firstItem is not a meal or only has 1 ingredient.

If the condition is true, the following will happen:

  • mNewTime is read from offset 0xc0, which corresponds to the least significant dword of firstItem.mIngredients.mWork[0].elem.mStringTop, and copied to mTime, which is at offset 0xb8 and corresponds to the least significant dword of firstItem.mIngredients.mWork[0].elem's vptr.
  • mNewTime (least significant dword of firstItem.mIngredients.mWork[0].elem.mStringTop) is set to -1.0f, or 0xbf800000.
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 for loop begins iteration at mList.robustBegin(), which is robustIterator(static_cast<TListNode<T> *>(mStartEnd.next())). mStartEnd is at offset 0x0 in sead::ListImpl, and mNext is at offset 0x8 in sead::ListNode, so a pointer is read from offset 0x10 from mNewDaySignal, which is mPtrNum and mPtrNumMax. mPtrNumMax will always be 5, and there is very little control over mPtrNum, being 0-5, so this will not point to a valid pointer. It is immediately dereferenced with -> in explicit robustIterator(TListNode<T> *ptr), crashing the game.

Footnotes

  1. Time you have worn the Hylian Hood ★★★ for, in seconds

  2. 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

  3. 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

  4. 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

  5. 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

  6. Times you have visited Divine Beast Vah Naboris 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  7. Time you have worn the Hylian Hood for, in seconds

  8. Number of activated terminals in Divine Beast Vah Naboris

  9. Whether Kah Okeo Shrine has been completed 2

  10. Whether Ha Dahamar Shrine has been completed 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment