1 #include "projectilemanager.hpp"
2 
3 #include <iomanip>
4 
5 #include <memory>
6 #include <optional>
7 #include <osg/PositionAttitudeTransform>
8 
9 #include <components/debug/debuglog.hpp>
10 
11 #include <components/esm/esmwriter.hpp>
12 #include <components/esm/projectilestate.hpp>
13 
14 #include <components/misc/constants.hpp>
15 #include <components/misc/convert.hpp>
16 
17 #include <components/resource/resourcesystem.hpp>
18 #include <components/resource/scenemanager.hpp>
19 
20 #include <components/sceneutil/controller.hpp>
21 #include <components/sceneutil/visitor.hpp>
22 #include <components/sceneutil/lightmanager.hpp>
23 
24 #include "../mwworld/manualref.hpp"
25 #include "../mwworld/class.hpp"
26 #include "../mwworld/esmstore.hpp"
27 #include "../mwworld/inventorystore.hpp"
28 
29 #include "../mwbase/soundmanager.hpp"
30 #include "../mwbase/world.hpp"
31 #include "../mwbase/environment.hpp"
32 
33 #include "../mwmechanics/combat.hpp"
34 #include "../mwmechanics/creaturestats.hpp"
35 #include "../mwmechanics/spellcasting.hpp"
36 #include "../mwmechanics/actorutil.hpp"
37 #include "../mwmechanics/aipackage.hpp"
38 #include "../mwmechanics/weapontype.hpp"
39 
40 #include "../mwrender/animation.hpp"
41 #include "../mwrender/vismask.hpp"
42 #include "../mwrender/renderingmanager.hpp"
43 #include "../mwrender/util.hpp"
44 
45 #include "../mwsound/sound.hpp"
46 
47 #include "../mwphysics/physicssystem.hpp"
48 #include "../mwphysics/projectile.hpp"
49 
50 namespace
51 {
getMagicBoltData(std::vector<std::string> & projectileIDs,std::set<std::string> & sounds,float & speed,std::string & texture,std::string & sourceName,const std::string & id)52     ESM::EffectList getMagicBoltData(std::vector<std::string>& projectileIDs, std::set<std::string>& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id)
53     {
54         const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore();
55         const ESM::EffectList* effects;
56         if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(id)) // check if it's a spell
57         {
58             sourceName = spell->mName;
59             effects = &spell->mEffects;
60         }
61         else // check if it's an enchanted item
62         {
63             MWWorld::ManualRef ref(esmStore, id);
64             MWWorld::Ptr ptr = ref.getPtr();
65             const ESM::Enchantment* ench = esmStore.get<ESM::Enchantment>().find(ptr.getClass().getEnchantment(ptr));
66             sourceName = ptr.getClass().getName(ptr);
67             effects = &ench->mEffects;
68         }
69 
70         int count = 0;
71         speed = 0.0f;
72         ESM::EffectList projectileEffects;
73         for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects->mList.begin());
74             iter!=effects->mList.end(); ++iter)
75         {
76             const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
77                 iter->mEffectID);
78 
79             // Speed of multi-effect projectiles should be the average of the constituent effects,
80             // based on observation of the original engine.
81             speed += magicEffect->mData.mSpeed;
82             count++;
83 
84             if (iter->mRange != ESM::RT_Target)
85                 continue;
86 
87             if (magicEffect->mBolt.empty())
88                 projectileIDs.emplace_back("VFX_DefaultBolt");
89             else
90                 projectileIDs.push_back(magicEffect->mBolt);
91 
92             static const std::string schools[] = {
93                 "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
94             };
95             if (!magicEffect->mBoltSound.empty())
96                 sounds.emplace(magicEffect->mBoltSound);
97             else
98                 sounds.emplace(schools[magicEffect->mData.mSchool] + " bolt");
99             projectileEffects.mList.push_back(*iter);
100         }
101 
102         if (count != 0)
103             speed /= count;
104 
105         // the particle texture is only used if there is only one projectile
106         if (projectileEffects.mList.size() == 1)
107         {
108             const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
109                 effects->mList.begin()->mEffectID);
110             texture = magicEffect->mParticle;
111         }
112 
113         if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects
114         {
115             const std::string ID = "VFX_Multiple" + std::to_string(effects->mList.size());
116             std::vector<std::string>::iterator it;
117             it = projectileIDs.begin();
118             it = projectileIDs.insert(it, ID);
119         }
120         return projectileEffects;
121     }
122 
getMagicBoltLightDiffuseColor(const ESM::EffectList & effects)123     osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects)
124     {
125         // Calculate combined light diffuse color from magical effects
126         osg::Vec4 lightDiffuseColor;
127         float lightDiffuseRed = 0.0f;
128         float lightDiffuseGreen = 0.0f;
129         float lightDiffuseBlue = 0.0f;
130         for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects.mList.begin());
131             iter != effects.mList.end(); ++iter)
132         {
133             const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(
134                 iter->mEffectID);
135             lightDiffuseRed += (static_cast<float>(magicEffect->mData.mRed) / 255.f);
136             lightDiffuseGreen += (static_cast<float>(magicEffect->mData.mGreen) / 255.f);
137             lightDiffuseBlue += (static_cast<float>(magicEffect->mData.mBlue) / 255.f);
138         }
139         int numberOfEffects = effects.mList.size();
140         lightDiffuseColor = osg::Vec4(lightDiffuseRed / numberOfEffects
141             , lightDiffuseGreen / numberOfEffects
142             , lightDiffuseBlue / numberOfEffects
143             , 1.0f);
144 
145         return lightDiffuseColor;
146     }
147 }
148 
149 namespace MWWorld
150 {
151 
ProjectileManager(osg::Group * parent,Resource::ResourceSystem * resourceSystem,MWRender::RenderingManager * rendering,MWPhysics::PhysicsSystem * physics)152     ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem,
153                                          MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics)
154         : mParent(parent)
155         , mResourceSystem(resourceSystem)
156         , mRendering(rendering)
157         , mPhysics(physics)
158         , mCleanupTimer(0.0f)
159     {
160 
161     }
162 
163     /// Rotates an osg::PositionAttitudeTransform over time.
164     class RotateCallback : public osg::NodeCallback
165     {
166     public:
RotateCallback(const osg::Vec3f & axis=osg::Vec3f (0,-1,0),float rotateSpeed=osg::PI * 2)167         RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2)
168             : mAxis(axis)
169             , mRotateSpeed(rotateSpeed)
170         {
171         }
172 
operator ()(osg::Node * node,osg::NodeVisitor * nv)173         void operator()(osg::Node* node, osg::NodeVisitor* nv) override
174         {
175             osg::PositionAttitudeTransform* transform = static_cast<osg::PositionAttitudeTransform*>(node);
176 
177             double time = nv->getFrameStamp()->getSimulationTime();
178 
179             osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis);
180             transform->setAttitude(orient);
181 
182             traverse(node, nv);
183         }
184 
185     private:
186         osg::Vec3f mAxis;
187         float mRotateSpeed;
188     };
189 
190 
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)191     void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient,
192                                         bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture)
193     {
194         state.mNode = new osg::PositionAttitudeTransform;
195         state.mNode->setNodeMask(MWRender::Mask_Effect);
196         state.mNode->setPosition(pos);
197         state.mNode->setAttitude(orient);
198 
199         osg::Group* attachTo = state.mNode;
200 
201         if (rotate)
202         {
203             osg::ref_ptr<osg::PositionAttitudeTransform> rotateNode (new osg::PositionAttitudeTransform);
204             rotateNode->addUpdateCallback(new RotateCallback());
205             state.mNode->addChild(rotateNode);
206             attachTo = rotateNode;
207         }
208 
209         osg::ref_ptr<osg::Node> projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo);
210 
211         if (state.mIdMagic.size() > 1)
212             for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter)
213             {
214                 std::ostringstream nodeName;
215                 nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter;
216                 const ESM::Weapon* weapon = MWBase::Environment::get().getWorld()->getStore().get<ESM::Weapon>().find (state.mIdMagic.at(iter));
217                 SceneUtil::FindByNameVisitor findVisitor(nodeName.str());
218                 attachTo->accept(findVisitor);
219                 if (findVisitor.mFoundNode)
220                     mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode);
221             }
222 
223         if (createLight)
224         {
225             osg::ref_ptr<osg::Light> projectileLight(new osg::Light);
226             projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
227             projectileLight->setDiffuse(lightDiffuseColor);
228             projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f));
229             projectileLight->setConstantAttenuation(0.f);
230             projectileLight->setLinearAttenuation(0.1f);
231             projectileLight->setQuadraticAttenuation(0.f);
232             projectileLight->setPosition(osg::Vec4(pos, 1.0));
233 
234             SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource;
235             projectileLightSource->setNodeMask(MWRender::Mask_Lighting);
236             projectileLightSource->setRadius(66.f);
237 
238             state.mNode->addChild(projectileLightSource);
239             projectileLightSource->setLight(projectileLight);
240         }
241 
242         SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
243         state.mNode->accept(disableFreezeOnCullVisitor);
244 
245         state.mNode->addCullCallback(new SceneUtil::LightListCallback);
246 
247         mParent->addChild(state.mNode);
248 
249         state.mEffectAnimationTime.reset(new MWRender::EffectAnimationTime);
250 
251         SceneUtil::AssignControllerSourcesVisitor assignVisitor (state.mEffectAnimationTime);
252         state.mNode->accept(assignVisitor);
253 
254         MWRender::overrideFirstRootTexture(texture, mResourceSystem, projectile);
255     }
256 
update(State & state,float duration)257     void ProjectileManager::update(State& state, float duration)
258     {
259         state.mEffectAnimationTime->addTime(duration);
260     }
261 
launchMagicBolt(const std::string & spellId,const Ptr & caster,const osg::Vec3f & fallbackDirection)262     void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection)
263     {
264         osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
265         if (caster.getClass().isActor())
266         {
267             // Note: we ignore the collision box offset, this is required to make some flying creatures work as intended.
268             pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight;
269         }
270 
271         if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible
272             return;
273 
274         osg::Quat orient;
275         if (caster.getClass().isActor())
276             orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0))
277                     * osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1));
278         else
279             orient.makeRotate(osg::Vec3f(0,1,0), osg::Vec3f(fallbackDirection));
280 
281         MagicBoltState state;
282         state.mSpellId = spellId;
283         state.mCasterHandle = caster;
284         if (caster.getClass().isActor())
285             state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
286         else
287             state.mActorId = -1;
288 
289         std::string texture;
290 
291         state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
292 
293         // Non-projectile should have been removed by getMagicBoltData
294         if (state.mEffects.mList.empty())
295             return;
296 
297         if (!caster.getClass().isActor() && fallbackDirection.length2() <= 0)
298         {
299             Log(Debug::Warning) << "Unable to launch magic bolt (direction to target is empty)";
300             return;
301         }
302 
303         MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
304         MWWorld::Ptr ptr = ref.getPtr();
305 
306         osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
307 
308         auto model = ptr.getClass().getModel(ptr);
309         createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture);
310 
311         MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
312         for (const std::string &soundid : state.mSoundIds)
313         {
314             MWBase::Sound *sound = sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f,
315                                                        MWSound::Type::Sfx, MWSound::PlayMode::Loop);
316             if (sound)
317                 state.mSounds.push_back(sound);
318         }
319 
320         // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape
321         if (state.mIdMagic.size() > 1)
322             model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get<ESM::Weapon>().find(state.mIdMagic.at(1))->mModel;
323         state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true);
324         state.mToDelete = false;
325         mMagicBolts.push_back(state);
326     }
327 
launchProjectile(Ptr actor,ConstPtr projectile,const osg::Vec3f & pos,const osg::Quat & orient,Ptr bow,float speed,float attackStrength)328     void ProjectileManager::launchProjectile(Ptr actor, ConstPtr projectile, const osg::Vec3f &pos, const osg::Quat &orient, Ptr bow, float speed, float attackStrength)
329     {
330         ProjectileState state;
331         state.mActorId = actor.getClass().getCreatureStats(actor).getActorId();
332         state.mBowId = bow.getCellRef().getRefId();
333         state.mVelocity = orient * osg::Vec3f(0,1,0) * speed;
334         state.mIdArrow = projectile.getCellRef().getRefId();
335         state.mCasterHandle = actor;
336         state.mAttackStrength = attackStrength;
337         int type = projectile.get<ESM::Weapon>()->mBase->mData.mType;
338         state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown;
339 
340         MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId());
341         MWWorld::Ptr ptr = ref.getPtr();
342 
343         const auto model = ptr.getClass().getModel(ptr);
344         createModel(state, model, pos, orient, false, false, osg::Vec4(0,0,0,0));
345         if (!ptr.getClass().getEnchantment(ptr).empty())
346             SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr));
347 
348         state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false);
349         state.mToDelete = false;
350         mProjectiles.push_back(state);
351     }
352 
updateCasters()353     void ProjectileManager::updateCasters()
354     {
355         for (auto& state : mProjectiles)
356             mPhysics->setCaster(state.mProjectileId, state.getCaster());
357 
358         for (auto& state : mMagicBolts)
359         {
360             // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back.
361             // TODO: should object-type caster be restored from savegame?
362             if (state.mActorId == -1)
363                 continue;
364 
365             auto caster = state.getCaster();
366             if (caster.isEmpty())
367             {
368                 Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId;
369                 cleanupMagicBolt(state);
370                 continue;
371             }
372             mPhysics->setCaster(state.mProjectileId, caster);
373         }
374     }
375 
update(float dt)376     void ProjectileManager::update(float dt)
377     {
378         periodicCleanup(dt);
379         moveProjectiles(dt);
380         moveMagicBolts(dt);
381     }
382 
periodicCleanup(float dt)383     void ProjectileManager::periodicCleanup(float dt)
384     {
385         mCleanupTimer -= dt;
386         if (mCleanupTimer <= 0.0f)
387         {
388             mCleanupTimer = 2.0f;
389 
390             auto isCleanable = [](const ProjectileManager::State& state) -> bool
391             {
392                 const float farawayThreshold = 72000.0f;
393                 osg::Vec3 playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3();
394                 return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold;
395             };
396 
397             for (auto& projectileState : mProjectiles)
398             {
399                 if (isCleanable(projectileState))
400                     cleanupProjectile(projectileState);
401             }
402 
403             for (auto& magicBoltState : mMagicBolts)
404             {
405                 if (isCleanable(magicBoltState))
406                     cleanupMagicBolt(magicBoltState);
407             }
408         }
409     }
410 
moveMagicBolts(float duration)411     void ProjectileManager::moveMagicBolts(float duration)
412     {
413         for (auto& magicBoltState : mMagicBolts)
414         {
415             if (magicBoltState.mToDelete)
416                 continue;
417 
418             auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
419             if (!projectile->isActive())
420                 continue;
421             // If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame.
422             MWWorld::Ptr caster = magicBoltState.getCaster();
423             if (!caster.isEmpty() && caster.getClass().isActor())
424             {
425                 if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead())
426                 {
427                     cleanupMagicBolt(magicBoltState);
428                     continue;
429                 }
430             }
431 
432             osg::Quat orient = magicBoltState.mNode->getAttitude();
433             static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
434                         .find("fTargetSpellMaxSpeed")->mValue.getFloat();
435             float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed;
436             osg::Vec3f direction = orient * osg::Vec3f(0,1,0);
437             direction.normalize();
438             osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed;
439 
440             update(magicBoltState, duration);
441 
442             // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
443             std::vector<MWWorld::Ptr> targetActors;
444             if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
445                 caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
446             projectile->setValidTargets(targetActors);
447 
448             mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos);
449         }
450     }
451 
moveProjectiles(float duration)452     void ProjectileManager::moveProjectiles(float duration)
453     {
454         for (auto& projectileState : mProjectiles)
455         {
456             if (projectileState.mToDelete)
457                 continue;
458 
459             auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
460             if (!projectile->isActive())
461                 continue;
462             // gravity constant - must be way lower than the gravity affecting actors, since we're not
463             // simulating aerodynamics at all
464             projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration;
465 
466             osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration;
467 
468             // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction.
469             if (!projectileState.mThrown)
470             {
471                 osg::Quat orient;
472                 orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity);
473                 projectileState.mNode->setAttitude(orient);
474             }
475 
476             update(projectileState, duration);
477 
478             MWWorld::Ptr caster = projectileState.getCaster();
479 
480             // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
481             std::vector<MWWorld::Ptr> targetActors;
482             if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
483                 caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
484             projectile->setValidTargets(targetActors);
485 
486             mPhysics->updateProjectile(projectileState.mProjectileId, newPos);
487         }
488     }
489 
processHits()490     void ProjectileManager::processHits()
491     {
492         for (auto& projectileState : mProjectiles)
493         {
494             if (projectileState.mToDelete)
495                 continue;
496 
497             auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
498 
499             const auto pos = projectile->getPosition();
500             projectileState.mNode->setPosition(pos);
501 
502             if (projectile->isActive())
503                 continue;
504 
505             const auto target = projectile->getTarget();
506             auto caster = projectileState.getCaster();
507             assert(target != caster);
508 
509             if (caster.isEmpty())
510                 caster = target;
511 
512             // Try to get a Ptr to the bow that was used. It might no longer exist.
513             MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow);
514             MWWorld::Ptr bow = projectileRef.getPtr();
515             if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId)
516             {
517                 MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster);
518                 MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
519                 if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId))
520                     bow = *invIt;
521             }
522             if (projectile->getHitWater())
523                 mRendering->emitWaterRipple(pos);
524 
525             MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength);
526             cleanupProjectile(projectileState);
527         }
528         for (auto& magicBoltState : mMagicBolts)
529         {
530             if (magicBoltState.mToDelete)
531                 continue;
532 
533             auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
534 
535             const auto pos = projectile->getPosition();
536             magicBoltState.mNode->setPosition(pos);
537             for (const auto& sound : magicBoltState.mSounds)
538                 sound->setPosition(pos);
539 
540             if (projectile->isActive())
541                 continue;
542 
543             const auto target = projectile->getTarget();
544             const auto caster = magicBoltState.getCaster();
545             assert(target != caster);
546 
547             MWMechanics::CastSpell cast(caster, target);
548             cast.mHitPosition = pos;
549             cast.mId = magicBoltState.mSpellId;
550             cast.mSourceName = magicBoltState.mSourceName;
551             cast.mStack = false;
552             cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true);
553 
554             MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName);
555             cleanupMagicBolt(magicBoltState);
556         }
557         mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }),
558                 mProjectiles.end());
559         mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }),
560                 mMagicBolts.end());
561     }
562 
cleanupProjectile(ProjectileManager::ProjectileState & state)563     void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state)
564     {
565         mParent->removeChild(state.mNode);
566         mPhysics->removeProjectile(state.mProjectileId);
567         state.mToDelete = true;
568     }
569 
cleanupMagicBolt(ProjectileManager::MagicBoltState & state)570     void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state)
571     {
572         mParent->removeChild(state.mNode);
573         mPhysics->removeProjectile(state.mProjectileId);
574         state.mToDelete = true;
575         for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++)
576         {
577             MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter));
578         }
579     }
580 
clear()581     void ProjectileManager::clear()
582     {
583         for (auto& mProjectile : mProjectiles)
584             cleanupProjectile(mProjectile);
585         mProjectiles.clear();
586 
587         for (auto& mMagicBolt : mMagicBolts)
588             cleanupMagicBolt(mMagicBolt);
589         mMagicBolts.clear();
590     }
591 
write(ESM::ESMWriter & writer,Loading::Listener & progress) const592     void ProjectileManager::write(ESM::ESMWriter &writer, Loading::Listener &progress) const
593     {
594         for (std::vector<ProjectileState>::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it)
595         {
596             writer.startRecord(ESM::REC_PROJ);
597 
598             ESM::ProjectileState state;
599             state.mId = it->mIdArrow;
600             state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
601             state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
602             state.mActorId = it->mActorId;
603 
604             state.mBowId = it->mBowId;
605             state.mVelocity = it->mVelocity;
606             state.mAttackStrength = it->mAttackStrength;
607 
608             state.save(writer);
609 
610             writer.endRecord(ESM::REC_PROJ);
611         }
612 
613         for (std::vector<MagicBoltState>::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it)
614         {
615             writer.startRecord(ESM::REC_MPRJ);
616 
617             ESM::MagicBoltState state;
618             state.mId = it->mIdMagic.at(0);
619             state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
620             state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
621             state.mActorId = it->mActorId;
622 
623             state.mSpellId = it->mSpellId;
624             state.mSpeed = it->mSpeed;
625 
626             state.save(writer);
627 
628             writer.endRecord(ESM::REC_MPRJ);
629         }
630     }
631 
readRecord(ESM::ESMReader & reader,uint32_t type)632     bool ProjectileManager::readRecord(ESM::ESMReader &reader, uint32_t type)
633     {
634         if (type == ESM::REC_PROJ)
635         {
636             ESM::ProjectileState esm;
637             esm.load(reader);
638 
639             ProjectileState state;
640             state.mActorId = esm.mActorId;
641             state.mBowId = esm.mBowId;
642             state.mVelocity = esm.mVelocity;
643             state.mIdArrow = esm.mId;
644             state.mAttackStrength = esm.mAttackStrength;
645             state.mToDelete = false;
646 
647             std::string model;
648             try
649             {
650                 MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId);
651                 MWWorld::Ptr ptr = ref.getPtr();
652                 model = ptr.getClass().getModel(ptr);
653                 int weaponType = ptr.get<ESM::Weapon>()->mBase->mData.mType;
654                 state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown;
655 
656                 state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false);
657             }
658             catch(...)
659             {
660                 return true;
661             }
662 
663             createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, osg::Vec4(0,0,0,0));
664 
665             mProjectiles.push_back(state);
666             return true;
667         }
668         if (type == ESM::REC_MPRJ)
669         {
670             ESM::MagicBoltState esm;
671             esm.load(reader);
672 
673             MagicBoltState state;
674             state.mIdMagic.push_back(esm.mId);
675             state.mSpellId = esm.mSpellId;
676             state.mActorId = esm.mActorId;
677             state.mToDelete = false;
678             std::string texture;
679 
680             try
681             {
682                 state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
683             }
684             catch(...)
685             {
686                 Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \"" << state.mSpellId << "\" no longer exists?)";
687                 return true;
688             }
689 
690             state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as
691                                        // projectile effects, so we can't calculate it from the save
692                                        // file's effect list, which is already trimmed of non-projectile
693                                        // effects. We need to use the stored value.
694 
695             std::string model;
696             try
697             {
698                 MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
699                 MWWorld::Ptr ptr = ref.getPtr();
700                 model = ptr.getClass().getModel(ptr);
701             }
702             catch(...)
703             {
704                 return true;
705             }
706 
707             osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
708             createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture);
709             state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true);
710 
711             MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
712             for (const std::string &soundid : state.mSoundIds)
713             {
714                 MWBase::Sound *sound = sndMgr->playSound3D(esm.mPosition, soundid, 1.0f, 1.0f,
715                                                            MWSound::Type::Sfx, MWSound::PlayMode::Loop);
716                 if (sound)
717                     state.mSounds.push_back(sound);
718             }
719 
720             mMagicBolts.push_back(state);
721             return true;
722         }
723 
724         return false;
725     }
726 
countSavedGameRecords() const727     int ProjectileManager::countSavedGameRecords() const
728     {
729         return mMagicBolts.size() + mProjectiles.size();
730     }
731 
getCaster()732     MWWorld::Ptr ProjectileManager::State::getCaster()
733     {
734         if (!mCasterHandle.isEmpty())
735             return mCasterHandle;
736 
737         return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId);
738     }
739 
740 }
741