diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt index fc5fae72e..0e742ff54 100644 --- a/apps/essimporter/CMakeLists.txt +++ b/apps/essimporter/CMakeLists.txt @@ -17,6 +17,7 @@ set(ESSIMPORTER_FILES importscri.cpp importscpt.cpp importproj.cpp + importsplm.cpp importercontext.cpp converter.cpp convertacdt.cpp diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index 5c65332be..2473daf95 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -1,12 +1,12 @@ #include "converter.hpp" #include +#include #include #include #include -#include #include "convertcrec.hpp" #include "convertcntc.hpp" @@ -461,11 +461,7 @@ namespace ESSImport if (!pnam.isMagic()) { ESM::ProjectileState out; - out.mId = pnam.mArrowId.toString(); - out.mPosition = pnam.mPosition; - out.mOrientation.mValues[0] = out.mOrientation.mValues[1] = out.mOrientation.mValues[2] = 0.0f; - out.mOrientation.mValues[3] = 1.0f; - out.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); + convertBaseState(out, pnam); out.mBowId = pnam.mBowId.toString(); out.mVelocity = pnam.mVelocity; @@ -477,16 +473,49 @@ namespace ESSImport } else { - // TODO: Implement magic projectile conversion. + ESM::MagicBoltState out; + convertBaseState(out, pnam); - /*esm.startRecord(ESM::REC_MPRJ); - out.save(esm); - esm.endRecord(ESM::REC_MPRJ);*/ + auto it = std::find_if(mContext->mActiveSpells.begin(), mContext->mActiveSpells.end(), + [&pnam](const SPLM::ActiveSpell& spell) -> bool { return spell.mIndex == pnam.mSplmIndex; }); - std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (not implemented)" << std::endl; - continue; + if (it == mContext->mActiveSpells.end()) + { + std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (invalid spell link)" << std::endl; + continue; + } + + out.mSpellId = it->mSPDT.mId.toString(); + out.mSpeed = pnam.mSpeed * 0.001f; // not sure where this factor comes from + + esm.startRecord(ESM::REC_MPRJ); + out.save(esm); + esm.endRecord(ESM::REC_MPRJ); } } } + void ConvertPROJ::convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam) + { + base.mId = pnam.mArrowId.toString(); + base.mPosition = pnam.mPosition; + + osg::Quat orient; + orient.makeRotate(osg::Vec3f(0,1,0), pnam.mVelocity); + base.mOrientation = orient; + + base.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); + } + + void ConvertSPLM::read(ESM::ESMReader& esm) + { + mSPLM.load(esm); + mContext->mActiveSpells = mSPLM.mActiveSpells; + } + + void ConvertSPLM::write(ESM::ESMWriter& esm) + { + std::cerr << "Warning: Skipped active spell conversion (not implemented)" << std::endl; + } + } diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index a640d6756..621b85709 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "importcrec.hpp" #include "importcntc.hpp" @@ -36,6 +37,7 @@ #include "importjour.hpp" #include "importscpt.hpp" #include "importproj.h" +#include "importsplm.h" #include "convertacdt.hpp" #include "convertnpcc.hpp" @@ -602,9 +604,19 @@ public: virtual void read(ESM::ESMReader& esm) override; virtual void write(ESM::ESMWriter& esm) override; private: + void convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam); PROJ mProj; }; +class ConvertSPLM : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) override; + virtual void write(ESM::ESMWriter& esm) override; +private: + SPLM mSPLM; +}; + } #endif diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 4c8222c56..73b15baae 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -271,6 +271,7 @@ namespace ESSImport const unsigned int recSTLN = ESM::FourCC<'S','T','L','N'>::value; const unsigned int recGAME = ESM::FourCC<'G','A','M','E'>::value; const unsigned int recJOUR = ESM::FourCC<'J','O','U','R'>::value; + const unsigned int recSPLM = ESM::FourCC<'S','P','L','M'>::value; std::map > converters; converters[ESM::REC_GLOB] = std::shared_ptr(new ConvertGlobal()); @@ -304,12 +305,12 @@ namespace ESSImport converters[recJOUR ] = std::shared_ptr(new ConvertJOUR()); converters[ESM::REC_SCPT] = std::shared_ptr(new ConvertSCPT()); converters[ESM::REC_PROJ] = std::shared_ptr(new ConvertPROJ()); + converters[recSPLM] = std::shared_ptr(new ConvertSPLM()); // TODO: // - REGN (weather in certain regions?) // - VFXM // - SPLM (active spell effects) - // - PROJ (magic projectiles in air) std::set unknownRecords; diff --git a/apps/essimporter/importercontext.hpp b/apps/essimporter/importercontext.hpp index 4896f5594..86501b3cb 100644 --- a/apps/essimporter/importercontext.hpp +++ b/apps/essimporter/importercontext.hpp @@ -15,7 +15,7 @@ #include "importcrec.hpp" #include "importcntc.hpp" #include "importplayer.hpp" - +#include "importsplm.h" @@ -54,6 +54,8 @@ namespace ESSImport std::map mCreatures; std::map mNpcs; + std::vector mActiveSpells; + Context() : mDay(0) , mMonth(0) diff --git a/apps/essimporter/importsplm.cpp b/apps/essimporter/importsplm.cpp new file mode 100644 index 000000000..9fdba4ddb --- /dev/null +++ b/apps/essimporter/importsplm.cpp @@ -0,0 +1,43 @@ +#include "importsplm.h" + +#include + +namespace ESSImport +{ + +void SPLM::load(ESM::ESMReader& esm) +{ + while (esm.isNextSub("NAME")) + { + ActiveSpell spell; + esm.getHT(spell.mIndex); + esm.getHNT(spell.mSPDT, "SPDT"); + spell.mTarget = esm.getHNOString("TNAM"); + + while (esm.isNextSub("NPDT")) + { + ActiveEffect effect; + esm.getHT(effect.mNPDT); + + // Effect-specific subrecords can follow: + // - INAM for disintegration and bound effects + // - CNAM for summoning and command effects + // - VNAM for vampirism + // NOTE: There can be multiple INAMs per effect. + // TODO: Needs more research. + + esm.skipHSubUntil("NAM0"); // sentinel + esm.getSubName(); + esm.skipHSub(); + + spell.mActiveEffects.push_back(effect); + } + + unsigned char xnam; // sentinel + esm.getHNT(xnam, "XNAM"); + + mActiveSpells.push_back(spell); + } +} + +} diff --git a/apps/essimporter/importsplm.h b/apps/essimporter/importsplm.h new file mode 100644 index 000000000..8fd5c2bb5 --- /dev/null +++ b/apps/essimporter/importsplm.h @@ -0,0 +1,81 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTSPLM_H +#define OPENMW_ESSIMPORT_IMPORTSPLM_H + +#include +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + +struct SPLM +{ + +#pragma pack(push) +#pragma pack(1) + struct SPDT // 160 bytes + { + int mType; // 1 = spell, 2 = enchantment, 3 = potion + ESM::NAME32 mId; // base ID of a spell/enchantment/potion + unsigned char mUnknown[4*4]; + ESM::NAME32 mCasterId; + ESM::NAME32 mSourceId; // empty for spells + unsigned char mUnknown2[4*11]; + }; + + struct NPDT // 56 bytes + { + ESM::NAME32 mAffectedActorId; + unsigned char mUnknown[4*2]; + int mMagnitude; + float mSecondsActive; + unsigned char mUnknown2[4*2]; + }; + + struct INAM // 40 bytes + { + int mUnknown; + unsigned char mUnknown2; + ESM::FIXED_STRING<35> mItemId; // disintegrated item / bound item / item to re-equip after expiration + }; + + struct CNAM // 36 bytes + { + int mUnknown; // seems to always be 0 + ESM::NAME32 mSummonedOrCommandedActor[32]; + }; + + struct VNAM // 4 bytes + { + int mUnknown; + }; + + +#pragma pack(pop) + + struct ActiveEffect + { + NPDT mNPDT; + }; + + struct ActiveSpell + { + int mIndex; + SPDT mSPDT; + std::string mTarget; + std::vector mActiveEffects; + }; + + std::vector mActiveSpells; + + void load(ESM::ESMReader& esm); +}; + +} + +#endif diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 5e76d82eb..038535939 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -487,8 +487,7 @@ namespace MWBase virtual void castSpell (const MWWorld::Ptr& actor) = 0; - virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) = 0; + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength) = 0; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index f7ee15bf4..a808b8285 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -329,7 +329,7 @@ namespace MWMechanics { } - void CastSpell::launchMagicBolt (const ESM::EffectList& effects) + void CastSpell::launchMagicBolt () { osg::Vec3f fallbackDirection (0,1,0); @@ -340,8 +340,7 @@ namespace MWMechanics osg::Vec3f(mTarget.getRefData().getPosition().asVec3())- osg::Vec3f(mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, false, effects, - mCaster, mSourceName, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, @@ -823,7 +822,7 @@ namespace MWMechanics inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); if (launchProjectile) - launchMagicBolt(enchantment->mEffects); + launchMagicBolt(); else if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target); @@ -915,7 +914,7 @@ namespace MWMechanics if (!mTarget.isEmpty()) inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); - launchMagicBolt(spell->mEffects); + launchMagicBolt(); return true; } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 9991c583d..2e368afcf 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -106,7 +106,7 @@ namespace MWMechanics void playSpellCastingEffects(const std::string &spellid); /// Launch a bolt with the given effects. - void launchMagicBolt (const ESM::EffectList& effects); + void launchMagicBolt (); /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 5b15583bf..6e716cb54 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -1,6 +1,7 @@ #include "projectilemanager.hpp" #include +#include #include @@ -41,13 +42,29 @@ namespace { - ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::vector& sounds, float& speed, std::string& texture, const ESM::EffectList& effects) + ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::vector& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id) { + const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::EffectList* effects; + if (const ESM::Spell* spell = esmStore.get().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().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::const_iterator iter (effects.mList.begin()); - iter!=effects.mList.end(); ++iter) + for (std::vector::const_iterator iter (effects->mList.begin()); + iter!=effects->mList.end(); ++iter) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( iter->mEffectID); @@ -82,14 +99,14 @@ namespace if (projectileEffects.mList.size() == 1) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( - effects.mList.begin()->mEffectID); + effects->mList.begin()->mEffectID); texture = magicEffect->mParticle; } if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects { std::ostringstream ID; - ID << "VFX_Multiple" << effects.mList.size(); + ID << "VFX_Multiple" << effects->mList.size(); std::vector::iterator it; it = projectileIDs.begin(); it = projectileIDs.insert(it, ID.str()); @@ -235,8 +252,7 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, bool stack, const ESM::EffectList &effects, const Ptr &caster, - const std::string &sourceName, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) @@ -257,18 +273,16 @@ namespace MWWorld orient.makeRotate(osg::Vec3f(0,1,0), osg::Vec3f(fallbackDirection)); MagicBoltState state; - state.mSourceName = sourceName; state.mSpellId = spellId; state.mCasterHandle = caster; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else state.mActorId = -1; - state.mStack = stack; std::string texture = ""; - state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, effects); + 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()) @@ -277,7 +291,7 @@ namespace MWWorld MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); - osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(effects); + osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, ptr.getClass().getModel(ptr), pos, orient, true, true, lightDiffuseColor, texture); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -364,7 +378,7 @@ namespace MWWorld cast.mHitPosition = pos; cast.mId = it->mSpellId; cast.mSourceName = it->mSourceName; - cast.mStack = it->mStack; + cast.mStack = false; cast.inflict(result.mHitObject, caster, it->mEffects, ESM::RT_Target, false, true); } } @@ -504,11 +518,7 @@ namespace MWWorld state.mActorId = it->mActorId; state.mSpellId = it->mSpellId; - state.mEffects = it->mEffects; - state.mSound = it->mSoundIds.at(0); - state.mSourceName = it->mSourceName; state.mSpeed = it->mSpeed; - state.mStack = it->mStack; state.save(writer); @@ -553,13 +563,21 @@ namespace MWWorld esm.load(reader); MagicBoltState state; - state.mSourceName = esm.mSourceName; state.mIdMagic.push_back(esm.mId); state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; - state.mStack = esm.mStack; std::string texture = ""; - state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, esm.mEffects); + + try + { + state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId); + } + catch(...) + { + std::cerr << "Warning: Failed to recreate magic projectile from saved data (id \"" << state.mSpellId << "\" no longer exists?)" << std::endl; + 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 @@ -577,7 +595,7 @@ namespace MWWorld return true; } - osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(esm.mEffects); + osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index c7025a3a0..1ef72a048 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -49,8 +49,7 @@ namespace MWWorld MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection); + void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); @@ -101,8 +100,6 @@ namespace MWWorld float mSpeed; - bool mStack; - std::vector mSounds; std::vector mSoundIds; }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index a9506385d..40bc13c94 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2816,10 +2816,9 @@ namespace MWWorld mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } - void World::launchMagicBolt (const std::string &spellId, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) + void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) { - mProjectileManager->launchMagicBolt(spellId, stack, effects, caster, sourceName, fallbackDirection); + mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); } const std::vector& World::getContentFiles() const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index a15dcaf3d..774753b6c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -601,8 +601,7 @@ namespace MWWorld */ virtual void castSpell (const MWWorld::Ptr& actor); - virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection); + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); diff --git a/components/esm/projectilestate.cpp b/components/esm/projectilestate.cpp index 70ae4c5ee..8ade9d5b2 100644 --- a/components/esm/projectilestate.cpp +++ b/components/esm/projectilestate.cpp @@ -27,11 +27,7 @@ namespace ESM BaseProjectileState::save(esm); esm.writeHNString ("SPEL", mSpellId); - esm.writeHNString ("SRCN", mSourceName); - mEffects.save(esm); esm.writeHNT ("SPED", mSpeed); - esm.writeHNT ("STCK", mStack); - esm.writeHNString ("SOUN", mSound); } void MagicBoltState::load(ESMReader &esm) @@ -39,11 +35,14 @@ namespace ESM BaseProjectileState::load(esm); mSpellId = esm.getHNString("SPEL"); - mSourceName = esm.getHNString ("SRCN"); - mEffects.load(esm); + if (esm.isNextSub("SRCN")) // for backwards compatibility + esm.skipHSub(); + ESM::EffectList().load(esm); // for backwards compatibility esm.getHNT (mSpeed, "SPED"); - esm.getHNT (mStack, "STCK"); - mSound = esm.getHNString ("SOUN"); + if (esm.isNextSub("STCK")) // for backwards compatibility + esm.skipHSub(); + if (esm.isNextSub("SOUN")) // for backwards compatibility + esm.skipHSub(); } void ProjectileState::save(ESMWriter &esm) const diff --git a/components/esm/projectilestate.hpp b/components/esm/projectilestate.hpp index 3471fbfc7..67ec89bb6 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm/projectilestate.hpp @@ -31,11 +31,7 @@ namespace ESM struct MagicBoltState : public BaseProjectileState { std::string mSpellId; - std::string mSourceName; - ESM::EffectList mEffects; float mSpeed; - bool mStack; - std::string mSound; void load (ESMReader &esm); void save (ESMWriter &esm) const;