#include "statsextensions.hpp"

#include <cmath>

#include <components/esm3/loadcrea.hpp>
#include <components/esm3/loadnpc.hpp>

#include "../mwworld/esmstore.hpp"

#include <components/compiler/opcodes.hpp>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadfact.hpp>
#include <components/interpreter/interpreter.hpp>
#include <components/interpreter/opcodes.hpp>
#include <components/interpreter/runtime.hpp>

#include <components/esm3/loadmgef.hpp>

#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"

#include "../mwworld/class.hpp"
#include "../mwworld/player.hpp"

#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/npcstats.hpp"

#include "ref.hpp"

namespace
{
    ESM::RefId getDialogueActorFaction(const MWWorld::ConstPtr& actor)
    {
        ESM::RefId factionId = actor.getClass().getPrimaryFaction(actor);
        if (factionId.empty())
            throw std::runtime_error("failed to determine dialogue actors faction (because actor is factionless)");

        return factionId;
    }

    void modStat(MWMechanics::AttributeValue& stat, float amount)
    {
        const float base = stat.getBase();
        const float modifier = stat.getModifier() - stat.getDamage();
        const float modified = base + modifier;
        // Clamp to 100 unless base < 100 and we have a fortification going
        if ((modifier <= 0.f || base >= 100.f) && amount > 0.f)
            amount = std::clamp(100.f - modified, 0.f, amount);
        // Clamp the modified value in a way that doesn't properly account for negative numbers
        float newModified = modified + amount;
        if (newModified < 0.f)
        {
            if (modified >= 0.f)
                newModified = 0.f;
            else if (newModified < modified)
                newModified = modified;
        }
        // Calculate damage/fortification based on the clamped base value
        stat.setBase(std::clamp(base + amount, 0.f, 100.f), true);
        stat.setModifier(newModified - stat.getBase());
    }

    template <class T>
    void updateBaseRecord(MWWorld::Ptr& ptr)
    {
        const auto& store = *MWBase::Environment::get().getESMStore();
        const T* base = store.get<T>().find(ptr.getCellRef().getRefId());
        ptr.get<T>()->mBase = base;
    }
}

namespace MWScript
{
    namespace Stats
    {
        template <class R>
        class OpGetLevel : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = -1;
                if (ptr.getClass().isActor())
                    value = ptr.getClass().getCreatureStats(ptr).getLevel();

                runtime.push(value);
            }
        };

        template <class R>
        class OpSetLevel : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = runtime[0].mInteger;
                runtime.pop();

                if (ptr.getClass().isActor())
                    ptr.getClass().getCreatureStats(ptr).setLevel(value);
            }
        };

        template <class R>
        class OpGetAttribute : public Interpreter::Opcode0
        {
            ESM::RefId mIndex;

        public:
            OpGetAttribute(ESM::RefId index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = 0.f;
                if (ptr.getClass().isActor())
                    value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified();

                runtime.push(value);
            }
        };

        template <class R>
        class OpSetAttribute : public Interpreter::Opcode0
        {
            ESM::RefId mIndex;

        public:
            OpSetAttribute(ESM::RefId index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex);
                attribute.setBase(value, true);
                ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute);
            }
        };

        template <class R>
        class OpModAttribute : public Interpreter::Opcode0
        {
            ESM::RefId mIndex;

        public:
            OpModAttribute(ESM::RefId index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex);
                modStat(attribute, value);
                ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute);
            }
        };

        template <class R>
        class OpGetDynamic : public Interpreter::Opcode0
        {
            int mIndex;

        public:
            OpGetDynamic(int index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);
                Interpreter::Type_Float value = 0.f;

                if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr))
                {
                    // health is a special case
                    value = static_cast<Interpreter::Type_Float>(ptr.getClass().getItemMaxHealth(ptr));
                }
                else if (ptr.getClass().isActor())
                {
                    value = ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).getCurrent();
                    // GetMagicka shouldn't return negative values
                    if (mIndex == 1 && value < 0)
                        value = 0;
                }
                runtime.push(value);
            }
        };

        template <class R>
        class OpSetDynamic : public Interpreter::Opcode0
        {
            int mIndex;

        public:
            OpSetDynamic(int index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::DynamicStat<float> stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex));

                stat.setBase(value);
                stat.setCurrent(stat.getModified(false), true, true);

                ptr.getClass().getCreatureStats(ptr).setDynamic(mIndex, stat);
            }
        };

        template <class R>
        class OpModDynamic : public Interpreter::Opcode0
        {
            int mIndex;

        public:
            OpModDynamic(int index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                int peek = R::implicit ? 0 : runtime[0].mInteger;

                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float diff = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                // workaround broken endgame scripts that kill dagoth ur
                if (!R::implicit && ptr.getCellRef().getRefId() == "dagoth_ur_1")
                {
                    runtime.push(peek);

                    if (R()(runtime, false, true).isEmpty())
                    {
                        Log(Debug::Warning) << "Warning: Compensating for broken script in Morrowind.esm by "
                                            << "ignoring remote access to dagoth_ur_1";

                        return;
                    }
                }

                MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);

                MWMechanics::DynamicStat<float> stat = stats.getDynamic(mIndex);

                float current = stat.getCurrent();
                float base = diff + stat.getBase();
                if (mIndex != 2)
                    base = std::max(base, 0.f);
                stat.setBase(base);
                stat.setCurrent(diff + current, true, true);

                stats.setDynamic(mIndex, stat);
            }
        };

        template <class R>
        class OpModCurrentDynamic : public Interpreter::Opcode0
        {
            int mIndex;

        public:
            OpModCurrentDynamic(int index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float diff = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);

                Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent();

                MWMechanics::DynamicStat<float> stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex));

                bool allowDecreaseBelowZero = false;
                if (mIndex == 2) // Fatigue-specific logic
                {
                    // For fatigue, a negative current value is allowed and means the actor will be knocked down
                    allowDecreaseBelowZero = true;
                    // Knock down the actor immediately if a non-positive new value is the case
                    if (diff + current <= 0.f)
                        ptr.getClass().getCreatureStats(ptr).setKnockedDown(true);
                }
                stat.setCurrent(diff + current, allowDecreaseBelowZero);

                ptr.getClass().getCreatureStats(ptr).setDynamic(mIndex, stat);
            }
        };

        template <class R>
        class OpGetDynamicGetRatio : public Interpreter::Opcode0
        {
            int mIndex;

        public:
            OpGetDynamicGetRatio(int index)
                : mIndex(index)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (!ptr.getClass().isActor())
                {
                    runtime.push(0.f);
                    return;
                }

                const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);

                runtime.push(stats.getDynamic(mIndex).getRatio(false));
            }
        };

        template <class R>
        class OpGetSkill : public Interpreter::Opcode0
        {
            ESM::RefId mId;

        public:
            OpGetSkill(ESM::RefId id)
                : mId(id)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (!ptr.getClass().isActor())
                {
                    runtime.push(0.f);
                    return;
                }

                Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId);

                runtime.push(value);
            }
        };

        template <class R>
        class OpSetSkill : public Interpreter::Opcode0
        {
            ESM::RefId mId;

        public:
            OpSetSkill(ESM::RefId id)
                : mId(id)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isNpc())
                    return;

                MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr);

                stats.getSkill(mId).setBase(value, true);
            }
        };

        template <class R>
        class OpModSkill : public Interpreter::Opcode0
        {
            ESM::RefId mId;

        public:
            OpModSkill(ESM::RefId id)
                : mId(id)
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Float value = runtime[0].mFloat;
                runtime.pop();

                if (!ptr.getClass().isNpc())
                    return;

                MWMechanics::SkillValue& skill = ptr.getClass().getNpcStats(ptr).getSkill(mId);
                modStat(skill, value);
            }
        };

        class OpGetPCCrimeLevel : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWBase::World* world = MWBase::Environment::get().getWorld();
                MWWorld::Ptr player = world->getPlayerPtr();
                runtime.push(static_cast<Interpreter::Type_Float>(player.getClass().getNpcStats(player).getBounty()));
            }
        };

        class OpSetPCCrimeLevel : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWBase::World* world = MWBase::Environment::get().getWorld();
                MWWorld::Ptr player = world->getPlayerPtr();

                int bounty = static_cast<int>(runtime[0].mFloat);
                runtime.pop();
                player.getClass().getNpcStats(player).setBounty(bounty);

                if (bounty == 0)
                    MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId();
            }
        };

        class OpModPCCrimeLevel : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWBase::World* world = MWBase::Environment::get().getWorld();
                MWWorld::Ptr player = world->getPlayerPtr();
                int bounty = std::max(
                    0, static_cast<int>(runtime[0].mFloat) + player.getClass().getNpcStats(player).getBounty());
                player.getClass().getNpcStats(player).setBounty(bounty);
                runtime.pop();
                if (bounty == 0)
                    MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId();
            }
        };

        template <class R>
        class OpAddSpell : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(id);

                MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
                creatureStats.getSpells().add(spell);
                ESM::Spell::SpellType type = static_cast<ESM::Spell::SpellType>(spell->mData.mType);
                if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power)
                {
                    // Add spell effect to *this actor's* queue immediately
                    creatureStats.getActiveSpells().addSpell(spell, ptr);
                    // Apply looping particles immediately for constant effects
                    MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr);
                }
            }
        };

        template <class R>
        class OpRemoveSpell : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
                const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(id);
                creatureStats.getSpells().remove(spell);
                if (spell->mData.mType == ESM::Spell::ST_Ability || spell->mData.mType == ESM::Spell::ST_Blight
                    || spell->mData.mType == ESM::Spell::ST_Curse || spell->mData.mType == ESM::Spell::ST_Disease)
                    creatureStats.getActiveSpells().removeEffectsBySourceSpellId(ptr, id);

                MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager();

                if (ptr == MWMechanics::getPlayer() && id == wm->getSelectedSpell())
                {
                    wm->unsetSelectedSpell();
                }
            }
        };

        template <class R>
        class OpRemoveSpellEffects : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime.pop();

                if (ptr.getClass().isActor())
                    ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid);
            }
        };

        template <class R>
        class OpRemoveEffects : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer effectId = runtime[0].mInteger;
                runtime.pop();

                if (ptr.getClass().isActor())
                    ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(
                        ptr, ESM::MagicEffect::indexToRefId(static_cast<int>(effectId)));
            }
        };

        template <class R>
        class OpGetSpell : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {

                MWWorld::Ptr ptr = R()(runtime);

                ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime.pop();

                Interpreter::Type_Integer value = 0;

                if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getSpells().hasSpell(id))
                    value = 1;

                runtime.push(value);
            }
        };

        template <class R>
        class OpPCJoinFaction : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr actor = R()(runtime, false);

                ESM::RefId factionID;

                if (arg0 == 0)
                {
                    factionID = getDialogueActorFaction(actor);
                }
                else
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                // Make sure this faction exists
                MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(factionID);

                if (!factionID.empty())
                {
                    MWWorld::Ptr player = MWMechanics::getPlayer();
                    player.getClass().getNpcStats(player).joinFaction(factionID);
                }
            }
        };

        template <class R>
        class OpPCRaiseRank : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr actor = R()(runtime, false);

                ESM::RefId factionID;

                if (arg0 == 0)
                {
                    factionID = getDialogueActorFaction(actor);
                }
                else
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                // Make sure this faction exists
                MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(factionID);

                if (!factionID.empty())
                {
                    MWWorld::Ptr player = MWMechanics::getPlayer();
                    if (!player.getClass().getNpcStats(player).isInFaction(factionID))
                    {
                        player.getClass().getNpcStats(player).joinFaction(factionID);
                    }
                    else
                    {
                        int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID);
                        player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank + 1);
                    }
                }
            }
        };

        template <class R>
        class OpPCLowerRank : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr actor = R()(runtime, false);

                ESM::RefId factionID;

                if (arg0 == 0)
                {
                    factionID = getDialogueActorFaction(actor);
                }
                else
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                // Make sure this faction exists
                MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(factionID);

                if (!factionID.empty())
                {
                    MWWorld::Ptr player = MWMechanics::getPlayer();
                    int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID);
                    player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank - 1);
                }
            }
        };

        template <class R>
        class OpGetPCRank : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                ESM::RefId factionID;
                if (arg0 > 0)
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionID = ptr.getClass().getPrimaryFaction(ptr);
                }
                // Make sure this faction exists
                MWBase::Environment::get().getESMStore()->get<ESM::Faction>().find(factionID);

                if (!factionID.empty())
                {
                    MWWorld::Ptr player = MWMechanics::getPlayer();
                    runtime.push(player.getClass().getNpcStats(player).getFactionRank(factionID));
                }
                else
                {
                    runtime.push(-1);
                }
            }
        };

        template <class R>
        class OpModDisposition : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = runtime[0].mInteger;
                runtime.pop();

                if (ptr.getClass().isNpc())
                    ptr.getClass().getNpcStats(ptr).setBaseDisposition(
                        ptr.getClass().getNpcStats(ptr).getBaseDisposition() + value);

                // else: must not throw exception (used by an Almalexia dialogue script)
            }
        };

        template <class R>
        class OpSetDisposition : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = runtime[0].mInteger;
                runtime.pop();

                if (ptr.getClass().isNpc())
                    ptr.getClass().getNpcStats(ptr).setBaseDisposition(value);
            }
        };

        template <class R>
        class OpGetDisposition : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (!ptr.getClass().isNpc())
                    runtime.push(0);
                else
                    runtime.push(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr));
            }
        };

        class OpGetDeadCount : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime[0].mInteger = MWBase::Environment::get().getMechanicsManager()->countDeaths(id);
            }
        };

        template <class R>
        class OpGetPCFacRep : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                ESM::RefId factionId;

                if (arg0 == 1)
                {
                    factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionId = getDialogueActorFaction(ptr);
                }

                if (factionId.empty())
                    throw std::runtime_error("failed to determine faction");

                MWWorld::Ptr player = MWMechanics::getPlayer();
                runtime.push(player.getClass().getNpcStats(player).getFactionReputation(factionId));
            }
        };

        template <class R>
        class OpSetPCFacRep : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                Interpreter::Type_Integer value = runtime[0].mInteger;
                runtime.pop();

                ESM::RefId factionId;

                if (arg0 == 1)
                {
                    factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionId = getDialogueActorFaction(ptr);
                }

                if (factionId.empty())
                    throw std::runtime_error("failed to determine faction");

                MWWorld::Ptr player = MWMechanics::getPlayer();
                player.getClass().getNpcStats(player).setFactionReputation(factionId, value);
            }
        };

        template <class R>
        class OpModPCFacRep : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                Interpreter::Type_Integer value = runtime[0].mInteger;
                runtime.pop();

                ESM::RefId factionId;

                if (arg0 == 1)
                {
                    factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionId = getDialogueActorFaction(ptr);
                }

                if (factionId.empty())
                    throw std::runtime_error("failed to determine faction");

                MWWorld::Ptr player = MWMechanics::getPlayer();
                player.getClass().getNpcStats(player).setFactionReputation(
                    factionId, player.getClass().getNpcStats(player).getFactionReputation(factionId) + value);
            }
        };

        template <class R>
        class OpGetCommonDisease : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (ptr.getClass().isActor())
                    runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease());
                else
                    runtime.push(0);
            }
        };

        template <class R>
        class OpGetBlightDisease : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (ptr.getClass().isActor())
                    runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease());
                else
                    runtime.push(0);
            }
        };

        template <class R>
        class OpGetRace : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::ConstPtr ptr = R()(runtime);

                ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                runtime.pop();

                if (ptr.getClass().isNpc())
                {
                    const ESM::RefId& npcRace = ptr.get<ESM::NPC>()->mBase->mRace;

                    runtime.push(race == npcRace);
                }
                else
                {
                    runtime.push(0);
                }
            }
        };

        class OpGetWerewolfKills : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr();

                runtime.push(ptr.getClass().getNpcStats(ptr).getWerewolfKills());
            }
        };

        template <class R>
        class OpPcExpelled : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                ESM::RefId factionID;
                if (arg0 > 0)
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionID = ptr.getClass().getPrimaryFaction(ptr);
                }
                MWWorld::Ptr player = MWMechanics::getPlayer();
                if (!factionID.empty())
                {
                    runtime.push(player.getClass().getNpcStats(player).getExpelled(factionID));
                }
                else
                {
                    runtime.push(0);
                }
            }
        };

        template <class R>
        class OpPcExpell : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                ESM::RefId factionID;
                if (arg0 > 0)
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionID = ptr.getClass().getPrimaryFaction(ptr);
                }
                MWWorld::Ptr player = MWMechanics::getPlayer();
                if (!factionID.empty())
                {
                    player.getClass().getNpcStats(player).expell(factionID, true);
                }
            }
        };

        template <class R>
        class OpPcClearExpelled : public Interpreter::Opcode1
        {
        public:
            void execute(Interpreter::Runtime& runtime, unsigned int arg0) override
            {
                MWWorld::ConstPtr ptr = R()(runtime, false);

                ESM::RefId factionID;
                if (arg0 > 0)
                {
                    factionID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger));
                    runtime.pop();
                }
                else
                {
                    factionID = ptr.getClass().getPrimaryFaction(ptr);
                }
                MWWorld::Ptr player = MWMechanics::getPlayer();
                if (!factionID.empty())
                    player.getClass().getNpcStats(player).clearExpelled(factionID);
            }
        };

        template <class R>
        class OpRaiseRank : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr);
                if (factionID.empty())
                    return;

                MWWorld::Ptr player = MWMechanics::getPlayer();

                // no-op when executed on the player
                if (ptr == player)
                    return;

                // If we already changed rank for this NPC, modify current rank in the NPC stats.
                // Otherwise take rank from base NPC record, increase it and put it to NPC data.
                int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID);
                if (currentRank >= 0)
                    ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, currentRank + 1);
                else
                {
                    int rank = ptr.getClass().getPrimaryFactionRank(ptr);
                    ptr.getClass().getNpcStats(ptr).joinFaction(factionID);
                    ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, rank + 1);
                }
            }
        };

        template <class R>
        class OpLowerRank : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr);
                if (factionID.empty())
                    return;

                MWWorld::Ptr player = MWMechanics::getPlayer();

                // no-op when executed on the player
                if (ptr == player)
                    return;

                // If we already changed rank for this NPC, modify current rank in the NPC stats.
                // Otherwise take rank from base NPC record, decrease it and put it to NPC data.
                int currentRank = ptr.getClass().getNpcStats(ptr).getFactionRank(factionID);
                if (currentRank == 0)
                    return;
                else if (currentRank > 0)
                    ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, currentRank - 1);
                else
                {
                    int rank = ptr.getClass().getPrimaryFactionRank(ptr);
                    ptr.getClass().getNpcStats(ptr).joinFaction(factionID);
                    ptr.getClass().getNpcStats(ptr).setFactionRank(factionID, std::max(0, rank - 1));
                }
            }
        };

        template <class R>
        class OpOnDeath : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = 0;
                if (ptr.getClass().isActor())
                {
                    auto& stats = ptr.getClass().getCreatureStats(ptr);
                    value = stats.hasDied();

                    if (value)
                        stats.clearHasDied();
                }

                runtime.push(value);
            }
        };

        template <class R>
        class OpOnMurder : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = 0;
                if (ptr.getClass().isActor())
                {
                    auto& stats = ptr.getClass().getCreatureStats(ptr);
                    value = stats.hasBeenMurdered();

                    if (value)
                        stats.clearHasBeenMurdered();
                }

                runtime.push(value);
            }
        };

        template <class R>
        class OpOnKnockout : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                Interpreter::Type_Integer value = 0;
                if (ptr.getClass().isActor())
                    value = ptr.getClass().getCreatureStats(ptr).getKnockedDownOneFrame();

                runtime.push(value);
            }
        };

        template <class R>
        class OpIsWerewolf : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);
                if (ptr.getClass().isNpc())
                    runtime.push(ptr.getClass().getNpcStats(ptr).isWerewolf());
                else
                    runtime.push(0);
            }
        };

        template <class R, bool set>
        class OpSetWerewolf : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);
                if (ptr.getClass().isNpc())
                    MWBase::Environment::get().getMechanicsManager()->setWerewolf(ptr, set);
            }
        };

        template <class R>
        class OpSetWerewolfAcrobatics : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);
                if (ptr.getClass().isNpc())
                    MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr);
            }
        };

        template <class R>
        class OpResurrect : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (!ptr.getClass().isActor())
                    return;

                if (ptr == MWMechanics::getPlayer())
                {
                    MWBase::Environment::get().getMechanicsManager()->resurrect(ptr);
                    if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Ended)
                        MWBase::Environment::get().getStateManager()->resumeGame();
                }
                else if (ptr.getClass().getCreatureStats(ptr).isDead())
                {
                    bool wasEnabled = ptr.getRefData().isEnabled();
                    MWBase::Environment::get().getWorld()->undeleteObject(ptr);
                    auto windowManager = MWBase::Environment::get().getWindowManager();
                    bool wasOpen = windowManager->containsMode(MWGui::GM_Container);
                    windowManager->onDeleteCustomData(ptr);
                    // HACK: disable/enable object to re-add it to the scene properly (need a new Animation).
                    MWBase::Environment::get().getWorld()->disable(ptr);
                    // The actor's base record may have changed after this specific reference was created.
                    // So we need to update to the current version
                    if (ptr.getClass().isNpc())
                        updateBaseRecord<ESM::NPC>(ptr);
                    else
                        updateBaseRecord<ESM::Creature>(ptr);
                    if (wasOpen && !windowManager->containsMode(MWGui::GM_Container))
                    {
                        // Reopen the loot GUI if it was closed because we resurrected the actor we were looting
                        MWBase::Environment::get().getMechanicsManager()->resurrect(ptr);
                        windowManager->forceLootMode(ptr);
                    }
                    else
                    {
                        MWBase::Environment::get().getWorld()->removeContainerScripts(ptr);
                        // resets runtime state such as inventory, stats and AI. does not reset position in the world
                        ptr.getRefData().setCustomData(nullptr);
                    }
                    if (wasEnabled)
                        MWBase::Environment::get().getWorld()->enable(ptr);
                }
            }
        };

        template <class R>
        class OpGetStat : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                // dummy
                runtime.pop();
                runtime.push(0);
            }
        };

        template <class R>
        class OpGetMagicEffect : public Interpreter::Opcode0
        {
            ESM::RefId mPositiveEffect;
            ESM::RefId mNegativeEffect;

        public:
            OpGetMagicEffect(int positiveEffect, int negativeEffect)
                : mPositiveEffect(ESM::MagicEffect::indexToRefId(positiveEffect))
                , mNegativeEffect(ESM::MagicEffect::indexToRefId(negativeEffect))
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                if (!ptr.getClass().isActor())
                {
                    runtime.push(0);
                    return;
                }

                const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects();
                float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude();
                if (!mNegativeEffect.empty())
                    currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude();

                // GetResist* should take in account elemental shields
                if (mPositiveEffect == ESM::MagicEffect::ResistFire)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude();
                if (mPositiveEffect == ESM::MagicEffect::ResistShock)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude();
                if (mPositiveEffect == ESM::MagicEffect::ResistFrost)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude();

                int ret = static_cast<int>(currentValue);
                runtime.push(ret);
            }
        };

        template <class R>
        class OpSetMagicEffect : public Interpreter::Opcode0
        {
            ESM::RefId mPositiveEffect;
            ESM::RefId mNegativeEffect;

        public:
            OpSetMagicEffect(int positiveEffect, int negativeEffect)
                : mPositiveEffect(ESM::MagicEffect::indexToRefId(positiveEffect))
                , mNegativeEffect(ESM::MagicEffect::indexToRefId(negativeEffect))
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                int arg = runtime[0].mInteger;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects();
                float currentValue = effects.getOrDefault(mPositiveEffect).getMagnitude();
                if (!mNegativeEffect.empty())
                    currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude();

                // SetResist* should take in account elemental shields
                if (mPositiveEffect == ESM::MagicEffect::ResistFire)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude();
                if (mPositiveEffect == ESM::MagicEffect::ResistShock)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude();
                if (mPositiveEffect == ESM::MagicEffect::ResistFrost)
                    currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude();

                effects.modifyBase(mPositiveEffect, (arg - static_cast<int>(currentValue)));
            }
        };

        template <class R>
        class OpModMagicEffect : public Interpreter::Opcode0
        {
            ESM::RefId mPositiveEffect;
            ESM::RefId mNegativeEffect;

        public:
            OpModMagicEffect(int positiveEffect, int negativeEffect)
                : mPositiveEffect(ESM::MagicEffect::indexToRefId(positiveEffect))
                , mNegativeEffect(ESM::MagicEffect::indexToRefId(negativeEffect))
            {
            }

            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr ptr = R()(runtime);

                int arg = runtime[0].mInteger;
                runtime.pop();

                if (!ptr.getClass().isActor())
                    return;

                MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
                stats.getMagicEffects().modifyBase(mPositiveEffect, arg);
            }
        };

        class OpGetPCVisionBonus : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                MWWorld::Ptr player = MWMechanics::getPlayer();
                MWMechanics::EffectParam nightEye
                    = player.getClass().getCreatureStats(player).getMagicEffects().getOrDefault(
                        ESM::MagicEffect::NightEye);
                runtime.push(std::clamp(nightEye.getMagnitude() / 100.f, 0.f, 1.f));
            }
        };

        class OpSetPCVisionBonus : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                float arg = runtime[0].mFloat;
                runtime.pop();
                MWWorld::Ptr player = MWMechanics::getPlayer();
                auto& effects = player.getClass().getCreatureStats(player).getMagicEffects();
                float delta = std::clamp(arg * 100.f, 0.f, 100.f)
                    - effects.getOrDefault(ESM::MagicEffect::NightEye).getMagnitude();
                effects.modifyBase(MWMechanics::EffectKey(ESM::MagicEffect::NightEye), static_cast<int>(delta));
            }
        };

        class OpModPCVisionBonus : public Interpreter::Opcode0
        {
        public:
            void execute(Interpreter::Runtime& runtime) override
            {
                float arg = runtime[0].mFloat;
                runtime.pop();
                MWWorld::Ptr player = MWMechanics::getPlayer();
                auto& effects = player.getClass().getCreatureStats(player).getMagicEffects();
                const MWMechanics::EffectParam nightEye = effects.getOrDefault(ESM::MagicEffect::NightEye);
                float newBase = std::clamp(nightEye.getMagnitude() + arg * 100.f, 0.f, 100.f);
                newBase -= nightEye.getModifier();
                float delta = std::clamp(newBase, 0.f, 100.f) - nightEye.getMagnitude();
                effects.modifyBase(MWMechanics::EffectKey(ESM::MagicEffect::NightEye), static_cast<int>(delta));
            }
        };

        struct MagicEffect
        {
            ESM::RefId mPositiveEffect;
            ESM::RefId mNegativeEffect;
        };

        void installOpcodes(Interpreter::Interpreter& interpreter)
        {
            for (int i = 0; i < Compiler::Stats::numberOfAttributes; ++i)
            {
                ESM::RefId id = ESM::Attribute::indexToRefId(i);
                interpreter.installSegment5<OpGetAttribute<ImplicitRef>>(Compiler::Stats::opcodeGetAttribute + i, id);
                interpreter.installSegment5<OpGetAttribute<ExplicitRef>>(
                    Compiler::Stats::opcodeGetAttributeExplicit + i, id);

                interpreter.installSegment5<OpSetAttribute<ImplicitRef>>(Compiler::Stats::opcodeSetAttribute + i, id);
                interpreter.installSegment5<OpSetAttribute<ExplicitRef>>(
                    Compiler::Stats::opcodeSetAttributeExplicit + i, id);

                interpreter.installSegment5<OpModAttribute<ImplicitRef>>(Compiler::Stats::opcodeModAttribute + i, id);
                interpreter.installSegment5<OpModAttribute<ExplicitRef>>(
                    Compiler::Stats::opcodeModAttributeExplicit + i, id);
            }

            for (int i = 0; i < Compiler::Stats::numberOfDynamics; ++i)
            {
                interpreter.installSegment5<OpGetDynamic<ImplicitRef>>(Compiler::Stats::opcodeGetDynamic + i, i);
                interpreter.installSegment5<OpGetDynamic<ExplicitRef>>(
                    Compiler::Stats::opcodeGetDynamicExplicit + i, i);

                interpreter.installSegment5<OpSetDynamic<ImplicitRef>>(Compiler::Stats::opcodeSetDynamic + i, i);
                interpreter.installSegment5<OpSetDynamic<ExplicitRef>>(
                    Compiler::Stats::opcodeSetDynamicExplicit + i, i);

                interpreter.installSegment5<OpModDynamic<ImplicitRef>>(Compiler::Stats::opcodeModDynamic + i, i);
                interpreter.installSegment5<OpModDynamic<ExplicitRef>>(
                    Compiler::Stats::opcodeModDynamicExplicit + i, i);

                interpreter.installSegment5<OpModCurrentDynamic<ImplicitRef>>(
                    Compiler::Stats::opcodeModCurrentDynamic + i, i);
                interpreter.installSegment5<OpModCurrentDynamic<ExplicitRef>>(
                    Compiler::Stats::opcodeModCurrentDynamicExplicit + i, i);

                interpreter.installSegment5<OpGetDynamicGetRatio<ImplicitRef>>(
                    Compiler::Stats::opcodeGetDynamicGetRatio + i, i);
                interpreter.installSegment5<OpGetDynamicGetRatio<ExplicitRef>>(
                    Compiler::Stats::opcodeGetDynamicGetRatioExplicit + i, i);
            }

            for (int i = 0; i < Compiler::Stats::numberOfSkills; ++i)
            {
                ESM::RefId id = ESM::Skill::indexToRefId(i);
                interpreter.installSegment5<OpGetSkill<ImplicitRef>>(Compiler::Stats::opcodeGetSkill + i, id);
                interpreter.installSegment5<OpGetSkill<ExplicitRef>>(Compiler::Stats::opcodeGetSkillExplicit + i, id);

                interpreter.installSegment5<OpSetSkill<ImplicitRef>>(Compiler::Stats::opcodeSetSkill + i, id);
                interpreter.installSegment5<OpSetSkill<ExplicitRef>>(Compiler::Stats::opcodeSetSkillExplicit + i, id);

                interpreter.installSegment5<OpModSkill<ImplicitRef>>(Compiler::Stats::opcodeModSkill + i, id);
                interpreter.installSegment5<OpModSkill<ExplicitRef>>(Compiler::Stats::opcodeModSkillExplicit + i, id);
            }

            interpreter.installSegment5<OpGetPCCrimeLevel>(Compiler::Stats::opcodeGetPCCrimeLevel);
            interpreter.installSegment5<OpSetPCCrimeLevel>(Compiler::Stats::opcodeSetPCCrimeLevel);
            interpreter.installSegment5<OpModPCCrimeLevel>(Compiler::Stats::opcodeModPCCrimeLevel);

            interpreter.installSegment5<OpAddSpell<ImplicitRef>>(Compiler::Stats::opcodeAddSpell);
            interpreter.installSegment5<OpAddSpell<ExplicitRef>>(Compiler::Stats::opcodeAddSpellExplicit);
            interpreter.installSegment5<OpRemoveSpell<ImplicitRef>>(Compiler::Stats::opcodeRemoveSpell);
            interpreter.installSegment5<OpRemoveSpell<ExplicitRef>>(Compiler::Stats::opcodeRemoveSpellExplicit);
            interpreter.installSegment5<OpRemoveSpellEffects<ImplicitRef>>(Compiler::Stats::opcodeRemoveSpellEffects);
            interpreter.installSegment5<OpRemoveSpellEffects<ExplicitRef>>(
                Compiler::Stats::opcodeRemoveSpellEffectsExplicit);
            interpreter.installSegment5<OpResurrect<ImplicitRef>>(Compiler::Stats::opcodeResurrect);
            interpreter.installSegment5<OpResurrect<ExplicitRef>>(Compiler::Stats::opcodeResurrectExplicit);
            interpreter.installSegment5<OpRemoveEffects<ImplicitRef>>(Compiler::Stats::opcodeRemoveEffects);
            interpreter.installSegment5<OpRemoveEffects<ExplicitRef>>(Compiler::Stats::opcodeRemoveEffectsExplicit);

            interpreter.installSegment5<OpGetSpell<ImplicitRef>>(Compiler::Stats::opcodeGetSpell);
            interpreter.installSegment5<OpGetSpell<ExplicitRef>>(Compiler::Stats::opcodeGetSpellExplicit);

            interpreter.installSegment3<OpPCRaiseRank<ImplicitRef>>(Compiler::Stats::opcodePCRaiseRank);
            interpreter.installSegment3<OpPCLowerRank<ImplicitRef>>(Compiler::Stats::opcodePCLowerRank);
            interpreter.installSegment3<OpPCJoinFaction<ImplicitRef>>(Compiler::Stats::opcodePCJoinFaction);
            interpreter.installSegment3<OpPCRaiseRank<ExplicitRef>>(Compiler::Stats::opcodePCRaiseRankExplicit);
            interpreter.installSegment3<OpPCLowerRank<ExplicitRef>>(Compiler::Stats::opcodePCLowerRankExplicit);
            interpreter.installSegment3<OpPCJoinFaction<ExplicitRef>>(Compiler::Stats::opcodePCJoinFactionExplicit);
            interpreter.installSegment3<OpGetPCRank<ImplicitRef>>(Compiler::Stats::opcodeGetPCRank);
            interpreter.installSegment3<OpGetPCRank<ExplicitRef>>(Compiler::Stats::opcodeGetPCRankExplicit);

            interpreter.installSegment5<OpModDisposition<ImplicitRef>>(Compiler::Stats::opcodeModDisposition);
            interpreter.installSegment5<OpModDisposition<ExplicitRef>>(Compiler::Stats::opcodeModDispositionExplicit);
            interpreter.installSegment5<OpSetDisposition<ImplicitRef>>(Compiler::Stats::opcodeSetDisposition);
            interpreter.installSegment5<OpSetDisposition<ExplicitRef>>(Compiler::Stats::opcodeSetDispositionExplicit);
            interpreter.installSegment5<OpGetDisposition<ImplicitRef>>(Compiler::Stats::opcodeGetDisposition);
            interpreter.installSegment5<OpGetDisposition<ExplicitRef>>(Compiler::Stats::opcodeGetDispositionExplicit);

            interpreter.installSegment5<OpGetLevel<ImplicitRef>>(Compiler::Stats::opcodeGetLevel);
            interpreter.installSegment5<OpGetLevel<ExplicitRef>>(Compiler::Stats::opcodeGetLevelExplicit);
            interpreter.installSegment5<OpSetLevel<ImplicitRef>>(Compiler::Stats::opcodeSetLevel);
            interpreter.installSegment5<OpSetLevel<ExplicitRef>>(Compiler::Stats::opcodeSetLevelExplicit);

            interpreter.installSegment5<OpGetDeadCount>(Compiler::Stats::opcodeGetDeadCount);

            interpreter.installSegment3<OpGetPCFacRep<ImplicitRef>>(Compiler::Stats::opcodeGetPCFacRep);
            interpreter.installSegment3<OpGetPCFacRep<ExplicitRef>>(Compiler::Stats::opcodeGetPCFacRepExplicit);
            interpreter.installSegment3<OpSetPCFacRep<ImplicitRef>>(Compiler::Stats::opcodeSetPCFacRep);
            interpreter.installSegment3<OpSetPCFacRep<ExplicitRef>>(Compiler::Stats::opcodeSetPCFacRepExplicit);
            interpreter.installSegment3<OpModPCFacRep<ImplicitRef>>(Compiler::Stats::opcodeModPCFacRep);
            interpreter.installSegment3<OpModPCFacRep<ExplicitRef>>(Compiler::Stats::opcodeModPCFacRepExplicit);

            interpreter.installSegment5<OpGetCommonDisease<ImplicitRef>>(Compiler::Stats::opcodeGetCommonDisease);
            interpreter.installSegment5<OpGetCommonDisease<ExplicitRef>>(
                Compiler::Stats::opcodeGetCommonDiseaseExplicit);
            interpreter.installSegment5<OpGetBlightDisease<ImplicitRef>>(Compiler::Stats::opcodeGetBlightDisease);
            interpreter.installSegment5<OpGetBlightDisease<ExplicitRef>>(
                Compiler::Stats::opcodeGetBlightDiseaseExplicit);

            interpreter.installSegment5<OpGetRace<ImplicitRef>>(Compiler::Stats::opcodeGetRace);
            interpreter.installSegment5<OpGetRace<ExplicitRef>>(Compiler::Stats::opcodeGetRaceExplicit);
            interpreter.installSegment5<OpGetWerewolfKills>(Compiler::Stats::opcodeGetWerewolfKills);

            interpreter.installSegment3<OpPcExpelled<ImplicitRef>>(Compiler::Stats::opcodePcExpelled);
            interpreter.installSegment3<OpPcExpelled<ExplicitRef>>(Compiler::Stats::opcodePcExpelledExplicit);
            interpreter.installSegment3<OpPcExpell<ImplicitRef>>(Compiler::Stats::opcodePcExpell);
            interpreter.installSegment3<OpPcExpell<ExplicitRef>>(Compiler::Stats::opcodePcExpellExplicit);
            interpreter.installSegment3<OpPcClearExpelled<ImplicitRef>>(Compiler::Stats::opcodePcClearExpelled);
            interpreter.installSegment3<OpPcClearExpelled<ExplicitRef>>(Compiler::Stats::opcodePcClearExpelledExplicit);
            interpreter.installSegment5<OpRaiseRank<ImplicitRef>>(Compiler::Stats::opcodeRaiseRank);
            interpreter.installSegment5<OpRaiseRank<ExplicitRef>>(Compiler::Stats::opcodeRaiseRankExplicit);
            interpreter.installSegment5<OpLowerRank<ImplicitRef>>(Compiler::Stats::opcodeLowerRank);
            interpreter.installSegment5<OpLowerRank<ExplicitRef>>(Compiler::Stats::opcodeLowerRankExplicit);

            interpreter.installSegment5<OpOnDeath<ImplicitRef>>(Compiler::Stats::opcodeOnDeath);
            interpreter.installSegment5<OpOnDeath<ExplicitRef>>(Compiler::Stats::opcodeOnDeathExplicit);
            interpreter.installSegment5<OpOnMurder<ImplicitRef>>(Compiler::Stats::opcodeOnMurder);
            interpreter.installSegment5<OpOnMurder<ExplicitRef>>(Compiler::Stats::opcodeOnMurderExplicit);
            interpreter.installSegment5<OpOnKnockout<ImplicitRef>>(Compiler::Stats::opcodeOnKnockout);
            interpreter.installSegment5<OpOnKnockout<ExplicitRef>>(Compiler::Stats::opcodeOnKnockoutExplicit);

            interpreter.installSegment5<OpIsWerewolf<ImplicitRef>>(Compiler::Stats::opcodeIsWerewolf);
            interpreter.installSegment5<OpIsWerewolf<ExplicitRef>>(Compiler::Stats::opcodeIsWerewolfExplicit);

            interpreter.installSegment5<OpSetWerewolf<ImplicitRef, true>>(Compiler::Stats::opcodeBecomeWerewolf);
            interpreter.installSegment5<OpSetWerewolf<ExplicitRef, true>>(
                Compiler::Stats::opcodeBecomeWerewolfExplicit);
            interpreter.installSegment5<OpSetWerewolf<ImplicitRef, false>>(Compiler::Stats::opcodeUndoWerewolf);
            interpreter.installSegment5<OpSetWerewolf<ExplicitRef, false>>(Compiler::Stats::opcodeUndoWerewolfExplicit);
            interpreter.installSegment5<OpSetWerewolfAcrobatics<ImplicitRef>>(
                Compiler::Stats::opcodeSetWerewolfAcrobatics);
            interpreter.installSegment5<OpSetWerewolfAcrobatics<ExplicitRef>>(
                Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit);
            interpreter.installSegment5<OpGetStat<ImplicitRef>>(Compiler::Stats::opcodeGetStat);
            interpreter.installSegment5<OpGetStat<ExplicitRef>>(Compiler::Stats::opcodeGetStatExplicit);

            static const MagicEffect sMagicEffects[] = {
                { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka },
                { ESM::MagicEffect::ResistFire, ESM::MagicEffect::WeaknessToFire },
                { ESM::MagicEffect::ResistFrost, ESM::MagicEffect::WeaknessToFrost },
                { ESM::MagicEffect::ResistShock, ESM::MagicEffect::WeaknessToShock },
                { ESM::MagicEffect::ResistCommonDisease, ESM::MagicEffect::WeaknessToCommonDisease },
                { ESM::MagicEffect::ResistBlightDisease, ESM::MagicEffect::WeaknessToBlightDisease },
                { ESM::MagicEffect::ResistCorprusDisease, ESM::MagicEffect::WeaknessToCorprusDisease },
                { ESM::MagicEffect::ResistPoison, ESM::MagicEffect::WeaknessToPoison },
                { ESM::MagicEffect::ResistParalysis, ESM::RefId() },
                { ESM::MagicEffect::ResistNormalWeapons, ESM::MagicEffect::WeaknessToNormalWeapons },
                { ESM::MagicEffect::WaterBreathing, ESM::RefId() },
                { ESM::MagicEffect::Chameleon, ESM::RefId() },
                { ESM::MagicEffect::WaterWalking, ESM::RefId() },
                { ESM::MagicEffect::SwiftSwim, ESM::RefId() },
                { ESM::MagicEffect::Jump, ESM::RefId() },
                { ESM::MagicEffect::Levitate, ESM::RefId() },
                { ESM::MagicEffect::Shield, ESM::RefId() },
                { ESM::MagicEffect::Sound, ESM::RefId() },
                { ESM::MagicEffect::Silence, ESM::RefId() },
                { ESM::MagicEffect::Blind, ESM::RefId() },
                { ESM::MagicEffect::Paralyze, ESM::RefId() },
                { ESM::MagicEffect::Invisibility, ESM::RefId() },
                { ESM::MagicEffect::FortifyAttack, ESM::RefId() },
                { ESM::MagicEffect::Sanctuary, ESM::RefId() },
            };

            for (int i = 0; i < 24; ++i)
            {
                int positive = ESM::MagicEffect::refIdToIndex(sMagicEffects[i].mPositiveEffect);
                int negative = ESM::MagicEffect::refIdToIndex(sMagicEffects[i].mNegativeEffect);

                interpreter.installSegment5<OpGetMagicEffect<ImplicitRef>>(
                    Compiler::Stats::opcodeGetMagicEffect + i, positive, negative);
                interpreter.installSegment5<OpGetMagicEffect<ExplicitRef>>(
                    Compiler::Stats::opcodeGetMagicEffectExplicit + i, positive, negative);

                interpreter.installSegment5<OpSetMagicEffect<ImplicitRef>>(
                    Compiler::Stats::opcodeSetMagicEffect + i, positive, negative);
                interpreter.installSegment5<OpSetMagicEffect<ExplicitRef>>(
                    Compiler::Stats::opcodeSetMagicEffectExplicit + i, positive, negative);

                interpreter.installSegment5<OpModMagicEffect<ImplicitRef>>(
                    Compiler::Stats::opcodeModMagicEffect + i, positive, negative);
                interpreter.installSegment5<OpModMagicEffect<ExplicitRef>>(
                    Compiler::Stats::opcodeModMagicEffectExplicit + i, positive, negative);
            }

            interpreter.installSegment5<OpGetPCVisionBonus>(Compiler::Stats::opcodeGetPCVisionBonus);
            interpreter.installSegment5<OpSetPCVisionBonus>(Compiler::Stats::opcodeSetPCVisionBonus);
            interpreter.installSegment5<OpModPCVisionBonus>(Compiler::Stats::opcodeModPCVisionBonus);
        }
    }
}
