Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Code/client/Games/Forms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 38 additions & 13 deletions Code/client/Games/Skyrim/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
#include <ExtraData/ExtraTextDisplayData.h>
#include <Forms/EnchantmentItem.h>
#include <Forms/AlchemyItem.h>
#include <Forms/SpellItem.h>
#include <Forms/TESShout.h>
#include <Forms/TESObjectCELL.h>

#include <Structs/Skyrim/AnimationGraphDescriptor_BHR_Master.h>
Expand Down Expand Up @@ -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<MagicItem>(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<TESShout>(pShout) && !Cast<MagicItem>(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);
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions Code/client/Games/Skyrim/Components/TESSpellList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,32 @@ void TESSpellList::Initialize()

lists = Memory::New<Lists>();
}

namespace
{
template <class T> 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);
}
2 changes: 2 additions & 0 deletions Code/client/Games/Skyrim/Components/TESSpellList.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
32 changes: 24 additions & 8 deletions Code/client/Games/Skyrim/Forms/EnchantmentItem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,42 @@ EnchantmentItem* EnchantmentItem::Create(const Inventory::EnchantmentData& aData

ModSystem& modSystem = World::Get().GetModSystem();

GameArray<EffectItem> 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<EffectItem> resolvedEffects;
resolvedEffects.reserve(aData.Effects.size());
for (const Inventory::EffectItem& effect : aData.Effects)
{
Inventory::EffectItem effect = aData.Effects[i];
auto* pEffectSetting = Cast<EffectSetting>(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<EffectSetting>(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<EffectItem> 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;
Expand Down
14 changes: 11 additions & 3 deletions Code/client/Games/Skyrim/Projectiles/Projectile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,20 @@ BSPointerHandle<Projectile>* TP_MAKE_THISCALL(HookLaunch, BSPointerHandle<Projec

auto result = TiltedPhoques::ThisCall(RealLaunch, apThis, arData);

TP_ASSERT(result, "No projectile handle returned.");
if (!result)
{
spdlog::warn("No projectile handle returned, not syncing the launch");
return result;
}

// There is nothing to sync when the returned handle doesn't resolve to a projectile.
TESObjectREFR* pObject = TESObjectREFR::GetByHandle(result->handle.iBits);
Projectile* pProjectile = Cast<Projectile>(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;

Expand Down
11 changes: 8 additions & 3 deletions Code/client/Games/Skyrim/TESObjectREFR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,14 @@ ExtraDataList* TESObjectREFR::GetExtraDataFromItem(const Inventory::Entry& arEnt
pEnchantment = Cast<EnchantmentItem>(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)
Expand Down