You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openmw/apps/openmw/mwworld/projectilemanager.cpp

797 lines
30 KiB
C++

#include "projectilemanager.hpp"
#include <iomanip>
#include <memory>
#include <optional>
#include <sstream>
#include <osg/PositionAttitudeTransform>
#include <components/debug/debuglog.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadrace.hpp>
#include <components/esm3/projectilestate.hpp>
#include <components/misc/constants.hpp>
#include <components/misc/convert.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/sceneutil/controller.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/sceneutil/visitor.hpp>
#include <components/settings/values.hpp>
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/manualref.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/combat.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/spellcasting.hpp"
#include "../mwmechanics/weapontype.hpp"
#include "../mwrender/animation.hpp"
#include "../mwrender/renderingmanager.hpp"
#include "../mwrender/util.hpp"
#include "../mwrender/vismask.hpp"
#include "../mwsound/sound.hpp"
#include "../mwphysics/physicssystem.hpp"
#include "../mwphysics/projectile.hpp"
namespace
{
ESM::EffectList getMagicBoltData(std::vector<ESM::RefId>& projectileIDs, std::set<ESM::RefId>& sounds, float& speed,
std::string& texture, std::string& sourceName, const ESM::RefId& id)
{
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
const ESM::EffectList* effects;
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(id)) // check if it's a spell
{
sourceName = spell->mName;
effects = &spell->mEffects;
}
else // check if it's an enchanted item
{
MWWorld::ManualRef ref(esmStore, id);
MWWorld::Ptr ptr = ref.getPtr();
const ESM::Enchantment* ench = esmStore.get<ESM::Enchantment>().find(ptr.getClass().getEnchantment(ptr));
sourceName = ptr.getClass().getName(ptr);
effects = &ench->mEffects;
}
int count = 0;
speed = 0.0f;
ESM::EffectList projectileEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects->mList.begin()); iter != effects->mList.end();
++iter)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(iter->mEffectID);
// Speed of multi-effect projectiles should be the average of the constituent effects,
// based on observation of the original engine.
speed += magicEffect->mData.mSpeed;
count++;
if (iter->mRange != ESM::RT_Target)
continue;
if (magicEffect->mBolt.empty())
projectileIDs.emplace_back(ESM::RefId::stringRefId("VFX_DefaultBolt"));
else
projectileIDs.push_back(magicEffect->mBolt);
if (!magicEffect->mBoltSound.empty())
sounds.emplace(magicEffect->mBoltSound);
else
sounds.emplace(MWBase::Environment::get()
.getESMStore()
->get<ESM::Skill>()
.find(magicEffect->mData.mSchool)
->mSchool->mBoltSound);
projectileEffects.mList.push_back(*iter);
}
if (count != 0)
speed /= count;
// the particle texture is only used if there is only one projectile
if (projectileEffects.mList.size() == 1)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effects->mList.begin()->mEffectID);
texture = magicEffect->mParticle;
}
if (projectileEffects.mList.size()
> 1) // insert a VFX_Multiple projectile if there are multiple projectile effects
{
const ESM::RefId ID = ESM::RefId::stringRefId("VFX_Multiple" + std::to_string(effects->mList.size()));
std::vector<ESM::RefId>::iterator it;
it = projectileIDs.begin();
it = projectileIDs.insert(it, ID);
}
return projectileEffects;
}
osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects)
{
// Calculate combined light diffuse color from magical effects
osg::Vec4 lightDiffuseColor;
for (const ESM::ENAMstruct& enam : effects.mList)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(enam.mEffectID);
lightDiffuseColor += magicEffect->getColor();
}
int numberOfEffects = effects.mList.size();
lightDiffuseColor /= numberOfEffects;
return lightDiffuseColor;
}
}
namespace MWWorld
{
ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem,
MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics)
: mParent(parent)
, mResourceSystem(resourceSystem)
, mRendering(rendering)
, mPhysics(physics)
, mCleanupTimer(0.0f)
{
}
/// Rotates an osg::PositionAttitudeTransform over time.
class RotateCallback : public SceneUtil::NodeCallback<RotateCallback, osg::PositionAttitudeTransform*>
{
public:
RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0, -1, 0), float rotateSpeed = osg::PI * 2)
: mAxis(axis)
, mRotateSpeed(rotateSpeed)
{
}
void operator()(osg::PositionAttitudeTransform* node, osg::NodeVisitor* nv)
{
double time = nv->getFrameStamp()->getSimulationTime();
osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis);
node->setAttitude(orient);
traverse(node, nv);
}
private:
osg::Vec3f mAxis;
float mRotateSpeed;
};
void ProjectileManager::createModel(State& state, const std::string& model, const osg::Vec3f& pos,
const osg::Quat& orient, bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture)
{
state.mNode = new osg::PositionAttitudeTransform;
state.mNode->setNodeMask(MWRender::Mask_Effect);
state.mNode->setPosition(pos);
state.mNode->setAttitude(orient);
osg::Group* attachTo = state.mNode;
if (rotate)
{
osg::ref_ptr<osg::PositionAttitudeTransform> rotateNode(new osg::PositionAttitudeTransform);
rotateNode->addUpdateCallback(new RotateCallback());
state.mNode->addChild(rotateNode);
attachTo = rotateNode;
}
osg::ref_ptr<osg::Node> projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo);
if (state.mIdMagic.size() > 1)
{
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter)
{
std::ostringstream nodeName;
nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter;
const ESM::Weapon* weapon
= MWBase::Environment::get().getESMStore()->get<ESM::Weapon>().find(state.mIdMagic.at(iter));
std::string nameToFind = nodeName.str();
SceneUtil::FindByNameVisitor findVisitor(nameToFind);
attachTo->accept(findVisitor);
if (findVisitor.mFoundNode)
mResourceSystem->getSceneManager()->getInstance(
Misc::ResourceHelpers::correctMeshPath(weapon->mModel, vfs), findVisitor.mFoundNode);
}
}
if (createLight)
{
osg::ref_ptr<osg::Light> projectileLight(new osg::Light);
projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
projectileLight->setDiffuse(lightDiffuseColor);
projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f));
projectileLight->setConstantAttenuation(0.f);
projectileLight->setLinearAttenuation(0.1f);
projectileLight->setQuadraticAttenuation(0.f);
projectileLight->setPosition(osg::Vec4(pos, 1.0));
SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource;
projectileLightSource->setNodeMask(MWRender::Mask_Lighting);
projectileLightSource->setRadius(66.f);
state.mNode->addChild(projectileLightSource);
projectileLightSource->setLight(projectileLight);
}
state.mNode->addCullCallback(new SceneUtil::LightListCallback);
mParent->addChild(state.mNode);
state.mEffectAnimationTime = std::make_shared<MWRender::EffectAnimationTime>();
SceneUtil::AssignControllerSourcesVisitor assignVisitor(state.mEffectAnimationTime);
state.mNode->accept(assignVisitor);
MWRender::overrideFirstRootTexture(texture, mResourceSystem, std::move(projectile));
}
void ProjectileManager::update(State& state, float duration)
{
state.mEffectAnimationTime->addTime(duration);
}
void ProjectileManager::launchMagicBolt(
const ESM::RefId& spellId, const Ptr& caster, const osg::Vec3f& fallbackDirection, ESM::RefNum item)
{
osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
if (caster.getClass().isActor())
{
// Note: we ignore the collision box offset, this is required to make some flying creatures work as
// intended.
pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight;
}
if (MWBase::Environment::get().getWorld()->isUnderwater(
caster.getCell(), pos)) // Underwater casting not possible
return;
osg::Quat orient;
if (caster.getClass().isActor())
orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1, 0, 0))
* osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, -1));
else
orient.makeRotate(osg::Vec3f(0, 1, 0), osg::Vec3f(fallbackDirection));
MagicBoltState state;
state.mSpellId = spellId;
state.mCasterHandle = caster;
state.mItem = item;
if (caster.getClass().isActor())
state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
else
state.mActorId = -1;
std::string texture;
state.mEffects = getMagicBoltData(
state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
// Non-projectile should have been removed by getMagicBoltData
if (state.mEffects.mList.empty())
return;
if (!caster.getClass().isActor() && fallbackDirection.length2() <= 0)
{
Log(Debug::Warning) << "Unable to launch magic bolt (direction to target is empty)";
return;
}
MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0));
MWWorld::Ptr ptr = ref.getPtr();
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
auto model = ptr.getClass().getModel(ptr);
createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture);
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
for (const auto& soundid : state.mSoundIds)
{
MWBase::Sound* sound
= sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop);
if (sound)
state.mSounds.push_back(sound);
}
// in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics
// shape
if (state.mIdMagic.size() > 1)
{
model = Misc::ResourceHelpers::correctMeshPath(
MWBase::Environment::get().getESMStore()->get<ESM::Weapon>().find(state.mIdMagic[1])->mModel,
MWBase::Environment::get().getResourceSystem()->getVFS());
}
state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true);
state.mToDelete = false;
mMagicBolts.push_back(state);
}
void ProjectileManager::launchProjectile(const Ptr& actor, const ConstPtr& projectile, const osg::Vec3f& pos,
const osg::Quat& orient, const Ptr& bow, float speed, float attackStrength)
{
ProjectileState state;
state.mActorId = actor.getClass().getCreatureStats(actor).getActorId();
state.mBowId = bow.getCellRef().getRefId();
state.mVelocity = orient * osg::Vec3f(0, 1, 0) * speed;
state.mIdArrow = projectile.getCellRef().getRefId();
state.mCasterHandle = actor;
state.mAttackStrength = attackStrength;
int type = projectile.get<ESM::Weapon>()->mBase->mData.mType;
state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown;
MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), projectile.getCellRef().getRefId());
MWWorld::Ptr ptr = ref.getPtr();
const auto model = ptr.getClass().getModel(ptr);
createModel(state, model, pos, orient, false, false, osg::Vec4(0, 0, 0, 0));
if (!ptr.getClass().getEnchantment(ptr).empty())
SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr));
state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false);
state.mToDelete = false;
mProjectiles.push_back(state);
}
void ProjectileManager::updateCasters()
{
for (auto& state : mProjectiles)
mPhysics->setCaster(state.mProjectileId, state.getCaster());
for (auto& state : mMagicBolts)
{
// casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified
// back.
// TODO: should object-type caster be restored from savegame?
if (state.mActorId == -1)
continue;
auto caster = state.getCaster();
if (caster.isEmpty())
{
Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId;
cleanupMagicBolt(state);
continue;
}
mPhysics->setCaster(state.mProjectileId, caster);
}
}
void ProjectileManager::update(float dt)
{
periodicCleanup(dt);
moveProjectiles(dt);
moveMagicBolts(dt);
}
void ProjectileManager::periodicCleanup(float dt)
{
mCleanupTimer -= dt;
if (mCleanupTimer <= 0.0f)
{
mCleanupTimer = 2.0f;
auto isCleanable = [](const ProjectileManager::State& state) -> bool {
const float farawayThreshold = 72000.0f;
osg::Vec3 playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3();
return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold * farawayThreshold;
};
for (auto& projectileState : mProjectiles)
{
if (isCleanable(projectileState))
cleanupProjectile(projectileState);
}
for (auto& magicBoltState : mMagicBolts)
{
if (isCleanable(magicBoltState))
cleanupMagicBolt(magicBoltState);
}
}
}
void ProjectileManager::moveMagicBolts(float duration)
{
const bool normaliseRaceSpeed = Settings::game().mNormaliseRaceSpeed;
for (auto& magicBoltState : mMagicBolts)
{
if (magicBoltState.mToDelete)
continue;
auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
if (!projectile->isActive())
continue;
// If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame.
MWWorld::Ptr caster = magicBoltState.getCaster();
if (!caster.isEmpty() && caster.getClass().isActor())
{
if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead())
{
cleanupMagicBolt(magicBoltState);
continue;
}
}
const auto& store = *MWBase::Environment::get().getESMStore();
osg::Quat orient = magicBoltState.mNode->getAttitude();
static float fTargetSpellMaxSpeed
= store.get<ESM::GameSetting>().find("fTargetSpellMaxSpeed")->mValue.getFloat();
float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed;
if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc())
{
const auto npc = caster.get<ESM::NPC>()->mBase;
const auto race = store.get<ESM::Race>().find(npc->mRace);
speed *= npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale;
}
osg::Vec3f direction = orient * osg::Vec3f(0, 1, 0);
direction.normalize();
projectile->setVelocity(direction * speed);
update(magicBoltState, duration);
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit
// result.
std::vector<MWWorld::Ptr> targetActors;
if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
projectile->setValidTargets(targetActors);
}
}
void ProjectileManager::moveProjectiles(float duration)
{
for (auto& projectileState : mProjectiles)
{
if (projectileState.mToDelete)
continue;
auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
if (!projectile->isActive())
continue;
// gravity constant - must be way lower than the gravity affecting actors, since we're not
// simulating aerodynamics at all
projectileState.mVelocity
-= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration;
projectile->setVelocity(projectileState.mVelocity);
// rotation does not work well for throwing projectiles - their roll angle will depend on shooting
// direction.
if (!projectileState.mThrown)
{
osg::Quat orient;
orient.makeRotate(osg::Vec3f(0, 1, 0), projectileState.mVelocity);
projectileState.mNode->setAttitude(orient);
}
update(projectileState, duration);
MWWorld::Ptr caster = projectileState.getCaster();
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit
// result.
std::vector<MWWorld::Ptr> targetActors;
if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
projectile->setValidTargets(targetActors);
}
}
void ProjectileManager::processHits()
{
for (auto& projectileState : mProjectiles)
{
if (projectileState.mToDelete)
continue;
auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
const auto pos = projectile->getSimulationPosition();
projectileState.mNode->setPosition(pos);
if (projectile->isActive())
continue;
const auto target = projectile->getTarget();
auto caster = projectileState.getCaster();
assert(target != caster);
if (caster.isEmpty())
caster = target;
// Try to get a Ptr to the bow that was used. It might no longer exist.
MWWorld::ManualRef projectileRef(*MWBase::Environment::get().getESMStore(), projectileState.mIdArrow);
MWWorld::Ptr bow = projectileRef.getPtr();
if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId)
{
MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster);
MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
if (invIt != inv.end() && invIt->getCellRef().getRefId() == projectileState.mBowId)
bow = *invIt;
}
const auto hitPosition = Misc::Convert::toOsg(projectile->getHitPosition());
if (projectile->getHitWater())
mRendering->emitWaterRipple(hitPosition);
MWMechanics::projectileHit(
caster, target, bow, projectileRef.getPtr(), hitPosition, projectileState.mAttackStrength);
projectileState.mToDelete = true;
}
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
for (auto& magicBoltState : mMagicBolts)
{
if (magicBoltState.mToDelete)
continue;
auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
const auto pos = projectile->getSimulationPosition();
magicBoltState.mNode->setPosition(pos);
for (const auto& sound : magicBoltState.mSounds)
sound->setPosition(pos);
if (projectile->isActive())
continue;
const auto target = projectile->getTarget();
const auto caster = magicBoltState.getCaster();
assert(target != caster);
MWMechanics::CastSpell cast(caster, target);
cast.mHitPosition = Misc::Convert::toOsg(projectile->getHitPosition());
cast.mId = magicBoltState.mSpellId;
cast.mSourceName = magicBoltState.mSourceName;
cast.mItem = magicBoltState.mItem;
// Grab original effect list so the indices are correct
const ESM::EffectList* effects;
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(magicBoltState.mSpellId))
effects = &spell->mEffects;
else
{
MWWorld::ManualRef ref(esmStore, magicBoltState.mSpellId);
const MWWorld::Ptr& ptr = ref.getPtr();
effects = &esmStore.get<ESM::Enchantment>().find(ptr.getClass().getEnchantment(ptr))->mEffects;
}
cast.inflict(target, *effects, ESM::RT_Target);
magicBoltState.mToDelete = true;
}
for (auto& projectileState : mProjectiles)
{
if (projectileState.mToDelete)
cleanupProjectile(projectileState);
}
for (auto& magicBoltState : mMagicBolts)
{
if (magicBoltState.mToDelete)
cleanupMagicBolt(magicBoltState);
}
mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(),
[](const State& state) { return state.mToDelete; }),
mProjectiles.end());
mMagicBolts.erase(
std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }),
mMagicBolts.end());
}
void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state)
{
mParent->removeChild(state.mNode);
mPhysics->removeProjectile(state.mProjectileId);
state.mToDelete = true;
}
void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state)
{
mParent->removeChild(state.mNode);
mPhysics->removeProjectile(state.mProjectileId);
state.mToDelete = true;
for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++)
{
MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter));
}
}
void ProjectileManager::clear()
{
for (auto& mProjectile : mProjectiles)
cleanupProjectile(mProjectile);
mProjectiles.clear();
for (auto& mMagicBolt : mMagicBolts)
cleanupMagicBolt(mMagicBolt);
mMagicBolts.clear();
}
void ProjectileManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) const
{
for (std::vector<ProjectileState>::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it)
{
writer.startRecord(ESM::REC_PROJ);
ESM::ProjectileState state;
state.mId = it->mIdArrow;
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId;
state.mBowId = it->mBowId;
state.mVelocity = it->mVelocity;
state.mAttackStrength = it->mAttackStrength;
state.save(writer);
writer.endRecord(ESM::REC_PROJ);
}
for (std::vector<MagicBoltState>::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it)
{
writer.startRecord(ESM::REC_MPRJ);
ESM::MagicBoltState state;
state.mId = it->mIdMagic.at(0);
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId;
state.mItem = it->mItem;
state.mSpellId = it->mSpellId;
state.mSpeed = it->mSpeed;
state.save(writer);
writer.endRecord(ESM::REC_MPRJ);
}
}
bool ProjectileManager::readRecord(ESM::ESMReader& reader, uint32_t type)
{
if (type == ESM::REC_PROJ)
{
ESM::ProjectileState esm;
esm.load(reader);
ProjectileState state;
state.mActorId = esm.mActorId;
state.mBowId = esm.mBowId;
state.mVelocity = esm.mVelocity;
state.mIdArrow = esm.mId;
state.mAttackStrength = esm.mAttackStrength;
state.mToDelete = false;
std::string model;
try
{
MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), esm.mId);
MWWorld::Ptr ptr = ref.getPtr();
model = ptr.getClass().getModel(ptr);
int weaponType = ptr.get<ESM::Weapon>()->mBase->mData.mType;
state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown;
state.mProjectileId
= mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false);
}
catch (...)
{
return true;
}
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false,
osg::Vec4(0, 0, 0, 0));
mProjectiles.push_back(state);
return true;
}
if (type == ESM::REC_MPRJ)
{
ESM::MagicBoltState esm;
esm.load(reader);
MagicBoltState state;
state.mIdMagic.push_back(esm.mId);
state.mSpellId = esm.mSpellId;
state.mActorId = esm.mActorId;
state.mToDelete = false;
state.mItem = esm.mItem;
std::string texture;
try
{
state.mEffects = getMagicBoltData(
state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
}
catch (...)
{
Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \""
<< state.mSpellId << "\" no longer exists?)";
return true;
}
state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as
// projectile effects, so we can't calculate it from the save
// file's effect list, which is already trimmed of non-projectile
// effects. We need to use the stored value.
std::string model;
try
{
MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0));
MWWorld::Ptr ptr = ref.getPtr();
model = ptr.getClass().getModel(ptr);
}
catch (...)
{
return true;
}
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true,
lightDiffuseColor, texture);
state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true);
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
for (const auto& soundid : state.mSoundIds)
{
MWBase::Sound* sound = sndMgr->playSound3D(
esm.mPosition, soundid, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop);
if (sound)
state.mSounds.push_back(sound);
}
mMagicBolts.push_back(state);
return true;
}
return false;
}
int ProjectileManager::countSavedGameRecords() const
{
return mMagicBolts.size() + mProjectiles.size();
}
MWWorld::Ptr ProjectileManager::State::getCaster()
{
if (!mCasterHandle.isEmpty())
return mCasterHandle;
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId);
}
}