diff --git a/Code/client/Games/Forms.cpp b/Code/client/Games/Forms.cpp index bc09df6a2..6995d0dad 100644 --- a/Code/client/Games/Forms.cpp +++ b/Code/client/Games/Forms.cpp @@ -48,6 +48,12 @@ void TESNPC::Deserialize(const String& acBuffer, uint32_t aChangeFlags) noexcept Load(&loadBuffer); loadBuffer.buffer = nullptr; + + // The spell list loader (TESSpellList::LoadGame) stores whatever the form lookup returns, so spells or + // shouts from a mod this client doesn't have (resolved to 0 by our ReadFormId hook) end up as null + // entries. Combat AI adds the base NPC's shouts to its inventory without a null check and crashes. + if (const uint32_t cRemoved = spellList.RemoveNullEntries()) + spdlog::warn("NPC {:X}: dropped {} spell/shout entries not found on this client (missing mod?)", formID, cRemoved); } void TESNPC::Initialize() noexcept diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 00a99684a..0d05d9bfc 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -44,6 +44,8 @@ #include #include #include +#include +#include #include #include @@ -701,25 +703,48 @@ void Actor::SetMagicEquipment(const MagicEquipment& acEquipment) noexcept auto* pEquipManager = EquipManager::Get(); auto& modSystem = World::Get().GetModSystem(); - if (acEquipment.LeftHandSpell) + // The owner may have magic from a mod this client doesn't have (e.g. a power a werewolf mod + // equips after transforming back). Such ids resolve to 0, and equipping a null form leaves the + // actor with an empty hand/voice entry that combat AI dereferences later (crash in + // Actor::VisitSpells while building the combat inventory). Skip anything that doesn't resolve. + const auto equipHandSpell = [&](const GameId& acSpellId, uint32_t aSlot, const char* apHand) { - uint32_t mainHandWeaponId = modSystem.GetGameId(acEquipment.LeftHandSpell); - spdlog::debug("Setting left hand spell: {:X}", mainHandWeaponId); - pEquipManager->EquipSpell(this, TESForm::GetById(mainHandWeaponId), 0); - } + const uint32_t cSpellId = modSystem.GetGameId(acSpellId); + auto* pSpell = Cast(TESForm::GetById(cSpellId)); + if (!pSpell) + { + spdlog::warn( + "Actor {:X}: skipping {} hand spell {:X}:{:X}, not a spell on this client (missing mod?)", formID, apHand, acSpellId.ModId, acSpellId.BaseId); + return; + } + + spdlog::debug("Setting {} hand spell: {:X}", apHand, cSpellId); + pEquipManager->EquipSpell(this, pSpell, aSlot); + }; + + if (acEquipment.LeftHandSpell) + equipHandSpell(acEquipment.LeftHandSpell, 0, "left"); if (acEquipment.RightHandSpell) - { - uint32_t secondaryHandWeaponId = modSystem.GetGameId(acEquipment.RightHandSpell); - spdlog::debug("Setting right hand spell: {:X}", secondaryHandWeaponId); - pEquipManager->EquipSpell(this, TESForm::GetById(secondaryHandWeaponId), 1); - } + equipHandSpell(acEquipment.RightHandSpell, 1, "right"); if (acEquipment.Shout) { - uint32_t shoutId = modSystem.GetGameId(acEquipment.Shout); - spdlog::debug("Setting shout: {:X}", shoutId); - pEquipManager->EquipShout(this, TESForm::GetById(shoutId)); + const uint32_t cShoutId = modSystem.GetGameId(acEquipment.Shout); + TESForm* pShout = TESForm::GetById(cShoutId); + + // The voice slot holds either a shout or a power (a spell) + if (!Cast(pShout) && !Cast(pShout)) + { + spdlog::warn( + "Actor {:X}: skipping shout/power {:X}:{:X}, not a shout or power on this client (missing mod?)", formID, acEquipment.Shout.ModId, + acEquipment.Shout.BaseId); + } + else + { + spdlog::debug("Setting shout: {:X}", cShoutId); + pEquipManager->EquipShout(this, pShout); + } } } diff --git a/Code/client/Games/Skyrim/Components/TESSpellList.cpp b/Code/client/Games/Skyrim/Components/TESSpellList.cpp index f4457e417..f76aff24c 100644 --- a/Code/client/Games/Skyrim/Components/TESSpellList.cpp +++ b/Code/client/Games/Skyrim/Components/TESSpellList.cpp @@ -10,3 +10,32 @@ void TESSpellList::Initialize() lists = Memory::New(); } + +namespace +{ +template uint32_t CompactNonNull(T** apEntries, uint32_t& aCount) noexcept +{ + if (!apEntries) + return 0; + + uint32_t kept = 0; + for (uint32_t i = 0; i < aCount; ++i) + { + if (apEntries[i]) + apEntries[kept++] = apEntries[i]; + } + + const uint32_t cRemoved = aCount - kept; + aCount = kept; + return cRemoved; +} +} // namespace + +uint32_t TESSpellList::RemoveNullEntries() noexcept +{ + if (!lists) + return 0; + + return CompactNonNull(lists->spells, lists->spellCount) + CompactNonNull(lists->unk4, lists->unk4Count) + + CompactNonNull(lists->shouts, lists->shoultCount); +} diff --git a/Code/client/Games/Skyrim/Components/TESSpellList.h b/Code/client/Games/Skyrim/Components/TESSpellList.h index f32f2d382..458a70df9 100644 --- a/Code/client/Games/Skyrim/Components/TESSpellList.h +++ b/Code/client/Games/Skyrim/Components/TESSpellList.h @@ -21,6 +21,8 @@ struct TESSpellList : BaseFormComponent Lists* lists; void Initialize(); + // Drops null spells/shouts left by a change-form load, returns how many were dropped + uint32_t RemoveNullEntries() noexcept; }; static_assert(sizeof(TESSpellList::Lists) == 0x28); diff --git a/Code/client/Games/Skyrim/Forms/EnchantmentItem.cpp b/Code/client/Games/Skyrim/Forms/EnchantmentItem.cpp index cc586d708..3e5fd3f20 100644 --- a/Code/client/Games/Skyrim/Forms/EnchantmentItem.cpp +++ b/Code/client/Games/Skyrim/Forms/EnchantmentItem.cpp @@ -14,26 +14,42 @@ EnchantmentItem* EnchantmentItem::Create(const Inventory::EnchantmentData& aData ModSystem& modSystem = World::Get().GetModSystem(); - GameArray effects{}; - effects.Resize(aData.Effects.size()); - for (int i = 0; i < aData.Effects.size(); i++) + // Effects from a mod this client doesn't have resolve to null. Keep only the ones that exist here; a created + // enchantment with a null effect crashes the game as soon as it is displayed, equipped or applied. + Vector resolvedEffects; + resolvedEffects.reserve(aData.Effects.size()); + for (const Inventory::EffectItem& effect : aData.Effects) { - Inventory::EffectItem effect = aData.Effects[i]; + auto* pEffectSetting = Cast(TESForm::GetById(modSystem.GetGameId(effect.EffectId))); + if (!pEffectSetting) + { + spdlog::warn("Dropping enchantment effect {:X}:{:X}, not found on this client (missing mod?)", effect.EffectId.ModId, effect.EffectId.BaseId); + continue; + } EffectItem effectItem{}; effectItem.data.fMagnitude = effect.Magnitude; effectItem.data.iArea = effect.Area; effectItem.data.iDuration = effect.Duration; effectItem.fRawCost = effect.RawCost; - effectItem.pEffectSetting = Cast(TESForm::GetById(modSystem.GetGameId(effect.EffectId))); - if (!effectItem.pEffectSetting) - spdlog::error("Effect setting not found: {:X}:{:X}", effect.EffectId.ModId, effect.EffectId.BaseId); + effectItem.pEffectSetting = pEffectSetting; // TODO: TESCondition? - effects[i] = effectItem; + resolvedEffects.push_back(effectItem); + } + + if (resolvedEffects.empty()) + { + spdlog::warn("Not creating enchantment, none of its {} effects exist on this client", aData.Effects.size()); + return nullptr; } + GameArray effects{}; + effects.Resize(resolvedEffects.size()); + for (size_t i = 0; i < resolvedEffects.size(); i++) + effects[i] = resolvedEffects[i]; + BGSCreatedObjectManager* objManager = BGSCreatedObjectManager::Get(); EnchantmentItem* pItem = nullptr; diff --git a/Code/client/Games/Skyrim/Projectiles/Projectile.cpp b/Code/client/Games/Skyrim/Projectiles/Projectile.cpp index 201b640f4..dec4f4357 100644 --- a/Code/client/Games/Skyrim/Projectiles/Projectile.cpp +++ b/Code/client/Games/Skyrim/Projectiles/Projectile.cpp @@ -97,12 +97,20 @@ BSPointerHandle* TP_MAKE_THISCALL(HookLaunch, BSPointerHandlehandle.iBits); Projectile* pProjectile = Cast(pObject); - - TP_ASSERT(pProjectile, "No projectile found."); + if (!pProjectile) + { + spdlog::warn("No projectile found for handle {:X}, not syncing the launch", result->handle.iBits); + return result; + } Event.Power = pProjectile->fPower; diff --git a/Code/client/Games/Skyrim/TESObjectREFR.cpp b/Code/client/Games/Skyrim/TESObjectREFR.cpp index e58526960..5d51b734b 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.cpp +++ b/Code/client/Games/Skyrim/TESObjectREFR.cpp @@ -622,9 +622,14 @@ ExtraDataList* TESObjectREFR::GetExtraDataFromItem(const Inventory::Entry& arEnt pEnchantment = Cast(TESForm::GetById(enchantId)); } - TP_ASSERT(pEnchantment, "No Enchantment created or found."); - - pExtraDataList->SetEnchantmentData(pEnchantment, arEntry.ExtraEnchantCharge, arEntry.ExtraEnchantRemoveUnequip); + // Null when the enchantment (or all of a created one's effects) comes from a mod this client doesn't have. + // Attaching a null enchantment crashes later when the item is equipped or shown, so sync the item without it. + if (pEnchantment) + pExtraDataList->SetEnchantmentData(pEnchantment, arEntry.ExtraEnchantCharge, arEntry.ExtraEnchantRemoveUnequip); + else + spdlog::warn( + "Item {:X}:{:X}: skipping enchantment {:X}:{:X}, not available on this client (missing mod?)", arEntry.BaseId.ModId, arEntry.BaseId.BaseId, + arEntry.ExtraEnchantId.ModId, arEntry.ExtraEnchantId.BaseId); } if (arEntry.ExtraPoisonId != 0)